use std::path::Path;
use std::process::Command;
use rto_graph::reviewer::FileUnderReview;
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
use {
rto_graph::compile_claim::{CheckRun, suppression},
rto_graph::review_score::{CandidateFinding, CandidateRun},
rto_graph::reviewer::{SINGLE_CALL_BUDGET_TOKENS, build_prompt, claim_site, parse_findings},
};
use rto_graph::reviewer::GraphContext;
use std::collections::BTreeSet;
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
const REVIEW_MAX_TOKENS: u32 = 4_096;
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
const REVIEW_N_CTX: u32 = 49_152;
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
const REVIEW_PROMPT_BUDGET: usize = SINGLE_CALL_BUDGET_TOKENS;
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
const _: () = assert!(
REVIEW_N_CTX as usize >= (REVIEW_PROMPT_BUDGET * 13 / 10) + REVIEW_MAX_TOKENS as usize,
"REVIEW_N_CTX must hold a 30%-underestimated prompt plus the generation"
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReviewArm {
DiffOnly,
Graph,
}
impl ReviewArm {
#[must_use]
pub fn tag(self) -> &'static str {
match self {
Self::DiffOnly => "diff-only",
Self::Graph => "graph",
}
}
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
pub struct FileOutcome {
pub findings: Vec<CandidateFinding>,
pub suppressed: Vec<(CandidateFinding, String)>,
pub unparsed: usize,
pub declared_clean: bool,
pub reasoning_truncated: bool,
pub dropped_tokens: usize,
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
pub fn review_file(
engine: &rto_llama::llama::LlamaEngine,
model: &str,
file: &FileUnderReview,
context: &GraphContext,
checks: &[CheckRun],
sources: &dyn Fn(&str) -> Option<String>,
) -> anyhow::Result<FileOutcome> {
use rto_llama::Engine as _;
let prompt = build_prompt(file, context, REVIEW_PROMPT_BUDGET);
let completion = engine
.chat(&rto_llama::ChatRequest {
model: model.to_owned(),
messages: vec![rto_llama::Message {
role: "user".to_owned(),
content: prompt.text,
}],
images: vec![],
audio: vec![],
temperature: 0.0,
max_tokens: REVIEW_MAX_TOKENS,
})
.map_err(|e| anyhow::anyhow!("reviewing {}: {e}", file.path))?;
let reply = crate::strip_thinking_public(&completion.content);
if std::env::var_os("ROTEIRO_REVIEW_DEBUG").is_some() {
eprintln!(
"--- {} ({} prompt tokens est.)\n{reply}\n---",
file.path, prompt.tokens
);
}
let parsed = parse_findings(&file.reviewed_sha, &file.path, &reply);
let mut findings = Vec::new();
let mut withheld = Vec::new();
for finding in parsed.findings {
if !finding.claims_compile_failure || checks.is_empty() {
findings.push(finding);
continue;
}
let source = sources(&file.path).unwrap_or_default();
let parent = parent_module_source(&file.path, sources);
let site = claim_site(
&file.reviewed_sha,
&file.path,
finding.line,
&source,
parent.as_deref(),
);
let verdict = suppression(&site, checks);
if verdict.is_refuted() {
withheld.push((finding, verdict.reason().to_owned()));
} else {
findings.push(finding);
}
}
Ok(FileOutcome {
findings,
suppressed: withheld,
unparsed: parsed.unparsed.len(),
declared_clean: parsed.declared_clean,
reasoning_truncated: parsed.reasoning_truncated,
dropped_tokens: prompt.dropped_tokens,
})
}
#[cfg(any(feature = "serve", feature = "inference-local-models", test))]
fn graph_at(
repo: &rto_graph::Repo,
cache: &rto_graph::ObjectCache,
ingest: rto_graph::IngestConfig,
arm: ReviewArm,
sha: &str,
) -> anyhow::Result<Option<rto_graph::Store>> {
if arm == ReviewArm::DiffOnly {
return Ok(None);
}
let mut store = rto_graph::Store::open_in_memory()?;
crate::build_graph_at_rev(repo, &mut store, cache, ingest, sha)?;
Ok(Some(store))
}
#[cfg(any(feature = "serve", feature = "inference-local-models", test))]
fn context_for(
graph: Option<&rto_graph::Store>,
file: &FileUnderReview,
sources: &dyn Fn(&str) -> Option<String>,
) -> anyhow::Result<GraphContext> {
match graph {
None => Ok(GraphContext::none()),
Some(store) => {
let annotated = rto_graph::reviewer::annotate_diff(&file.diff);
graph_context_for(store, file, &annotated, sources)
}
}
}
#[cfg(any(feature = "serve", feature = "inference-local-models", test))]
fn worktree_graph(
repo: &Path,
ingest: rto_graph::IngestConfig,
) -> anyhow::Result<rto_graph::Store> {
let graph_repo = rto_graph::Repo::discover(repo)?;
let cache =
rto_graph::ObjectCache::open(graph_repo.common_dir().join("roteiro").join("objects"))?;
let mut store = rto_graph::Store::open_in_memory()?;
let registry = rto_graph::Registry::new(ingest);
rto_graph::sync_worktree(&mut store, &graph_repo, &cache, ®istry)?;
crate::apply_authored_layer(&mut store, graph_repo.walk_blobs()?, &|blob| {
Ok(graph_repo
.workdir()
.and_then(|w| std::fs::read(w.join(&blob.path)).ok()))
})?;
Ok(store)
}
#[cfg(any(feature = "serve", feature = "inference-local-models", test))]
pub fn graph_context_for(
store: &rto_graph::Store,
file: &FileUnderReview,
annotated_diff: &str,
markdown_at: &dyn Fn(&str) -> Option<String>,
) -> anyhow::Result<GraphContext> {
use rto_graph::reviewer::{ContextItem, doc_already_shown, section_body};
use rto_graph::{NodeKind, Provenance};
let symbols: Vec<rto_graph::Node> = store
.nodes_by_path(&file.path)?
.into_iter()
.filter(|n| !matches!(n.kind, NodeKind::File | NodeKind::Marker))
.collect();
let mut governing: std::collections::BTreeMap<String, BTreeSet<String>> =
std::collections::BTreeMap::new();
for sym in &symbols {
for edge in store.edges_to(&sym.key)? {
if edge.provenance == Provenance::Authored {
governing
.entry(edge.src.clone())
.or_default()
.insert(sym.name.clone());
}
}
}
let mut items = Vec::new();
for (section_key, governed) in governing {
let Some(node) = store.get_node(§ion_key)? else {
continue;
};
let Some(path) = node.path.as_deref() else {
continue;
};
let Some(markdown) = markdown_at(path) else {
continue;
};
let Some(body) = section_body(&markdown, &node.name) else {
continue;
};
if body.is_empty() {
continue;
}
let governed: Vec<&str> = governed.iter().map(String::as_str).collect();
items.push(ContextItem {
label: format!(
"{} \u{a7}{} ({}) \u{2014} governs {}",
node.key.split('#').next().unwrap_or(&node.key),
node.name,
path,
governed.join(", ")
),
provenance: "authored".to_owned(),
body,
});
}
for sym in &symbols {
let Some(doc) = sym.meta.get("content").and_then(serde_json::Value::as_str) else {
continue;
};
if doc_already_shown(doc, annotated_diff) {
continue;
}
items.push(ContextItem {
label: format!(
"doc comment of {} `{}` \u{2014} elsewhere in this file, not in the diff",
sym.kind.as_str(),
sym.name
),
provenance: "derived".to_owned(),
body: doc.to_owned(),
});
}
Ok(GraphContext::fit(
items,
rto_graph::reviewer::estimate_tokens(annotated_diff),
))
}
fn parent_module_source(path: &str, sources: &dyn Fn(&str) -> Option<String>) -> Option<String> {
let dir = match path.rsplit_once('/')? {
(d, "mod.rs") => d.rsplit_once('/').map_or(d, |(up, _)| up),
(d, _) => d,
};
for candidate in [
format!("{dir}/mod.rs"),
format!("{dir}/lib.rs"),
format!("{dir}/main.rs"),
format!("{dir}.rs"),
] {
if candidate == path {
continue;
}
if let Some(text) = sources(&candidate) {
return Some(text);
}
}
None
}
fn git(repo: &Path, args: &[&str]) -> Option<String> {
let out = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.ok()?;
out.status
.success()
.then(|| String::from_utf8_lossy(&out.stdout).trim_end().to_owned())
}
pub fn fork_point(repo: &Path, sha: &str, main: &str) -> anyhow::Result<String> {
let is_ancestor = |a: &str, b: &str| {
Command::new("git")
.arg("-C")
.arg(repo)
.args(["merge-base", "--is-ancestor", a, b])
.status()
.is_ok_and(|s| s.success())
};
let merges = git(
repo,
&[
"rev-list",
"--merges",
"--ancestry-path",
&format!("{sha}..{main}"),
],
)
.unwrap_or_default();
let found = merges.lines().rev().find_map(|m| {
let parents = git(repo, &["rev-list", "--parents", "-n1", m])?;
let mut it = parents.split_whitespace().skip(1);
let (p1, p2) = (it.next()?, it.next()?);
(is_ancestor(sha, p2) && !is_ancestor(sha, p1))
.then(|| git(repo, &["merge-base", p1, sha]))
.flatten()
});
match found {
Some(base) => Ok(base),
None => git(repo, &["merge-base", main, sha])
.ok_or_else(|| anyhow::anyhow!("git cannot resolve a merge base for {sha}")),
}
}
#[derive(Debug, Default)]
pub struct ReviewSet {
pub files: Vec<FileUnderReview>,
pub skipped: Vec<String>,
}
impl ReviewSet {
fn collect(reviewed_sha: &str, names: &str, diff_of: &dyn Fn(&str) -> Option<String>) -> Self {
let mut set = Self::default();
for path in names.lines().filter(|p| !p.is_empty()) {
let diff = diff_of(path).unwrap_or_default();
if !diff.contains("@@") {
set.skipped.push(path.to_owned());
continue;
}
set.files.push(FileUnderReview {
reviewed_sha: reviewed_sha.to_owned(),
path: path.to_owned(),
diff,
});
}
set
}
}
pub fn files_at(repo: &Path, sha: &str, main: &str) -> anyhow::Result<ReviewSet> {
let fork = fork_point(repo, sha, main)?;
anyhow::ensure!(
fork != sha,
"the reconstruction base for {sha} is the review commit itself, so the diff \
would be empty and every finding would score zero"
);
let names = git(repo, &["diff", "--name-only", &fork, sha])
.ok_or_else(|| anyhow::anyhow!("git diff --name-only {fork}..{sha} failed"))?;
Ok(ReviewSet::collect(sha, &names, &|path| {
git(repo, &["diff", "-U3", &fork, sha, "--", path])
}))
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
fn blob_at(repo: &Path, sha: &str, path: &str) -> Option<String> {
git(repo, &["show", &format!("{sha}:{path}")])
}
fn main_ref(repo: &Path) -> anyhow::Result<String> {
["origin/main", "main"]
.into_iter()
.find(|r| git(repo, &["rev-parse", "--verify", "--quiet", r]).is_some())
.map(str::to_owned)
.ok_or_else(|| {
anyhow::anyhow!(
"neither `origin/main` nor `main` resolves in {}",
repo.display()
)
})
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
#[derive(Debug, Default)]
pub struct ReplayReport {
pub files: usize,
pub commits: usize,
pub findings: usize,
pub suppressed: usize,
pub clean: usize,
pub unparsed: usize,
pub truncated: usize,
pub reasoning_truncated: usize,
pub anchored_files: usize,
pub skipped: usize,
pub context_items: usize,
pub context_dropped: usize,
pub context_tokens: usize,
pub files_with_context: usize,
pub refused: Vec<String>,
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
pub fn run_replay(
repo: &Path,
out: &str,
checks_path: Option<&str>,
limit: Option<usize>,
arm: ReviewArm,
ingest: rto_graph::IngestConfig,
) -> anyhow::Result<()> {
let corpus = rto_graph::review_corpus::builtin()?;
let main = main_ref(repo)?;
let checks = match checks_path {
Some(p) => read_checks(p)?,
None => Vec::new(),
};
if checks.is_empty() {
eprintln!(
"note: no --checks evidence supplied, so no compile claim can be refuted \
and none will be withheld. That is the conservative default, not a \
disabled filter: `compile_claim` is opt-in on evidence."
);
}
let choice = rto_graph::resolve_model(rto_graph::ModelTask::Review)?;
let model = choice.require_installed()?;
eprintln!("reviewing with {model} — {}", choice.why());
let engine = rto_llama::llama::LlamaEngine::new(
vec![rto_llama::llama::Served {
name: model.to_owned(),
path: rto_graph::model_dir(model).join("model.gguf"),
mmproj: None,
}],
REVIEW_N_CTX,
)
.map_err(|e| anyhow::anyhow!("starting llama.cpp: {e}"))?;
let anchors: BTreeSet<(&str, &str)> = corpus
.rows()
.iter()
.map(|r| (r.reviewed_sha.as_str(), r.path.as_str()))
.collect();
let shas: Vec<&str> = corpus.reviewed_shas().into_iter().collect();
let shas = match limit {
Some(n) => &shas[..n.min(shas.len())],
None => &shas[..],
};
let mut run = CandidateRun {
arm: Some(rto_graph::review_score::RunArm {
context: arm.tag().to_owned(),
model: model.to_owned(),
}),
..CandidateRun::default()
};
let mut report = ReplayReport::default();
let graph_repo = rto_graph::Repo::discover(repo)?;
let object_cache =
rto_graph::ObjectCache::open(graph_repo.common_dir().join("roteiro").join("objects"))?;
for (idx, sha) in shas.iter().enumerate() {
let set = files_at(repo, sha, &main)?;
let graph = graph_at(&graph_repo, &object_cache, ingest, arm, sha)?;
run.attempted_shas.insert((*sha).to_owned());
report.commits += 1;
report.skipped += set.skipped.len();
eprintln!(
"[{}/{}] {} — {} file(s){}",
idx + 1,
shas.len(),
&sha[..8],
set.files.len(),
if set.skipped.is_empty() {
String::new()
} else {
format!(", {} with no reviewable diff", set.skipped.len())
}
);
for file in &set.files {
let sources = |p: &str| blob_at(repo, sha, p);
let context = context_for(graph.as_ref(), file, &sources)?;
report.context_items += context.items.len();
report.context_dropped += context.dropped_items;
report.context_tokens += context.tokens();
report.files_with_context += usize::from(!context.is_empty());
let outcome = match review_file(&engine, model, file, &context, &checks, &sources) {
Ok(outcome) => outcome,
Err(e) => {
eprintln!(" refused {}: {e}", file.path);
report.refused.push(file.path.clone());
continue;
}
};
report.files += 1;
report.findings += outcome.findings.len();
report.suppressed += outcome.suppressed.len();
report.unparsed += outcome.unparsed;
report.clean += usize::from(outcome.declared_clean);
report.truncated += usize::from(outcome.dropped_tokens > 0);
report.reasoning_truncated += usize::from(outcome.reasoning_truncated);
report.anchored_files += usize::from(anchors.contains(&(*sha, file.path.as_str())));
run.findings.extend(outcome.findings);
run.suppressed
.extend(outcome.suppressed.into_iter().map(|(f, _)| f));
}
}
let json = serde_json::to_string_pretty(&run)?;
std::fs::write(out, format!("{json}\n")).map_err(|e| anyhow::anyhow!("writing {out}: {e}"))?;
print_replay(&report, out);
Ok(())
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
fn print_replay(report: &ReplayReport, out: &str) {
println!(
"\nreviewed {} file(s) over {} commit(s)",
report.files, report.commits
);
println!(
" {} finding(s) emitted, of which the corpus can judge at most those on \
the {} file(s) carrying an adjudicated row",
report.findings, report.anchored_files
);
if report.files > 0 {
#[expect(
clippy::cast_precision_loss,
reason = "file and finding counts here are in the hundreds"
)]
let per_file = report.findings as f64 / report.files as f64;
println!(" {per_file:.2} finding(s) per file — the human-cost rate");
}
if report.files_with_context > 0 || report.context_dropped > 0 {
println!(
" graph context: {} item(s), ~{} token(s), over {} of {} file(s){}",
report.context_items,
report.context_tokens,
report.files_with_context,
report.files,
if report.context_dropped > 0 {
format!("; {} item(s) dropped by the cap", report.context_dropped)
} else {
String::new()
}
);
}
if report.skipped > 0 {
println!(
" {} changed path(s) had no reviewable diff (binary, mode or rename) \
and were not sent to the model",
report.skipped
);
}
println!(
" {} file(s) declared clean in the required form",
report.clean
);
println!(
" {} compile claim(s) withheld by the filter",
report.suppressed
);
if report.unparsed > 0 {
println!(
" {} line(s) looked like findings but carried no usable anchor — \
a prompt problem, not a recall one",
report.unparsed
);
}
if report.truncated > 0 {
println!(
" {} file(s) had their diff truncated to fit the budget, so those \
reviews are of PART of the file",
report.truncated
);
}
if !report.refused.is_empty() {
println!(
" {} file(s) the engine refused, so they were not reviewed at all:",
report.refused.len()
);
for path in &report.refused {
println!(" {path}");
}
}
println!("\nwrote {out} — score it with: roteiro review --score {out}");
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
fn read_checks(path: &str) -> anyhow::Result<Vec<CheckRun>> {
let text =
std::fs::read_to_string(path).map_err(|e| anyhow::anyhow!("reading checks {path}: {e}"))?;
serde_json::from_str(&text).map_err(|e| anyhow::anyhow!("{path}: {e}"))
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
fn changed_files(repo: &Path, base: Option<&str>) -> ReviewSet {
let head = git(repo, &["rev-parse", "HEAD"]).unwrap_or_else(|| "HEAD".to_owned());
let range: Vec<String> = match base {
Some(b) => vec![b.to_owned(), "HEAD".to_owned()],
None => vec!["HEAD".to_owned()],
};
let mut args: Vec<&str> = vec!["diff", "--name-only"];
args.extend(range.iter().map(String::as_str));
let names = git(repo, &args).unwrap_or_default();
ReviewSet::collect(&head, &names, &|path| {
let mut d: Vec<&str> = vec!["diff", "-U3"];
d.extend(range.iter().map(String::as_str));
d.extend(["--", path]);
git(repo, &d)
})
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
fn announce_unreviewable(skipped: &[String]) {
if skipped.is_empty() {
return;
}
println!(
"{} changed path(s) NOT REVIEWED — no hunk to anchor a finding to (binary \
blob, or a mode/rename-only change), so the model is not asked for a \
`line=` it could not cite:",
skipped.len()
);
for path in skipped {
println!(" {path}");
}
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
pub fn run_llm(
repo: &Path,
base: Option<&str>,
checks_path: Option<&str>,
arm: ReviewArm,
ingest: rto_graph::IngestConfig,
) -> anyhow::Result<()> {
let checks = match checks_path {
Some(p) => read_checks(p)?,
None => Vec::new(),
};
let ReviewSet { files, skipped } = changed_files(repo, base);
if files.is_empty() && skipped.is_empty() {
println!("no changes to review");
return Ok(());
}
announce_unreviewable(&skipped);
if files.is_empty() {
println!("\nnothing reviewable in the change");
return Ok(());
}
let choice = rto_graph::resolve_model(rto_graph::ModelTask::Review)?;
let model = choice.require_installed()?;
eprintln!(
"reviewing {} file(s) with {model} — {}",
files.len(),
choice.why()
);
if cfg!(debug_assertions) {
eprintln!(
"note: unoptimized build — local generation is very slow; use a \
release build (`cargo build --release`) for usable speed."
);
}
let engine = rto_llama::llama::LlamaEngine::new(
vec![rto_llama::llama::Served {
name: model.to_owned(),
path: rto_graph::model_dir(model).join("model.gguf"),
mmproj: None,
}],
REVIEW_N_CTX,
)
.map_err(|e| anyhow::anyhow!("starting llama.cpp: {e}"))?;
let graph = match arm {
ReviewArm::DiffOnly => None,
ReviewArm::Graph => Some(worktree_graph(repo, ingest)?),
};
let mut total = 0usize;
let mut withheld = 0usize;
let mut never_reviewed: Vec<&str> = Vec::new();
for file in &files {
let sources = |p: &str| std::fs::read_to_string(repo.join(p)).ok();
let context = context_for(graph.as_ref(), file, &sources)?;
let outcome = review_file(&engine, model, file, &context, &checks, &sources)?;
if outcome.reasoning_truncated {
never_reviewed.push(file.path.as_str());
}
if outcome.findings.is_empty() && outcome.suppressed.is_empty() {
continue;
}
println!("\n{}", file.path);
for f in &outcome.findings {
let class = f.defect_class.map_or("unclassified", |c| c.as_str());
println!(" {}:{} [{class}] {}", file.path, f.line, f.description);
total += 1;
}
for (f, reason) in &outcome.suppressed {
println!(" {}:{} [withheld] {}", file.path, f.line, f.description);
println!(" {reason}");
withheld += 1;
}
}
println!(
"\n{total} finding(s) over {} file(s); {withheld} compile claim(s) withheld",
files.len()
);
if !never_reviewed.is_empty() {
println!(
"\n{} of those file(s) were NOT REVIEWED — the reply stopped inside a \
reasoning block before reaching an answer, so a low finding count here \
says nothing about the code:",
never_reviewed.len()
);
for path in &never_reviewed {
println!(" {path}");
}
println!(
" Use a non-reasoning model, or raise the generation cap \
(currently {REVIEW_MAX_TOKENS} tokens)."
);
}
println!(
"These are one model's opinions, unadjudicated. `docs/REVIEW_CHECKLIST.md` \
has the triage rule; the corpus in `crates/rto-graph/tests/fixtures/review/` \
is what any of it is measured against."
);
Ok(())
}
#[cfg(test)]
#[must_use]
pub fn corpus_shas() -> Vec<String> {
rto_graph::review_corpus::builtin()
.map(|c| c.reviewed_shas().into_iter().map(str::to_owned).collect())
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::{
FileUnderReview, ReviewArm, ReviewSet, context_for, corpus_shas, files_at, fork_point,
graph_at, main_ref, parent_module_source,
};
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
fn repo() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..")
}
fn history_available(repo: &Path) -> bool {
let ok = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "--is-inside-work-tree"])
.output()
.is_ok_and(|o| o.status.success());
if !ok {
eprintln!("SKIP: not a git work tree, cannot reconstruct reviewed diffs");
return false;
}
let shallow = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "--is-shallow-repository"])
.output()
.is_ok_and(|o| String::from_utf8_lossy(&o.stdout).trim() == "true");
if shallow {
eprintln!("SKIP: shallow clone — run `git fetch --unshallow` to reconstruct");
return false;
}
if main_ref(repo).is_err() {
eprintln!("SKIP: neither origin/main nor main resolves here");
return false;
}
true
}
fn graph_inputs() -> Option<(rto_graph::Repo, rto_graph::ObjectCache)> {
let repo = rto_graph::Repo::discover(&repo()).ok()?;
let cache =
rto_graph::ObjectCache::open(repo.common_dir().join("roteiro").join("objects")).ok()?;
Some((repo, cache))
}
#[test]
fn the_graph_arm_is_built_at_the_reviewed_commit_not_at_head() {
let repo_path = repo();
if !history_available(&repo_path) {
return;
}
let Some((repo, cache)) = graph_inputs() else {
eprintln!("SKIP: cannot open the repository's object cache");
return;
};
for sha in &corpus_shas() {
let on_disk = std::process::Command::new("git")
.arg("-C")
.arg(&repo_path)
.args(["ls-tree", "-r", "--name-only", sha, "--", "docs/adr/"])
.output()
.expect("git ls-tree runs");
let expected = String::from_utf8_lossy(&on_disk.stdout)
.lines()
.filter(|p| {
std::path::Path::new(p)
.extension()
.is_some_and(|e| e.eq_ignore_ascii_case("md"))
&& !p.ends_with("README.md")
})
.count();
let store = graph_at(
&repo,
&cache,
rto_graph::IngestConfig::default(),
ReviewArm::Graph,
sha,
)
.expect("the graph at a corpus commit assembles")
.expect("the graph arm yields a store");
let adrs = store
.nodes_by_kind(&rto_graph::NodeKind::Adr)
.expect("the store answers");
assert_eq!(
adrs.len(),
expected,
"{sha} carries {expected} ADR file(s) but the graph holds {} — \
built at the wrong tree",
adrs.len()
);
}
}
#[test]
fn the_arm_tags_are_stable_and_distinct() {
assert_eq!(ReviewArm::DiffOnly.tag(), "diff-only");
assert_eq!(ReviewArm::Graph.tag(), "graph");
assert_ne!(ReviewArm::DiffOnly.tag(), ReviewArm::Graph.tag());
}
#[test]
fn the_live_surface_builds_the_same_graph_as_the_replay() {
let repo_path = repo();
if !history_available(&repo_path) {
return;
}
let Ok(store) = super::worktree_graph(&repo_path, rto_graph::IngestConfig::default())
else {
eprintln!("SKIP: the working-tree graph could not be assembled here");
return;
};
let on_disk = std::fs::read_dir(repo_path.join("docs/adr"))
.expect("this repository has an ADR directory")
.filter_map(Result::ok)
.filter(|e| {
let name = e.file_name();
let name = name.to_string_lossy();
std::path::Path::new(name.as_ref())
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("md"))
&& name != "README.md"
})
.count();
let adrs = store
.nodes_by_kind(&rto_graph::NodeKind::Adr)
.expect("the store answers");
assert_eq!(
adrs.len(),
on_disk,
"the live graph holds {} ADR node(s) against {on_disk} on disk — the \
authored layer did not reach it",
adrs.len()
);
}
#[test]
fn the_diff_only_arm_sends_no_context_at_all() {
let file = FileUnderReview {
reviewed_sha: "0".repeat(40),
path: "src/lib.rs".to_owned(),
diff: "@@ -1 +1 @@\n+x\n".to_owned(),
};
let context = context_for(None, &file, &|_| None).expect("no store, no work");
assert!(context.is_empty());
assert_eq!(context.dropped_items, 0);
assert_eq!(context.tokens(), 0);
}
#[test]
fn the_graph_arm_supplies_provenance_tagged_context_on_the_corpus() {
let repo_path = repo();
if !history_available(&repo_path) {
return;
}
let Some((repo, cache)) = graph_inputs() else {
eprintln!("SKIP: cannot open the repository's object cache");
return;
};
let main = main_ref(&repo_path).expect("checked above");
let mut files_with_context = 0usize;
let mut authored = 0usize;
let mut derived = 0usize;
let mut items = 0usize;
let mut tokens = 0usize;
let mut dropped = 0usize;
for sha in &corpus_shas() {
let store = graph_at(
&repo,
&cache,
rto_graph::IngestConfig::default(),
ReviewArm::Graph,
sha,
)
.expect("the graph at a corpus commit assembles")
.expect("the graph arm yields a store");
let set = files_at(&repo_path, sha, &main).expect("the diff reconstructs");
for file in &set.files {
let sources = |p: &str| {
std::process::Command::new("git")
.arg("-C")
.arg(&repo_path)
.args(["show", &format!("{sha}:{p}")])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
};
let context =
context_for(Some(&store), file, &sources).expect("the context assembles");
assert!(
context.tokens() <= rto_graph::reviewer::CONTEXT_CAP_TOKENS,
"{}: {} context tokens exceeds the cap — the dose is not bounded \
by the policy that was pre-registered for it",
file.path,
context.tokens()
);
if !context.is_empty() {
files_with_context += 1;
items += context.items.len();
tokens += context.tokens();
dropped += context.dropped_items;
}
for item in &context.items {
assert!(
!item.body.trim().is_empty(),
"{}: an item claiming context quoted nothing",
item.label
);
match item.provenance.as_str() {
"authored" => authored += 1,
"derived" => derived += 1,
other => panic!("unknown provenance layer {other:?} on {}", item.label),
}
}
}
}
println!(
"graph-arm dose over the corpus: {files_with_context} file(s) carried \
context, {items} item(s) ({authored} authored, {derived} derived), \
~{tokens} token(s), {dropped} item(s) dropped by the cap"
);
assert!(
files_with_context > 0,
"the graph arm produced no context on any corpus file — it is the \
diff-only arm under another name"
);
assert!(
authored > 0,
"no governing ADR reached any file: the arm's central item is missing \
({derived} derived item(s) were sent)"
);
}
#[test]
fn every_corpus_commit_reconstructs_a_diff_touching_its_anchor() {
let repo = repo();
if !history_available(&repo) {
return;
}
let main = main_ref(&repo).expect("checked above");
let corpus = rto_graph::review_corpus::builtin().expect("the shipped corpus parses");
let mut by_sha: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for row in corpus.rows() {
by_sha
.entry(row.reviewed_sha.as_str())
.or_default()
.push(row.path.as_str());
}
for (sha, anchors) in by_sha {
let fork = fork_point(&repo, sha, &main).expect("a fork point resolves");
assert_ne!(
fork,
sha,
"{}: the base is the review commit itself, so the diff is empty — \
the silent zero this recipe exists to avoid",
&sha[..8]
);
let set = files_at(&repo, sha, &main).expect("the diff reconstructs");
assert!(!set.files.is_empty(), "{}: empty diff", &sha[..8]);
let paths: Vec<&str> = set.files.iter().map(|f| f.path.as_str()).collect();
for anchor in anchors {
assert!(
paths.contains(&anchor),
"{}: the reconstructed diff does not touch {anchor}, the file a \
comment is anchored to. Touched: {}",
&sha[..8],
paths.join(", ")
);
}
assert!(
set.files.iter().all(|f| f.diff.contains("@@")),
"{}: a file with no hunk reached the reviewable set",
&sha[..8]
);
}
}
#[test]
fn nothing_the_harness_skips_carries_an_adjudicated_row() {
let repo = repo();
if !history_available(&repo) {
return;
}
let main = main_ref(&repo).expect("checked above");
let corpus = rto_graph::review_corpus::builtin().expect("parses");
let mut skipped_total = 0;
for sha in corpus_shas() {
let set = files_at(&repo, &sha, &main).expect("reconstructs");
skipped_total += set.skipped.len();
for path in &set.skipped {
assert!(
!corpus
.rows()
.iter()
.any(|r| r.reviewed_sha == sha && &r.path == path),
"{}: {path} is skipped as unreviewable but carries a corpus row",
&sha[..8]
);
}
}
assert_eq!(
skipped_total, 6,
"the measured count of unreviewable paths in this corpus"
);
}
#[test]
fn a_replay_covers_the_measured_number_of_files() {
let repo = repo();
if !history_available(&repo) {
return;
}
let main = main_ref(&repo).expect("checked above");
let (mut reviewable, mut changed) = (0usize, 0usize);
for sha in corpus_shas() {
let set = files_at(&repo, &sha, &main).expect("reconstructs");
reviewable += set.files.len();
changed += set.files.len() + set.skipped.len();
}
assert_eq!(
(corpus_shas().len(), changed, reviewable),
(15, 190, 184),
"15 commits, 190 changed paths, 184 with a reviewable diff"
);
}
#[cfg(any(feature = "serve", feature = "inference-local-models"))]
#[test]
fn the_context_window_holds_the_whole_budget() {
use super::{REVIEW_MAX_TOKENS, REVIEW_N_CTX, REVIEW_PROMPT_BUDGET};
const ENGINE_DEFAULT_N_CTX: u32 = 4_096;
const _: () = assert!(
REVIEW_N_CTX > ENGINE_DEFAULT_N_CTX,
"the engine default is what broke this"
);
let worst_case = REVIEW_PROMPT_BUDGET * 13 / 10 + REVIEW_MAX_TOKENS as usize;
assert!(
REVIEW_N_CTX as usize >= worst_case,
"a prompt `len / 4` understated by 30% plus its generation is \
{worst_case} tokens, over the {REVIEW_N_CTX}-token window"
);
}
#[test]
fn the_reviewable_rule_is_one_rule_and_skips_are_kept() {
let diffs: BTreeMap<&str, &str> = [
("src/real.rs", "@@ -1,2 +1,3 @@\n context\n+added\n"),
(
"assets/beep.wav",
"Binary files a/assets/beep.wav and b/assets/beep.wav differ\n",
),
(
"scripts/run.sh",
"diff --git a/scripts/run.sh b/scripts/run.sh\nold mode 100644\nnew mode 100755\n",
),
]
.into_iter()
.collect();
let names = "src/real.rs\nassets/beep.wav\nscripts/run.sh\ngone.rs\n";
let set = ReviewSet::collect("deadbeef", names, &|p| {
diffs.get(p).map(|d| (*d).to_owned())
});
let reviewed: Vec<&str> = set.files.iter().map(|f| f.path.as_str()).collect();
assert_eq!(
reviewed,
vec!["src/real.rs"],
"only a diff with a hunk carries a citable line number"
);
assert_eq!(set.files[0].reviewed_sha, "deadbeef");
assert_eq!(
set.skipped,
vec![
"assets/beep.wav".to_owned(),
"scripts/run.sh".to_owned(),
"gone.rs".to_owned(),
],
"a binary blob, a mode-only record and an unreadable diff are all \
counted rather than silently reducing the denominator"
);
}
#[test]
fn the_parent_module_is_found_and_is_never_the_file_itself() {
let files: BTreeMap<&str, &str> = [
("crates/rto-exec/src/lib.rs", "pub mod boxlite;"),
("crates/rto-exec/src/boxlite.rs", "fn run() {}"),
]
.into_iter()
.collect();
let sources = |p: &str| files.get(p).map(|s| (*s).to_owned());
let parent = parent_module_source("crates/rto-exec/src/boxlite.rs", &sources);
assert_eq!(parent.as_deref(), Some("pub mod boxlite;"));
let own = parent_module_source("crates/rto-exec/src/lib.rs", &sources);
assert_ne!(own.as_deref(), Some("pub mod boxlite;"));
assert!(parent_module_source("main.rs", &sources).is_none());
}
#[test]
fn a_mod_rss_parent_is_searched_one_directory_up() {
let files: BTreeMap<&str, &str> = [
(
"crates/rto-exec/src/lib.rs",
"#[cfg(feature = \"exec-boxlite\")]\npub mod boxlite;",
),
("crates/rto-exec/src/boxlite/mod.rs", "fn run() {}"),
]
.into_iter()
.collect();
let sources = |p: &str| files.get(p).map(|s| (*s).to_owned());
let parent = parent_module_source("crates/rto-exec/src/boxlite/mod.rs", &sources);
assert_eq!(
parent.as_deref(),
Some("#[cfg(feature = \"exec-boxlite\")]\npub mod boxlite;"),
"`boxlite/mod.rs` is declared in `src`, not in `src/boxlite`"
);
}
#[test]
fn review_llm_has_no_allow_remote_flag_until_the_allow_list_carries_source() {
use clap::CommandFactory;
let cli = <crate::Cli as CommandFactory>::command();
let review = cli
.get_subcommands()
.find(|c| c.get_name() == "review")
.expect("`review` is a subcommand");
let flags: Vec<&str> = review
.get_arguments()
.map(clap::Arg::get_id)
.map(clap::Id::as_str)
.collect();
assert!(flags.contains(&"llm"), "the surface exists: {flags:?}");
assert!(flags.contains(&"replay"), "the harness exists: {flags:?}");
assert!(
!flags.contains(&"allow_remote"),
"review must not offer the remote tier while the payload allow-list \
cannot carry source: {flags:?}"
);
assert!(rto_graph::ModelTask::Review.goes_remote());
}
}