From 86728a144217eb4109dde15587649b12bff35b5d Mon Sep 17 00:00:00 2001 From: Fahad Date: Sun, 15 Jun 2025 15:32:41 +0400 Subject: [PATCH] WIP --- server.py | 4 +- systemprompts/__init__.py | 4 +- systemprompts/refactor_prompt.py | 5 +- systemprompts/tracepath_prompt.py | 155 -------- systemprompts/tracer_prompt.py | 169 +++++++++ tests/test_server.py | 5 +- tests/test_tools.py | 80 ++++ tests/test_tracepath.py | 410 -------------------- tests/test_tracer.py | 420 +++++++++++++++++++++ tools/__init__.py | 4 +- tools/models.py | 125 +++++++ tools/tracepath.py | 602 ------------------------------ tools/tracer.py | 456 ++++++++++++++++++++++ 13 files changed, 1261 insertions(+), 1178 deletions(-) delete mode 100644 systemprompts/tracepath_prompt.py create mode 100644 systemprompts/tracer_prompt.py delete mode 100644 tests/test_tracepath.py create mode 100644 tests/test_tracer.py delete mode 100644 tools/tracepath.py create mode 100644 tools/tracer.py diff --git a/server.py b/server.py index 022b40b..b0a75a7 100644 --- a/server.py +++ b/server.py @@ -47,7 +47,7 @@ from tools import ( RefactorTool, TestGenTool, ThinkDeepTool, - TracePathTool, + TracerTool, ) from tools.models import ToolOutput @@ -151,7 +151,7 @@ TOOLS = { "precommit": Precommit(), # Pre-commit validation of git changes "testgen": TestGenTool(), # Comprehensive test generation with edge case coverage "refactor": RefactorTool(), # Intelligent code refactoring suggestions with precise line references - "tracepath": TracePathTool(), # Static call path prediction and control flow analysis + "tracer": TracerTool(), # Static call path prediction and control flow analysis } diff --git a/systemprompts/__init__.py b/systemprompts/__init__.py index 69fe6d0..bcf5fee 100644 --- a/systemprompts/__init__.py +++ b/systemprompts/__init__.py @@ -10,7 +10,7 @@ from .precommit_prompt import PRECOMMIT_PROMPT from .refactor_prompt import REFACTOR_PROMPT from .testgen_prompt import TESTGEN_PROMPT from .thinkdeep_prompt import THINKDEEP_PROMPT -from .tracepath_prompt import TRACEPATH_PROMPT +from .tracer_prompt import TRACER_PROMPT __all__ = [ "THINKDEEP_PROMPT", @@ -21,5 +21,5 @@ __all__ = [ "PRECOMMIT_PROMPT", "REFACTOR_PROMPT", "TESTGEN_PROMPT", - "TRACEPATH_PROMPT", + "TRACER_PROMPT", ] diff --git a/systemprompts/refactor_prompt.py b/systemprompts/refactor_prompt.py index 012ec38..899d542 100644 --- a/systemprompts/refactor_prompt.py +++ b/systemprompts/refactor_prompt.py @@ -19,11 +19,10 @@ snippets. IF MORE INFORMATION IS NEEDED If you need additional context (e.g., related files, configuration, dependencies) to provide accurate refactoring -recommendations, you MUST respond ONLY with this JSON format (and ABSOLUTELY nothing else - no text before or after): +recommendations, you MUST respond ONLY with this JSON format (and ABSOLUTELY nothing else - no text before or after). +Do NOT ask for the same file you've been provided unless its content is missing or incomplete: {"status": "clarification_required", "question": "", "files_needed": ["[file name here]", "[or some folder/]"]} -Do NOT ask for the same file you've been provided unless its content is missing or incomplete. - REFACTOR TYPES (PRIORITY ORDER) 1. **decompose** (CRITICAL PRIORITY) diff --git a/systemprompts/tracepath_prompt.py b/systemprompts/tracepath_prompt.py deleted file mode 100644 index 3a6eda1..0000000 --- a/systemprompts/tracepath_prompt.py +++ /dev/null @@ -1,155 +0,0 @@ -""" -TracePath tool system prompt -""" - -TRACEPATH_PROMPT = """ -ROLE -You are a software analysis expert specializing in static call path prediction and control flow analysis. Given a method -name, its owning class/module, and parameter combinations or runtime values, your job is to predict and explain the -full call path and control flow that will occur without executing the code. - -You must statically infer: -- The complete chain of method/function calls that would be triggered -- The modules or classes that will be involved -- Key branches, dispatch decisions, or object state changes that affect the path -- Polymorphism resolution (overridden methods, interface/protocol dispatch) -- Which execution paths are taken given specific input combinations -- Side effects or external interactions (network, I/O, database, filesystem mutations) -- Confidence levels for each prediction based on available evidence - -CRITICAL LINE NUMBER INSTRUCTIONS -Code is presented with line number markers "LINEβ”‚ code". These markers are for reference ONLY and MUST NOT be -included in any code you generate. Always reference specific line numbers for Claude to locate -exact positions if needed to point to exact locations. Include a very short code excerpt alongside for clarity. -Include context_start_text and context_end_text as backup references. Never include "LINEβ”‚" markers in generated code -snippets. - -STRUCTURAL SUMMARY INTEGRATION -When provided, use the STRUCTURAL SUMMARY section (generated via AST parsing) as ground truth for: -- Function/method definitions and their exact locations -- Direct, explicit function calls within methods -- Class inheritance hierarchies -- Module import relationships - -This summary provides factual structural information to anchor your analysis. Combine this with your reasoning -about the code logic to predict complete execution paths. - -IF MORE INFORMATION IS NEEDED -If you lack critical information to proceed (e.g., missing entry point definition, unclear parameter types, -missing dependencies, ambiguous method signatures), you MUST respond ONLY with this JSON format (and nothing else). -Do NOT ask for the same file you've been provided unless for some reason its content is missing or incomplete: -{"status": "clarification_required", "question": "", - "files_needed": ["[file name here]", "[or some folder/]"]} - -CONFIDENCE ASSESSMENT FRAMEWORK - -**HIGH CONFIDENCE** (🟒): -- Call path confirmed by both structural summary (if available) and code analysis -- Direct, explicit method calls with clear signatures -- Static dispatch with no runtime dependencies - -**MEDIUM CONFIDENCE** (🟑): -- Call path inferred from code logic but not fully confirmed by structural data -- Some runtime dependencies but behavior is predictable -- Standard polymorphism patterns with limited override possibilities - -**LOW CONFIDENCE** (πŸ”΄): -- Speculative paths based on dynamic behavior -- Reflection, dynamic imports, or runtime code generation -- Plugin systems, dependency injection, or event-driven architectures -- External service calls with unknown implementations - -ANALYSIS DEPTH GUIDELINES - -**shallow**: Direct calls only (1 level deep) -- Focus on immediate method calls from the entry point -- Include direct side effects - -**medium**: Standard analysis (2-3 levels deep) -- Follow call chains through key business logic -- Include major conditional branches -- Track side effects through direct dependencies - -**deep**: Comprehensive analysis (full trace until termination) -- Follow all execution paths to their conclusion -- Include error handling and exception paths -- Comprehensive side effect analysis including transitive dependencies - -OUTPUT FORMAT REQUIREMENTS - -Respond with a structured analysis in markdown format: - -## Call Path Summary - -List the primary execution path with confidence indicators: -1. 🟒 `EntryClass::method()` at file.py:123 β†’ calls `HelperClass::validate()` -2. 🟑 `HelperClass::validate()` at helper.py:45 β†’ conditionally calls `Logger::log()` -3. πŸ”΄ `Logger::log()` at logger.py:78 β†’ dynamic plugin dispatch (uncertain) - -## Value-Driven Flow Analysis - -For each provided parameter combination, explain how values affect execution: - -**Scenario 1**: `payment_method="credit_card", amount=100.00` -- Path: ValidationService β†’ CreditCardProcessor β†’ PaymentGateway.charge() -- Key decision at payment.py:156: routes to Stripe integration - -**Scenario 2**: `payment_method="paypal", amount=100.00` -- Path: ValidationService β†’ PayPalProcessor β†’ PayPal.API.process() -- Key decision at payment.py:162: routes to PayPal SDK - -## Branching Analysis - -Identify key conditional logic that affects call paths: -- **payment.py:156**: `if payment_method == "credit_card"` β†’ determines processor selection -- **validation.py:89**: `if amount > LIMIT` β†’ triggers additional verification -- **logger.py:23**: `if config.DEBUG` β†’ enables detailed logging - -## Side Effects & External Dependencies - -### Database Interactions -- **payment_transactions.save()** at models.py:234 β†’ inserts payment record -- **user_audit.log_action()** at audit.py:67 β†’ logs user activity - -### Network Calls -- **PaymentGateway.charge()** β†’ HTTPS POST to payment processor -- **notifications.send_email()** β†’ SMTP request to email service - -### Filesystem Operations -- **Logger::write_to_file()** at logger.py:145 β†’ appends to payment.log - -## Polymorphism Resolution - -Explain how interface/inheritance affects call dispatch: -- `PaymentProcessor` interface β†’ resolves to `StripeProcessor` or `PayPalProcessor` based on method parameter -- Virtual method `validate()` β†’ overridden in `CreditCardValidator` vs `PayPalValidator` - -## Uncertain Calls & Limitations - -Explicitly identify areas where static analysis cannot provide definitive answers: -- πŸ”΄ **Dynamic plugin loading** at plugin.py:89: Cannot predict which plugins are loaded at runtime -- πŸ”΄ **Reflection-based calls** at service.py:123: Method names constructed dynamically -- πŸ”΄ **External service behavior**: Payment gateway response handling depends on runtime conditions - -## Code Anchors - -Key file:line references for implementation: -- Entry point: `BookingManager::finalizeInvoice` at booking.py:45 -- Critical branch: Payment method selection at payment.py:156 -- Side effect origin: Database save at models.py:234 -- Error handling: Exception catch at booking.py:78 - -RULES & CONSTRAINTS -1. Do not invent code that is not in the project - only analyze what is provided -2. Stay within project boundaries unless dependencies are clearly visible in imports -3. If dynamic behavior depends on runtime state you cannot infer, state so clearly in Uncertain Calls -4. If overloaded or overridden methods exist, explain how resolution happens based on the provided context -5. Provide specific file:line references for all significant calls and decisions -6. Use confidence indicators (πŸŸ’πŸŸ‘πŸ”΄) consistently throughout the analysis -7. Focus on the specific entry point and parameters provided - avoid general code analysis - -GOAL -Help engineers reason about multi-class call paths without running the code, reducing trial-and-error debugging -or test scaffolding needed to understand complex logic flow. Provide actionable insights for understanding -code behavior, impact analysis, and debugging assistance. -""" diff --git a/systemprompts/tracer_prompt.py b/systemprompts/tracer_prompt.py new file mode 100644 index 0000000..4b03b76 --- /dev/null +++ b/systemprompts/tracer_prompt.py @@ -0,0 +1,169 @@ +""" +Tracer tool system prompt +""" + +TRACER_PROMPT = """ +ROLE +You are a principal software analysis engine. You examine source code across a multi-language repository and statically analyze the behavior of a method, function, or class. +Your task is to return either a full **execution flow trace** (`precision`) or a **bidirectional dependency map** (`dependencies`) based solely on code β€” never speculation. +You must respond in strict JSON that Claude (the receiving model) can use to visualize, query, and validate. + +CRITICAL: You MUST respond ONLY in valid JSON format. NO explanations, introductions, or text outside JSON structure. +Claude cannot parse your response if you include any non-JSON content. + +CRITICAL LINE NUMBER INSTRUCTIONS +Code is presented with line number markers "LINEβ”‚ code". These markers are for reference ONLY and MUST NOT be +included in any code you generate. Always reference specific line numbers for Claude to locate exact positions. +Include context_start_text and context_end_text as backup references. Never include "LINEβ”‚" markers in generated code +snippets. + +TRACE MODES + +1. **precision** – Follow the actual code path from a given method across functions, classes, and modules. + Resolve method calls, branching, type dispatch, and potential side effects. If parameters are provided, use them to resolve branching; if not, flag ambiguous paths. + +2. **dependencies** – Analyze all dependencies flowing into and out from the method/class, including method calls, state usage, class-level imports, and inheritance. + Show both **incoming** (what uses this) and **outgoing** (what it uses) connections. + +INPUT FORMAT +You will receive: +- Method/class name +- Code with File Names +- Optional parameters (used only in precision mode) + +IF MORE INFORMATION IS NEEDED OR CONTEXT IS MISSING +If you cannot analyze accurately, respond ONLY with this JSON (and ABSOLUTELY nothing else - no text before or after). +Do NOT ask for the same file you've been provided unless its content is missing or incomplete: +{"status": "clarification_required", "question": "", "files_needed": ["[file name here]", "[or some folder/]"]} + +OUTPUT FORMAT +Respond ONLY with the following JSON format depending on the trace mode. + +MODE: precision +EXPECTED OUTPUT: +{ + "status": "trace_complete", + "trace_type": "precision", + "entry_point": { + "file": "/absolute/path/to/file.ext", + "class_or_struct": "ClassOrModuleName", + "method": "methodName", + "signature": "func methodName(param1: Type1, param2: Type2) -> ReturnType", + "parameters": { + "param1": "value_or_type", + "param2": "value_or_type" + } + }, + "call_path": [ + { + "from": { + "file": "/file/path", + "class": "ClassName", + "method": "methodName", + "line": 42 + }, + "to": { + "file": "/file/path", + "class": "ClassName", + "method": "calledMethod", + "line": 123 + }, + "reason": "direct call / protocol dispatch / conditional branch", + "condition": "if param.isEnabled", // null if unconditional + "ambiguous": false + } + ], + "branching_points": [ + { + "file": "/file/path", + "method": "methodName", + "line": 77, + "condition": "if user.role == .admin", + "branches": ["audit()", "restrict()"], + "ambiguous": true + } + ], + "side_effects": [ + { + "type": "database|network|filesystem|state|log|ui|external", + "description": "calls remote endpoint / modifies user record", + "file": "/file/path", + "method": "methodName", + "line": 88 + } + ], + "unresolved": [ + { + "reason": "param.userRole not provided", + "affected_file": "/file/path", + "line": 77 + } + ] +} + +MODE: dependencies +EXPECTED OUTPUT: +{ + "status": "trace_complete", + "trace_type": "dependencies", + "target": { + "file": "/absolute/path/to/file.ext", + "class_or_struct": "ClassOrModuleName", + "method": "methodName", + "signature": "func methodName(param1: Type1, param2: Type2) -> ReturnType" + }, + "incoming_dependencies": [ + { + "from_file": "/file/path", + "from_class": "CallingClass", + "from_method": "callerMethod", + "line": 101, + "type": "direct_call|protocol_impl|event_handler|override|reflection" + } + ], + "outgoing_dependencies": [ + { + "to_file": "/file/path", + "to_class": "DependencyClass", + "to_method": "calledMethod", + "line": 57, + "type": "method_call|instantiates|uses_constant|reads_property|writes_property|network|db|log" + } + ], + "type_dependencies": [ + { + "dependency_type": "extends|implements|conforms_to|uses_generic|imports", + "source_file": "/file/path", + "source_entity": "ClassOrStruct", + "target": "TargetProtocolOrClass" + } + ], + "state_access": [ + { + "file": "/file/path", + "method": "methodName", + "access_type": "reads|writes|mutates|injects", + "state_entity": "user.balance" + } + ] +} + +RULES +- All data must come from the actual codebase. No invented paths or method guesses. +- If parameters are missing in precision mode, include all possible branches and mark them "ambiguous": true. +- Use full file paths, class names, method names, and line numbers exactly as they appear. +- Use the "reason" field to explain why the call or dependency exists. +- In dependencies mode, the incoming_dependencies list may be empty if nothing in the repo currently calls the target. + +GOAL + +Enable Claude and the user to clearly visualize how a method: +- Flows across the system (in precision mode) +- Connects with other classes and modules (in dependencies mode) + +FINAL REMINDER: CRITICAL OUTPUT FORMAT ENFORCEMENT +Your response MUST start with "{" and end with "}". NO other text is allowed. +If you include ANY text outside the JSON structure, Claude will be unable to parse your response and the tool will fail. +DO NOT provide explanations, introductions, conclusions, or reasoning outside the JSON. +ALL information must be contained within the JSON structure itself. +""" diff --git a/tests/test_server.py b/tests/test_server.py index 2af485d..3adb95c 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -28,10 +28,11 @@ class TestServerTools: assert "precommit" in tool_names assert "testgen" in tool_names assert "refactor" in tool_names + assert "tracer" in tool_names assert "version" in tool_names - # Should have exactly 9 tools (including refactor) - assert len(tools) == 9 + # Should have exactly 10 tools (including refactor and tracer) + assert len(tools) == 10 # Check descriptions are verbose for tool in tools: diff --git a/tests/test_tools.py b/tests/test_tools.py index 77a4776..c3f9cbd 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -321,3 +321,83 @@ class TestAbsolutePathValidation: response = json.loads(result[0].text) assert response["status"] == "success" assert "Analysis complete" in response["content"] + + +class TestSpecialStatusModels: + """Test SPECIAL_STATUS_MODELS registry and structured response handling""" + + def test_trace_complete_status_in_registry(self): + """Test that trace_complete status is properly registered""" + from tools.models import SPECIAL_STATUS_MODELS, TraceComplete + + assert "trace_complete" in SPECIAL_STATUS_MODELS + assert SPECIAL_STATUS_MODELS["trace_complete"] == TraceComplete + + def test_trace_complete_model_validation(self): + """Test TraceComplete model validation""" + from tools.models import TraceComplete + + # Test precision mode + precision_data = { + "status": "trace_complete", + "trace_type": "precision", + "entry_point": { + "file": "/path/to/file.py", + "class_or_struct": "MyClass", + "method": "myMethod", + "signature": "def myMethod(self, param1: str) -> bool", + "parameters": {"param1": "test"}, + }, + "call_path": [ + { + "from": {"file": "/path/to/file.py", "class": "MyClass", "method": "myMethod", "line": 10}, + "to": {"file": "/path/to/other.py", "class": "OtherClass", "method": "otherMethod", "line": 20}, + "reason": "direct call", + "condition": None, + "ambiguous": False, + } + ], + } + + model = TraceComplete(**precision_data) + assert model.status == "trace_complete" + assert model.trace_type == "precision" + assert model.entry_point.file == "/path/to/file.py" + assert len(model.call_path) == 1 + + # Test dependencies mode + dependencies_data = { + "status": "trace_complete", + "trace_type": "dependencies", + "target": { + "file": "/path/to/file.py", + "class_or_struct": "MyClass", + "method": "myMethod", + "signature": "def myMethod(self, param1: str) -> bool", + }, + "incoming_dependencies": [ + { + "from_file": "/path/to/caller.py", + "from_class": "CallerClass", + "from_method": "callerMethod", + "line": 15, + "type": "direct_call", + } + ], + "outgoing_dependencies": [ + { + "to_file": "/path/to/dependency.py", + "to_class": "DepClass", + "to_method": "depMethod", + "line": 25, + "type": "method_call", + } + ], + } + + model = TraceComplete(**dependencies_data) + assert model.status == "trace_complete" + assert model.trace_type == "dependencies" + assert model.target.file == "/path/to/file.py" + assert len(model.incoming_dependencies) == 1 + assert len(model.outgoing_dependencies) == 1 diff --git a/tests/test_tracepath.py b/tests/test_tracepath.py deleted file mode 100644 index 1598168..0000000 --- a/tests/test_tracepath.py +++ /dev/null @@ -1,410 +0,0 @@ -""" -Tests for the tracepath tool functionality -""" - -from unittest.mock import Mock, patch - -import pytest - -from tools.models import ToolModelCategory -from tools.tracepath import TracePathRequest, TracePathTool - - -class TestTracePathTool: - """Test suite for the TracePath tool""" - - @pytest.fixture - def tracepath_tool(self): - """Create a tracepath tool instance for testing""" - return TracePathTool() - - @pytest.fixture - def mock_model_response(self): - """Create a mock model response for call path analysis""" - - def _create_response(content=None): - if content is None: - content = """## Call Path Summary - -1. 🟒 `BookingManager::finalizeInvoice()` at booking.py:45 β†’ calls `PaymentProcessor.process()` -2. 🟒 `PaymentProcessor::process()` at payment.py:123 β†’ calls `validation.validate_payment()` -3. 🟑 `validation.validate_payment()` at validation.py:67 β†’ conditionally calls `Logger.log()` - -## Value-Driven Flow Analysis - -**Scenario 1**: `invoice_id=123, payment_method="credit_card"` -- Path: BookingManager β†’ PaymentProcessor β†’ CreditCardValidator β†’ StripeGateway -- Key decision at payment.py:156: routes to Stripe integration - -## Side Effects & External Dependencies - -### Database Interactions -- **Transaction.save()** at models.py:234 β†’ inserts payment record - -### Network Calls -- **StripeGateway.charge()** β†’ HTTPS POST to Stripe API - -## Code Anchors - -- Entry point: `BookingManager::finalizeInvoice` at booking.py:45 -- Critical branch: Payment method selection at payment.py:156 -""" - - return Mock( - content=content, - usage={"input_tokens": 150, "output_tokens": 300, "total_tokens": 450}, - model_name="test-model", - metadata={"finish_reason": "STOP"}, - ) - - return _create_response - - def test_get_name(self, tracepath_tool): - """Test that the tool returns the correct name""" - assert tracepath_tool.get_name() == "tracepath" - - def test_get_description(self, tracepath_tool): - """Test that the tool returns a comprehensive description""" - description = tracepath_tool.get_description() - assert "STATIC CALL PATH ANALYSIS" in description - assert "control flow" in description - assert "confidence levels" in description - assert "polymorphism" in description - assert "side effects" in description - - def test_get_input_schema(self, tracepath_tool): - """Test that the input schema includes all required fields""" - schema = tracepath_tool.get_input_schema() - - assert schema["type"] == "object" - assert "entry_point" in schema["properties"] - assert "files" in schema["properties"] - - # Check required fields - required_fields = schema["required"] - assert "entry_point" in required_fields - assert "files" in required_fields - - # Check optional parameters - assert "parameters" in schema["properties"] - assert "analysis_depth" in schema["properties"] - assert "language" in schema["properties"] - assert "confidence_threshold" in schema["properties"] - - # Check enum values for analysis_depth - depth_enum = schema["properties"]["analysis_depth"]["enum"] - expected_depths = ["shallow", "medium", "deep"] - assert all(depth in depth_enum for depth in expected_depths) - - # Check enum values for language - language_enum = schema["properties"]["language"]["enum"] - expected_languages = ["python", "javascript", "typescript", "csharp", "java"] - assert all(lang in language_enum for lang in expected_languages) - - def test_get_model_category(self, tracepath_tool): - """Test that the tool uses extended reasoning category""" - category = tracepath_tool.get_model_category() - assert category == ToolModelCategory.EXTENDED_REASONING - - def test_request_model_validation(self): - """Test request model validation""" - # Valid request - request = TracePathRequest( - entry_point="BookingManager::finalizeInvoice", - files=["/test/booking.py", "/test/payment.py"], - parameters={"invoice_id": 123, "payment_method": "credit_card"}, - analysis_depth="medium", - ) - assert request.entry_point == "BookingManager::finalizeInvoice" - assert len(request.files) == 2 - assert request.analysis_depth == "medium" - assert request.confidence_threshold == 0.7 # default value - - # Test validation with invalid confidence threshold - with pytest.raises(ValueError): - TracePathRequest( - entry_point="test::method", files=["/test/file.py"], confidence_threshold=1.5 # Invalid: > 1.0 - ) - - # Invalid request (missing required fields) - with pytest.raises(ValueError): - TracePathRequest(files=["/test/file.py"]) # Missing entry_point - - def test_language_detection_python(self, tracepath_tool): - """Test language detection for Python files""" - files = ["/test/booking.py", "/test/payment.py", "/test/utils.py"] - language = tracepath_tool.detect_primary_language(files) - assert language == "python" - - def test_language_detection_javascript(self, tracepath_tool): - """Test language detection for JavaScript files""" - files = ["/test/app.js", "/test/component.jsx", "/test/utils.js"] - language = tracepath_tool.detect_primary_language(files) - assert language == "javascript" - - def test_language_detection_typescript(self, tracepath_tool): - """Test language detection for TypeScript files""" - files = ["/test/app.ts", "/test/component.tsx", "/test/utils.ts"] - language = tracepath_tool.detect_primary_language(files) - assert language == "typescript" - - def test_language_detection_csharp(self, tracepath_tool): - """Test language detection for C# files""" - files = ["/test/BookingService.cs", "/test/PaymentProcessor.cs"] - language = tracepath_tool.detect_primary_language(files) - assert language == "csharp" - - def test_language_detection_java(self, tracepath_tool): - """Test language detection for Java files""" - files = ["/test/BookingManager.java", "/test/PaymentService.java"] - language = tracepath_tool.detect_primary_language(files) - assert language == "java" - - def test_language_detection_mixed(self, tracepath_tool): - """Test language detection for mixed language files""" - files = ["/test/app.py", "/test/service.js", "/test/model.java"] - language = tracepath_tool.detect_primary_language(files) - assert language == "mixed" - - def test_language_detection_unknown(self, tracepath_tool): - """Test language detection for unknown extensions""" - files = ["/test/config.xml", "/test/readme.txt"] - language = tracepath_tool.detect_primary_language(files) - assert language == "unknown" - - def test_parse_entry_point_class_method_double_colon(self, tracepath_tool): - """Test parsing entry point with double colon syntax""" - result = tracepath_tool.parse_entry_point("BookingManager::finalizeInvoice", "python") - - assert result["raw"] == "BookingManager::finalizeInvoice" - assert result["class_or_module"] == "BookingManager" - assert result["method_or_function"] == "finalizeInvoice" - assert result["type"] == "method" - - def test_parse_entry_point_module_function_dot(self, tracepath_tool): - """Test parsing entry point with dot syntax""" - result = tracepath_tool.parse_entry_point("utils.validate_input", "python") - - assert result["raw"] == "utils.validate_input" - assert result["class_or_module"] == "utils" - assert result["method_or_function"] == "validate_input" - assert result["type"] == "function" - - def test_parse_entry_point_nested_module(self, tracepath_tool): - """Test parsing entry point with nested module syntax""" - result = tracepath_tool.parse_entry_point("payment.services.process_payment", "python") - - assert result["raw"] == "payment.services.process_payment" - assert result["class_or_module"] == "payment.services" - assert result["method_or_function"] == "process_payment" - assert result["type"] == "function" - - def test_parse_entry_point_function_only(self, tracepath_tool): - """Test parsing entry point with function name only""" - result = tracepath_tool.parse_entry_point("validate_payment", "python") - - assert result["raw"] == "validate_payment" - assert result["class_or_module"] == "" - assert result["method_or_function"] == "validate_payment" - assert result["type"] == "function" - - def test_parse_entry_point_camelcase_class(self, tracepath_tool): - """Test parsing entry point with CamelCase class (method detection)""" - result = tracepath_tool.parse_entry_point("PaymentProcessor.process", "java") - - assert result["raw"] == "PaymentProcessor.process" - assert result["class_or_module"] == "PaymentProcessor" - assert result["method_or_function"] == "process" - assert result["type"] == "method" # CamelCase suggests class method - - @pytest.mark.asyncio - async def test_generate_structural_summary_phase1(self, tracepath_tool): - """Test structural summary generation (Phase 1 returns empty)""" - files = ["/test/booking.py", "/test/payment.py"] - summary = await tracepath_tool._generate_structural_summary(files, "python") - - # Phase 1 implementation should return empty string - assert summary == "" - - @pytest.mark.asyncio - async def test_prepare_prompt_basic(self, tracepath_tool): - """Test basic prompt preparation""" - request = TracePathRequest( - entry_point="BookingManager::finalizeInvoice", - files=["/test/booking.py"], - parameters={"invoice_id": 123}, - analysis_depth="medium", - ) - - # Mock file content preparation - with patch.object(tracepath_tool, "_prepare_file_content_for_prompt") as mock_prep: - mock_prep.return_value = "def finalizeInvoice(self, invoice_id):\n pass" - with patch.object(tracepath_tool, "_validate_token_limit"): - prompt = await tracepath_tool.prepare_prompt(request) - - assert "ANALYSIS REQUEST" in prompt - assert "BookingManager::finalizeInvoice" in prompt - assert "medium" in prompt - assert "CODE TO ANALYZE" in prompt - - @pytest.mark.asyncio - async def test_prepare_prompt_with_parameters(self, tracepath_tool): - """Test prompt preparation with parameter values""" - request = TracePathRequest( - entry_point="payment.process_payment", - files=["/test/payment.py"], - parameters={"amount": 100.50, "method": "credit_card"}, - analysis_depth="deep", - include_db=True, - include_network=True, - include_fs=False, - ) - - with patch.object(tracepath_tool, "_prepare_file_content_for_prompt") as mock_prep: - mock_prep.return_value = "def process_payment(amount, method):\n pass" - with patch.object(tracepath_tool, "_validate_token_limit"): - prompt = await tracepath_tool.prepare_prompt(request) - - assert "Parameter Values: {'amount': 100.5, 'method': 'credit_card'}" in prompt - assert "Analysis Depth: deep" in prompt - assert "Include Side Effects: database, network" in prompt - - @pytest.mark.asyncio - async def test_prepare_prompt_with_context(self, tracepath_tool): - """Test prompt preparation with additional context""" - request = TracePathRequest( - entry_point="UserService::authenticate", - files=["/test/auth.py"], - context="Focus on security implications and potential vulnerabilities", - focus_areas=["security", "error_handling"], - ) - - with patch.object(tracepath_tool, "_prepare_file_content_for_prompt") as mock_prep: - mock_prep.return_value = "def authenticate(self, username, password):\n pass" - with patch.object(tracepath_tool, "_validate_token_limit"): - prompt = await tracepath_tool.prepare_prompt(request) - - assert "Additional Context: Focus on security implications" in prompt - assert "Focus Areas: security, error_handling" in prompt - - def test_format_response_markdown(self, tracepath_tool): - """Test response formatting for markdown output""" - request = TracePathRequest( - entry_point="BookingManager::finalizeInvoice", files=["/test/booking.py"], export_format="markdown" - ) - - response = "## Call Path Summary\n1. BookingManager::finalizeInvoice..." - model_info = {"model_response": Mock(friendly_name="Gemini Pro")} - - formatted = tracepath_tool.format_response(response, request, model_info) - - assert response in formatted - assert "Analysis Complete" in formatted - assert "Gemini Pro" in formatted - assert "confidence assessments" in formatted - - def test_format_response_json(self, tracepath_tool): - """Test response formatting for JSON output""" - request = TracePathRequest(entry_point="payment.process", files=["/test/payment.py"], export_format="json") - - response = '{"call_path": [...], "confidence": "high"}' - - formatted = tracepath_tool.format_response(response, request) - - assert response in formatted - assert "structured JSON analysis" in formatted - assert "confidence levels" in formatted - - def test_format_response_plantuml(self, tracepath_tool): - """Test response formatting for PlantUML output""" - request = TracePathRequest(entry_point="service.execute", files=["/test/service.py"], export_format="plantuml") - - response = "@startuml\nBooking -> Payment\n@enduml" - - formatted = tracepath_tool.format_response(response, request) - - assert response in formatted - assert "PlantUML diagram" in formatted - assert "Render the PlantUML" in formatted - - def test_get_default_temperature(self, tracepath_tool): - """Test that the tool uses analytical temperature""" - from config import TEMPERATURE_ANALYTICAL - - assert tracepath_tool.get_default_temperature() == TEMPERATURE_ANALYTICAL - - def test_wants_line_numbers_by_default(self, tracepath_tool): - """Test that line numbers are enabled by default""" - # The base class should enable line numbers by default for precise references - # We test that this isn't overridden to disable them - assert hasattr(tracepath_tool, "wants_line_numbers_by_default") - - def test_side_effects_configuration(self): - """Test side effects boolean configuration""" - request = TracePathRequest( - entry_point="test.function", - files=["/test/file.py"], - include_db=True, - include_network=False, - include_fs=True, - ) - - assert request.include_db is True - assert request.include_network is False - assert request.include_fs is True - - def test_confidence_threshold_bounds(self): - """Test confidence threshold validation bounds""" - # Valid thresholds - request1 = TracePathRequest(entry_point="test.function", files=["/test/file.py"], confidence_threshold=0.0) - assert request1.confidence_threshold == 0.0 - - request2 = TracePathRequest(entry_point="test.function", files=["/test/file.py"], confidence_threshold=1.0) - assert request2.confidence_threshold == 1.0 - - # Invalid thresholds should raise ValidationError - with pytest.raises(ValueError): - TracePathRequest(entry_point="test.function", files=["/test/file.py"], confidence_threshold=-0.1) - - with pytest.raises(ValueError): - TracePathRequest(entry_point="test.function", files=["/test/file.py"], confidence_threshold=1.1) - - def test_signature_parameter(self): - """Test signature parameter for overload resolution""" - request = TracePathRequest( - entry_point="Calculator.add", - files=["/test/calc.cs"], - signature="public int Add(int a, int b)", - language="csharp", - ) - - assert request.signature == "public int Add(int a, int b)" - assert request.language == "csharp" - - @pytest.mark.asyncio - async def test_prepare_prompt_with_language_override(self, tracepath_tool): - """Test prompt preparation with language override""" - request = TracePathRequest( - entry_point="Calculator::Add", - files=["/test/calc.py"], # Python extension - language="csharp", # Override to C# - ) - - with patch.object(tracepath_tool, "_prepare_file_content_for_prompt") as mock_prep: - mock_prep.return_value = "public class Calculator { }" - with patch.object(tracepath_tool, "_validate_token_limit"): - prompt = await tracepath_tool.prepare_prompt(request) - - assert "Language: csharp" in prompt # Should use override, not detected - - def test_export_format_options(self): - """Test all export format options""" - formats = ["markdown", "json", "plantuml"] - - for fmt in formats: - request = TracePathRequest(entry_point="test.function", files=["/test/file.py"], export_format=fmt) - assert request.export_format == fmt - - # Invalid format should raise ValidationError - with pytest.raises(ValueError): - TracePathRequest(entry_point="test.function", files=["/test/file.py"], export_format="invalid_format") diff --git a/tests/test_tracer.py b/tests/test_tracer.py new file mode 100644 index 0000000..26b42c5 --- /dev/null +++ b/tests/test_tracer.py @@ -0,0 +1,420 @@ +""" +Tests for the tracer tool functionality +""" + +from unittest.mock import Mock, patch + +import pytest + +from tools.models import ToolModelCategory +from tools.tracer import TracerRequest, TracerTool + + +class TestTracerTool: + """Test suite for the Tracer tool""" + + @pytest.fixture + def tracer_tool(self): + """Create a tracer tool instance for testing""" + return TracerTool() + + @pytest.fixture + def mock_model_response(self): + """Create a mock model response for call path analysis""" + + def _create_response(content=None): + if content is None: + content = """## Call Path Summary + +1. 🟒 `BookingManager::finalizeInvoice()` at booking.py:45 β†’ calls `PaymentProcessor.process()` +2. 🟒 `PaymentProcessor::process()` at payment.py:123 β†’ calls `validation.validate_payment()` +3. 🟑 `validation.validate_payment()` at validation.py:67 β†’ conditionally calls `Logger.log()` + +## Value-Driven Flow Analysis + +**Scenario 1**: `invoice_id=123, payment_method="credit_card"` +- Path: BookingManager β†’ PaymentProcessor β†’ CreditCardValidator β†’ StripeGateway +- Key decision at payment.py:156: routes to Stripe integration + +## Side Effects & External Dependencies + +### Database Interactions +- **Transaction.save()** at models.py:234 β†’ inserts payment record + +### Network Calls +- **StripeGateway.charge()** β†’ HTTPS POST to Stripe API + +## Code Anchors + +- Entry point: `BookingManager::finalizeInvoice` at booking.py:45 +- Critical branch: Payment method selection at payment.py:156 +""" + + return Mock( + content=content, + usage={"input_tokens": 150, "output_tokens": 300, "total_tokens": 450}, + model_name="test-model", + metadata={"finish_reason": "STOP"}, + ) + + return _create_response + + def test_get_name(self, tracer_tool): + """Test that the tool returns the correct name""" + assert tracer_tool.get_name() == "tracer" + + def test_get_description(self, tracer_tool): + """Test that the tool returns a comprehensive description""" + description = tracer_tool.get_description() + assert "STATIC CODE ANALYSIS" in description + assert "execution flow" in description + assert "dependency mappings" in description + assert "precision" in description + assert "dependencies" in description + + def test_get_input_schema(self, tracer_tool): + """Test that the input schema includes all required fields""" + schema = tracer_tool.get_input_schema() + + assert schema["type"] == "object" + assert "prompt" in schema["properties"] + assert "files" in schema["properties"] + assert "trace_mode" in schema["properties"] + + # Check required fields + required_fields = schema["required"] + assert "prompt" in required_fields + assert "files" in required_fields + assert "trace_mode" in required_fields + + # Check enum values for trace_mode + trace_mode_enum = schema["properties"]["trace_mode"]["enum"] + assert "precision" in trace_mode_enum + assert "dependencies" in trace_mode_enum + + def test_get_model_category(self, tracer_tool): + """Test that the tool uses extended reasoning category""" + category = tracer_tool.get_model_category() + assert category == ToolModelCategory.EXTENDED_REASONING + + def test_request_model_validation(self): + """Test request model validation""" + # Valid request + request = TracerRequest( + prompt="Trace BookingManager::finalizeInvoice method with invoice_id=123", + files=["/test/booking.py", "/test/payment.py"], + trace_mode="precision", + ) + assert request.prompt == "Trace BookingManager::finalizeInvoice method with invoice_id=123" + assert len(request.files) == 2 + assert request.trace_mode == "precision" + + # Invalid request (missing required fields) + with pytest.raises(ValueError): + TracerRequest(files=["/test/file.py"]) # Missing prompt and trace_mode + + # Invalid trace_mode value + with pytest.raises(ValueError): + TracerRequest(prompt="Test", files=["/test/file.py"], trace_mode="invalid_type") + + def test_language_detection_python(self, tracer_tool): + """Test language detection for Python files""" + files = ["/test/booking.py", "/test/payment.py", "/test/utils.py"] + language = tracer_tool.detect_primary_language(files) + assert language == "python" + + def test_language_detection_javascript(self, tracer_tool): + """Test language detection for JavaScript files""" + files = ["/test/app.js", "/test/component.jsx", "/test/utils.js"] + language = tracer_tool.detect_primary_language(files) + assert language == "javascript" + + def test_language_detection_typescript(self, tracer_tool): + """Test language detection for TypeScript files""" + files = ["/test/app.ts", "/test/component.tsx", "/test/utils.ts"] + language = tracer_tool.detect_primary_language(files) + assert language == "typescript" + + def test_language_detection_csharp(self, tracer_tool): + """Test language detection for C# files""" + files = ["/test/BookingService.cs", "/test/PaymentProcessor.cs"] + language = tracer_tool.detect_primary_language(files) + assert language == "csharp" + + def test_language_detection_java(self, tracer_tool): + """Test language detection for Java files""" + files = ["/test/BookingManager.java", "/test/PaymentService.java"] + language = tracer_tool.detect_primary_language(files) + assert language == "java" + + def test_language_detection_mixed(self, tracer_tool): + """Test language detection for mixed language files""" + files = ["/test/app.py", "/test/service.js", "/test/model.java"] + language = tracer_tool.detect_primary_language(files) + assert language == "mixed" + + def test_language_detection_unknown(self, tracer_tool): + """Test language detection for unknown extensions""" + files = ["/test/config.xml", "/test/readme.txt"] + language = tracer_tool.detect_primary_language(files) + assert language == "unknown" + + # Removed parse_entry_point tests as method no longer exists in simplified interface + + @pytest.mark.asyncio + async def test_prepare_prompt_basic(self, tracer_tool): + """Test basic prompt preparation""" + request = TracerRequest( + prompt="Trace BookingManager::finalizeInvoice method with invoice_id=123", + files=["/test/booking.py"], + trace_mode="precision", + ) + + # Mock file content preparation + with patch.object(tracer_tool, "_prepare_file_content_for_prompt") as mock_prep: + mock_prep.return_value = "def finalizeInvoice(self, invoice_id):\n pass" + with patch.object(tracer_tool, "check_prompt_size") as mock_check: + mock_check.return_value = None + prompt = await tracer_tool.prepare_prompt(request) + + assert "ANALYSIS REQUEST" in prompt + assert "Trace BookingManager::finalizeInvoice method" in prompt + assert "precision" in prompt + assert "CODE TO ANALYZE" in prompt + + @pytest.mark.asyncio + async def test_prepare_prompt_with_dependencies(self, tracer_tool): + """Test prompt preparation with dependencies type""" + request = TracerRequest( + prompt="Analyze dependencies for payment.process_payment function with amount=100.50", + files=["/test/payment.py"], + trace_mode="dependencies", + ) + + with patch.object(tracer_tool, "_prepare_file_content_for_prompt") as mock_prep: + mock_prep.return_value = "def process_payment(amount, method):\n pass" + with patch.object(tracer_tool, "check_prompt_size") as mock_check: + mock_check.return_value = None + prompt = await tracer_tool.prepare_prompt(request) + + assert "Analyze dependencies for payment.process_payment" in prompt + assert "Trace Mode: dependencies" in prompt + + @pytest.mark.asyncio + async def test_prepare_prompt_with_security_context(self, tracer_tool): + """Test prompt preparation with security context""" + request = TracerRequest( + prompt="Trace UserService::authenticate method focusing on security implications and potential vulnerabilities", + files=["/test/auth.py"], + trace_mode="precision", + ) + + with patch.object(tracer_tool, "_prepare_file_content_for_prompt") as mock_prep: + mock_prep.return_value = "def authenticate(self, username, password):\n pass" + with patch.object(tracer_tool, "check_prompt_size") as mock_check: + mock_check.return_value = None + prompt = await tracer_tool.prepare_prompt(request) + + assert "security implications and potential vulnerabilities" in prompt + assert "Trace Mode: precision" in prompt + + def test_format_response_precision(self, tracer_tool): + """Test response formatting for precision trace""" + request = TracerRequest( + prompt="Trace BookingManager::finalizeInvoice method", files=["/test/booking.py"], trace_mode="precision" + ) + + response = '{"status": "trace_complete", "trace_type": "precision"}' + model_info = {"model_response": Mock(friendly_name="Gemini Pro")} + + formatted = tracer_tool.format_response(response, request, model_info) + + assert response in formatted + assert "Analysis Complete" in formatted + assert "Gemini Pro" in formatted + assert "precision analysis" in formatted + assert "CALL FLOW DIAGRAM" in formatted + assert "BRANCHING & SIDE EFFECT TABLE" in formatted + + def test_format_response_dependencies(self, tracer_tool): + """Test response formatting for dependencies trace""" + request = TracerRequest( + prompt="Analyze dependencies for payment.process function", + files=["/test/payment.py"], + trace_mode="dependencies", + ) + + response = '{"status": "trace_complete", "trace_type": "dependencies"}' + + formatted = tracer_tool.format_response(response, request) + + assert response in formatted + assert "dependencies analysis" in formatted + assert "DEPENDENCY FLOW GRAPH" in formatted + assert "DEPENDENCY TABLE" in formatted + + # Removed PlantUML test as export_format is no longer a parameter + + def test_get_default_temperature(self, tracer_tool): + """Test that the tool uses analytical temperature""" + from config import TEMPERATURE_ANALYTICAL + + assert tracer_tool.get_default_temperature() == TEMPERATURE_ANALYTICAL + + def test_wants_line_numbers_by_default(self, tracer_tool): + """Test that line numbers are enabled by default""" + # The base class should enable line numbers by default for precise references + # We test that this isn't overridden to disable them + assert hasattr(tracer_tool, "wants_line_numbers_by_default") + + def test_trace_mode_validation(self): + """Test trace mode validation""" + # Valid trace modes + request1 = TracerRequest(prompt="Test precision", files=["/test/file.py"], trace_mode="precision") + assert request1.trace_mode == "precision" + + request2 = TracerRequest(prompt="Test dependencies", files=["/test/file.py"], trace_mode="dependencies") + assert request2.trace_mode == "dependencies" + + # Invalid trace mode should raise ValidationError + with pytest.raises(ValueError): + TracerRequest(prompt="Test", files=["/test/file.py"], trace_mode="invalid_type") + + def test_get_rendering_instructions(self, tracer_tool): + """Test the main rendering instructions dispatcher method""" + # Test precision mode + precision_instructions = tracer_tool._get_rendering_instructions("precision") + assert "MANDATORY RENDERING INSTRUCTIONS FOR PRECISION TRACE" in precision_instructions + assert "CALL FLOW DIAGRAM" in precision_instructions + assert "BRANCHING & SIDE EFFECT TABLE" in precision_instructions + + # Test dependencies mode + dependencies_instructions = tracer_tool._get_rendering_instructions("dependencies") + assert "MANDATORY RENDERING INSTRUCTIONS FOR DEPENDENCIES TRACE" in dependencies_instructions + assert "DEPENDENCY FLOW GRAPH" in dependencies_instructions + assert "DEPENDENCY TABLE" in dependencies_instructions + + def test_get_precision_rendering_instructions(self, tracer_tool): + """Test precision mode rendering instructions""" + instructions = tracer_tool._get_precision_rendering_instructions() + + # Check for required sections + assert "MANDATORY RENDERING INSTRUCTIONS FOR PRECISION TRACE" in instructions + assert "1. CALL FLOW DIAGRAM (TOP-DOWN)" in instructions + assert "2. BRANCHING & SIDE EFFECT TABLE" in instructions + + # Check for specific formatting requirements + assert "[Class::Method] (file: /path, line: ##)" in instructions + assert "Chain each call using ↓ or β†’ for readability" in instructions + assert "If ambiguous, mark with `⚠️ ambiguous branch`" in instructions + assert "Side Effects:" in instructions + assert "[database] description (File.ext:##)" in instructions + + # Check for critical rules + assert "CRITICAL RULES:" in instructions + assert "Use exact filenames, class names, and line numbers from JSON" in instructions + assert "DO NOT invent function names or examples" in instructions + + def test_get_dependencies_rendering_instructions(self, tracer_tool): + """Test dependencies mode rendering instructions""" + instructions = tracer_tool._get_dependencies_rendering_instructions() + + # Check for required sections + assert "MANDATORY RENDERING INSTRUCTIONS FOR DEPENDENCIES TRACE" in instructions + assert "1. DEPENDENCY FLOW GRAPH" in instructions + assert "2. DEPENDENCY TABLE" in instructions + + # Check for specific formatting requirements + assert "Called by:" in instructions + assert "[CallerClass::callerMethod] ← /path/file.ext:##" in instructions + assert "Calls:" in instructions + assert "[Logger::logAction] β†’ /utils/log.ext:##" in instructions + assert "Type Dependencies:" in instructions + assert "State Access:" in instructions + + # Check for arrow rules + assert "`←` for incoming (who calls this)" in instructions + assert "`β†’` for outgoing (what this calls)" in instructions + + # Check for dependency table format + assert "| Type | From/To | Method | File | Line |" in instructions + assert "| direct_call | From: CallerClass | callerMethod |" in instructions + + # Check for critical rules + assert "CRITICAL RULES:" in instructions + assert "Use exact filenames, class names, and line numbers from JSON" in instructions + assert "Show directional dependencies with proper arrows" in instructions + + def test_format_response_uses_private_methods(self, tracer_tool): + """Test that format_response correctly uses the refactored private methods""" + # Test precision mode + precision_request = TracerRequest(prompt="Test precision", files=["/test/file.py"], trace_mode="precision") + precision_response = tracer_tool.format_response('{"test": "response"}', precision_request) + + # Should contain precision-specific instructions + assert "CALL FLOW DIAGRAM" in precision_response + assert "BRANCHING & SIDE EFFECT TABLE" in precision_response + assert "precision analysis" in precision_response + + # Test dependencies mode + dependencies_request = TracerRequest( + prompt="Test dependencies", files=["/test/file.py"], trace_mode="dependencies" + ) + dependencies_response = tracer_tool.format_response('{"test": "response"}', dependencies_request) + + # Should contain dependencies-specific instructions + assert "DEPENDENCY FLOW GRAPH" in dependencies_response + assert "DEPENDENCY TABLE" in dependencies_response + assert "dependencies analysis" in dependencies_response + + def test_rendering_instructions_consistency(self, tracer_tool): + """Test that private methods return consistent instructions""" + # Get instructions through both paths + precision_direct = tracer_tool._get_precision_rendering_instructions() + precision_via_dispatcher = tracer_tool._get_rendering_instructions("precision") + + dependencies_direct = tracer_tool._get_dependencies_rendering_instructions() + dependencies_via_dispatcher = tracer_tool._get_rendering_instructions("dependencies") + + # Should be identical + assert precision_direct == precision_via_dispatcher + assert dependencies_direct == dependencies_via_dispatcher + + def test_rendering_instructions_completeness(self, tracer_tool): + """Test that rendering instructions contain all required elements""" + precision_instructions = tracer_tool._get_precision_rendering_instructions() + dependencies_instructions = tracer_tool._get_dependencies_rendering_instructions() + + # Both should have mandatory sections + for instructions in [precision_instructions, dependencies_instructions]: + assert "MANDATORY RENDERING INSTRUCTIONS" in instructions + assert "You MUST render" in instructions + assert "exactly two views" in instructions + assert "CRITICAL RULES:" in instructions + assert "ALWAYS render both views unless data is missing" in instructions + assert "Use exact filenames, class names, and line numbers from JSON" in instructions + assert "DO NOT invent function names or examples" in instructions + + def test_rendering_instructions_mode_specific_content(self, tracer_tool): + """Test that each mode has unique content""" + precision_instructions = tracer_tool._get_precision_rendering_instructions() + dependencies_instructions = tracer_tool._get_dependencies_rendering_instructions() + + # Precision-specific content should not be in dependencies + assert "CALL FLOW DIAGRAM" in precision_instructions + assert "CALL FLOW DIAGRAM" not in dependencies_instructions + assert "BRANCHING & SIDE EFFECT TABLE" in precision_instructions + assert "BRANCHING & SIDE EFFECT TABLE" not in dependencies_instructions + + # Dependencies-specific content should not be in precision + assert "DEPENDENCY FLOW GRAPH" in dependencies_instructions + assert "DEPENDENCY FLOW GRAPH" not in precision_instructions + assert "DEPENDENCY TABLE" in dependencies_instructions + assert "DEPENDENCY TABLE" not in precision_instructions + + # Mode-specific symbols and patterns + assert "↓" in precision_instructions # Flow arrows + assert "←" in dependencies_instructions # Incoming arrow + assert "β†’" in dependencies_instructions # Outgoing arrow + assert "Side Effects:" in precision_instructions + assert "Called by:" in dependencies_instructions diff --git a/tools/__init__.py b/tools/__init__.py index 0e0ae17..9707de4 100644 --- a/tools/__init__.py +++ b/tools/__init__.py @@ -10,7 +10,7 @@ from .precommit import Precommit from .refactor import RefactorTool from .testgen import TestGenTool from .thinkdeep import ThinkDeepTool -from .tracepath import TracePathTool +from .tracer import TracerTool __all__ = [ "ThinkDeepTool", @@ -21,5 +21,5 @@ __all__ = [ "Precommit", "RefactorTool", "TestGenTool", - "TracePathTool", + "TracerTool", ] diff --git a/tools/models.py b/tools/models.py index e3667c6..0b261f3 100644 --- a/tools/models.py +++ b/tools/models.py @@ -41,6 +41,7 @@ class ToolOutput(BaseModel): "test_sample_needed", "more_tests_required", "refactor_analysis_complete", + "trace_complete", "resend_prompt", "continuation_available", ] = "success" @@ -150,6 +151,129 @@ class ResendPromptRequest(BaseModel): metadata: dict[str, Any] = Field(default_factory=dict) +class TraceEntryPoint(BaseModel): + """Entry point information for trace analysis""" + + file: str = Field(..., description="Absolute path to the file") + class_or_struct: str = Field(..., description="Class or module name") + method: str = Field(..., description="Method or function name") + signature: str = Field(..., description="Full method signature") + parameters: Optional[dict[str, Any]] = Field(default_factory=dict, description="Parameter values used in analysis") + + +class TraceTarget(BaseModel): + """Target information for dependency analysis""" + + file: str = Field(..., description="Absolute path to the file") + class_or_struct: str = Field(..., description="Class or module name") + method: str = Field(..., description="Method or function name") + signature: str = Field(..., description="Full method signature") + + +class CallPathStep(BaseModel): + """A single step in the call path trace""" + + from_info: dict[str, Any] = Field(..., description="Source location information", alias="from") + to: dict[str, Any] = Field(..., description="Target location information") + reason: str = Field(..., description="Reason for the call or dependency") + condition: Optional[str] = Field(None, description="Conditional logic if applicable") + ambiguous: bool = Field(False, description="Whether this call is ambiguous") + + +class BranchingPoint(BaseModel): + """A branching point in the execution flow""" + + file: str = Field(..., description="File containing the branching point") + method: str = Field(..., description="Method containing the branching point") + line: int = Field(..., description="Line number of the branching point") + condition: str = Field(..., description="Branching condition") + branches: list[str] = Field(..., description="Possible execution branches") + ambiguous: bool = Field(False, description="Whether the branching is ambiguous") + + +class SideEffect(BaseModel): + """A side effect detected in the trace""" + + type: str = Field(..., description="Type of side effect") + description: str = Field(..., description="Description of the side effect") + file: str = Field(..., description="File where the side effect occurs") + method: str = Field(..., description="Method where the side effect occurs") + line: int = Field(..., description="Line number of the side effect") + + +class UnresolvedDependency(BaseModel): + """An unresolved dependency in the trace""" + + reason: str = Field(..., description="Reason why the dependency is unresolved") + affected_file: str = Field(..., description="File affected by the unresolved dependency") + line: int = Field(..., description="Line number of the unresolved dependency") + + +class IncomingDependency(BaseModel): + """An incoming dependency (what calls this target)""" + + from_file: str = Field(..., description="Source file of the dependency") + from_class: str = Field(..., description="Source class of the dependency") + from_method: str = Field(..., description="Source method of the dependency") + line: int = Field(..., description="Line number of the dependency") + type: str = Field(..., description="Type of dependency") + + +class OutgoingDependency(BaseModel): + """An outgoing dependency (what this target calls)""" + + to_file: str = Field(..., description="Target file of the dependency") + to_class: str = Field(..., description="Target class of the dependency") + to_method: str = Field(..., description="Target method of the dependency") + line: int = Field(..., description="Line number of the dependency") + type: str = Field(..., description="Type of dependency") + + +class TypeDependency(BaseModel): + """A type-level dependency (inheritance, imports, etc.)""" + + dependency_type: str = Field(..., description="Type of dependency") + source_file: str = Field(..., description="Source file of the dependency") + source_entity: str = Field(..., description="Source entity (class, module)") + target: str = Field(..., description="Target entity") + + +class StateAccess(BaseModel): + """State access information""" + + file: str = Field(..., description="File where state is accessed") + method: str = Field(..., description="Method accessing the state") + access_type: str = Field(..., description="Type of access (reads, writes, etc.)") + state_entity: str = Field(..., description="State entity being accessed") + + +class TraceComplete(BaseModel): + """Complete trace analysis response""" + + status: Literal["trace_complete"] = "trace_complete" + trace_type: Literal["precision", "dependencies"] = Field(..., description="Type of trace performed") + + # Precision mode fields + entry_point: Optional[TraceEntryPoint] = Field(None, description="Entry point for precision trace") + call_path: Optional[list[CallPathStep]] = Field(default_factory=list, description="Call path for precision trace") + branching_points: Optional[list[BranchingPoint]] = Field(default_factory=list, description="Branching points") + side_effects: Optional[list[SideEffect]] = Field(default_factory=list, description="Side effects detected") + unresolved: Optional[list[UnresolvedDependency]] = Field( + default_factory=list, description="Unresolved dependencies" + ) + + # Dependencies mode fields + target: Optional[TraceTarget] = Field(None, description="Target for dependency analysis") + incoming_dependencies: Optional[list[IncomingDependency]] = Field( + default_factory=list, description="Incoming dependencies" + ) + outgoing_dependencies: Optional[list[OutgoingDependency]] = Field( + default_factory=list, description="Outgoing dependencies" + ) + type_dependencies: Optional[list[TypeDependency]] = Field(default_factory=list, description="Type dependencies") + state_access: Optional[list[StateAccess]] = Field(default_factory=list, description="State access information") + + # Registry mapping status strings to their corresponding Pydantic models SPECIAL_STATUS_MODELS = { "clarification_required": ClarificationRequest, @@ -158,6 +282,7 @@ SPECIAL_STATUS_MODELS = { "test_sample_needed": TestSampleNeeded, "more_tests_required": MoreTestsRequired, "refactor_analysis_complete": RefactorAnalysisComplete, + "trace_complete": TraceComplete, "resend_prompt": ResendPromptRequest, } diff --git a/tools/tracepath.py b/tools/tracepath.py deleted file mode 100644 index b7f2701..0000000 --- a/tools/tracepath.py +++ /dev/null @@ -1,602 +0,0 @@ -""" -TracePath tool - Static call path prediction and control flow analysis - -This tool analyzes code to predict and explain full call paths and control flow without executing code. -Given a method name, its owning class/module, and parameter combinations or runtime values, it predicts -the complete chain of method/function calls that would be triggered. - -Key Features: -- Static call path prediction with confidence levels -- Polymorphism and dynamic dispatch analysis -- Value-driven flow analysis based on parameter combinations -- Side effects identification (database, network, filesystem) -- Branching analysis for conditional logic -- Hybrid AI-first approach with optional AST preprocessing for enhanced accuracy -""" - -import logging -import os -import re -from typing import Any, Literal, Optional - -from pydantic import Field - -from config import TEMPERATURE_ANALYTICAL -from systemprompts import TRACEPATH_PROMPT - -from .base import BaseTool, ToolRequest - -logger = logging.getLogger(__name__) - - -class TracePathRequest(ToolRequest): - """ - Request model for the tracepath tool. - - This model defines all parameters for customizing the call path analysis process. - """ - - entry_point: str = Field( - ..., - description="Method/function to trace (e.g., 'BookingManager::finalizeInvoice', 'utils.validate_input')", - ) - files: list[str] = Field( - ..., - description="Code files or directories to analyze (must be absolute paths)", - ) - parameters: Optional[dict[str, Any]] = Field( - None, - description="Parameter values to analyze - format: {param_name: value_or_type}", - ) - context: Optional[str] = Field( - None, - description="Additional context about analysis goals or specific scenarios to focus on", - ) - analysis_depth: Literal["shallow", "medium", "deep"] = Field( - "medium", - description="Analysis depth: shallow (direct calls), medium (2-3 levels), deep (full trace)", - ) - language: Optional[str] = Field( - None, - description="Override auto-detection: python, javascript, typescript, csharp, java", - ) - signature: Optional[str] = Field( - None, - description="Fully-qualified signature for overload resolution in languages like C#/Java", - ) - confidence_threshold: Optional[float] = Field( - 0.7, - description="Filter speculative branches (0-1, default 0.7)", - ge=0.0, - le=1.0, - ) - include_db: bool = Field( - True, - description="Include database interactions in side effects analysis", - ) - include_network: bool = Field( - True, - description="Include network calls in side effects analysis", - ) - include_fs: bool = Field( - True, - description="Include filesystem operations in side effects analysis", - ) - export_format: Literal["markdown", "json", "plantuml"] = Field( - "markdown", - description="Output format for the analysis results", - ) - focus_areas: Optional[list[str]] = Field( - None, - description="Specific aspects to focus on (e.g., 'performance', 'security', 'error_handling')", - ) - - -class TracePathTool(BaseTool): - """ - TracePath tool implementation. - - This tool analyzes code to predict static call paths and control flow without execution. - Uses a hybrid AI-first approach with optional AST preprocessing for enhanced accuracy. - """ - - def get_name(self) -> str: - return "tracepath" - - def get_description(self) -> str: - return ( - "STATIC CALL PATH ANALYSIS - Predicts and explains full call paths and control flow without executing code. " - "Given a method/function name and parameter values, traces the complete execution path including " - "conditional branches, polymorphism resolution, and side effects. " - "Perfect for: understanding complex code flows, impact analysis, debugging assistance, architecture review. " - "Provides confidence levels for predictions and identifies uncertain calls due to dynamic behavior. " - "Choose thinking_mode based on code complexity: 'low' for simple functions, " - "'medium' for standard analysis (default), 'high' for complex systems, " - "'max' for legacy codebases requiring deep analysis. " - "Note: If you're not currently using a top-tier model such as Opus 4 or above, these tools can provide enhanced capabilities." - ) - - def get_input_schema(self) -> dict[str, Any]: - schema = { - "type": "object", - "properties": { - "entry_point": { - "type": "string", - "description": "Method/function to trace (e.g., 'BookingManager::finalizeInvoice', 'utils.validate_input')", - }, - "files": { - "type": "array", - "items": {"type": "string"}, - "description": "Code files or directories to analyze (must be absolute paths)", - }, - "model": self.get_model_field_schema(), - "parameters": { - "type": "object", - "description": "Parameter values to analyze - format: {param_name: value_or_type}", - }, - "context": { - "type": "string", - "description": "Additional context about analysis goals or specific scenarios to focus on", - }, - "analysis_depth": { - "type": "string", - "enum": ["shallow", "medium", "deep"], - "default": "medium", - "description": "Analysis depth: shallow (direct calls), medium (2-3 levels), deep (full trace)", - }, - "language": { - "type": "string", - "enum": ["python", "javascript", "typescript", "csharp", "java"], - "description": "Override auto-detection for programming language", - }, - "signature": { - "type": "string", - "description": "Fully-qualified signature for overload resolution", - }, - "confidence_threshold": { - "type": "number", - "minimum": 0.0, - "maximum": 1.0, - "default": 0.7, - "description": "Filter speculative branches (0-1)", - }, - "include_db": { - "type": "boolean", - "default": True, - "description": "Include database interactions in analysis", - }, - "include_network": { - "type": "boolean", - "default": True, - "description": "Include network calls in analysis", - }, - "include_fs": { - "type": "boolean", - "default": True, - "description": "Include filesystem operations in analysis", - }, - "export_format": { - "type": "string", - "enum": ["markdown", "json", "plantuml"], - "default": "markdown", - "description": "Output format for analysis results", - }, - "focus_areas": { - "type": "array", - "items": {"type": "string"}, - "description": "Specific aspects to focus on", - }, - "temperature": { - "type": "number", - "description": "Temperature (0-1, default 0.2 for analytical precision)", - "minimum": 0, - "maximum": 1, - }, - "thinking_mode": { - "type": "string", - "enum": ["minimal", "low", "medium", "high", "max"], - "description": "Thinking depth: minimal (0.5% of model max), low (8%), medium (33%), high (67%), max (100% of model max)", - }, - "use_websearch": { - "type": "boolean", - "description": "Enable web search for framework documentation and patterns", - "default": True, - }, - "continuation_id": { - "type": "string", - "description": "Thread continuation ID for multi-turn conversations across tools", - }, - }, - "required": ["entry_point", "files"] + (["model"] if self.is_effective_auto_mode() else []), - } - - return schema - - def get_system_prompt(self) -> str: - return TRACEPATH_PROMPT - - def get_default_temperature(self) -> float: - return TEMPERATURE_ANALYTICAL - - # Line numbers are enabled by default for precise code references - - def get_model_category(self): - """TracePath requires extended reasoning for complex flow analysis""" - from tools.models import ToolModelCategory - - return ToolModelCategory.EXTENDED_REASONING - - def get_request_model(self): - return TracePathRequest - - def detect_primary_language(self, file_paths: list[str]) -> str: - """ - Detect the primary programming language from file extensions. - - Args: - file_paths: List of file paths to analyze - - Returns: - str: Detected language or "mixed" if multiple languages found - """ - # Language detection based on file extensions - language_extensions = { - "python": {".py", ".pyx", ".pyi"}, - "javascript": {".js", ".jsx", ".mjs", ".cjs"}, - "typescript": {".ts", ".tsx", ".mts", ".cts"}, - "java": {".java"}, - "csharp": {".cs"}, - "cpp": {".cpp", ".cc", ".cxx", ".c", ".h", ".hpp"}, - "go": {".go"}, - "rust": {".rs"}, - "swift": {".swift"}, - "kotlin": {".kt", ".kts"}, - "ruby": {".rb"}, - "php": {".php"}, - "scala": {".scala"}, - } - - # Count files by language - language_counts = {} - for file_path in file_paths: - extension = os.path.splitext(file_path.lower())[1] - for lang, exts in language_extensions.items(): - if extension in exts: - language_counts[lang] = language_counts.get(lang, 0) + 1 - break - - if not language_counts: - return "unknown" - - # Return most common language, or "mixed" if multiple languages - max_count = max(language_counts.values()) - dominant_languages = [lang for lang, count in language_counts.items() if count == max_count] - - if len(dominant_languages) == 1: - return dominant_languages[0] - else: - return "mixed" - - def parse_entry_point(self, entry_point: str, language: str) -> dict[str, str]: - """ - Parse entry point string to extract class/module and method/function information. - - Args: - entry_point: Entry point string (e.g., "BookingManager::finalizeInvoice", "utils.validate_input") - language: Detected or specified programming language - - Returns: - dict: Parsed entry point information - """ - result = { - "raw": entry_point, - "class_or_module": "", - "method_or_function": "", - "type": "unknown", - } - - # Common patterns across languages - patterns = { - # Class::method (C++, PHP style) - "class_method_double_colon": r"^([A-Za-z_][A-Za-z0-9_]*?)::([A-Za-z_][A-Za-z0-9_]*?)$", - # Module.function or Class.method (Python, JavaScript, etc.) - "module_function_dot": r"^([A-Za-z_][A-Za-z0-9_]*?)\.([A-Za-z_][A-Za-z0-9_]*?)$", - # Nested module.submodule.function - "nested_module_dot": r"^([A-Za-z_][A-Za-z0-9_.]*?)\.([A-Za-z_][A-Za-z0-9_]*?)$", - # Just function name - "function_only": r"^([A-Za-z_][A-Za-z0-9_]*?)$", - } - - # Try patterns in order of specificity - for pattern_name, pattern in patterns.items(): - match = re.match(pattern, entry_point.strip()) - if match: - if pattern_name == "function_only": - result["method_or_function"] = match.group(1) - result["type"] = "function" - else: - result["class_or_module"] = match.group(1) - result["method_or_function"] = match.group(2) - - # Determine if it's a class method or module function based on naming conventions - if pattern_name == "class_method_double_colon": - result["type"] = "method" - elif result["class_or_module"][0].isupper(): - result["type"] = "method" # Likely class method (CamelCase) - else: - result["type"] = "function" # Likely module function (snake_case) - break - - logger.debug(f"[TRACEPATH] Parsed entry point '{entry_point}' as: {result}") - return result - - async def _generate_structural_summary(self, files: list[str], language: str) -> str: - """ - Generate structural summary of the code using AST parsing. - - Phase 1: Returns empty string (pure AI-driven approach) - Phase 2: Will contain language-specific AST parsing logic - - Args: - files: List of file paths to analyze - language: Detected programming language - - Returns: - str: Structural summary or empty string for Phase 1 - """ - # Phase 1 implementation: Pure AI-driven approach - # Phase 2 will add AST parsing for enhanced context - - if language == "python": - # Placeholder for Python AST parsing using built-in 'ast' module - # Will extract class definitions, method signatures, and direct calls - pass - elif language in ["javascript", "typescript"]: - # Placeholder for JavaScript/TypeScript parsing using acorn or TS compiler API - pass - elif language == "csharp": - # Placeholder for C# parsing using Microsoft Roslyn SDK - pass - elif language == "java": - # Placeholder for Java parsing (future implementation) - pass - - # For Phase 1, return empty to rely on pure LLM analysis - logger.debug(f"[TRACEPATH] Phase 1: No structural summary generated for {language}") - return "" - - async def prepare_prompt(self, request: TracePathRequest) -> str: - """ - Prepare the complete prompt for call path analysis. - - This method combines: - - System prompt with analysis instructions - - User context and entry point information - - File contents with line numbers - - Structural summary (Phase 2) - - Analysis parameters and constraints - - Args: - request: The validated tracepath request - - Returns: - str: Complete prompt for the model - - Raises: - ValueError: If the prompt exceeds token limits - """ - logger.info( - f"[TRACEPATH] Preparing prompt for entry point '{request.entry_point}' with {len(request.files)} files" - ) - logger.debug(f"[TRACEPATH] Analysis depth: {request.analysis_depth}, Export format: {request.export_format}") - - # Check for prompt.txt in files - prompt_content, updated_files = self.handle_prompt_file(request.files) - - # If prompt.txt was found, incorporate it into the context - if prompt_content: - logger.debug("[TRACEPATH] Found prompt.txt file, incorporating content") - if request.context: - request.context = prompt_content + "\n\n" + request.context - else: - request.context = prompt_content - - # Update request files list - if updated_files is not None: - logger.debug(f"[TRACEPATH] Updated files list after prompt.txt processing: {len(updated_files)} files") - request.files = updated_files - - # Check user input size at MCP transport boundary (before adding internal content) - if request.context: - size_check = self.check_prompt_size(request.context) - if size_check: - from tools.models import ToolOutput - - raise ValueError(f"MCP_SIZE_CHECK:{ToolOutput(**size_check).model_dump_json()}") - - # Detect or use specified language - if request.language: - primary_language = request.language - logger.debug(f"[TRACEPATH] Using specified language: {primary_language}") - else: - primary_language = self.detect_primary_language(request.files) - logger.debug(f"[TRACEPATH] Detected primary language: {primary_language}") - - # Parse entry point - entry_point_info = self.parse_entry_point(request.entry_point, primary_language) - logger.debug(f"[TRACEPATH] Entry point parsed as: {entry_point_info}") - - # Generate structural summary (Phase 1: returns empty, Phase 2: AST analysis) - continuation_id = getattr(request, "continuation_id", None) - structural_summary = await self._generate_structural_summary(request.files, primary_language) - - # Use centralized file processing logic for main code files (with line numbers enabled) - logger.debug(f"[TRACEPATH] Preparing {len(request.files)} code files for analysis") - code_content = self._prepare_file_content_for_prompt(request.files, continuation_id, "Code to analyze") - - if code_content: - from utils.token_utils import estimate_tokens - - code_tokens = estimate_tokens(code_content) - logger.info(f"[TRACEPATH] Code files embedded successfully: {code_tokens:,} tokens") - else: - logger.warning("[TRACEPATH] No code content after file processing") - - # Build the complete prompt - prompt_parts = [] - - # Add system prompt - prompt_parts.append(self.get_system_prompt()) - - # Add structural summary if available (Phase 2) - if structural_summary: - prompt_parts.append("\n=== STRUCTURAL SUMMARY ===") - prompt_parts.append(structural_summary) - prompt_parts.append("=== END STRUCTURAL SUMMARY ===") - - # Add user context and analysis parameters - prompt_parts.append("\n=== ANALYSIS REQUEST ===") - prompt_parts.append(f"Entry Point: {request.entry_point}") - if entry_point_info["type"] != "unknown": - prompt_parts.append( - f"Parsed as: {entry_point_info['type']} '{entry_point_info['method_or_function']}' in {entry_point_info['class_or_module'] or 'global scope'}" - ) - - prompt_parts.append(f"Language: {primary_language}") - prompt_parts.append(f"Analysis Depth: {request.analysis_depth}") - prompt_parts.append(f"Confidence Threshold: {request.confidence_threshold}") - - if request.signature: - prompt_parts.append(f"Method Signature: {request.signature}") - - if request.parameters: - prompt_parts.append(f"Parameter Values: {request.parameters}") - - # Side effects configuration - side_effects = [] - if request.include_db: - side_effects.append("database") - if request.include_network: - side_effects.append("network") - if request.include_fs: - side_effects.append("filesystem") - if side_effects: - prompt_parts.append(f"Include Side Effects: {', '.join(side_effects)}") - - if request.focus_areas: - prompt_parts.append(f"Focus Areas: {', '.join(request.focus_areas)}") - - if request.context: - prompt_parts.append(f"Additional Context: {request.context}") - - prompt_parts.append(f"Export Format: {request.export_format}") - prompt_parts.append("=== END REQUEST ===") - - # Add web search instruction if enabled - websearch_instruction = self.get_websearch_instruction( - request.use_websearch, - f"""When analyzing call paths for {primary_language} code, consider if searches for these would help: -- Framework-specific call patterns and lifecycle methods -- Language-specific dispatch mechanisms and polymorphism -- Common side-effect patterns for libraries used in the code -- Documentation for external APIs and services called -- Known design patterns that affect call flow""", - ) - if websearch_instruction: - prompt_parts.append(websearch_instruction) - - # Add main code to analyze - prompt_parts.append("\n=== CODE TO ANALYZE ===") - prompt_parts.append(code_content) - prompt_parts.append("=== END CODE ===") - - # Add analysis instructions - analysis_instructions = [ - f"\nPlease perform a {request.analysis_depth} static call path analysis for the entry point '{request.entry_point}'." - ] - - if request.parameters: - analysis_instructions.append( - "Pay special attention to how the provided parameter values affect the execution flow." - ) - - if request.confidence_threshold < 1.0: - analysis_instructions.append( - f"Filter out speculative paths with confidence below {request.confidence_threshold}." - ) - - analysis_instructions.append(f"Format the output as {request.export_format}.") - - prompt_parts.extend(analysis_instructions) - - full_prompt = "\n".join(prompt_parts) - - # Log final prompt statistics - from utils.token_utils import estimate_tokens - - total_tokens = estimate_tokens(full_prompt) - logger.info(f"[TRACEPATH] Complete prompt prepared: {total_tokens:,} tokens, {len(full_prompt):,} characters") - - return full_prompt - - def format_response(self, response: str, request: TracePathRequest, model_info: Optional[dict] = None) -> str: - """ - Format the call path analysis response. - - The base tool handles structured response validation via SPECIAL_STATUS_MODELS, - so this method focuses on providing clear guidance for next steps. - - Args: - response: The raw analysis from the model - request: The original request for context - model_info: Optional dict with model metadata - - Returns: - str: The response with additional guidance - """ - logger.debug(f"[TRACEPATH] Formatting response for entry point '{request.entry_point}'") - - # Get the friendly model name - model_name = "the model" - if model_info and model_info.get("model_response"): - model_name = model_info["model_response"].friendly_name or "the model" - - # Add contextual footer based on analysis depth and format - if request.export_format == "json": - footer = f""" ---- - -**Analysis Complete**: {model_name} has provided a structured JSON analysis of the call path for `{request.entry_point}`. - -**Next Steps**: -- Review the confidence levels for each predicted call -- Investigate any uncertain calls marked with low confidence -- Use this analysis for impact assessment, debugging, or architecture review -- For deeper analysis, increase analysis_depth to 'deep' or provide additional context files -""" - elif request.export_format == "plantuml": - footer = f""" ---- - -**Analysis Complete**: {model_name} has generated a PlantUML diagram showing the call path for `{request.entry_point}`. - -**Next Steps**: -- Render the PlantUML diagram to visualize the call flow -- Review branching points and conditional logic -- Verify the predicted paths against your understanding of the code -- Use this for documentation or architectural discussions -""" - else: # markdown - footer = f""" ---- - -**Analysis Complete**: {model_name} has traced the execution path for `{request.entry_point}` at {request.analysis_depth} depth. - -**Next Steps**: -- Review the call path summary and confidence assessments -- Pay attention to uncertain calls that may require runtime verification -- Use the code anchors (file:line references) to navigate to critical decision points -- Consider this analysis for debugging, impact assessment, or refactoring decisions -""" - - return f"{response}{footer}" diff --git a/tools/tracer.py b/tools/tracer.py new file mode 100644 index 0000000..242ce8e --- /dev/null +++ b/tools/tracer.py @@ -0,0 +1,456 @@ +""" +Tracer tool - Static call path prediction and control flow analysis + +This tool analyzes code to predict and explain full call paths and control flow without executing code. +Given a method name, its owning class/module, and parameter combinations or runtime values, it predicts +the complete chain of method/function calls that would be triggered. + +Key Features: +- Static call path prediction with confidence levels +- Polymorphism and dynamic dispatch analysis +- Value-driven flow analysis based on parameter combinations +- Side effects identification (database, network, filesystem) +- Branching analysis for conditional logic +- Hybrid AI-first approach with optional AST preprocessing for enhanced accuracy +""" + +import logging +import os +from typing import Any, Literal, Optional + +from pydantic import Field + +from config import TEMPERATURE_ANALYTICAL +from systemprompts import TRACER_PROMPT + +from .base import BaseTool, ToolRequest + +logger = logging.getLogger(__name__) + + +class TracerRequest(ToolRequest): + """ + Request model for the tracer tool. + + This model defines the simplified parameters for static code analysis. + """ + + prompt: str = Field( + ..., + description="Description of what to trace including method/function name and class/file context (e.g., 'Trace BookingManager::finalizeInvoice method' or 'Analyze dependencies for validate_input function in utils module')", + ) + files: list[str] = Field( + ..., + description="Code files or directories to analyze (must be absolute paths)", + ) + trace_mode: Literal["precision", "dependencies"] = Field( + ..., + description="Trace mode: 'precision' (follows actual code execution path from entry point) or 'dependencies' (analyzes bidirectional dependency mapping showing what calls this target and what it calls)", + ) + + +class TracerTool(BaseTool): + """ + Tracer tool implementation. + + This tool analyzes code to predict static call paths and control flow without execution. + Uses a hybrid AI-first approach with optional AST preprocessing for enhanced accuracy. + """ + + def get_name(self) -> str: + return "tracer" + + def get_description(self) -> str: + return ( + "STATIC CODE ANALYSIS - Analyzes code to provide either execution flow traces or dependency mappings without executing code. " + "Type 'precision': Follows the actual code path from a specified method/function, resolving calls, branching, and side effects. " + "Type 'dependencies': Analyzes bidirectional dependencies showing what calls the target and what it calls, including imports and inheritance. " + "Perfect for: understanding complex code flows, impact analysis, debugging assistance, architecture review. " + "Responds in structured JSON format for easy parsing and visualization. " + "Choose thinking_mode based on code complexity: 'medium' for standard analysis (default), " + "'high' for complex systems, 'max' for legacy codebases requiring deep analysis. " + "Note: If you're not currently using a top-tier model such as Opus 4 or above, these tools can provide enhanced capabilities." + ) + + def get_input_schema(self) -> dict[str, Any]: + schema = { + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Description of what to trace including method/function name and class/file context (e.g., 'Trace BookingManager::finalizeInvoice method' or 'Analyze dependencies for validate_input function in utils module')", + }, + "files": { + "type": "array", + "items": {"type": "string"}, + "description": "Code files or directories to analyze (must be absolute paths)", + }, + "trace_mode": { + "type": "string", + "enum": ["precision", "dependencies"], + "description": "Trace mode: 'precision' (follows actual code execution path from entry point) or 'dependencies' (analyzes bidirectional dependency mapping showing what calls this target and what it calls)", + }, + "model": self.get_model_field_schema(), + "temperature": { + "type": "number", + "description": "Temperature (0-1, default 0.2 for analytical precision)", + "minimum": 0, + "maximum": 1, + }, + "thinking_mode": { + "type": "string", + "enum": ["minimal", "low", "medium", "high", "max"], + "description": "Thinking depth: minimal (0.5% of model max), low (8%), medium (33%), high (67%), max (100% of model max)", + }, + "use_websearch": { + "type": "boolean", + "description": "Enable web search for framework documentation and patterns", + "default": True, + }, + "continuation_id": { + "type": "string", + "description": "Thread continuation ID for multi-turn conversations across tools", + }, + }, + "required": ["prompt", "files", "trace_mode"] + (["model"] if self.is_effective_auto_mode() else []), + } + + return schema + + def get_system_prompt(self) -> str: + return TRACER_PROMPT + + def get_default_temperature(self) -> float: + return TEMPERATURE_ANALYTICAL + + # Line numbers are enabled by default for precise code references + + def get_model_category(self): + """Tracer requires extended reasoning for complex flow analysis""" + from tools.models import ToolModelCategory + + return ToolModelCategory.EXTENDED_REASONING + + def get_request_model(self): + return TracerRequest + + def detect_primary_language(self, file_paths: list[str]) -> str: + """ + Detect the primary programming language from file extensions. + + Args: + file_paths: List of file paths to analyze + + Returns: + str: Detected language or "mixed" if multiple languages found + """ + # Language detection based on file extensions + language_extensions = { + "python": {".py", ".pyx", ".pyi"}, + "javascript": {".js", ".jsx", ".mjs", ".cjs"}, + "typescript": {".ts", ".tsx", ".mts", ".cts"}, + "java": {".java"}, + "csharp": {".cs"}, + "cpp": {".cpp", ".cc", ".cxx", ".c", ".h", ".hpp"}, + "go": {".go"}, + "rust": {".rs"}, + "swift": {".swift"}, + "kotlin": {".kt", ".kts"}, + "ruby": {".rb"}, + "php": {".php"}, + "scala": {".scala"}, + } + + # Count files by language + language_counts = {} + for file_path in file_paths: + extension = os.path.splitext(file_path.lower())[1] + for lang, exts in language_extensions.items(): + if extension in exts: + language_counts[lang] = language_counts.get(lang, 0) + 1 + break + + if not language_counts: + return "unknown" + + # Return most common language, or "mixed" if multiple languages + max_count = max(language_counts.values()) + dominant_languages = [lang for lang, count in language_counts.items() if count == max_count] + + if len(dominant_languages) == 1: + return dominant_languages[0] + else: + return "mixed" + + async def prepare_prompt(self, request: TracerRequest) -> str: + """ + Prepare the complete prompt for code analysis. + + This method combines: + - System prompt with analysis instructions + - User request and trace type + - File contents with line numbers + - Analysis parameters + + Args: + request: The validated tracer request + + Returns: + str: Complete prompt for the model + + Raises: + ValueError: If the prompt exceeds token limits + """ + logger.info( + f"[TRACER] Preparing prompt for {request.trace_mode} trace analysis with {len(request.files)} files" + ) + logger.debug(f"[TRACER] User request: {request.prompt[:100]}...") + + # Check for prompt.txt in files + prompt_content, updated_files = self.handle_prompt_file(request.files) + + # If prompt.txt was found, incorporate it into the request prompt + if prompt_content: + logger.debug("[TRACER] Found prompt.txt file, incorporating content") + request.prompt = prompt_content + "\n\n" + request.prompt + + # Update request files list + if updated_files is not None: + logger.debug(f"[TRACER] Updated files list after prompt.txt processing: {len(updated_files)} files") + request.files = updated_files + + # Check user input size at MCP transport boundary (before adding internal content) + size_check = self.check_prompt_size(request.prompt) + if size_check: + from tools.models import ToolOutput + + raise ValueError(f"MCP_SIZE_CHECK:{ToolOutput(**size_check).model_dump_json()}") + + # Detect primary language + primary_language = self.detect_primary_language(request.files) + logger.debug(f"[TRACER] Detected primary language: {primary_language}") + + # Use centralized file processing logic for main code files (with line numbers enabled) + continuation_id = getattr(request, "continuation_id", None) + logger.debug(f"[TRACER] Preparing {len(request.files)} code files for analysis") + code_content = self._prepare_file_content_for_prompt(request.files, continuation_id, "Code to analyze") + + if code_content: + from utils.token_utils import estimate_tokens + + code_tokens = estimate_tokens(code_content) + logger.info(f"[TRACER] Code files embedded successfully: {code_tokens:,} tokens") + else: + logger.warning("[TRACER] No code content after file processing") + + # Build the complete prompt + prompt_parts = [] + + # Add system prompt + prompt_parts.append(self.get_system_prompt()) + + # Add user request and analysis parameters + prompt_parts.append("\n=== ANALYSIS REQUEST ===") + prompt_parts.append(f"User Request: {request.prompt}") + prompt_parts.append(f"Trace Mode: {request.trace_mode}") + prompt_parts.append(f"Language: {primary_language}") + prompt_parts.append("=== END REQUEST ===") + + # Add web search instruction if enabled + websearch_instruction = self.get_websearch_instruction( + getattr(request, "use_websearch", True), + f"""When analyzing code for {primary_language}, consider if searches for these would help: +- Framework-specific call patterns and lifecycle methods +- Language-specific dispatch mechanisms and polymorphism +- Common side-effect patterns for libraries used in the code +- Documentation for external APIs and services called +- Known design patterns that affect call flow""", + ) + if websearch_instruction: + prompt_parts.append(websearch_instruction) + + # Add main code to analyze + prompt_parts.append("\n=== CODE TO ANALYZE ===") + prompt_parts.append(code_content) + prompt_parts.append("=== END CODE ===") + + # Add analysis instructions + prompt_parts.append(f"\nPlease perform a {request.trace_mode} trace analysis based on the user request.") + + full_prompt = "\n".join(prompt_parts) + + # Log final prompt statistics + from utils.token_utils import estimate_tokens + + total_tokens = estimate_tokens(full_prompt) + logger.info(f"[TRACER] Complete prompt prepared: {total_tokens:,} tokens, {len(full_prompt):,} characters") + + return full_prompt + + def format_response(self, response: str, request: TracerRequest, model_info: Optional[dict] = None) -> str: + """ + Format the code analysis response with mode-specific rendering instructions. + + The base tool handles structured response validation via SPECIAL_STATUS_MODELS, + so this method focuses on providing clear rendering instructions for Claude. + + Args: + response: The raw analysis from the model + request: The original request for context + model_info: Optional dict with model metadata + + Returns: + str: The response with mode-specific rendering instructions + """ + logger.debug(f"[TRACER] Formatting response for {request.trace_mode} trace analysis") + + # Get the friendly model name + model_name = "the model" + if model_info and model_info.get("model_response"): + model_name = model_info["model_response"].friendly_name or "the model" + + # Base tool will handle trace_complete JSON responses via SPECIAL_STATUS_MODELS + # No need for manual JSON parsing here + + # Generate mode-specific rendering instructions + rendering_instructions = self._get_rendering_instructions(request.trace_mode) + + # Create the complete response with rendering instructions + footer = f""" +--- + +**Analysis Complete**: {model_name} has completed a {request.trace_mode} analysis as requested. + +{rendering_instructions} + +**GENERAL REQUIREMENTS:** +- Follow the rendering instructions EXACTLY as specified above +- Use only the data provided in the JSON response +- Maintain exact formatting for readability +- Include file paths and line numbers as provided +- Do not add explanations or commentary outside the specified format""" + + return f"{response}{footer}" + + def _get_rendering_instructions(self, trace_mode: str) -> str: + """ + Get mode-specific rendering instructions for Claude. + + Args: + trace_mode: Either "precision" or "dependencies" + + Returns: + str: Complete rendering instructions for the specified mode + """ + if trace_mode == "precision": + return self._get_precision_rendering_instructions() + else: # dependencies mode + return self._get_dependencies_rendering_instructions() + + def _get_precision_rendering_instructions(self) -> str: + """Get rendering instructions for precision trace mode.""" + return """ +## MANDATORY RENDERING INSTRUCTIONS FOR PRECISION TRACE + +You MUST render the trace analysis in exactly two views: + +### 1. CALL FLOW DIAGRAM (TOP-DOWN) + +Use this exact format: +``` +[Class::Method] (file: /path, line: ##) +↓ +[Class::CalledMethod] (file: /path, line: ##) +↓ +... +``` + +**Rules:** +- Chain each call using ↓ or β†’ for readability +- Include file name and line number per method +- If the call is conditional, append `? if condition` +- If ambiguous, mark with `⚠️ ambiguous branch` +- Indent nested calls appropriately + +### 2. BRANCHING & SIDE EFFECT TABLE + +Render exactly this table format: + +| Location | Condition | Branches | Ambiguous | +|----------|-----------|----------|-----------| +| /file/path:## | if condition | method1(), method2() | βœ…/❌ | + +**Side Effects section:** +``` +Side Effects: +- [database] description (File.ext:##) +- [network] description (File.ext:##) +- [filesystem] description (File.ext:##) +``` + +**CRITICAL RULES:** +- ALWAYS render both views unless data is missing +- Use exact filenames, class names, and line numbers from JSON +- DO NOT invent function names or examples +- Mark ambiguous branches with ⚠️ or βœ… +- If sections are empty, omit them cleanly""" + + def _get_dependencies_rendering_instructions(self) -> str: + """Get rendering instructions for dependencies trace mode.""" + return """ +## MANDATORY RENDERING INSTRUCTIONS FOR DEPENDENCIES TRACE + +You MUST render the trace analysis in exactly two views: + +### 1. DEPENDENCY FLOW GRAPH + +Use this exact format: + +**Incoming:** +``` +Called by: +- [CallerClass::callerMethod] ← /path/file.ext:## +- [ServiceImpl::run] ← /path/file.ext:## +``` + +**Outgoing:** +``` +Calls: +- [Logger::logAction] β†’ /utils/log.ext:## +- [PaymentClient::send] β†’ /clients/pay.ext:## +``` + +**Type Dependencies:** +``` +- conforms_to: ProtocolName +- implements: InterfaceName +- imports: ModuleName, LibraryName +``` + +**State Access:** +``` +- reads: property.name (line ##) +- writes: object.field (line ##) +``` + +**Arrow Rules:** +- `←` for incoming (who calls this) +- `β†’` for outgoing (what this calls) + +### 2. DEPENDENCY TABLE + +Render exactly this table format: + +| Type | From/To | Method | File | Line | +|------|---------|--------|------|------| +| direct_call | From: CallerClass | callerMethod | /path/file.ext | ## | +| method_call | To: TargetClass | targetMethod | /path/file.ext | ## | +| uses_property | To: ObjectClass | .propertyName | /path/file.ext | ## | +| conforms_to | Self: ThisClass | β€” | /path/file.ext | β€” | + +**CRITICAL RULES:** +- ALWAYS render both views unless data is missing +- Use exact filenames, class names, and line numbers from JSON +- DO NOT invent function names or examples +- If sections (state access, type dependencies) are empty, omit them cleanly +- Show directional dependencies with proper arrows"""