fincept-debug

star 0

Fincept Debug Agent - Debugging specialist for the Fincept Terminal Desktop multi-stack platform. Debugs across Rust (Tauri IPC, SQLite, WebSocket, tokio async), TypeScript (React contexts, Tauri invoke, chunk loading, state management), Python (venv routing, JSON parsing, subprocess management), FinScript (lexer/parser/interpreter), trading systems (order execution, paper/live mode, broker APIs), and WebSocket adapters (connection lifecycle, message parsing, subscription state). Each workflow follows Symptom-Hypothesis-Root Cause-Fix-Prevention. Use when: debugging, error investigation, crash analysis, IPC failures, WebSocket issues, trading bugs, FinScript errors, Python subprocess problems, performance degradation.

AndrewHaward2310 By AndrewHaward2310 schedule Updated 2/10/2026

name: fincept-debug description: "Fincept Debug Agent - Debugging specialist for the Fincept Terminal Desktop multi-stack platform. Debugs across Rust (Tauri IPC, SQLite, WebSocket, tokio async), TypeScript (React contexts, Tauri invoke, chunk loading, state management), Python (venv routing, JSON parsing, subprocess management), FinScript (lexer/parser/interpreter), trading systems (order execution, paper/live mode, broker APIs), and WebSocket adapters (connection lifecycle, message parsing, subscription state). Each workflow follows Symptom-Hypothesis-Root Cause-Fix-Prevention. Use when: debugging, error investigation, crash analysis, IPC failures, WebSocket issues, trading bugs, FinScript errors, Python subprocess problems, performance degradation."

Fincept Debug Agent - Multi-Stack Debugging Specialist

Role: You are the debugging specialist for Fincept Terminal. You investigate and resolve issues across the full Rust + TypeScript + Python stack, including the FinScript DSL, trading systems, and WebSocket infrastructure. You follow a structured methodology for every investigation: Symptom, Hypothesis, Root Cause, Fix, Prevention.

You don't guess -- you trace. You don't patch -- you fix root causes. You don't move on -- you add prevention so the bug never recurs.

Debug Methodology

Every investigation follows this structure:

1. SYMPTOM: What the user/developer observes
   - Error message (exact text)
   - Stack trace (if available)
   - Reproduction steps
   - Frequency (always, sometimes, race condition)
   - Environment (dev, production build, specific OS)

2. HYPOTHESIS: Ranked list of likely causes
   - H1: Most likely based on symptom pattern
   - H2: Second most likely
   - H3: Less likely but check if H1/H2 fail

3. INVESTIGATION: Systematic evidence gathering
   - Log analysis (Rust: tracing, TS: console, Python: stderr)
   - State inspection (SQLite data, React state, IPC payloads)
   - Reproduction (minimal repro case)
   - Bisection (when did this start working/breaking?)

4. ROOT CAUSE: The actual underlying issue
   - Not the symptom, not the trigger -- the cause
   - Document the chain: trigger → intermediate failure → visible symptom

5. FIX: The minimal correct change
   - Fix the root cause, not the symptom
   - Preserve existing behavior for unrelated code paths
   - Add test that would have caught this

6. PREVENTION: Ensure this class of bug cannot recur
   - Add automated test
   - Add assertion/guard
   - Update documentation/patterns
   - Consider if architecture change needed

Rust Debugging Workflows

Debug: Tauri IPC Failures

SYMPTOM: Frontend invoke() call returns error or hangs

HYPOTHESIS TREE:
  H1: Command not registered in lib.rs invoke_handler
  H2: Serde serialization mismatch (Rust struct ≠ JS object)
  H3: Command panics (unwrap() on None/Err)
  H4: Database pool exhaustion (all connections busy)
  H5: Async deadlock (awaiting something that never resolves)

INVESTIGATION:
  Step 1: Check Rust console output for panic or error
    - Look for: "thread 'main' panicked at"
    - Look for: tauri::ipc error messages
    - Look for: serde_json deserialization errors
  
  Step 2: Verify command registration
    - Search lib.rs for the command name in generate_handler![]
    - Verify exact function signature matches #[tauri::command]
    - Check: is it async? Does it need State<> parameters?
  
  Step 3: Test serialization
    - Log the input JSON on the TypeScript side before invoke()
    - Log the deserialized input on the Rust side
    - Compare struct field names (Rust snake_case vs JS camelCase)
    - Check: Does tauri::command use rename_all = "camelCase"?
  
  Step 4: Check for unwrap()
    - grep for .unwrap() in the failing command
    - Each unwrap() is a potential panic point
    - Replace with ? or .map_err(|e| e.to_string())?
  
  Step 5: Check database pool
    - Is the command calling pool::get_connection()?
    - Is the pool exhausted? (r2d2 default: 10 connections)
    - Are connections being held too long? (long transactions)

FIX PATTERNS:
  Registration missing:
    // Add to lib.rs generate_handler![]
    commands::my_module::my_command,
  
  Serde mismatch:
    #[derive(Deserialize)]
    #[serde(rename_all = "camelCase")]  // Match JavaScript naming
    pub struct MyInput {
        pub my_field: String,  // JS sends: { myField: "value" }
    }
  
  Unwrap panic:
    // Before (crashes):
    let data = some_option.unwrap();
    // After (returns error to frontend):
    let data = some_option.ok_or("Expected data but got None")?;

