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::{
GraphContext, SINGLE_CALL_BUDGET_TOKENS, build_prompt, claim_site, parse_findings,
},
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"
);
#[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,
})
}
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 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>,
) -> 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::default();
let mut report = ReplayReport::default();
for (idx, sha) in shas.iter().enumerate() {
let set = files_at(repo, sha, &main)?;
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 outcome = match review_file(
&engine,
model,
file,
&GraphContext::none(),
&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.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>) -> 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 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 outcome = review_file(
&engine,
model,
file,
&GraphContext::none(),
&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::{ReviewSet, corpus_shas, files_at, fork_point, 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
}
#[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());
}
}