Watch
0
0
Fork
You've already forked hyperhive
0

review classifier: the most classifications a run carries is a setting

The limit was a constant of ten in swarm-wire, so a repo with eleven rules
could not be reviewed and nothing short of a rebuild changed it. It is now
`max_classifications` in the classifier settings, beside
`max_concurrent_reviews`: 1 to 1000, ten until set, and a document stored
before the field decodes with the default. The classifier settings form in
the swarm UI gets a "rules per review" field, and the try-out's add button
stops at the saved limit instead of a hard-coded ten.

The controller checks a try-out, and a requested review's rules file, against
the setting where the run starts. The rules file itself may hold up to the
1000 the setting allows, so raising the limit needs no change to a repo.
A review whose rules exceed the limit posts a no-verdict comment saying so.

A run's reply deadline was a constant that covered ten classifications, a
minute plus three each. It is now computed from the run's own size, so a
bigger run waits as long as it needs and a default-sized one still waits 31
minutes. The classifier's answer window in the queue permissions covers the
biggest run the setting allows.
This commit is contained in:
iris 2026-11-07 10:37:52 +01:00 • committed by mara
commit cffe8895df

View file

@ -36,6 +36,7 @@ use futures_util::StreamExt as _;
use serde::{Deserialize, Serialize};
use swarm_wire::review_classifier::{
Overall, REQUEST_CONSUMER, REQUEST_STREAM, REQUEST_SUBJECT_PREFIX, Reply, Row, TryOut, Verdict,
run_deadline,
};
use tokio::sync::Notify;
use tokio::task::JoinSet;
@ -821,7 +822,7 @@ impl ReviewPoster for forge::Client {
struct Queue(Option<async_nats::Client>);
impl RunSink for Queue {
async fn run(&self, request: Vec<u8>) -> Reply {
async fn run(&self, request: Vec<u8>, deadline: Duration) -> Reply {
let Some(client) = &self.0 else {
return Reply::Failed {
error: "no swarm queue is configured on this host".to_owned(),
@ -832,7 +833,7 @@ impl RunSink for Queue {
error: swarm_queue_client::chain(&e),
};
}
client.run(request).await
client.run(request, deadline).await
}
}
@ -1087,6 +1088,14 @@ async fn classify(
);
}
};
if let Err(why) = super::check_count(rules.classifications.len(), settings.max_classifications)
{
return (
head,
narrowed,
Outcome::NotRun(format!("{repo}'s rules do not fit a run: {why}")),
);
}
let runtime = match settings.runtime() {
Ok(runtime) => runtime,
Err(e) => {
@ -1113,7 +1122,7 @@ async fn classify(
let outcome = match serde_json::to_vec(&request) {
Ok(payload) => Outcome::Ran {
sent,
reply: sink.run(payload).await,
reply: sink.run(payload, run_deadline(sent)).await,
},
Err(e) => Outcome::NotRun(format!("encoding the run failed: {e}")),
};
@ -1602,7 +1611,7 @@ mod tests {
}
impl RunSink for Gate {
async fn run(&self, _request: Vec<u8>) -> Reply {
async fn run(&self, _request: Vec<u8>, _deadline: Duration) -> Reply {
self.reached.fetch_add(1, Ordering::SeqCst);
let _permit = self.permits.acquire().await.expect("never closed");
Reply::Done {
@ -2717,6 +2726,7 @@ mod tests {
model: "sonnet".to_owned(),
effort: Some(Effort::Medium),
max_concurrent_reviews: 2,
max_classifications: 10,
runtime: super::super::settings::RuntimeKind::Acp,
acp: Some(acp.clone()),
};
@ -2734,6 +2744,29 @@ mod tests {
);
}
#[tokio::test]
async fn a_repo_with_more_rules_than_the_settings_allow_runs_nothing_and_posts_a_comment() {
let mut fake = Fake::new(
Reply::Failed {
error: "must not run".to_owned(),
},
vec![crate::review_classifier::tests::rules_json_of(3)],
);
fake.settings.max_classifications = 2;
let poster = Posted::default();
run_read(&fake, &fake, &poster, &request("r")).await;
let (_, _, review) = poster.0.into_inner().expect("unpoisoned").remove(0);
assert_eq!(review.event, "COMMENT");
assert!(
review.body.contains("rules do not fit a run")
&& review.body.contains("3 classifications")
&& review.body.contains("1 to 2"),
"{}",
review.body
);
assert!(fake.sent.lock().expect("unpoisoned").is_empty());
}
#[tokio::test]
async fn unreadable_settings_run_nothing_and_post_a_comment() {
let fake = Fake::new(