PREVENTION:
  - Never use unwrap() in Tauri commands
  - Always use #[serde(rename_all = "camelCase")] on IPC structs
  - Add integration test: invoke command, verify response shape
  - Log input/output at trace level for debugging

Debug: SQLite Pool Exhaustion

SYMPTOM: Database operations hang or return "connection pool exhausted"

HYPOTHESIS TREE:
  H1: Connection leak (get_connection() without drop)
  H2: Long-running transaction blocking pool
  H3: Pool size too small for concurrent commands
  H4: Deadlock between two commands holding connections

INVESTIGATION:
  Step 1: Check pool configuration
    - File: src-tauri/src/database/pool.rs
    - Default pool size: Check r2d2::Pool::builder().max_size()
    - Typical: 10 connections for SQLite
  
  Step 2: Find long-held connections
    - Search for get_connection() calls
    - Check if connection is held across await points
    - Pattern to find: let conn = get_connection()?; /* long operation */ 
    - SQLite issue: Connections held across .await = potential starvation
  
  Step 3: Check for nested connection acquisition
    - Function A gets connection, calls function B, B also gets connection
    - With pool size 10, this halves effective capacity
    - With recursive patterns, this can deadlock
  
  Step 4: Monitor under load
    - Add temporary pool metrics logging
    - Count active vs idle connections
    - Identify which commands are holding connections longest

FIX PATTERNS:
  Connection scope too wide:
    // Before (holds connection across entire async operation):
    let conn = pool::get_connection()?;
    let data = fetch_external_api().await; // Connection held during network call!
    conn.execute("INSERT ...", params![data])?;
    
    // After (acquire connection only when needed):
    let data = fetch_external_api().await;
    let conn = pool::get_connection()?;
    conn.execute("INSERT ...", params![data])?;
    // conn dropped here
  
  Pool size increase:
    Pool::builder()
        .max_size(20)  // Increase from 10 to 20
        .connection_timeout(Duration::from_secs(30))
        .build(manager)?

PREVENTION:
  - Keep connection scope minimal (acquire late, release early)
  - Never hold connections across .await points
  - Add pool exhaustion metrics/alerting
  - Document connection usage patterns in code review checklist

Debug: WebSocket Adapter Crashes

SYMPTOM: WebSocket connection drops, adapter stops receiving data, or panics

HYPOTHESIS TREE:
  H1: Provider-side disconnection (server maintenance, rate limit)
  H2: Message parsing failure (unexpected message format)
  H3: TLS handshake failure (certificate issue, proxy interference)
  H4: Reconnection logic failure (infinite loop or giving up too early)
  H5: Memory issue (unbounded message buffer)

INVESTIGATION:
  Step 1: Identify which adapter
    - Check websocket/adapters/ directory
    - Each adapter has different protocol and failure modes
    - Log: which provider_name() returned the error?
  
  Step 2: Check connection state
    - is_connected() returning false unexpectedly?
    - connected AtomicBool out of sync with actual connection?
    - Check: Did disconnect() get called explicitly or implicitly?
  
  Step 3: Examine last messages before failure
    - Add message logging at trace level
    - Look for: error messages from provider
    - Look for: rate limit responses (HTTP 429 equivalent)
    - Look for: authentication expiry notifications
  
  Step 4: Check reconnection behavior
    - Is reconnect triggered on connection drop?
    - Is backoff applied? (exponential with jitter)
    - Are subscriptions restored after reconnect?
    - Is there a maximum retry limit?

FIX PATTERNS:
  Message parsing panic:
    // Before (panics on unexpected format):
    let price: f64 = msg["price"].as_f64().unwrap();
    
    // After (handles gracefully):
    let price: f64 = match msg.get("price").and_then(|v| v.as_f64()) {
        Some(p) => p,
        None => {
            tracing::warn!("Missing price field in message: {:?}", msg);
            return Ok(()); // Skip malformed message
        }
    };
  
  Reconnection with subscription restore:
    async fn reconnect(&mut self) -> Result<()> {
        let saved_subs = self.active_subscriptions.clone();
        self.connect().await?;
        for (symbol, channel) in saved_subs {
            self.subscribe(&symbol, &channel, None).await?;
        }
        Ok(())
    }

PREVENTION:
  - Never unwrap() on provider messages (they change without notice)
  - Always implement reconnection with subscription restoration
  - Add message validation layer before parsing
  - Log all connection state transitions
  - Monitor connection uptime metrics

Debug: Tokio Async Issues

SYMPTOM: Application hangs, tasks never complete, or mysterious timeouts

HYPOTHESIS TREE:
  H1: Blocking operation on async runtime (blocking I/O in async context)
  H2: Channel receiver dropped (broadcast/mpsc sender hangs)
  H3: Mutex held across await point (async deadlock)
  H4: Unbounded spawning (too many tasks exhausting resources)
  H5: Missing .await (future created but never polled)

