Skip to content

feat: Pre-LLM deterministic rule engine — zero-cost pattern matching #116

Description

@ajianaz

Problem

Cora v0.2.0 relies entirely on the LLM for issue detection. This means:

  1. Cost — Every review costs LLM tokens, even for obvious patterns like println! in production code or hardcoded API keys
  2. Inconsistency — The LLM may catch unwrap() on one run but miss it on the next (especially with non-zero temperature)
  3. Coverage gaps — Some patterns are universally wrong (hardcoded secrets, SQL injection) and should NEVER be missed, regardless of LLM quality

Alibaba's Open Code Review uses "Template-engine-based rule matching" as the first pass — deterministic rules that catch known-bad patterns before the LLM even runs. This is a no-brainer for cora.

Proposed Solution: Pre-LLM Rule Engine

Architecture

Step 1: Deterministic Rules (zero LLM tokens, instant)
─────────────────────────────────────────────────────
  Run all rules against changed files → produce findings
  Time: ~10-50ms (regex + simple AST matching)

Step 2: Inject Rule Findings into LLM Prompt
──────────────────────────────────────────────
  [Pre-LLM Analysis]
  ⚠️ src/auth/login.rs:42 — println!() in production code (rule: no-debug)
  🔴 src/api/handler.rs:78 — String concatenation in SQL query (rule: sql-injection)
  ⚠️ Cargo.toml:15 — dependency "serde@1.0.0" has known CVE (rule: audit)

  LLM validates these findings AND discovers deeper issues.

Step 3: LLM Review (same as current)
──────────────────────────────────
  LLM receives rule findings as additional context.
  It can:
  - Confirm the finding (add to output)
  - Dismiss as false positive (omit from output)
  - Expand with deeper analysis (e.g., "yes this is SQL injection, AND here's the fix")

Step 4: Merge
─────────────
  Final output = LLM issues (deduped with rule findings)
  Rule findings NOT confirmed by LLM are still reported
  (deterministic rules don't need LLM validation)

Built-in Rules

Priority 0 — Security (always on):

Rule ID Pattern Severity Languages
sec-hardcoded-secret password =, api_key =, token =, secret = with string literals critical All
sec-sql-concat String concatenation in SQL query patterns ("SELECT *" +, format!("SELECT, f"SELECT) critical All
sec-hardcoded-url http:// (non-HTTPS) in production code major All
sec-tls-disabled tls_built_in_root_certs(false), verify=False, InsecureRequestWarning critical Rust, Python

Priority 1 — Bugs (always on):

Rule ID Pattern Severity Languages
bug-unwrap .unwrap() without error handling on Result/Option minor Rust
bug-expect .expect("...") in production paths (not tests) minor Rust
bug-println println!, dbg!, print! in non-test files minor Rust
bug-todo TODO, FIXME, HACK, XXX comments info All
bug-dead-code Unused imports (language-specific patterns) info All

Priority 2 — Quality (opt-in):

Rule ID Pattern Severity Languages
qual-naming Non-snake_case function names, non-camelCase types info Rust, Go
qual-clone .clone() on large types (potential perf issue) info Rust
qual-error-ignore let _ = result, result?; without logging minor All
qual-long-function Functions >100 lines (heuristic) info All

Custom Rules via Config

Users can define project-specific rules:

# .cora.yaml
rules:
  enabled: true  # default: true
  
  custom:
    - id: "no-console-log"
      pattern: "console\\.(log|debug|info|warn|error)\\("
      severity: "minor"
      languages: ["javascript", "typescript"]
      message: "Remove console.log statements"
      
    - id: "no-hardcoded-port"
      pattern: ":8080|:3000|:5000"
      severity: "info"
      languages: ["all"]
      message: "Use environment variable for port configuration"
      exclude: ["test/", "tests/"]
      
    - id: "no-unwrap-in-handler"
      pattern: "\\.unwrap\\(\\)"
      severity: "major"
      languages: ["rust"]
      message: "Use proper error handling in API handlers"
      exclude: ["tests/", "#\[test"]

Rule Matching Engine

pub struct RuleEngine {
    rules: Vec<Rule>,
}

pub struct Rule {
    id: String,
    pattern: Regex,
    severity: Severity,
    languages: Vec<String>,
    message: String,
    exclude_patterns: Vec<Regex>,  // paths to skip (tests/, generated/)
}

impl RuleEngine {
    pub fn run(&self, diff: &DiffChunk, file_path: &str) -> Vec<RuleFinding> {
        // 1. Check if file language matches rule
        // 2. Check if file path matches any exclude pattern
        // 3. Match regex against changed lines only (not full file)
        // 4. Return findings with file, line, severity, message
    }
}

Performance

  • Time: ~10-50ms for typical PR (regex matching on changed lines only)
  • Token cost: ZERO LLM tokens for rule execution
  • Prompt impact: ~200-400 tokens for rule findings injected into LLM prompt
  • Net effect: Slightly higher prompt tokens (+200) but catches 100% of deterministic patterns

Interaction with Other v0.3 Features

Feature Interaction
Context Chain (#114) Rules run on changed lines → context chain provides depth for LLM confirmation
File Bundling (#115) Rules run PER BUNDLE → findings scoped to bundle's files
Line Positioning (#116) Rule findings have exact line numbers (deterministic) → no positioning drift

--progress Events

{"type":"rules_started","rule_count":12}
{"type":"rules_complete","findings":3,"time_ms":23}
{"type":"rule_finding","rule_id":"sec-sql-concat","file":"src/api/handler.rs","line":78,"severity":"critical"}
{"type":"rule_finding","rule_id":"bug-println","file":"src/auth/login.rs","line":42,"severity":"minor"}
{"type":"rule_finding","rule_id":"bug-todo","file":"src/engine/llm.rs","line":156,"severity":"info"}

Acceptance Criteria

  • 12+ built-in rules covering security, bugs, and quality
  • Rules run only on changed lines (not full file content)
  • Language detection matches rule scope (Rust rules don't fire on Python)
  • exclude patterns skip test files by default
  • Custom rules defined via .cora.yaml with regex pattern + severity
  • Rule findings injected into LLM prompt as context
  • Rule findings reported even if LLM doesn't confirm them
  • rules.enabled: false disables feature
  • --progress emits per-rule events with timing

Risks

Risk Mitigation
False positives on regex rules Exclude patterns for test/generated files; severity "info" for noisy rules
Regex evasion (obfuscated secrets) LLM still catches what rules miss (defense in depth)
Performance on 100+ file PRs Rules only scan changed lines, not full files; ~1ms per file

References

  • Alibaba Open Code Review: "Fine-grained rule matching" — template-engine rules vs language-driven
  • Semgrep: static analysis rule patterns (good reference for regex rule design)
  • Rust clippy: lint rule categorization (must/warn/deny → our critical/major/minor/info)---

Part of #147 (v0.4 Engine Pipeline) — Phase 1

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions