difflore_core/mcp_server/
mod.rs1mod hook;
8mod hook_short_circuit;
9mod recall_sampler;
10mod schemas;
11mod serve_render;
12mod server;
13mod skill_docs;
14mod tools;
15mod trust_proof;
16
17pub use hook::{HookRuleContext, fetch_relevant_rules_for_hook, run};
18pub(crate) use tools::{HistoricalPr, predict_scope_from_corpus};
19pub use tools::{
20 detect_active_model, haiku_auto_disable_active, is_haiku_model, origin_to_kind,
21 parse_file_patterns,
22};
23
24pub async fn predict_pr_scope(
30 db: &sqlx::SqlitePool,
31 intent: &str,
32 top_k: usize,
33) -> serde_json::Value {
34 let corpus = tools::load_pr_corpus(db).await;
35 predict_scope_from_corpus(&corpus, intent, top_k.clamp(1, 20))
36}
37
38pub async fn predict_pr_scope_for_repos(
45 db: &sqlx::SqlitePool,
46 intent: &str,
47 top_k: usize,
48 repo_scopes: &[String],
49) -> serde_json::Value {
50 let corpus = tools::load_pr_corpus(db).await;
51 let scoped = repo_scoped_plan_corpus(&corpus, repo_scopes);
52 let no_repo_scope_memory = !repo_scopes.is_empty() && scoped.is_empty();
53 let mut prediction = if no_repo_scope_memory {
54 predict_scope_from_corpus(&[], intent, top_k.clamp(1, 20))
55 } else if scoped.is_empty() {
56 predict_scope_from_corpus(&corpus, intent, top_k.clamp(1, 20))
57 } else {
58 predict_scope_from_corpus(&scoped, intent, top_k.clamp(1, 20))
59 };
60 if let Some(obj) = prediction.as_object_mut() {
61 obj.insert(
62 "repo_scope".to_owned(),
63 serde_json::json!({
64 "requested": repo_scopes,
65 "matched_prs": scoped.len(),
66 "no_repo_scope_memory": no_repo_scope_memory,
67 }),
68 );
69 }
70 prediction
71}
72
73pub(crate) fn repo_scoped_plan_corpus(
74 corpus: &[HistoricalPr],
75 repo_scopes: &[String],
76) -> Vec<HistoricalPr> {
77 let scopes = repo_scopes
78 .iter()
79 .map(|repo| repo.trim().to_ascii_lowercase())
80 .filter(|repo| !repo.is_empty())
81 .collect::<std::collections::BTreeSet<_>>();
82 if scopes.is_empty() {
83 return Vec::new();
84 }
85 corpus
86 .iter()
87 .filter(|pr| scopes.contains(&pr.repo.to_ascii_lowercase()))
88 .cloned()
89 .collect()
90}
91
92#[cfg(test)]
93mod repo_scope_tests {
94 use super::*;
95
96 #[test]
97 fn repo_scoped_plan_corpus_keeps_current_repo_aliases() {
98 let corpus = vec![
99 historical_pr("gin-gonic/gin", 4542),
100 historical_pr("tanstack/router", 7150),
101 historical_pr("difflore-fixtures/gin", 4542),
102 ];
103
104 let scoped = repo_scoped_plan_corpus(
105 &corpus,
106 &[
107 "difflore-fixtures/gin".to_owned(),
108 "gin-gonic/gin".to_owned(),
109 ],
110 );
111
112 assert_eq!(scoped.len(), 2);
113 assert!(scoped.iter().all(|pr| pr.repo.ends_with("/gin")));
114 }
115
116 #[test]
117 fn repo_scoped_plan_corpus_returns_empty_without_matches() {
118 let corpus = vec![historical_pr("tanstack/router", 7150)];
119
120 let scoped = repo_scoped_plan_corpus(&corpus, &["gin-gonic/gin".to_owned()]);
121
122 assert!(scoped.is_empty());
123 }
124
125 fn historical_pr(repo: &str, pr_number: i32) -> HistoricalPr {
126 HistoricalPr {
127 repo: repo.to_owned(),
128 pr_number,
129 text: String::new(),
130 files: Vec::new(),
131 tokens: Vec::new(),
132 }
133 }
134}
135
136pub(crate) use hook::detect_git_remote_owner_repos;
139#[cfg(test)]
140pub(crate) use hook::parse_github_owner_repo;
141pub(crate) use server::{
142 AVG_FULL_RULE_TOKENS, McpState, build_cost_meta, emit_trajectory_step, estimate_tokens,
143 handle_message, jsonrpc_error, rule_hits_by_origin,
144};
145#[cfg(test)]
146pub(crate) use server::{parse_signature_uri, parse_verdict_uri};
147#[cfg(test)]
148pub(crate) use tools::{disabled_response, rule_injection_disabled};
149
150#[cfg(test)]
151mod tests;