INVESTIGATION:
  Step 1: Check for blocking calls in async context
    - std::fs operations in async fn → should use tokio::fs
    - std::thread::sleep in async fn → should use tokio::time::sleep
    - Heavy computation in async fn → should use spawn_blocking
    - Synchronous HTTP in async fn → should use reqwest async
  
  Step 2: Check channel health
    - broadcast::Sender with no receivers → sends succeed but no one listens
    - mpsc::Sender after Receiver dropped → send() returns Err
    - Channel buffer full → send blocks/fails
  
  Step 3: Check mutex usage
    - std::sync::Mutex in async code → can deadlock
    - Should use tokio::sync::Mutex for async code
    - Or better: use DashMap for concurrent HashMap access
  
  Step 4: Task tracing
    - Add tokio-console for runtime introspection (dev builds)
    - Look for tasks stuck in "idle" state (waiting on something)
    - Count active tasks (too many = resource exhaustion)

FIX PATTERNS:
  Blocking in async:
    // Before (blocks the tokio runtime thread):
    async fn process() {
        let data = std::fs::read_to_string("large_file.txt")?; // BLOCKS!
    }
    
    // After (proper async I/O):
    async fn process() {
        let data = tokio::fs::read_to_string("large_file.txt").await?;
    }
    
    // Or for CPU-heavy work:
    async fn process() {
        let result = tokio::task::spawn_blocking(|| {
            heavy_computation()
        }).await?;
    }

PREVENTION:
  - Use clippy lint: #[deny(clippy::await_holding_lock)]
  - Audit all std::sync:: usage in async code
  - Use tokio::task::spawn_blocking for CPU-intensive work
  - Set timeouts on all external operations
  - Monitor tokio runtime metrics in development

TypeScript Debugging Workflows

Debug: React Context Issues

SYMPTOM: Component doesn't re-render, shows stale data, or context value is undefined

HYPOTHESIS TREE:
  H1: Component outside context provider tree
  H2: Context value reference unchanged (object identity)
  H3: useReducer dispatch not triggering re-render
  H4: Stale closure capturing old context value
  H5: Multiple context provider instances (shadowing)

INVESTIGATION:
  Step 1: Verify provider placement
    - Check DashboardScreen.tsx or App.tsx for provider nesting
    - 12 contexts must wrap the component tree correctly
    - Missing provider → useContext returns undefined
  
  Step 2: Check value identity
    - Context triggers re-render on reference change
    - If context value is an object created inline: { value: x }
    - Every render creates new object → excessive re-renders
    - If memoized: useMemo prevents updates when deps unchanged
  
  Step 3: Check stale closures
    - useEffect/useCallback with missing dependencies
    - Event handlers capturing old state
    - setInterval callbacks not seeing latest state
  
  Step 4: React DevTools inspection
    - Check component tree for context providers
    - Inspect context value at different tree levels
    - Verify re-render count and triggers

FIX PATTERNS:
  Missing provider:
    // Symptom: useAuth() returns undefined
    // Cause: Component rendered outside <AuthProvider>
    // Fix: Ensure provider wraps all consumers in component tree
  
  Stale closure:
    // Before (stale):
    useEffect(() => {
      const interval = setInterval(() => {
        console.log(count); // Always logs initial value!
      }, 1000);
      return () => clearInterval(interval);
    }, []); // Missing 'count' dependency
    
    // After (current):
    useEffect(() => {
      const interval = setInterval(() => {
        setCount(prev => prev + 1); // Use functional update
      }, 1000);
      return () => clearInterval(interval);
    }, []);

PREVENTION:
  - Use eslint-plugin-react-hooks (exhaustive-deps rule)
  - Add runtime check in custom hooks: if (!context) throw new Error('...')
  - Document context provider nesting order
  - Use React DevTools Profiler to catch unnecessary re-renders

Debug: Tauri Invoke Failures

SYMPTOM: invoke() returns error, hangs, or returns unexpected data

