1use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct PackItem {
23 pub path: String,
24 pub lang: String,
25 pub name: String,
26 pub kind: String,
27 pub line_start: u32,
28 pub line_end: u32,
29 #[serde(skip_serializing_if = "Option::is_none")]
30 pub signature: Option<String>,
31 pub snippet_start: u32,
33 pub code: String,
35 pub reason: String,
37 pub score: f32,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ContextPack {
43 pub task: String,
44 pub budget_tokens: u64,
45 pub used_tokens: u64,
46 pub truncated: bool,
48 pub items: Vec<PackItem>,
49}
50
51pub const CHARS_PER_TOKEN: u64 = 4;
53
54pub fn est_tokens(chars: u64) -> u64 {
56 chars / CHARS_PER_TOKEN
57}
58
59pub fn tokenize(task: &str) -> Vec<String> {
63 let mut terms: Vec<String> = Vec::new();
64 let mut seen = std::collections::HashSet::new();
65 for raw in task.split(|c: char| !(c.is_alphanumeric() || c == '_')) {
66 if raw.is_empty() {
67 continue;
68 }
69 for tok in split_identifier(raw) {
70 if tok.len() < 2 || is_stopword(&tok) {
71 continue;
72 }
73 if seen.insert(tok.clone()) {
74 terms.push(tok);
75 }
76 }
77 }
78 terms
79}
80
81fn is_stopword(t: &str) -> bool {
82 matches!(
83 t,
84 "the"
85 | "a"
86 | "an"
87 | "of"
88 | "to"
89 | "in"
90 | "is"
91 | "for"
92 | "and"
93 | "or"
94 | "how"
95 | "where"
96 | "what"
97 | "does"
98 | "do"
99 | "with"
100 | "on"
101 | "by"
102 | "this"
103 | "that"
104 | "it"
105 | "be"
106 | "as"
107 | "at"
108 | "we"
109 | "i"
110 | "add"
111 | "fix"
112 | "use"
113 | "using"
114 | "make"
115 | "get"
116 | "set"
117 | "all"
118 | "when"
119 | "from"
120 | "into"
121 | "via"
122 | "can"
123 | "should"
124 | "code"
125 | "function"
126 | "method"
127 )
128}
129
130pub fn lexical_score(
132 name: &str,
133 kind: &str,
134 signature: Option<&str>,
135 container: Option<&str>,
136 path: &str,
137 terms: &[String],
138) -> f32 {
139 if terms.is_empty() {
140 return 0.0;
141 }
142 let name_lower = name.to_ascii_lowercase();
143 let name_tokens = split_identifier(name);
144 let sig_lower = signature.map(|s| s.to_ascii_lowercase());
145 let cont_tokens = container.map(split_identifier).unwrap_or_default();
146 let path_lower = path.to_ascii_lowercase();
147
148 let mut score = 0.0f32;
149 for term in terms {
150 if name_lower == *term {
151 score += 20.0;
152 } else if name_tokens.iter().any(|t| t == term) {
153 score += 12.0;
154 } else if name_lower.contains(term.as_str()) {
155 score += 6.0;
156 }
157 if cont_tokens.iter().any(|t| t == term) {
158 score += 4.0;
159 }
160 if let Some(sig) = &sig_lower {
161 if sig.contains(term.as_str()) {
162 score += 3.0;
163 }
164 }
165 if path_lower.contains(term.as_str()) {
166 score += 2.0;
167 }
168 }
169 if score > 0.0 && is_priority_kind(kind) {
170 score += 2.0;
171 }
172 score
173}
174
175fn is_priority_kind(kind: &str) -> bool {
176 matches!(
177 kind,
178 "function"
179 | "method"
180 | "struct"
181 | "class"
182 | "trait"
183 | "interface"
184 | "enum"
185 | "type"
186 | "constructor"
187 | "module"
188 )
189}
190
191pub fn split_identifier(s: &str) -> Vec<String> {
193 let mut tokens = Vec::new();
194 let mut cur = String::new();
195 let mut prev_lower = false;
196 for ch in s.chars() {
197 if ch == '_' || ch == '-' || ch == ' ' {
198 if !cur.is_empty() {
199 tokens.push(std::mem::take(&mut cur));
200 }
201 prev_lower = false;
202 continue;
203 }
204 if ch.is_uppercase() && prev_lower && !cur.is_empty() {
205 tokens.push(std::mem::take(&mut cur));
206 }
207 cur.extend(ch.to_lowercase());
208 prev_lower = ch.is_lowercase() || ch.is_numeric();
209 }
210 if !cur.is_empty() {
211 tokens.push(cur);
212 }
213 tokens
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn tokenizes_and_drops_stopwords() {
222 let t = tokenize("How does the SegmentWriter flush to disk?");
223 assert!(t.contains(&"segment".to_string()));
224 assert!(t.contains(&"writer".to_string()));
225 assert!(t.contains(&"flush".to_string()));
226 assert!(t.contains(&"disk".to_string()));
227 assert!(!t.contains(&"the".to_string()));
228 assert!(!t.contains(&"how".to_string()));
229 }
230
231 #[test]
232 fn scores_name_matches_highest() {
233 let terms = tokenize("flush segment writer");
234 let exact = lexical_score("flush", "function", None, None, "src/a.rs", &terms);
235 let unrelated = lexical_score("zebra", "function", None, None, "src/a.rs", &terms);
236 assert!(exact > unrelated);
237 assert_eq!(unrelated, 0.0);
238 }
239}