1use std::fmt::Write as _;
4use std::path::{Path, PathBuf};
5use std::time::{Duration, Instant};
6
7use fallow_engine::source::similar_code::SimilarCodeSourceDigest;
8use rustc_hash::FxHashMap;
9
10mod cache;
11mod protocol;
12mod transport;
13
14pub use protocol::SimilarCodeProviderStatus;
15
16const EMBED_BATCH_SIZE: usize = 1;
17const EMBED_RUN_TIMEOUT: Duration = Duration::from_mins(15);
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub(crate) enum ProviderError {
22 NotReady(String),
24 Failed(String),
26}
27
28impl ProviderError {
29 pub(crate) fn message(&self) -> &str {
30 match self {
31 Self::NotReady(message) | Self::Failed(message) => message,
32 }
33 }
34}
35
36pub(crate) struct ReadyProvider {
38 path: PathBuf,
39 pub(crate) status: SimilarCodeProviderStatus,
40}
41
42pub(crate) struct EmbeddingInput<'a> {
44 pub(crate) source_sha256: SimilarCodeSourceDigest,
45 pub(crate) source: &'a str,
46}
47
48pub(crate) struct EmbeddingResult {
50 pub(crate) vectors: Vec<Option<Vec<f32>>>,
52 pub(crate) cache_hits: usize,
53 pub(crate) cache_misses: usize,
54 pub(crate) cache_writes: usize,
55 pub(crate) cache_invalid_entries: usize,
56 pub(crate) cache_disabled: bool,
57 pub(crate) cache_problem: Option<String>,
58 pub(crate) provider_problem: Option<String>,
59 pub(crate) inference_ms: f64,
60 pub(crate) truncated_functions: usize,
61}
62
63pub(crate) struct EmbeddingBatch {
65 pub(crate) vectors: Vec<EmbeddingBatchVector>,
66 pub(crate) inference_ms: f64,
67 pub(crate) problem: Option<String>,
68}
69
70pub(crate) struct EmbeddingBatchVector {
72 pub(crate) key: u32,
73 pub(crate) values: Vec<f32>,
74 pub(crate) truncated: bool,
75}
76
77pub(crate) trait EmbeddingSession {
79 fn embed(&mut self, functions: &[(u32, &str)]) -> Result<EmbeddingBatch, String>;
80}
81
82pub(crate) trait EmbeddingSessionFactory {
84 fn spawn(&mut self) -> Result<Box<dyn EmbeddingSession>, String>;
85}
86
87struct LocalEmbeddingSession {
88 inner: transport::ProviderSession,
89}
90
91impl EmbeddingSession for LocalEmbeddingSession {
92 fn embed(&mut self, functions: &[(u32, &str)]) -> Result<EmbeddingBatch, String> {
93 let response = self.inner.embed(functions)?;
94 let problem = (response.status != protocol::EmbedCompletionStatus::Complete)
95 .then(|| transport::embed_problem(&response));
96 Ok(EmbeddingBatch {
97 vectors: response
98 .vectors
99 .into_iter()
100 .map(|vector| EmbeddingBatchVector {
101 key: vector.key,
102 values: vector.values,
103 truncated: vector.truncated,
104 })
105 .collect(),
106 inference_ms: response.timing.inference_ms,
107 problem,
108 })
109 }
110}
111
112struct LocalEmbeddingSessionFactory<'a> {
113 path: &'a Path,
114}
115
116impl EmbeddingSessionFactory for LocalEmbeddingSessionFactory<'_> {
117 fn spawn(&mut self) -> Result<Box<dyn EmbeddingSession>, String> {
118 transport::ProviderSession::spawn(self.path)
119 .map(|inner| Box::new(LocalEmbeddingSession { inner }) as Box<dyn EmbeddingSession>)
120 }
121}
122
123struct EmbeddingMiss {
124 representative_index: usize,
125 occurrence_indices: Vec<usize>,
126}
127
128struct EmbeddingPlan {
129 vectors: Vec<Option<Vec<f32>>>,
130 misses: Vec<EmbeddingMiss>,
131 cache_hits: usize,
132 cache_misses: usize,
133 truncated_functions: usize,
134}
135
136pub fn status() -> Result<SimilarCodeProviderStatus, String> {
143 let sidecar = transport::discover_provider()?;
144 let status = transport::provider_status(&sidecar)?;
145 validate_status(&status)?;
146 Ok(status)
147}
148
149pub fn setup_local() -> Result<SimilarCodeProviderStatus, String> {
159 let sidecar = transport::discover_provider()?;
160 let status = transport::setup_provider(&sidecar)?;
161 validate_status(&status)?;
162 if !status.model_ready {
163 return Err("similar-code setup completed without a verified local model".to_owned());
164 }
165 Ok(status)
166}
167
168#[must_use]
170pub fn provider_identity() -> (&'static str, &'static str, usize, &'static str) {
171 (
172 protocol::MODEL_ID,
173 protocol::MODEL_REVISION,
174 protocol::MODEL_DIMENSIONS,
175 protocol::MODEL_LICENSE,
176 )
177}
178
179#[must_use]
181pub fn model_download_bytes() -> u64 {
182 protocol::MODEL_ARTIFACTS
183 .iter()
184 .map(|artifact| artifact.size)
185 .sum()
186}
187
188pub(crate) fn ready_provider() -> Result<ReadyProvider, ProviderError> {
190 let path = transport::discover_provider().map_err(ProviderError::NotReady)?;
191 let status = transport::provider_status(&path).map_err(ProviderError::Failed)?;
192 validate_status(&status).map_err(ProviderError::Failed)?;
193 if !status.model_ready {
194 let problem = status.problem.unwrap_or_else(|| {
195 "the pinned local model is not installed; run `fallow similar-code setup --local`"
196 .to_owned()
197 });
198 return Err(ProviderError::NotReady(problem));
199 }
200 Ok(ReadyProvider { path, status })
201}
202
203pub(crate) fn ready_provider_from_adapter_path(
210 path: &Path,
211) -> Result<ReadyProvider, ProviderError> {
212 if !path.is_file() {
213 return Err(ProviderError::NotReady(format!(
214 "verified similar-code companion is unavailable at {}",
215 path.display()
216 )));
217 }
218 let path = path.to_path_buf();
219 let status = transport::provider_status(&path).map_err(ProviderError::Failed)?;
220 validate_status(&status).map_err(ProviderError::Failed)?;
221 if !status.model_ready {
222 let problem = status.problem.unwrap_or_else(|| {
223 "the pinned local model is not installed; run `fallow similar-code setup --local`"
224 .to_owned()
225 });
226 return Err(ProviderError::NotReady(problem));
227 }
228 Ok(ReadyProvider { path, status })
229}
230
231pub(crate) fn embed_selected(
233 provider: &ReadyProvider,
234 project_root: &Path,
235 no_cache: bool,
236 inputs: &[EmbeddingInput<'_>],
237) -> Result<EmbeddingResult, ProviderError> {
238 let mut factory = LocalEmbeddingSessionFactory {
239 path: &provider.path,
240 };
241 embed_selected_with_factory(
242 Path::new(&provider.status.cache_dir),
243 project_root,
244 no_cache,
245 inputs,
246 EMBED_RUN_TIMEOUT,
247 &mut factory,
248 )
249}
250
251pub(crate) fn embed_selected_with_factory(
253 provider_cache_dir: &Path,
254 project_root: &Path,
255 no_cache: bool,
256 inputs: &[EmbeddingInput<'_>],
257 run_timeout: Duration,
258 factory: &mut dyn EmbeddingSessionFactory,
259) -> Result<EmbeddingResult, ProviderError> {
260 let mut cache = cache::VectorCache::load(provider_cache_dir, project_root, no_cache);
261 let cache_invalid_entries = usize::from(cache.load_state == cache::CacheLoadState::Corrupt);
262 let cache_disabled = cache.load_state == cache::CacheLoadState::Disabled;
263 let mut plan = prepare_embedding_plan(&mut cache, inputs);
264 let mut inference_ms = 0.0f64;
265 let mut provider_problem = None;
266 if !plan.misses.is_empty() {
267 let started = Instant::now();
268 let mut session = None;
269 let mut batch_start = 0usize;
270 while batch_start < plan.misses.len() {
271 if started.elapsed() >= run_timeout {
272 provider_problem =
273 Some("similar-code embedding stopped at the bounded run limit".to_owned());
274 break;
275 }
276 let session = match session.as_mut() {
277 Some(session) => session,
278 None => session.insert(factory.spawn().map_err(ProviderError::Failed)?),
279 };
280 let batch_end = batch_start
281 .saturating_add(EMBED_BATCH_SIZE)
282 .min(plan.misses.len());
283 let request = (batch_start..batch_end)
284 .map(|group_index| {
285 let miss = &plan.misses[group_index];
286 let key = u32::try_from(group_index).map_err(|_| {
287 ProviderError::Failed(
288 "similar-code digest group exceeded protocol capacity".to_owned(),
289 )
290 })?;
291 Ok((key, inputs[miss.representative_index].source))
292 })
293 .collect::<Result<Vec<_>, ProviderError>>()?;
294 match session.embed(&request) {
295 Ok(batch) => {
296 validate_embedding_batch(&batch, &request).map_err(ProviderError::Failed)?;
297 inference_ms += batch.inference_ms;
298 if batch.problem.is_some() {
299 provider_problem = batch.problem;
300 }
301 for vector in batch.vectors {
302 let group_index = usize::try_from(vector.key).map_err(|_| {
303 ProviderError::Failed(
304 "similar-code provider returned an invalid digest-group key"
305 .to_owned(),
306 )
307 })?;
308 apply_embedding_vector(
309 &mut cache,
310 inputs,
311 &mut plan,
312 group_index,
313 &vector.values,
314 vector.truncated,
315 )?;
316 }
317 }
318 Err(error) => {
319 provider_problem = Some(error);
320 break;
321 }
322 }
323 batch_start = batch_end;
324 }
325 }
326 let save = cache.save();
327
328 Ok(EmbeddingResult {
329 vectors: plan.vectors,
330 cache_hits: plan.cache_hits,
331 cache_misses: plan.cache_misses,
332 cache_writes: save.durable_writes,
333 cache_invalid_entries,
334 cache_disabled,
335 cache_problem: save.problem,
336 provider_problem,
337 inference_ms,
338 truncated_functions: plan.truncated_functions,
339 })
340}
341
342fn validate_embedding_batch(batch: &EmbeddingBatch, request: &[(u32, &str)]) -> Result<(), String> {
343 if !batch.inference_ms.is_finite() || batch.inference_ms < 0.0 {
344 return Err("similar-code provider returned invalid timing".to_owned());
345 }
346 let mut expected = request.iter().map(|(key, _)| *key).collect::<Vec<_>>();
347 let mut actual = batch
348 .vectors
349 .iter()
350 .map(|vector| vector.key)
351 .collect::<Vec<_>>();
352 expected.sort_unstable();
353 actual.sort_unstable();
354 if actual.windows(2).any(|pair| pair[0] == pair[1])
355 || actual
356 .iter()
357 .any(|key| expected.binary_search(key).is_err())
358 || batch.vectors.iter().any(|vector| {
359 vector.values.len() != protocol::MODEL_DIMENSIONS
360 || vector.values.iter().any(|value| !value.is_finite())
361 || vector.values.iter().all(|value| *value == 0.0)
362 })
363 || (batch.vectors.len() == request.len()) == batch.problem.is_some()
364 {
365 return Err("similar-code provider returned an invalid embedding batch".to_owned());
366 }
367 Ok(())
368}
369
370fn prepare_embedding_plan(
371 cache: &mut cache::VectorCache,
372 inputs: &[EmbeddingInput<'_>],
373) -> EmbeddingPlan {
374 let mut vectors = vec![None; inputs.len()];
375 let mut misses = Vec::<EmbeddingMiss>::new();
376 let mut miss_groups: FxHashMap<SimilarCodeSourceDigest, usize> = FxHashMap::default();
377 let mut cache_hits = 0usize;
378 let mut cache_misses = 0usize;
379 let mut truncated_functions = 0usize;
380 for (index, input) in inputs.iter().enumerate() {
381 if let Some(entry) = cache.get(&input.source_sha256) {
382 vectors[index] = Some(entry.values.clone());
383 cache_hits = cache_hits.saturating_add(1);
384 truncated_functions =
385 truncated_functions.saturating_add(usize::from(entry.token_truncated));
386 continue;
387 }
388
389 cache_misses = cache_misses.saturating_add(1);
390 if let Some(group_index) = miss_groups.get(&input.source_sha256).copied() {
391 misses[group_index].occurrence_indices.push(index);
392 } else {
393 let group_index = misses.len();
394 miss_groups.insert(input.source_sha256, group_index);
395 misses.push(EmbeddingMiss {
396 representative_index: index,
397 occurrence_indices: vec![index],
398 });
399 }
400 }
401 EmbeddingPlan {
402 vectors,
403 misses,
404 cache_hits,
405 cache_misses,
406 truncated_functions,
407 }
408}
409
410fn apply_embedding_vector(
411 cache: &mut cache::VectorCache,
412 inputs: &[EmbeddingInput<'_>],
413 plan: &mut EmbeddingPlan,
414 group_index: usize,
415 values: &[f32],
416 token_truncated: bool,
417) -> Result<(), ProviderError> {
418 let miss = plan.misses.get(group_index).ok_or_else(|| {
419 ProviderError::Failed(
420 "similar-code provider returned an unknown digest-group key".to_owned(),
421 )
422 })?;
423 let digest = inputs[miss.representative_index].source_sha256;
424 cache.insert(digest, values.to_owned(), token_truncated);
425 if token_truncated {
426 plan.truncated_functions = plan
427 .truncated_functions
428 .saturating_add(miss.occurrence_indices.len());
429 }
430 for occurrence_index in &miss.occurrence_indices {
431 plan.vectors[*occurrence_index] = Some(values.to_owned());
432 }
433 Ok(())
434}
435
436fn clear_vector_cache(provider_cache_dir: &Path, project_root: &Path) -> Result<bool, String> {
438 cache::clear(provider_cache_dir, project_root)
439}
440
441pub fn clear_project_cache(
449 root: &Path,
450 config_path: Option<&Path>,
451 allow_remote_extends: bool,
452) -> Result<bool, String> {
453 let project = fallow_engine::project_config::config_for_project_with_load_options(
454 root,
455 config_path,
456 fallow_config::ConfigLoadOptions {
457 allow_remote_extends,
458 },
459 )
460 .map_err(|error| format!("failed to load config: {error}"))?;
461 let provider = status()?;
462 clear_vector_cache(Path::new(&provider.cache_dir), &project.config.root)
463}
464
465pub(crate) fn model_artifact_sha256() -> &'static str {
466 protocol::MODEL_ARTIFACTS
467 .first()
468 .map_or("", |artifact| artifact.sha256)
469}
470
471pub(crate) fn parameter_sha256() -> String {
472 let digest = cache::parameter_digest();
473 digest.iter().fold(
474 String::with_capacity(digest.len().saturating_mul(2)),
475 |mut output, byte| {
476 let _ = write!(output, "{byte:02x}");
477 output
478 },
479 )
480}
481
482pub(crate) const fn embedding_batch_size() -> usize {
483 EMBED_BATCH_SIZE
484}
485
486pub(crate) const fn embedding_semantics_version() -> u32 {
487 protocol::EMBEDDING_SEMANTICS_VERSION
488}
489
490fn validate_status(status: &SimilarCodeProviderStatus) -> Result<(), String> {
491 if status.protocol_version != protocol::WIRE_PROTOCOL_VERSION
492 || status.embedding_semantics_version != protocol::EMBEDDING_SEMANTICS_VERSION
493 || status.sidecar_version != env!("CARGO_PKG_VERSION")
494 || status.model_id != protocol::MODEL_ID
495 || status.model_revision != protocol::MODEL_REVISION
496 || status.dimensions != protocol::MODEL_DIMENSIONS
497 || status.max_tokens != protocol::MODEL_MAX_TOKENS
498 || status.license != protocol::MODEL_LICENSE
499 || status.download_bytes != model_download_bytes()
500 || !status.analysis_offline
501 || (status.model_ready && !status.integrity_verified)
502 {
503 return Err(
504 "similar-code companion provenance does not match this Fallow release".to_owned(),
505 );
506 }
507 Ok(())
508}
509
510#[cfg(test)]
511#[expect(
512 clippy::unwrap_used,
513 reason = "test fixture construction must fail immediately"
514)]
515mod tests {
516 use super::*;
517
518 #[test]
519 fn duplicate_digest_is_inferred_once_and_warm_truncation_is_occurrence_aware() {
520 let temp = tempfile::tempdir().unwrap();
521 let cache_root = temp.path().join("user-cache");
522 let provider_cache_dir = cache_root.join("models").join(protocol::MODEL_REVISION);
523 let project_root = temp.path().join("project");
524 std::fs::create_dir_all(&cache_root).unwrap();
525 std::fs::create_dir_all(&project_root).unwrap();
526 let digest = SimilarCodeSourceDigest::new([8; 32]);
527 let inputs = [
528 EmbeddingInput {
529 source_sha256: digest,
530 source: "function same() { return 1; }",
531 },
532 EmbeddingInput {
533 source_sha256: digest,
534 source: "function same() { return 1; }",
535 },
536 ];
537
538 let mut cache = cache::VectorCache::load(&provider_cache_dir, &project_root, false);
539 let mut cold = prepare_embedding_plan(&mut cache, &inputs);
540 assert_eq!(cold.cache_misses, 2);
541 assert_eq!(cold.misses.len(), 1);
542 assert_eq!(cold.misses[0].occurrence_indices, vec![0, 1]);
543 apply_embedding_vector(
544 &mut cache,
545 &inputs,
546 &mut cold,
547 0,
548 &[0.25; protocol::MODEL_DIMENSIONS],
549 true,
550 )
551 .unwrap();
552 assert_eq!(cold.truncated_functions, 2);
553 assert!(cold.vectors.iter().all(Option::is_some));
554 assert_eq!(cache.save().durable_writes, 1);
555
556 let mut cache = cache::VectorCache::load(&provider_cache_dir, &project_root, false);
557 let warm = prepare_embedding_plan(&mut cache, &inputs);
558 assert_eq!(warm.cache_hits, 2);
559 assert_eq!(warm.cache_misses, 0);
560 assert_eq!(warm.truncated_functions, 2);
561 assert!(warm.misses.is_empty());
562 assert!(warm.vectors.iter().all(Option::is_some));
563 }
564}