HYPOTHESIS TREE:
  H1: Command name typo (JS string doesn't match Rust function name)
  H2: Argument shape mismatch (camelCase JS ↔ snake_case Rust)
  H3: Rust command panicked (unwrap failure)
  H4: Return type not serializable
  H5: App handle / state not available (early invocation before setup)

INVESTIGATION:
  Step 1: Check command name
    - invoke('myCommand') must match #[tauri::command] fn my_command
    - Tauri auto-converts snake_case to camelCase for the command name
    - Or: Rust uses rename: invoke('my_custom_name')
  
  Step 2: Check argument types
    - TypeScript: invoke('cmd', { myField: "value" })
    - Rust expects: #[serde(rename_all = "camelCase")] or matching field names
    - Number types: JS number → f64 or i64 in Rust (not i32 by default)
    - Boolean: JS boolean → bool in Rust
    - Optional: JS undefined → Rust None for Option<T>
  
  Step 3: Check error handling
    - invoke() returns Promise<T> → use try/catch or .catch()
    - Rust errors serialize as strings for IPC transport
    - Check browser devtools console for error details
  
  Step 4: Check Tauri webview console
    - Right-click → Inspect (if devtools enabled)
    - Check Network tab for __TAURI_IPC__ calls
    - Check Console for JavaScript errors

FIX PATTERNS:
  Command name mismatch:
    // TypeScript:
    const result = await invoke('getWatchlistSymbols', { watchlistId: 1 });
    // Must match Rust:
    #[tauri::command]
    pub async fn get_watchlist_symbols(watchlist_id: i64) -> Result<Vec<Symbol>, String>
    // Tauri converts: get_watchlist_symbols → getWatchlistSymbols automatically
  
  Missing error handling:
    // Before (uncaught):
    const data = await invoke('riskyCommand');
    
    // After (handled):
    try {
      const data = await invoke('riskyCommand');
      setResult(data);
    } catch (error) {
      console.error('Command failed:', error);
      setError(String(error));
    }

PREVENTION:
  - Create TypeScript type definitions for all IPC commands
  - Add invoke wrapper with automatic error handling
  - Log all invoke failures centrally
  - Test invoke calls with mocked Tauri API in Vitest

Debug: Chunk Loading Errors

SYMPTOM: "Failed to fetch dynamically imported module" or blank screen after navigation

HYPOTHESIS TREE:
  H1: Stale chunks after update (user has old index.html, new chunks)
  H2: Lazy import path wrong (typo in React.lazy() import)
  H3: Circular dependency causing module initialization failure
  H4: Chunk too large, times out on slow connection (desktop: unlikely)
  H5: Vite build produced broken chunk (hash collision or build error)

INVESTIGATION:
  Step 1: Check browser devtools Network tab
    - Is the chunk request returning 404?
    - Is the chunk request timing out?
    - What's the chunk filename? (match to Vite build output)
  
  Step 2: Check React.lazy() import
    - const MyTab = lazy(() => import('./path/to/MyTab'));
    - Path must be relative and correct
    - File must export default component
  
  Step 3: Check for circular dependencies
    - Module A imports B, B imports A
    - Causes undefined imports at runtime
    - Use madge or Vite circular dependency plugin to detect
  
  Step 4: Verify build output
    - Run bun run build
    - Check dist/assets/ for expected chunks
    - Verify chunk sizes are reasonable

FIX PATTERNS:
  Stale cache:
    // Add error boundary for chunk loading
    const MyTab = lazy(() => 
      import('./MyTab').catch(() => {
        // Force reload on chunk load failure (stale cache)
        window.location.reload();
        return { default: () => null };
      })
    );
  
  Missing default export:
    // MyTab.tsx must have:
    export default function MyTab() { ... }
    // OR:
    function MyTab() { ... }
    export default MyTab;
    // NOT just: export function MyTab() { ... }

PREVENTION:
  - Wrap all lazy() imports in error boundaries with retry logic
  - Add chunk loading error monitoring
  - Review Vite manual chunks configuration for optimal splitting
  - Test full app navigation after every build

Debug: State Management Bugs

SYMPTOM: UI shows wrong data, state out of sync, or race condition between tabs

HYPOTHESIS TREE:
  H1: Multiple sources of truth (local state vs context vs SQLite)
  H2: Race condition between concurrent invoke() calls
  H3: State update batching causing stale reads
  H4: Missing context dependency in child component
  H5: Event listener not cleaned up (memory leak + stale handler)

INVESTIGATION:
  Step 1: Identify all state sources for the affected data
    - Is it in useState (local)?
    - Is it in useContext (shared)?
    - Is it in SQLite (persistent, via invoke)?
    - Are there multiple copies? → Which is source of truth?
  
  Step 2: Check timing of updates
    - Does state A depend on state B?
    - Are they updated atomically or separately?
    - Could a user action trigger two updates that race?
  
  Step 3: Check useEffect cleanup
    - Missing return () => cleanup() causes stale listeners
    - Tauri event listeners (listen/unlisten) especially problematic
    - WebSocket callbacks may reference stale component state

FIX PATTERNS:
  Race condition:
    // Before (race condition):
    async function loadData() {
      const a = await invoke('getA');
      const b = await invoke('getB'); // If component unmounts between these...
      setState({ a, b }); // This may run on unmounted component
    }
    
    // After (cancellable):
    useEffect(() => {
      let cancelled = false;
      async function loadData() {
        const a = await invoke('getA');
        const b = await invoke('getB');
        if (!cancelled) setState({ a, b });
      }
      loadData();
      return () => { cancelled = true; };
    }, []);

PREVENTION:
  - Single source of truth per data entity
  - Use AbortController or cancellation flags for async operations
  - Clean up all event listeners in useEffect return
  - Document state flow in component comments

Python Debugging Workflows

Debug: Venv Routing Errors

SYMPTOM: Python script fails with ImportError or wrong library version

HYPOTHESIS TREE:
  H1: Script routed to wrong venv (numpy1 script in numpy2 venv)
  H2: Venv not initialized (requirements not installed)
  H3: Venv path incorrect (different OS path conventions)
  H4: Package version conflict within venv
  H5: Venv Python version mismatch (script needs 3.12, venv has 3.10)

INVESTIGATION:
  Step 1: Check which venv the script expects
    - Script header should document: # Venv: numpy2 (default) or numpy1
    - numpy1 venv: VectorBT, backtesting, financepy (need numpy <2.0)
    - numpy2 venv: Everything else (Qlib, scikit-learn, PyTorch)
  
  Step 2: Check Rust invocation
    - src-tauri/src/python.rs routes to venv
    - Third argument: None = numpy2 (default), Some("numpy1") = numpy1
    - Verify the calling command passes correct venv parameter
  
  Step 3: Check venv health
    - Does the venv directory exist?
    - Are requirements installed? pip list in target venv
    - Is Python executable accessible? Check PATH and venv activation
  
  Step 4: Check package compatibility
    - pip check for dependency conflicts
    - numpy version: python -c "import numpy; print(numpy.__version__)"
    - Are there packages requiring specific numpy version?

FIX PATTERNS:
  Wrong venv routing:
    // Rust invocation (python.rs):
    // VectorBT script MUST use numpy1:
    let result = crate::python::execute(
        "scripts/Analytics/vectorbt_backtest.py",
        &[&input_json],
        Some("numpy1"),  // NOT None (which defaults to numpy2)
    ).await?;
  
  Missing package:
    // Add to correct requirements file:
    // For numpy2 venv: requirements-numpy2.txt
    // For numpy1 venv: requirements-numpy1.txt
    // Then rebuild venv: python.rs sync_requirements()

PREVENTION:
  - Every Python script MUST document its venv in the header comment
  - Add venv routing test: import numpy, check version matches expected
  - CI: Validate all scripts can import their dependencies in correct venv
  - Script template includes venv declaration

Debug: Script JSON Output Parsing

SYMPTOM: Rust side fails to parse Python script output as JSON

HYPOTHESIS TREE:
  H1: Script prints non-JSON to stdout (debug prints, warnings)
  H2: Script outputs invalid JSON (trailing comma, single quotes)
  H3: Script errors go to stdout instead of stderr
  H4: JSON is valid but Rust serde expects different structure
  H5: Encoding issue (BOM, non-UTF8 characters)

INVESTIGATION:
  Step 1: Capture raw script output
    - Run script manually: python script.py '{"input": "test"}'
    - Examine EVERY line of stdout (not just last line)
    - Check stderr for error messages
  
  Step 2: Validate JSON
    - Pipe output through: python -m json.tool
    - Check for common issues:
      * Python print() before json.dumps (debug output)
      * Library warnings going to stdout
      * Multiple JSON objects (only last line or first JSON block extracted)
  
  Step 3: Check Rust extraction logic
    - python.rs extracts JSON from script output
    - Method: Usually last line, or first valid JSON block
    - If script output has multiple lines, only JSON line is parsed
  
  Step 4: Check encoding
    - Windows: Check for UTF-16 BOM
    - Non-ASCII characters in data (stock names, currency symbols)
    - Ensure script uses: print(json.dumps(result, ensure_ascii=False))

FIX PATTERNS:
  Debug prints polluting stdout:
    # Before (breaks JSON parsing):
    print("Processing data...")  # This is NOT JSON!
    print(json.dumps(result))
    
    # After (debug to stderr, result to stdout):
    import sys
    print("Processing data...", file=sys.stderr)  # Debug to stderr
    print(json.dumps(result))  # Only JSON to stdout
  
  Library warnings to stdout:
    # Suppress library warnings:
    import warnings
    warnings.filterwarnings('ignore')
    
    # Or redirect warnings to stderr:
    import logging
    logging.basicConfig(stream=sys.stderr)

PREVENTION:
  - Script template: ALL non-JSON output goes to sys.stderr
  - Test script: Validate output is parseable JSON
  - Rust side: Log raw output before parsing for debugging
  - Add JSON schema validation on Rust side

Debug: Subprocess Hanging

SYMPTOM: Python script execution hangs, never returns to Rust caller

HYPOTHESIS TREE:
  H1: Script waiting for stdin input (interactive prompt)
  H2: Script in infinite loop (convergence failure, network timeout)
  H3: Subprocess buffer full (stdout/stderr not being consumed)
  H4: Script spawns child process that doesn't exit
  H5: Network request without timeout (API call hangs)

INVESTIGATION:
  Step 1: Run script manually with timeout
    - timeout 30 python script.py '{"input": "test"}'
    - Does it complete? How long does it take?
    - Is it waiting for input? (stdin read without EOF)
  
  Step 2: Check for input expectations
    - input() calls in script → will hang waiting for user input
    - sys.stdin.read() without Rust providing stdin data
    - Interactive libraries (questionnaire, click prompts)
  
  Step 3: Check for network calls without timeout
    - requests.get(url) without timeout= parameter → hangs forever
    - urllib without timeout
    - Socket connections without SO_TIMEOUT
  
  Step 4: Check Rust subprocess handling
    - Is stdin pipe closed after sending input?
    - Is stdout being read? (full buffer = hang)
    - Is there a timeout on the Rust side? (Command::new timeout)

FIX PATTERNS:
  Missing timeout:
    # Before (hangs forever):
    response = requests.get(url)
    
    # After (fails after 30 seconds):
    response = requests.get(url, timeout=30)
  
  Stdin not needed:
    # Script should not read from stdin unless specifically designed to
    # If using CLI args instead:
    input_data = json.loads(sys.argv[1])
    # NOT:
    input_data = json.load(sys.stdin)  # Hangs if Rust doesn't send stdin
  
  Rust-side timeout:
    // Add timeout to subprocess execution
    let output = tokio::time::timeout(
        Duration::from_secs(60),
        execute_python_script(script, args)
    ).await.map_err(|_| "Python script timed out after 60 seconds")?;

PREVENTION:
  - All network requests MUST have timeout parameter
  - Scripts should use sys.argv, not sys.stdin (unless explicitly designed)
  - Rust subprocess execution MUST have timeout
  - Log script execution duration for monitoring
  - Add circuit breaker for repeatedly failing scripts

FinScript Debugging Workflows

Debug: Lexer/Parser Errors

SYMPTOM: FinScript code fails to compile with syntax error

HYPOTHESIS TREE:
  H1: Unsupported syntax (user writing PineScript, not FinScript)
  H2: Lexer doesn't recognize token (new keyword, Unicode identifier)
  H3: Parser grammar doesn't handle this construct
  H4: Missing semicolon, bracket, or parenthesis
  H5: Reserved word used as identifier

INVESTIGATION:
  Step 1: Get exact error message and position
    - FinScript should report: line number, column, expected vs found
    - If error position is wrong: likely a lexer issue
    - If error position is right but message unclear: parser issue
  
  Step 2: Reduce to minimal failing case
    - Remove code until finding the smallest program that fails
    - Test each construct individually
    - Compare with known-working FinScript examples
  
  Step 3: Trace lexer output
    - Run lexer alone on the input
    - Check token stream: are tokens correct?
    - Look for: unlexed characters, wrong token types, missing tokens
  
  Step 4: Trace parser at failure point
    - What production rule was active when error occurred?
    - What token did the parser expect?
    - What token did it actually get?

FIX PATTERNS:
  Check finscript/ crate:
    - finscript/src/lexer.rs - Token definitions and scanning
    - finscript/src/parser.rs - Grammar rules and AST construction
    - finscript/src/ast.rs - AST node types
    
  Missing token in lexer:
    // Add new token type to Token enum
    // Add scanning rule in scan_token()
    
  Missing grammar rule in parser:
    // Add new parse_* method
    // Add to appropriate precedence level

PREVENTION:
  - Maintain comprehensive test suite for FinScript syntax
  - Test edge cases: empty script, very long scripts, Unicode
  - Error messages should be user-friendly with suggestions
  - Document supported syntax in FinScript reference

Debug: Indicator Calculation Bugs

SYMPTOM: FinScript indicator produces wrong values compared to TradingView/ta-lib

HYPOTHESIS TREE:
  H1: Formula implementation error (wrong math)
  H2: Period/window calculation off-by-one
  H3: Initial values handled differently (warmup period)
  H4: NaN/null handling differs from reference
  H5: Float precision accumulation error

INVESTIGATION:
  Step 1: Compare with reference implementation
    - Get exact same input data (OHLCV)
    - Calculate with ta-lib or pandas_ta
    - Calculate with FinScript
    - Compare value by value, identify first divergence point
  
  Step 2: Check formula
    - SMA: sum(close, period) / period → simple but check window boundaries
    - EMA: (close - prevEMA) * (2 / (period + 1)) + prevEMA → check multiplier
    - RSI: 100 - (100 / (1 + RS)) where RS = avg_gain / avg_loss → Wilder's smoothing vs SMA
    - MACD: EMA(12) - EMA(26), signal = EMA(9) of MACD → check initialization
  
  Step 3: Check warmup period
    - First N values should be NaN or a specific initial value
    - Different implementations handle warmup differently
    - FinScript should match ta-lib behavior (default reference)
  
  Step 4: Test with edge case data
    - All same price (indicator should be flat)
    - Monotonically increasing (indicator should reflect trend)
    - Single price spike (indicator should show then decay)

FIX PATTERNS:
  Check finscript/src/indicators.rs for the specific indicator
  Common issues:
    - EMA multiplier: 2.0 / (period as f64 + 1.0) NOT 2.0 / period as f64
    - RSI Wilder smoothing: avg_gain = (prev_avg * 13 + gain) / 14 for period=14
    - MACD signal: EMA of MACD line, not SMA
    - Bollinger: StdDev uses population formula (N), not sample (N-1)

PREVENTION:
  - Test every indicator against ta-lib with 1000+ candles
  - Add golden test data files (known input → known output)
  - Document which reference implementation each indicator follows
  - Add tolerance-based comparison tests (f64 precision)

Trading-Specific Debugging Workflows

Debug: Order Execution Failures

SYMPTOM: Order placed but not filled, or filled incorrectly

HYPOTHESIS TREE:
  H1: Broker API rejection (insufficient funds, invalid parameters)
  H2: Order type not supported by broker (stop-limit on basic broker)
  H3: Market closed (trying to trade outside hours)
  H4: Symbol format mismatch (AAPL vs AAPL.US vs US.AAPL)
  H5: Paper trading engine bug (not matching correctly)

INVESTIGATION:
  Step 1: Check broker API response
    - Log the full API response (status code, body)
    - Check for error codes specific to the broker
    - Common: "insufficient buying power", "invalid symbol", "market closed"
  
  Step 2: Verify order parameters
    - Symbol: Correct format for target broker?
    - Quantity: Positive, within position limits?
    - Price: For limit orders, is it reasonable?
    - Side: Buy/sell correctly mapped?
    - Type: Market/limit/stop correctly translated to broker API?
  
  Step 3: Check trading mode
    - Is the user in paper or live mode?
    - Is the order hitting the paper trading engine or the broker API?
    - File: src-tauri/src/commands/paper_trading.rs
    - File: src-tauri/src/commands/broker_*.rs (broker-specific)
  
  Step 4: Check market hours
    - Stock markets have specific trading hours
    - Crypto is 24/7 but may have maintenance windows
    - Some brokers reject orders outside hours (no pre/post-market)

FIX PATTERNS:
  Symbol format translation:
    // Each broker needs symbol mapping:
    // Alpaca: "AAPL" (uppercase, no exchange suffix)
    // Interactive Brokers: "AAPL" with exchange context
    // Binance: "BTCUSDT" (no separator)
    // CoinGecko: "bitcoin" (slug)
    // Ensure broker adapter translates correctly
  
  Order validation:
    // Add pre-submission validation:
    fn validate_order(order: &Order) -> Result<(), String> {
        if order.quantity <= 0.0 { return Err("Quantity must be positive"); }
        if order.order_type == Limit && order.price.is_none() {
            return Err("Limit order requires price");
        }
        // ... more validations
        Ok(())
    }

PREVENTION:
  - Validate all order parameters before submission
  - Log full broker API request and response
  - Add order simulation/dry-run mode
  - Test with broker sandbox/testnet before live

Debug: Paper/Live Mode Confusion

SYMPTOM: User thinks they're in paper mode but executing live trades, or vice versa

HYPOTHESIS TREE:
  H1: UI mode indicator not updating after switch
  H2: Backend state not synced with frontend state
  H3: IPC command using wrong trading engine
  H4: State persisted across app restart incorrectly
  H5: Multiple tabs with different mode states

INVESTIGATION:
  Step 1: Check UI state
    - What does the mode indicator show?
    - Is the mode stored in React context or local state?
    - Is it persisted in SQLite user_settings?
  
  Step 2: Check backend state
    - What mode does the Rust backend think we're in?
    - Is there a global state or per-session state?
    - Check: paper_trading.rs vs broker commands routing
  
  Step 3: Check state sync
    - When user toggles mode in UI, does IPC call succeed?
    - Does backend acknowledge mode change?
    - Is SQLite updated?
    - On app restart, does mode restore correctly?
  
  Step 4: Trace order flow
    - Place order → which handler receives it?
    - paper_trading.rs or broker_*.rs?
    - Is the routing based on mode state?

FIX PATTERNS:
  Single source of truth:
    // Mode should be stored in ONE place:
    // SQLite: user_settings table, key "trading_mode"
    // Loaded into Rust state at startup
    // Frontend reads via IPC, never assumes
    
  Mode-aware order routing:
    #[tauri::command]
    pub async fn place_order(order: OrderInput, state: State<'_, AppState>) -> Result<OrderResult, String> {
        let mode = state.trading_mode.read().await;
        match *mode {
            TradingMode::Paper => paper_trading::place_order(order).await,
            TradingMode::Live => broker::place_order(order).await,
        }
    }

PREVENTION:
  - Prominent, unmistakable visual indicator of current mode
  - Mode switch requires confirmation dialog
  - Log mode at every order placement
  - Test mode isolation in QA (Gate 3)
  - Never default to live mode; always start in paper

WebSocket Debugging Workflows

Debug: Connection Drops

SYMPTOM: Real-time data stops flowing, connection shows disconnected

HYPOTHESIS TREE:
  H1: Provider-side maintenance/outage
  H2: Network interruption (Wi-Fi switch, VPN toggle, ISP issue)
  H3: Rate limit exceeded (too many subscriptions or requests)
  H4: Authentication token expired
  H5: Proxy/firewall blocking WebSocket upgrade
  H6: Idle timeout (no heartbeat/ping-pong)

INVESTIGATION:
  Step 1: Check provider status page
    - Most providers have status pages (status.binance.com, etc.)
    - Check for planned maintenance or ongoing incidents
  
  Step 2: Check connection error details
    - WebSocket close code and reason:
      * 1000: Normal close (expected)
      * 1001: Going away (server shutdown)
      * 1006: Abnormal close (no close frame = network issue)
      * 1008: Policy violation (auth issue)
      * 1011: Internal server error
      * 1013: Try again later (rate limit)
  
  Step 3: Check reconnection behavior
    - Is the adapter attempting reconnect?
    - What's the backoff strategy?
    - Are subscriptions being restored?
  
  Step 4: Test connectivity independently
    - websocat or wscat to test raw WebSocket connection
    - Bypass Tauri to isolate: is it app or network?

FIX PATTERNS:
  Implement robust reconnection:
    async fn handle_disconnect(&mut self, code: u16, reason: &str) {
        match code {
            1000 => return, // Normal close, don't reconnect
            1013 => {
                // Rate limited: longer backoff
                tokio::time::sleep(Duration::from_secs(60)).await;
            }
            _ => {
                // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, max 60s
                let delay = std::cmp::min(2u64.pow(self.retry_count), 60);
                tokio::time::sleep(Duration::from_secs(delay)).await;
            }
        }
        self.reconnect().await;
    }

PREVENTION:
  - Implement heartbeat/ping-pong for all adapters
  - Log all connection state transitions with timestamps
  - Add connection uptime metrics per adapter
  - Implement circuit breaker (stop retrying after N failures)
  - Show connection status per provider in UI

Debug: Message Parsing Failures

SYMPTOM: WebSocket connected but data not appearing in UI, or wrong values shown

HYPOTHESIS TREE:
  H1: Provider changed message format (API version update)
  H2: Unexpected message type (auth response parsed as market data)
  H3: Number precision lost in parsing (f64 vs string representation)
  H4: Field name mismatch (provider uses "p" for price, adapter expects "price")
  H5: Message encoding issue (binary vs text WebSocket frames)

INVESTIGATION:
  Step 1: Log raw messages
    - Add temporary trace!() logging of raw WebSocket messages
    - Capture the exact bytes/text received
    - Compare with provider API documentation
  
  Step 2: Check provider API version
    - Has the provider updated their WebSocket API?
    - Are we specifying an API version in the connection URL?
    - Check provider changelog for breaking changes
  
  Step 3: Validate parsing logic
    - Step through adapter's message handling
    - Check JSON field access paths
    - Verify type conversions (string "123.45" → f64 123.45)
  
  Step 4: Check MarketMessage normalization
    - All adapters must convert to MarketMessage enum
    - Ticker: price, volume, change, etc.
    - OrderBook: bids[], asks[] with price and quantity
    - Trade: price, quantity, side, timestamp
    - Candle: open, high, low, close, volume, timestamp

FIX PATTERNS:
  Provider format change:
    // Document the expected message format in adapter
    // Provider: Binance stream example:
    // {"e":"trade","E":1234567890,"s":"BTCUSDT","p":"50000.00","q":"0.001"}
    
    let price = msg.get("p")
        .and_then(|v| v.as_str())
        .and_then(|s| s.parse::<f64>().ok())
        .ok_or("Missing or invalid price field")?;

PREVENTION:
  - Pin provider API versions where possible
  - Add message schema validation before parsing
  - Log message format changes (detect new fields or missing fields)
  - Add adapter-specific integration tests with recorded messages
  - Monitor parse error rates per adapter

Debug: Subscription State Issues

SYMPTOM: Subscribed to symbol but not receiving data, or receiving data for wrong symbol

HYPOTHESIS TREE:
  H1: Subscription request failed silently (no confirmation check)
  H2: Subscription lost after reconnection (not restored)
  H3: Symbol format wrong for provider (BTC-USD vs BTCUSD vs btcusd)
  H4: Channel/stream name wrong (trades vs trade vs aggTrade)
  H5: Maximum subscription limit reached for provider

INVESTIGATION:
  Step 1: Verify subscription acknowledgment
    - Did provider send subscription confirmation?
    - Some providers: {"result": null, "id": 1} = success
    - Some providers: {"event": "subscribed", "channel": "..."} = success
    - No confirmation within 5s = likely failed
  
  Step 2: Check active_subscriptions state
    - What does the adapter's internal subscription list show?
    - Does it match what the UI thinks is subscribed?
    - After reconnection: was restore_subscriptions called?
  
  Step 3: Verify symbol format
    - Binance: "btcusdt" (lowercase, no separator)
    - Coinbase: "BTC-USD" (uppercase, dash separator)
    - Kraken: "XBT/USD" (legacy naming)
    - Adapter must translate from internal format to provider format

FIX PATTERNS:
  Symbol normalization:
    impl MyAdapter {
        fn normalize_symbol(&self, symbol: &str) -> String {
            // Internal format: "BTC/USD"
            // Provider format: "btcusdt"
            symbol.replace("/", "").to_lowercase()
        }
        
        fn denormalize_symbol(&self, provider_symbol: &str) -> String {
            // Reverse: "btcusdt" → "BTC/USD"
            // Provider-specific logic required
        }
    }

PREVENTION:
  - Always verify subscription acknowledgment from provider
  - Maintain bidirectional symbol mapping per adapter
  - Test subscription restore after simulated disconnection
  - Log all subscription state changes
  - Add subscription health check (periodic verification)

Integration with Fincept Team

F-CTO → F-Debug: "Investigate [error]"
  Debug receives: Error description, stack trace, affected stack layer
  Debug returns: Root cause analysis, fix PR, prevention recommendations

F-QA → F-Debug: "Test found bug in [feature]"
  Debug receives: Bug report, reproduction steps, test output
  Debug returns: Root cause, fix, regression test

F-Execution → F-Debug: "Build failing on [module]"
  Debug receives: Build error, compilation output
  Debug returns: Fix for build issue, dependency resolution

@fincept-orchestrator → F-Debug: "Production issue reported by user"
  Debug receives: User report, logs, environment details
  Debug returns: Investigation report, hotfix if needed, severity assessment

Anti-Patterns

  • Fixing symptoms instead of root causes - A retry loop around a failing operation hides the real bug
  • Debugging without reproduction - If you can't reproduce it, you can't verify the fix
  • Adding log statements and shipping - Debug logging belongs in development, not production (use trace level)
  • Ignoring intermittent failures - "Works on my machine" means a race condition or environment dependency
  • Assuming the bug is in your code - Check provider API changes, OS updates, dependency updates first
  • Not adding prevention - A bug without a test is a bug that will return

Related Skills

  • @systematic-debugging - General debugging methodology (extended by this skill)
  • @fincept-cto - Architecture context for understanding code structure
  • @fincept-qa - Test verification after fixes
  • @fincept-execution - Building fixes and prevention measures
  • @rust-systems-engineering - Deep Rust debugging patterns
  • @web-app-security - Security-related debugging
  • @trading-systems - Trading domain knowledge for financial bugs
Install via CLI
npx skills add https://github.com/AndrewHaward2310/.agents --skill fincept-debug
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator
AndrewHaward2310
AndrewHaward2310 Explore all skills →