use crate::jev::{client::JevClient, questions, schemas::*};
use crate::walk::FileKind;
use crate::windows::{window_batches, windows, CodeWindow};
use futures::{stream, StreamExt};
use serde_json::json;
use std::collections::BTreeMap;
pub struct WindowBatch {
pub file: String,
pub kind: FileKind,
pub windows: Vec<CodeWindow>,
}
pub fn build_batches(file: &str, kind: FileKind, text: &str) -> Vec<WindowBatch> {
let all = windows(text);
window_batches(&all)
.into_iter()
.map(|chunk| WindowBatch {
file: file.to_owned(),
kind,
windows: chunk.to_vec(),
})
.collect()
}
#[derive(serde::Serialize)]
pub struct FileVerdict {
pub file: String,
pub score: f64,
pub best_window: Option<(usize, usize, f64)>,
pub best_snippet: Option<String>,
pub error: Option<String>,
}
struct BatchOutcome {
file: String,
score: f64,
best: Option<(usize, usize, f64, String)>,
error: Option<String>,
}
pub async fn verify_files(
client: &JevClient,
model: &str,
concept: &str,
batches: Vec<WindowBatch>,
concurrency: usize,
) -> Vec<FileVerdict> {
let outcomes: Vec<BatchOutcome> = stream::iter(batches)
.map(|batch| async move { run_batch(client, model, concept, batch).await })
.buffer_unordered(concurrency.max(1))
.collect()
.await;
aggregate(outcomes)
}
async fn run_batch(
client: &JevClient,
model: &str,
concept: &str,
batch: WindowBatch,
) -> BatchOutcome {
let is_doc = batch.kind == FileKind::Doc;
let window_question: fn(usize) -> Question = if is_doc {
questions::doc_window_membership
} else {
questions::window_membership
};
let file_question: fn() -> Question = if is_doc {
questions::doc_file_membership
} else {
questions::file_membership
};
let content_key = if is_doc { "text" } else { "code" };
let mut questions: BTreeMap<String, Question> = batch
.windows
.iter()
.enumerate()
.map(|(i, _)| (format!("window_{i}"), window_question(i)))
.collect();
questions.insert("file".into(), file_question());
questions.insert("file_b".into(), file_question());
let request = JevRequest {
state: json!({
"concept": concept,
"file": batch.file,
"windows": batch.windows.iter().enumerate().map(|(i, w)| json!({
"id": format!("window_{i}"),
"start_line": w.start,
"end_line": w.end,
content_key: w.snippet,
})).collect::<Vec<_>>(),
}),
model: model.into(),
questions,
};
match client.system_one(&request).await {
Ok(resp) => into_outcome(batch, &resp),
Err(e) => BatchOutcome {
file: batch.file,
score: 0.0,
best: None,
error: Some(e.to_string()),
},
}
}
fn into_outcome(batch: WindowBatch, resp: &JevResponse) -> BatchOutcome {
let noul = |key: &str| match resp.answers.get(key) {
Some(Answer::Noul { noul }) => *noul,
_ => 0.0,
};
let mut best: Option<(usize, usize, f64, String)> = None;
for (i, window) in batch.windows.iter().enumerate() {
let p = noul(&format!("window_{i}"));
if best.as_ref().is_none_or(|(_, _, b, _)| p > *b) {
best = Some((window.start, window.end, p, window.snippet.clone()));
}
}
let window_best = best.as_ref().map_or(0.0, |(_, _, p, _)| *p);
let file_score = (noul("file") + noul("file_b")) / 2.0;
BatchOutcome {
file: batch.file,
score: window_best.max(file_score),
best,
error: None,
}
}
fn aggregate(outcomes: Vec<BatchOutcome>) -> Vec<FileVerdict> {
let mut per_file: BTreeMap<String, FileVerdict> = BTreeMap::new();
for outcome in outcomes {
if let Some(error) = &outcome.error {
eprintln!("jevr: warning: {}: {error}", outcome.file);
}
let verdict = per_file
.entry(outcome.file.clone())
.or_insert_with(|| FileVerdict {
file: outcome.file.clone(),
score: 0.0,
best_window: None,
best_snippet: None,
error: None,
});
match outcome.error {
Some(error) => {
if verdict.best_window.is_none() {
verdict.error = Some(error);
}
}
None => {
verdict.error = None;
verdict.score = verdict.score.max(outcome.score);
if let Some((start, end, p, snippet)) = outcome.best {
if verdict.best_window.is_none_or(|(_, _, b)| p > b) {
verdict.best_window = Some((start, end, p));
verdict.best_snippet = Some(snippet);
}
}
}
}
}
per_file.into_values().collect()
}