Skip to main content

difflore_core/review/
mod.rs

1mod diff_context;
2mod parse;
3mod pipeline;
4mod prompts;
5mod providers;
6
7pub use diff_context::{
8    DiffContextFile, DiffContextFileChange, DiffContextMode, DiffContextOptions,
9    DiffContextSummary, DiffContextSummaryReason, PackedDiffContext, PackedDiffFile,
10    pack_diff_context,
11};
12pub use pipeline::{
13    ReviewEngine, merge_perspective_issues, run_review, run_review_multi,
14    run_review_multi_with_trajectory, run_review_smart, run_review_with_trajectory,
15    select_review_mode,
16};
17pub use prompts::{SegmentedPrompt, TeamRuleDigest, build_segmented_prompt};
18pub use providers::{AGENT_CLI_SCHEME, agent_cli_sentinel};
19
20use gate4agent::CliTool;
21use providers::call_ai_provider;
22
23/// One-shot completion against the user's active LLM provider.
24///
25/// Cheaper escape hatch for callers (like `difflore fix`'s patch
26/// generator) that need a single round-trip without setting up the full
27/// review pipeline. Looks up the active provider from the DB, decrypts
28/// its API key, and dispatches through the same provider matrix the
29/// review uses (Anthropic native / agent CLI / OpenAI-compatible).
30///
31/// Returns the raw text response — callers parse / validate further.
32pub async fn complete_with_active_provider(
33    db: &sqlx::SqlitePool,
34    system_prompt: &str,
35    user_prompt: &str,
36) -> crate::Result<String> {
37    // Mirror the review pipeline's engine resolution so this helper
38    // honors the same agent-CLI fallback when no HTTP provider is
39    // configured (or active). Without this, fix's patch generator
40    // hard-fails on a clean install while review itself works fine.
41    let engine = pipeline::resolve_review_engine(db).await?;
42    let (provider_name, base_url, api_key, model) = match engine {
43        ReviewEngine::HttpProvider {
44            provider_name,
45            base_url,
46            api_key,
47            model,
48        } => (provider_name, base_url, api_key, model),
49        ReviewEngine::AgentCli { tool, model } => {
50            // call_ai_provider routes to gate4agent when base_url is an
51            // agent-cli sentinel; `model` is forwarded as the CLI's
52            // --model flag (or its per-tool equivalent).
53            let provider_name = match tool {
54                CliTool::ClaudeCode => "claude-cli",
55                CliTool::Codex => "codex-cli",
56                CliTool::Gemini => "gemini-cli",
57                CliTool::OpenCode => "opencode-cli",
58            };
59            (
60                provider_name.to_owned(),
61                agent_cli_sentinel(tool).to_owned(),
62                String::new(),
63                model,
64            )
65        }
66    };
67    call_ai_provider(
68        &provider_name,
69        &base_url,
70        &api_key,
71        &model,
72        system_prompt,
73        user_prompt,
74    )
75    .await
76}
77
78// ── Perspectives ──
79
80/// Review perspective used to specialize the system prompt for a pass.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
82pub enum ReviewPerspective {
83    Safety,
84    Performance,
85    Style,
86    Docs,
87    ApiDesign,
88}
89
90impl ReviewPerspective {
91    /// Stable snake-case identifier used in logs / metadata.
92    pub const fn name(self) -> &'static str {
93        match self {
94            Self::Safety => "safety",
95            Self::Performance => "performance",
96            Self::Style => "style",
97            Self::Docs => "docs",
98            Self::ApiDesign => "api_design",
99        }
100    }
101
102    /// Extra instructions appended to the base system prompt when this
103    /// perspective is active. Each addendum stays narrowly focused so the
104    /// LLM does not re-discover the same issues across passes.
105    pub const fn system_prompt_addendum(self) -> &'static str {
106        match self {
107            Self::Safety => {
108                "\n\n## Perspective: Safety\n\
109                 Focus exclusively on safety, security and correctness concerns: \
110                 unsafe code, injection, auth/authorization, input validation, \
111                 memory safety, null/undefined dereferences, panics, data races, \
112                 secrets exposure, and crash-causing error handling. \
113                 Do NOT report performance or style nits."
114            }
115            Self::Performance => {
116                "\n\n## Perspective: Performance\n\
117                 Focus exclusively on performance and resource-usage concerns: \
118                 algorithmic complexity, unnecessary allocations, N+1 queries, \
119                 blocking calls on hot paths, excessive clones, cache-unfriendly \
120                 access patterns, and memory footprint. \
121                 Do NOT report safety bugs or style nits."
122            }
123            Self::Style => {
124                "\n\n## Perspective: Style\n\
125                 Focus exclusively on style, readability, idioms and maintainability: \
126                 naming, dead code, duplication, API ergonomics, formatting, \
127                 documentation gaps, and convention adherence. \
128                 Do NOT report safety bugs or performance issues."
129            }
130            Self::Docs => {
131                "\n\n## Perspective: Docs\n\
132                 Focus exclusively on documentation completeness and accuracy: \
133                 missing or outdated doc comments, absent public-API rustdoc / \
134                 jsdoc / docstrings, unclear naming that needs explanatory \
135                 commentary, README drift from actual behavior, and examples \
136                 that no longer compile or match the current API. \
137                 Do NOT report safety, performance, or style issues."
138            }
139            Self::ApiDesign => {
140                "\n\n## Perspective: ApiDesign\n\
141                 Focus exclusively on public-API design quality: \
142                 surface-area bloat, leaky abstractions, inconsistent \
143                 naming/casing across the API, footguns (easy-to-misuse \
144                 signatures), breaking-change risk on stable interfaces, \
145                 missing builder patterns where they would reduce \
146                 argument-order mistakes, and return types that should be \
147                 enums or `Result` instead of `bool` / `Option`. \
148                 Do NOT report safety, performance, style, or docs issues."
149            }
150        }
151    }
152
153    /// All perspectives, in the order they should be executed.
154    pub const fn all() -> [Self; 5] {
155        [
156            Self::Safety,
157            Self::Performance,
158            Self::Style,
159            Self::Docs,
160            Self::ApiDesign,
161        ]
162    }
163}
164
165// ── Types ──
166
167#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct ReviewCheckInput {
170    pub project_id: String,
171    pub diff_content: String,
172    pub file_path: Option<String>,
173    pub engine: Option<String>,
174    /// Cloud-side `pr_reviews` row id, when the caller already created
175    /// one (e.g. the VS Code extension host after hitting the cloud
176    /// `createReview` endpoint). When `Some`, `run_review_smart` collects
177    /// trajectory, wall-clock, and token-estimate metrics and posts them to the
178    /// cloud after the review completes. When `None`, cloud telemetry is skipped.
179    #[serde(default)]
180    pub review_id: Option<String>,
181    /// GitHub `owner/repo` for this project. Scopes past-verdict recall
182    /// to THIS repo's rules so the slogan "make AI understand your repo better" holds:
183    /// a diff from repo X should retrieve rules learned from repo X, not
184    /// unrelated repos the user has also indexed. `None` means no
185    /// repo-scoped runtime recall; callers should populate it from
186    /// `git remote get-url origin` whenever possible.
187    #[serde(default)]
188    pub repo_full_name: Option<String>,
189    /// Additional same-project repo scopes discovered from git remotes.
190    /// This lets forked worktrees retrieve rules learned from their
191    /// upstream repository while still avoiding unrelated projects.
192    #[serde(default)]
193    pub repo_full_name_aliases: Vec<String>,
194    /// Latency-sensitive preview mode. Callers such as `difflore fix --preview`
195    /// need the first useful findings or a diagnostic quickly, so the review
196    /// pipeline skips secondary recall/verification/summary passes.
197    #[serde(default)]
198    pub fast_preview: bool,
199}
200
201#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct ReviewIssueRecord {
204    pub severity: String,
205    pub rule: String,
206    pub rule_id: Option<String>,
207    pub message: String,
208    pub file: Option<String>,
209    pub line: Option<i32>,
210    pub suggestion: Option<String>,
211    pub source_badge: Option<String>,
212    /// Perspectives whose pass flagged this issue. Empty for single-pass
213    /// reviews; populated by `review_diff_multi` / merge.
214    #[serde(default)]
215    pub perspectives: Vec<String>,
216    /// Self-check confidence score. Range `[0.0, 1.0]`; higher is more
217    /// confident the issue is a true positive. Defaults to `1.0` so older JSON
218    /// records deserialize unchanged.
219    #[serde(default = "default_confidence")]
220    pub confidence: f32,
221}
222
223pub(crate) const fn default_confidence() -> f32 {
224    1.0
225}
226
227#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
228#[serde(rename_all = "camelCase")]
229pub struct ReviewCheckResult {
230    pub issues: Vec<ReviewIssueRecord>,
231    pub matched_rules: i32,
232    pub matched_rule_ids: Vec<String>,
233    pub matched_rule_titles: Vec<String>,
234    pub prompt_tokens_estimate: i32,
235    pub trace_id: String,
236    /// Optional one-line summary, per-file walkthrough, and blocking counts.
237    #[serde(default)]
238    pub summary: Option<crate::models::ReviewSummary>,
239    /// Per-review stats surfaced to the IDE.
240    #[serde(default)]
241    pub stats: Option<ReviewStats>,
242}
243
244/// Review stats surfaced to the IDE.
245#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
246#[serde(rename_all = "camelCase")]
247pub struct ReviewStats {
248    pub input_tokens: u32,
249    #[serde(default)]
250    pub duration_ms: Option<u64>,
251    pub perspective_count: u32,
252    pub past_verdicts_used: u32,
253    #[serde(default)]
254    pub trajectory_step_count: Option<u32>,
255}
256
257// Self-check and review summary.
258
259/// Thin seam around `call_ai_provider` used by `verify_pass` and
260/// `run_review_summary` so tests can inject canned responses without
261/// spinning up a real HTTP client.
262#[async_trait::async_trait]
263pub trait ReviewLlm: Send + Sync {
264    /// Send a system + user prompt and return the raw model response.
265    async fn chat(&self, system_prompt: &str, user_prompt: &str) -> crate::Result<String>;
266}
267
268/// Production `ReviewLlm` impl that dispatches to the Anthropic native
269/// Messages API when the provider base URL points at `api.anthropic.com`,
270/// and falls through to the OpenAI-compatible `chat/completions` path
271/// otherwise.
272pub struct HttpReviewLlm {
273    pub provider_name: String,
274    pub base_url: String,
275    pub api_key: String,
276    pub model: String,
277}
278
279#[async_trait::async_trait]
280impl ReviewLlm for HttpReviewLlm {
281    async fn chat(&self, system_prompt: &str, user_prompt: &str) -> crate::Result<String> {
282        call_ai_provider(
283            &self.provider_name,
284            &self.base_url,
285            &self.api_key,
286            &self.model,
287            system_prompt,
288            user_prompt,
289        )
290        .await
291    }
292}
293
294/// Local agent-CLI `ReviewLlm` impl. Drives one of `claude` / `codex` /
295/// `gemini` / `opencode` through `gate4agent` and collects the streamed
296/// assistant text. Used when no HTTP provider is active.
297pub struct AgentCliReviewLlm {
298    pub tool: CliTool,
299    pub model: String,
300}
301
302#[async_trait::async_trait]
303impl ReviewLlm for AgentCliReviewLlm {
304    async fn chat(&self, system_prompt: &str, user_prompt: &str) -> crate::Result<String> {
305        providers::call_agent_cli_provider(self.tool, &self.model, system_prompt, user_prompt).await
306    }
307}
308
309#[cfg(test)]
310mod tests;