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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions