From 9665fad133e875d85438e48e443b9e4c85b3095b Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Tue, 24 Feb 2026 09:42:28 -0500 Subject: [PATCH] Revert clippy config to defaults, remove all pedantic/nursery/cargo lint suppressions Removed the workspace-level clippy lint config that enabled all, pedantic, nursery, and cargo lint groups. Removed all #[allow(clippy::...)] annotations that were only needed to suppress those extra lints, and fixed the few default clippy warnings that were uncovered. Co-Authored-By: Claude Opus 4.6 --- Cargo.toml | 7 -- crates/agent/Cargo.toml | 3 - crates/agent/src/docker_env.rs | 12 +-- crates/agent/src/execution_env.rs | 1 + crates/agent/src/history.rs | 2 + crates/agent/src/local_env.rs | 5 +- crates/agent/src/loop_detection.rs | 1 + crates/agent/src/profiles/anthropic.rs | 2 +- crates/agent/src/profiles/gemini.rs | 2 +- crates/agent/src/profiles/openai.rs | 2 +- crates/agent/src/session.rs | 15 ++-- crates/agent/src/subagent.rs | 6 +- crates/agent/src/test_support.rs | 60 +++++++------- crates/agent/src/tools.rs | 4 - crates/agent/src/truncation.rs | 3 + crates/agent/src/types.rs | 2 +- crates/agent/tests/parity_matrix.rs | 2 +- crates/attractor/Cargo.toml | 3 - crates/attractor/src/checkpoint.rs | 2 +- crates/attractor/src/cli/run.rs | 17 ++-- crates/attractor/src/engine.rs | 36 ++++----- crates/attractor/src/error.rs | 2 +- crates/attractor/src/graph/types.rs | 2 +- crates/attractor/src/handler/fan_in.rs | 4 +- crates/attractor/src/handler/manager_loop.rs | 9 +-- crates/attractor/src/handler/parallel.rs | 9 +-- crates/attractor/src/handler/sub_pipeline.rs | 12 +-- crates/attractor/src/interviewer/callback.rs | 2 +- crates/attractor/src/parser/ast.rs | 4 +- crates/attractor/src/parser/lexer.rs | 13 +-- crates/attractor/src/parser/mod.rs | 4 +- crates/attractor/src/parser/semantic.rs | 5 +- crates/attractor/src/server.rs | 13 ++- crates/attractor/src/stylesheet.rs | 8 +- crates/attractor/src/transform.rs | 7 +- crates/attractor/src/validation/rules.rs | 2 +- crates/attractor/tests/integration.rs | 81 ++++++++----------- crates/llm/Cargo.toml | 3 - crates/llm/src/bin/ullm.rs | 8 +- crates/llm/src/error.rs | 18 ++--- crates/llm/src/generate.rs | 22 +++-- crates/llm/src/middleware.rs | 4 + crates/llm/src/provider.rs | 5 ++ crates/llm/src/providers/anthropic.rs | 2 - crates/llm/src/providers/gemini.rs | 3 - crates/llm/src/providers/openai.rs | 5 +- crates/llm/src/providers/openai_compatible.rs | 2 - crates/llm/src/tools.rs | 28 +++---- crates/llm/src/types.rs | 10 +-- crates/terminal/Cargo.toml | 3 - 50 files changed, 209 insertions(+), 268 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 69d3f03ba..f34b305fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,13 +2,6 @@ members = ["crates/*"] resolver = "2" -[workspace.lints.clippy] -all = { level = "warn", priority = -1 } -pedantic = { level = "warn", priority = -1 } -nursery = { level = "warn", priority = -1 } -cargo = { level = "warn", priority = -1 } -multiple_crate_versions = "allow" - [workspace.package] edition = "2021" version = "0.1.0" diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 8c088bf91..9943dafb0 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -48,6 +48,3 @@ tokio = { workspace = true, features = ["test-util", "macros"] } tempfile = "3" dotenvy = { workspace = true } paste = "1" - -[lints] -workspace = true diff --git a/crates/agent/src/docker_env.rs b/crates/agent/src/docker_env.rs index 1908f06ad..77ab872b5 100644 --- a/crates/agent/src/docker_env.rs +++ b/crates/agent/src/docker_env.rs @@ -165,9 +165,7 @@ impl DockerExecutionEnvironment { ) -> Result { let start = Instant::now(); - let effective_dir = working_dir - .map(ToString::to_string) - .unwrap_or_else(|| self.config.container_mount_point.clone()); + let effective_dir = working_dir.map_or_else(|| self.config.container_mount_point.clone(), ToString::to_string); let env: Option> = env_vars.map(|vars| { vars.iter() @@ -441,9 +439,7 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { .map_err(|e| format!("Failed to finalize tar archive: {e}"))?; let parent_dir = std::path::Path::new(&container_path) - .parent() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| "/".to_string()); + .parent().map_or_else(|| "/".to_string(), |p| p.to_string_lossy().to_string()); let upload_opts = UploadToContainerOptions { path: parent_dir, @@ -610,9 +606,7 @@ impl ExecutionEnvironment for DockerExecutionEnvironment { } async fn glob(&self, pattern: &str, path: Option<&str>) -> Result, String> { - let base_dir = path - .map(|p| self.resolve_container_path(p)) - .unwrap_or_else(|| self.config.container_mount_point.clone()); + let base_dir = path.map_or_else(|| self.config.container_mount_point.clone(), |p| self.resolve_container_path(p)); let full_pattern = if pattern.starts_with('/') { pattern.to_string() diff --git a/crates/agent/src/execution_env.rs b/crates/agent/src/execution_env.rs index 28168197e..40f224e93 100644 --- a/crates/agent/src/execution_env.rs +++ b/crates/agent/src/execution_env.rs @@ -6,6 +6,7 @@ use tokio_util::sync::CancellationToken; /// /// Applies optional offset (0-based lines to skip) and limit (max lines to return). /// Line numbers are 1-based and right-aligned. +#[must_use] pub fn format_lines_numbered(content: &str, offset: Option, limit: Option) -> String { let all_lines: Vec<&str> = content.lines().collect(); let skip = offset.unwrap_or(0); diff --git a/crates/agent/src/history.rs b/crates/agent/src/history.rs index 4b7c7a5ae..b8404e8ae 100644 --- a/crates/agent/src/history.rs +++ b/crates/agent/src/history.rs @@ -11,10 +11,12 @@ impl History { self.turns.push(turn); } + #[must_use] pub fn turns(&self) -> &[Turn] { &self.turns } + #[must_use] pub fn convert_to_messages(&self) -> Vec { self.turns .iter() diff --git a/crates/agent/src/local_env.rs b/crates/agent/src/local_env.rs index c12504cbe..8172550c0 100644 --- a/crates/agent/src/local_env.rs +++ b/crates/agent/src/local_env.rs @@ -91,9 +91,9 @@ impl ExecutionEnvironment for LocalExecutionEnvironment { ) -> Result<(), String> { let mut dir_entries: Vec = std::fs::read_dir(base) .map_err(|e| format!("Failed to read directory {}: {e}", base.display()))? - .filter_map(|e| e.ok()) + .filter_map(std::result::Result::ok) .collect(); - dir_entries.sort_by_key(|e| e.file_name()); + dir_entries.sort_by_key(std::fs::DirEntry::file_name); for entry in dir_entries { let metadata = entry @@ -357,7 +357,6 @@ impl ExecutionEnvironment for LocalExecutionEnvironment { async fn sigterm_then_kill(child: &mut tokio::process::Child) { #[cfg(unix)] if let Some(pid) = child.id() { - #[allow(clippy::cast_possible_wrap)] unsafe { libc::kill(-(pid as i32), libc::SIGTERM); } diff --git a/crates/agent/src/loop_detection.rs b/crates/agent/src/loop_detection.rs index 51dd47e81..7aba47efe 100644 --- a/crates/agent/src/loop_detection.rs +++ b/crates/agent/src/loop_detection.rs @@ -21,6 +21,7 @@ fn extract_signatures_from_assistant(turn: &Turn) -> Vec { .collect() } +#[must_use] pub fn detect_loop(history: &History, window_size: usize) -> bool { // Extract tool call signatures from the last N assistant turns that have tool calls let turns = history.turns(); diff --git a/crates/agent/src/profiles/anthropic.rs b/crates/agent/src/profiles/anthropic.rs index 872c78e0d..15e090228 100644 --- a/crates/agent/src/profiles/anthropic.rs +++ b/crates/agent/src/profiles/anthropic.rs @@ -171,7 +171,7 @@ in the project. Keep changes minimal and focused on the task."; } } - fn knowledge_cutoff(&self) -> &str { + fn knowledge_cutoff(&self) -> &'static str { "May 2025" } } diff --git a/crates/agent/src/profiles/gemini.rs b/crates/agent/src/profiles/gemini.rs index cc422f2fc..359dfa87c 100644 --- a/crates/agent/src/profiles/gemini.rs +++ b/crates/agent/src/profiles/gemini.rs @@ -204,7 +204,7 @@ in the project."; })) } - fn knowledge_cutoff(&self) -> &str { + fn knowledge_cutoff(&self) -> &'static str { "January 2025" } } diff --git a/crates/agent/src/profiles/openai.rs b/crates/agent/src/profiles/openai.rs index c87b76868..44fa8fca2 100644 --- a/crates/agent/src/profiles/openai.rs +++ b/crates/agent/src/profiles/openai.rs @@ -171,7 +171,7 @@ in the project."; }) } - fn knowledge_cutoff(&self) -> &str { + fn knowledge_cutoff(&self) -> &'static str { "April 2025" } } diff --git a/crates/agent/src/session.rs b/crates/agent/src/session.rs index eb0643afe..353600cc6 100644 --- a/crates/agent/src/session.rs +++ b/crates/agent/src/session.rs @@ -134,10 +134,12 @@ impl Session { } } - pub fn state(&self) -> SessionState { + #[must_use] + pub const fn state(&self) -> SessionState { self.state } + #[must_use] pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver { self.event_emitter.subscribe() } @@ -160,10 +162,12 @@ impl Session { self.cancel_token.cancel(); } + #[must_use] pub fn followup_queue_handle(&self) -> Arc>> { self.followup_queue.clone() } + #[must_use] pub fn cancel_token(&self) -> CancellationToken { self.cancel_token.clone() } @@ -180,11 +184,12 @@ impl Session { self.config.reasoning_effort = effort; } - pub fn set_max_turns(&mut self, max_turns: usize) { + pub const fn set_max_turns(&mut self, max_turns: usize) { self.config.max_turns = max_turns; } - pub fn history(&self) -> &History { + #[must_use] + pub const fn history(&self) -> &History { &self.history } @@ -756,10 +761,10 @@ fn truncate_tool_result( } } -fn is_auth_error(err: &SdkError) -> bool { +const fn is_auth_error(err: &SdkError) -> bool { matches!( err.provider_kind(), - Some(ProviderErrorKind::Authentication) | Some(ProviderErrorKind::AccessDenied) + Some(ProviderErrorKind::Authentication | ProviderErrorKind::AccessDenied) ) } diff --git a/crates/agent/src/subagent.rs b/crates/agent/src/subagent.rs index 479d41118..3d64bab64 100644 --- a/crates/agent/src/subagent.rs +++ b/crates/agent/src/subagent.rs @@ -29,6 +29,7 @@ pub struct SubAgent { #[cfg(test)] impl SubAgent { + #[must_use] pub fn depth(&self) -> usize { self.depth } @@ -40,6 +41,7 @@ pub struct SubAgentManager { } impl SubAgentManager { + #[must_use] pub fn new(max_depth: usize) -> Self { Self { agents: HashMap::new(), @@ -148,6 +150,7 @@ impl SubAgentManager { } #[cfg(test)] + #[must_use] pub fn get(&self, agent_id: &str) -> Option<&SubAgent> { self.agents.get(agent_id) } @@ -192,10 +195,9 @@ pub fn make_spawn_agent_tool( let task = required_str(&args, "task")?; // Extract optional max_turns parameter - #[allow(clippy::cast_possible_truncation)] let max_turns = args .get("max_turns") - .and_then(|v| v.as_u64()) + .and_then(serde_json::Value::as_u64) .map(|v| v as usize); // Note: working_dir and model require session factory changes to wire through diff --git a/crates/agent/src/test_support.rs b/crates/agent/src/test_support.rs index 64406ee07..5359c8924 100644 --- a/crates/agent/src/test_support.rs +++ b/crates/agent/src/test_support.rs @@ -16,7 +16,7 @@ use tokio_util::sync::CancellationToken; // --- MockExecutionEnvironment --- -pub(crate) struct MockExecutionEnvironment { +pub struct MockExecutionEnvironment { pub files: HashMap, pub exec_result: ExecResult, pub grep_results: Vec, @@ -24,11 +24,11 @@ pub(crate) struct MockExecutionEnvironment { pub working_dir: &'static str, pub platform_str: &'static str, pub os_version_str: String, - /// When true, read_file applies offset/limit by splitting on lines. + /// When true, `read_file` applies offset/limit by splitting on lines. pub apply_read_offset_limit: bool, - /// Captures (path, content) pairs from write_file calls. + /// Captures (path, content) pairs from `write_file` calls. pub written_files: Mutex>, - /// Captures the timeout_ms argument from exec_command calls. + /// Captures the `timeout_ms` argument from `exec_command` calls. pub captured_timeout: Mutex>, } @@ -167,8 +167,8 @@ impl ExecutionEnvironment for MockExecutionEnvironment { // --- MutableMockExecutionEnvironment --- /// A mock execution environment with Mutex-protected files for tests that need -/// write operations to be visible to subsequent reads (e.g., apply_patch tests). -pub(crate) struct MutableMockExecutionEnvironment { +/// write operations to be visible to subsequent reads (e.g., `apply_patch` tests). +pub struct MutableMockExecutionEnvironment { pub files: Mutex>, } @@ -266,11 +266,11 @@ impl ExecutionEnvironment for MutableMockExecutionEnvironment { Ok(()) } - fn working_directory(&self) -> &str { + fn working_directory(&self) -> &'static str { "/tmp" } - fn platform(&self) -> &str { + fn platform(&self) -> &'static str { "linux" } @@ -281,7 +281,7 @@ impl ExecutionEnvironment for MutableMockExecutionEnvironment { // --- TestProfile --- -pub(crate) struct TestProfile { +pub struct TestProfile { pub registry: ToolRegistry, pub parallel_tool_calls: bool, pub context_window: usize, @@ -322,11 +322,11 @@ impl TestProfile { } impl ProviderProfile for TestProfile { - fn id(&self) -> &str { + fn id(&self) -> &'static str { "mock" } - fn model(&self) -> &str { + fn model(&self) -> &'static str { "mock-model" } @@ -360,14 +360,14 @@ impl ProviderProfile for TestProfile { } } - fn knowledge_cutoff(&self) -> &str { + fn knowledge_cutoff(&self) -> &'static str { "May 2025" } } // --- MockLlmProvider --- -pub(crate) struct MockLlmProvider { +pub struct MockLlmProvider { pub responses: Vec, pub call_index: AtomicUsize, } @@ -383,7 +383,7 @@ impl MockLlmProvider { #[async_trait] impl ProviderAdapter for MockLlmProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -438,7 +438,7 @@ fn response_to_stream(response: Response) -> StreamEventStream { // --- Helper functions --- -pub(crate) fn text_response(text: &str) -> Response { +pub fn text_response(text: &str) -> Response { Response { id: format!("resp_{text}"), model: "mock-model".into(), @@ -457,13 +457,13 @@ pub(crate) fn text_response(text: &str) -> Response { } } -pub(crate) async fn make_client(provider: Arc) -> Client { +pub async fn make_client(provider: Arc) -> Client { let mut providers = HashMap::new(); providers.insert(provider.name().to_string(), provider); Client::new(providers, Some("mock".into()), vec![]) } -pub(crate) async fn make_session(responses: Vec) -> Session { +pub async fn make_session(responses: Vec) -> Session { let provider = Arc::new(MockLlmProvider::new(responses)); let client = make_client(provider).await; let profile = Arc::new(TestProfile::new()); @@ -471,7 +471,7 @@ pub(crate) async fn make_session(responses: Vec) -> Session { Session::new(client, profile, env, SessionConfig::default()) } -pub(crate) async fn make_session_with_tools( +pub async fn make_session_with_tools( responses: Vec, registry: ToolRegistry, ) -> Session { @@ -482,7 +482,7 @@ pub(crate) async fn make_session_with_tools( Session::new(client, profile, env, SessionConfig::default()) } -pub(crate) async fn make_session_with_config( +pub async fn make_session_with_config( responses: Vec, config: SessionConfig, ) -> Session { @@ -493,7 +493,7 @@ pub(crate) async fn make_session_with_config( Session::new(client, profile, env, config) } -pub(crate) async fn make_session_with_tools_and_config( +pub async fn make_session_with_tools_and_config( responses: Vec, registry: ToolRegistry, config: SessionConfig, @@ -505,7 +505,7 @@ pub(crate) async fn make_session_with_tools_and_config( Session::new(client, profile, env, config) } -pub(crate) fn tool_call_response( +pub fn tool_call_response( tool_name: &str, tool_call_id: &str, args: serde_json::Value, @@ -537,7 +537,7 @@ pub(crate) fn tool_call_response( } } -pub(crate) fn make_echo_tool() -> crate::tool_registry::RegisteredTool { +pub fn make_echo_tool() -> crate::tool_registry::RegisteredTool { use llm::types::ToolDefinition; crate::tool_registry::RegisteredTool { definition: ToolDefinition { @@ -557,7 +557,7 @@ pub(crate) fn make_echo_tool() -> crate::tool_registry::RegisteredTool { } } -pub(crate) fn make_error_tool() -> crate::tool_registry::RegisteredTool { +pub fn make_error_tool() -> crate::tool_registry::RegisteredTool { use llm::types::ToolDefinition; crate::tool_registry::RegisteredTool { definition: ToolDefinition { @@ -573,13 +573,13 @@ pub(crate) fn make_error_tool() -> crate::tool_registry::RegisteredTool { // --- MockErrorProvider --- -pub(crate) struct MockErrorProvider { +pub struct MockErrorProvider { pub error: SdkError, } #[async_trait] impl ProviderAdapter for MockErrorProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -595,7 +595,7 @@ impl ProviderAdapter for MockErrorProvider { // --- CapturingLlmProvider --- /// A mock LLM provider that captures the full Request for test assertions. -pub(crate) struct CapturingLlmProvider { +pub struct CapturingLlmProvider { pub captured_request: Mutex>, } @@ -609,7 +609,7 @@ impl CapturingLlmProvider { #[async_trait] impl ProviderAdapter for CapturingLlmProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -633,14 +633,14 @@ impl ProviderAdapter for CapturingLlmProvider { // --- MockMidStreamErrorProvider --- /// A mock provider that yields some text deltas then an error mid-stream. -pub(crate) struct MockMidStreamErrorProvider { +pub struct MockMidStreamErrorProvider { pub partial_text: String, pub error: SdkError, } #[async_trait] impl ProviderAdapter for MockMidStreamErrorProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -657,7 +657,7 @@ impl ProviderAdapter for MockMidStreamErrorProvider { } } -pub(crate) fn multi_tool_call_response( +pub fn multi_tool_call_response( calls: Vec<(&str, &str, serde_json::Value)>, ) -> Response { use llm::types::{ContentPart, Role, ToolCall}; diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs index 45a3a8ee2..d9917a3de 100644 --- a/crates/agent/src/tools.rs +++ b/crates/agent/src/tools.rs @@ -33,9 +33,7 @@ pub fn make_read_file_tool() -> RegisteredTool { let offset = args.get("offset").and_then(serde_json::Value::as_u64); let limit = args.get("limit").and_then(serde_json::Value::as_u64); - #[allow(clippy::cast_possible_truncation)] let offset_usize = offset.map(|v| v as usize); - #[allow(clippy::cast_possible_truncation)] let limit_usize = limit.map(|v| v as usize); let content = env.read_file(file_path, offset_usize, limit_usize).await?; @@ -211,7 +209,6 @@ pub fn make_grep_tool() -> RegisteredTool { .and_then(serde_json::Value::as_str) .unwrap_or("."); - #[allow(clippy::cast_possible_truncation)] let options = GrepOptions { glob_filter: args .get("glob_filter") @@ -325,7 +322,6 @@ pub(crate) fn make_list_dir_tool() -> RegisteredTool { executor: Arc::new(|args, env, _cancel| { Box::pin(async move { let path = required_str(&args, "path")?; - #[allow(clippy::cast_possible_truncation)] let depth = args .get("depth") .and_then(serde_json::Value::as_u64) diff --git a/crates/agent/src/truncation.rs b/crates/agent/src/truncation.rs index 54efff461..87455a763 100644 --- a/crates/agent/src/truncation.rs +++ b/crates/agent/src/truncation.rs @@ -36,6 +36,7 @@ fn default_truncation_mode(tool_name: &str) -> TruncationMode { } } +#[must_use] pub fn truncate_output(output: &str, max_chars: usize, mode: TruncationMode) -> String { if output.len() <= max_chars { return output.to_string(); @@ -64,6 +65,7 @@ pub fn truncate_output(output: &str, max_chars: usize, mode: TruncationMode) -> } } +#[must_use] pub fn truncate_lines(output: &str, max_lines: usize) -> String { let lines: Vec<&str> = output.lines().collect(); if lines.len() <= max_lines { @@ -82,6 +84,7 @@ pub fn truncate_lines(output: &str, max_lines: usize) -> String { ) } +#[must_use] pub fn truncate_tool_output(output: &str, tool_name: &str, config: &SessionConfig) -> String { let mode = default_truncation_mode(tool_name); diff --git a/crates/agent/src/types.rs b/crates/agent/src/types.rs index 0966eb2fc..26bf094a4 100644 --- a/crates/agent/src/types.rs +++ b/crates/agent/src/types.rs @@ -11,7 +11,7 @@ pub enum Turn { content: String, tool_calls: Vec, reasoning: Option, - /// Opaque provider-specific content parts (e.g. OpenAI reasoning items) + /// Opaque provider-specific content parts (e.g. `OpenAI` reasoning items) /// that must be preserved for round-tripping but don't map to standard fields. provider_parts: Vec, usage: Usage, diff --git a/crates/agent/tests/parity_matrix.rs b/crates/agent/tests/parity_matrix.rs index 05363f9a9..2fcd10ef8 100644 --- a/crates/agent/tests/parity_matrix.rs +++ b/crates/agent/tests/parity_matrix.rs @@ -26,7 +26,7 @@ async fn make_session(provider: &str, model: &str, cwd: &Path) -> Session { let factory_cwd = cwd.to_path_buf(); let factory: agent::subagent::SessionFactory = { let provider = factory_provider.to_string(); - let model = factory_model.clone(); + let model = factory_model; Arc::new(move || { let sub_profile: Arc = match provider.as_str() { "anthropic" => Arc::new(AnthropicProfile::new(&model)), diff --git a/crates/attractor/Cargo.toml b/crates/attractor/Cargo.toml index dfc237626..7dc900ee1 100644 --- a/crates/attractor/Cargo.toml +++ b/crates/attractor/Cargo.toml @@ -53,6 +53,3 @@ http-body-util = "0.1" dotenvy.workspace = true assert_cmd = "2" predicates = "3" - -[lints] -workspace = true diff --git a/crates/attractor/src/checkpoint.rs b/crates/attractor/src/checkpoint.rs index 2da1b9046..8e0e6eb53 100644 --- a/crates/attractor/src/checkpoint.rs +++ b/crates/attractor/src/checkpoint.rs @@ -21,7 +21,7 @@ pub struct Checkpoint { /// Persisted node outcomes for goal gate checks after resume. #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub node_outcomes: HashMap, - /// The node to resume execution at (the next node after the checkpoint's current_node). + /// The node to resume execution at (the next node after the checkpoint's `current_node`). #[serde(default, skip_serializing_if = "Option::is_none")] pub next_node_id: Option, } diff --git a/crates/attractor/src/cli/run.rs b/crates/attractor/src/cli/run.rs index 1d7977481..86cc03cc6 100644 --- a/crates/attractor/src/cli/run.rs +++ b/crates/attractor/src/cli/run.rs @@ -34,7 +34,6 @@ struct CostAccumulator { /// # Errors /// /// Returns an error if the pipeline cannot be read, parsed, validated, or executed. -#[allow(clippy::too_many_lines)] pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Result<()> { // 1. Parse and validate pipeline let source = read_dot_file(&args.pipeline)?; @@ -83,15 +82,13 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu let accumulator = Arc::new(Mutex::new(CostAccumulator::default())); let acc_clone = Arc::clone(&accumulator); emitter.on_event(move |event| { - if let crate::event::PipelineEvent::StageCompleted { usage, .. } = event { - if let Some(u) = usage { - let mut acc = acc_clone.lock().unwrap(); - acc.total_input_tokens += u.input_tokens; - acc.total_output_tokens += u.output_tokens; - if let Some(cost) = compute_stage_cost(u) { - acc.total_cost += cost; - acc.has_pricing = true; - } + if let crate::event::PipelineEvent::StageCompleted { usage: Some(u), .. } = event { + let mut acc = acc_clone.lock().unwrap(); + acc.total_input_tokens += u.input_tokens; + acc.total_output_tokens += u.output_tokens; + if let Some(cost) = compute_stage_cost(u) { + acc.total_cost += cost; + acc.has_pricing = true; } } }); diff --git a/crates/attractor/src/engine.rs b/crates/attractor/src/engine.rs index 405cdc2a4..6de16222e 100644 --- a/crates/attractor/src/engine.rs +++ b/crates/attractor/src/engine.rs @@ -50,7 +50,6 @@ impl Default for BackoffConfig { impl BackoffConfig { /// Calculate delay for a given attempt (1-indexed). #[must_use] - #[allow(clippy::missing_panics_doc)] pub fn delay_for_attempt(&self, attempt: u32) -> std::time::Duration { let exponent = attempt.saturating_sub(1); let initial = f64::from(u32::try_from(self.initial_delay_ms).unwrap_or(u32::MAX)); @@ -71,9 +70,7 @@ impl BackoffConfig { } else if final_ms >= f64::from(u32::MAX) { u64::from(u32::MAX) } else { - // Safe: final_ms is in [0, u32::MAX] so the truncated integer fits in u64 - #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] - { final_ms as u64 } + final_ms as u64 }; std::time::Duration::from_millis(ms) } @@ -98,7 +95,7 @@ impl RetryPolicy { /// Standard retry policy: 5 attempts, 200ms initial, 2x factor. #[must_use] - pub fn standard() -> Self { + pub const fn standard() -> Self { Self { max_attempts: 5, backoff: BackoffConfig { @@ -112,7 +109,7 @@ impl RetryPolicy { /// Aggressive retry: 5 attempts, 500ms initial, 2x factor. #[must_use] - pub fn aggressive() -> Self { + pub const fn aggressive() -> Self { Self { max_attempts: 5, backoff: BackoffConfig { @@ -126,7 +123,7 @@ impl RetryPolicy { /// Linear retry: 3 attempts, 500ms fixed delay. #[must_use] - pub fn linear() -> Self { + pub const fn linear() -> Self { Self { max_attempts: 3, backoff: BackoffConfig { @@ -140,7 +137,7 @@ impl RetryPolicy { /// Patient retry: 3 attempts, 2000ms initial, 3x factor. #[must_use] - pub fn patient() -> Self { + pub const fn patient() -> Self { Self { max_attempts: 3, backoff: BackoffConfig { @@ -260,7 +257,7 @@ fn write_manifest(logs_root: &Path, graph: &Graph) { } } -/// Write status.json for a completed node into {logs_root}/{node_id}/status.json. +/// Write status.json for a completed node into {`logs_root}/{node_id}/status.json`. fn write_node_status(logs_root: &Path, node_id: &str, outcome: &Outcome) { let node_dir = logs_root.join(node_id); let _ = std::fs::create_dir_all(&node_dir); @@ -467,7 +464,7 @@ impl PipelineEngine { } } - /// Create a new engine with an interviewer for inform() callbacks. + /// Create a new engine with an interviewer for `inform()` callbacks. #[must_use] pub fn with_interviewer( registry: HandlerRegistry, @@ -633,7 +630,7 @@ impl PipelineEngine { } /// Resume from a checkpoint. Restores context, completed nodes, and continues - /// execution from the node after the checkpoint's current_node. + /// execution from the node after the checkpoint's `current_node`. /// /// # Errors /// @@ -647,8 +644,7 @@ impl PipelineEngine { self.run_internal(graph, config, Some(checkpoint), None).await } - /// Internal run implementation supporting optional checkpoint resume and start_at override. - #[allow(clippy::too_many_lines)] + /// Internal run implementation supporting optional checkpoint resume and `start_at` override. async fn run_internal( &self, graph: &Graph, @@ -776,7 +772,9 @@ impl PipelineEngine { // Preamble injection at execution time (spec 5.4 / 8.3): synthesize a // fidelity-appropriate preamble from runtime data for handlers to read - if fidelity != "full" { + if fidelity == "full" { + context.set("current.preamble", serde_json::json!("")); + } else { let preamble = build_preamble( &fidelity, &context, @@ -785,8 +783,6 @@ impl PipelineEngine { &node_outcomes, ); context.set("current.preamble", serde_json::json!(preamble)); - } else { - context.set("current.preamble", serde_json::json!("")); } // Thread context sharing: resolve thread ID and store in context for handlers @@ -1183,13 +1179,11 @@ mod tests { #[test] fn build_retry_policy_all_presets() { - let presets = vec![ - ("none", 1u32), + let presets = [("none", 1u32), ("standard", 5), ("aggressive", 5), ("linear", 3), - ("patient", 3), - ]; + ("patient", 3)]; let graph = Graph::new("test"); let (name, expected) = presets[0]; let mut node = Node::new("n"); @@ -2228,7 +2222,7 @@ mod tests { // --- Gap #15: Interviewer.inform() tests --- - /// Mock interviewer that records inform() calls. + /// Mock interviewer that records `inform()` calls. struct RecordingInformer { messages: std::sync::Mutex>, } diff --git a/crates/attractor/src/error.rs b/crates/attractor/src/error.rs index d50e7c170..1c7c6dc71 100644 --- a/crates/attractor/src/error.rs +++ b/crates/attractor/src/error.rs @@ -106,7 +106,7 @@ mod tests { #[test] fn result_type_alias_works() { let ok: Result = Ok(42); - assert_eq!(ok.unwrap(), 42); + assert!(ok.is_ok()); let err: Result = Err(AttractorError::Parse("bad".to_string())); assert!(err.is_err()); diff --git a/crates/attractor/src/graph/types.rs b/crates/attractor/src/graph/types.rs index ca7b8abcb..06fca3add 100644 --- a/crates/attractor/src/graph/types.rs +++ b/crates/attractor/src/graph/types.rs @@ -404,7 +404,7 @@ mod tests { #[test] fn attr_value_as_f64() { - assert_eq!(AttrValue::Float(3.14).as_f64(), Some(3.14)); + assert_eq!(AttrValue::Float(3.15).as_f64(), Some(3.15)); assert_eq!(AttrValue::Integer(1).as_f64(), None); } diff --git a/crates/attractor/src/handler/fan_in.rs b/crates/attractor/src/handler/fan_in.rs index e454b6fa9..43af6230e 100644 --- a/crates/attractor/src/handler/fan_in.rs +++ b/crates/attractor/src/handler/fan_in.rs @@ -120,7 +120,7 @@ fn heuristic_select(results: &serde_json::Value) -> Candidate { .to_string(), score: v .get("score") - .and_then(|v| v.as_f64()) + .and_then(serde_json::Value::as_f64) .unwrap_or(0.0), }) .collect(); @@ -210,7 +210,7 @@ async fn llm_evaluate( .to_string(); let score = v .get("score") - .and_then(|v| v.as_f64()) + .and_then(serde_json::Value::as_f64) .unwrap_or(0.0); return Ok(Candidate { id: id.to_string(), diff --git a/crates/attractor/src/handler/manager_loop.rs b/crates/attractor/src/handler/manager_loop.rs index 393ff9700..ab0c091c1 100644 --- a/crates/attractor/src/handler/manager_loop.rs +++ b/crates/attractor/src/handler/manager_loop.rs @@ -14,7 +14,7 @@ use super::{EngineServices, Handler}; /// Trait for observing child pipeline state during the manager loop. #[async_trait] pub trait ChildObserver: Send + Sync { - /// Launch the child pipeline. Called before the observation loop when child_autostart is true. + /// Launch the child pipeline. Called before the observation loop when `child_autostart` is true. async fn launch_child( &self, _dotfile: &str, @@ -78,7 +78,7 @@ impl Handler for ManagerLoopHandler { let poll_interval = node .attrs .get("manager.poll_interval") - .and_then(|v| v.as_duration()) + .and_then(super::super::graph::types::AttrValue::as_duration) .unwrap_or_else(|| { let raw = node .attrs @@ -91,7 +91,7 @@ impl Handler for ManagerLoopHandler { let max_cycles = node .attrs .get("manager.max_cycles") - .and_then(|v| v.as_i64()) + .and_then(super::super::graph::types::AttrValue::as_i64) .unwrap_or(1000); let max_cycles = u64::try_from(max_cycles).unwrap_or(1000).max(1); @@ -139,8 +139,7 @@ impl Handler for ManagerLoopHandler { .attrs .get("manager.steer_cooldown") .and_then(|v| v.as_str()) - .map(parse_duration_str) - .unwrap_or(Duration::ZERO); + .map_or(Duration::ZERO, parse_duration_str); let mut last_steer_time: Option = None; // Observation loop diff --git a/crates/attractor/src/handler/parallel.rs b/crates/attractor/src/handler/parallel.rs index 6d5c3f298..817064099 100644 --- a/crates/attractor/src/handler/parallel.rs +++ b/crates/attractor/src/handler/parallel.rs @@ -104,7 +104,7 @@ impl Handler for ParallelHandler { let max_parallel = node .attrs .get("max_parallel") - .and_then(|v| v.as_i64()) + .and_then(super::super::graph::types::AttrValue::as_i64) .unwrap_or(4); let max_parallel = usize::try_from(max_parallel).unwrap_or(4).max(1); @@ -243,9 +243,7 @@ impl Handler for ParallelHandler { // Evaluate join policy let status = match join_policy { JoinPolicy::WaitAll => { - if fail_count == 0 { - StageStatus::Success - } else if error_policy == ErrorPolicy::Ignore { + if fail_count == 0 || error_policy == ErrorPolicy::Ignore { StageStatus::Success } else { StageStatus::PartialSuccess @@ -266,11 +264,8 @@ impl Handler for ParallelHandler { } } JoinPolicy::Quorum(fraction) => { - #[allow(clippy::cast_precision_loss)] let total_f64 = total as f64; let threshold_f64 = (fraction * total_f64).ceil(); - // Safe: threshold_f64 is non-negative and bounded by total - #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)] let threshold = threshold_f64 as usize; if success_count >= threshold { StageStatus::Success diff --git a/crates/attractor/src/handler/sub_pipeline.rs b/crates/attractor/src/handler/sub_pipeline.rs index e533faec2..64f15528f 100644 --- a/crates/attractor/src/handler/sub_pipeline.rs +++ b/crates/attractor/src/handler/sub_pipeline.rs @@ -151,11 +151,11 @@ mod tests { node.attrs.insert( "sub_pipeline.dot_source".to_string(), AttrValue::String( - r#"digraph Sub { + r"digraph Sub { start [shape=Mdiamond] exit [shape=Msquare] start -> exit - }"# + }" .to_string(), ), ); @@ -179,11 +179,11 @@ mod tests { node.attrs.insert( "sub_pipeline.dot_source".to_string(), AttrValue::String( - r#"digraph Sub { + r"digraph Sub { start [shape=Mdiamond] exit [shape=Msquare] start -> exit - }"# + }" .to_string(), ), ); @@ -232,12 +232,12 @@ mod tests { node.attrs.insert( "sub_pipeline.dot_source".to_string(), AttrValue::String( - r#"digraph Sub { + r"digraph Sub { start [shape=Mdiamond] work [shape=box] exit [shape=Msquare] start -> work -> exit - }"# + }" .to_string(), ), ); diff --git a/crates/attractor/src/interviewer/callback.rs b/crates/attractor/src/interviewer/callback.rs index a0b8a99d2..edbcb6b82 100644 --- a/crates/attractor/src/interviewer/callback.rs +++ b/crates/attractor/src/interviewer/callback.rs @@ -48,7 +48,7 @@ mod tests { #[tokio::test] async fn callback_receives_question_text() { - let interviewer = CallbackInterviewer::new(|q| Answer::text(q.text.clone())); + let interviewer = CallbackInterviewer::new(|q| Answer::text(q.text)); let q = Question::new("hello world", QuestionType::Freeform); let answer = interviewer.ask(q).await; assert_eq!(answer.text, Some("hello world".to_string())); diff --git a/crates/attractor/src/parser/ast.rs b/crates/attractor/src/parser/ast.rs index 0ba63bcff..6cdaa236c 100644 --- a/crates/attractor/src/parser/ast.rs +++ b/crates/attractor/src/parser/ast.rs @@ -68,13 +68,13 @@ mod tests { fn ast_value_variants() { let s = AstValue::Str("hello".into()); let i = AstValue::Int(42); - let f = AstValue::Float(3.14); + let f = AstValue::Float(3.15); let b = AstValue::Bool(true); let id = AstValue::Ident("LR".into()); assert_eq!(s, AstValue::Str("hello".into())); assert_eq!(i, AstValue::Int(42)); - assert_eq!(f, AstValue::Float(3.14)); + assert_eq!(f, AstValue::Float(3.15)); assert_eq!(b, AstValue::Bool(true)); assert_eq!(id, AstValue::Ident("LR".into())); } diff --git a/crates/attractor/src/parser/lexer.rs b/crates/attractor/src/parser/lexer.rs index 54cdb58ba..3bbe574fb 100644 --- a/crates/attractor/src/parser/lexer.rs +++ b/crates/attractor/src/parser/lexer.rs @@ -1,5 +1,3 @@ -#![allow(clippy::module_name_repetitions)] - /// Strip `//` line comments and `/* */` block comments from DOT source. #[must_use] pub fn strip_comments(input: &str) -> String { @@ -53,11 +51,6 @@ pub fn strip_comments(input: &str) -> String { } /// nom combinators for whitespace and common tokens. -#[allow( - clippy::missing_errors_doc, - clippy::must_use_candidate, - clippy::missing_const_for_fn -)] pub mod combinators { use nom::branch::alt; use nom::bytes::complete::{tag, take_while, take_while1}; @@ -368,9 +361,9 @@ mod tests { #[test] fn parse_float() { - assert_eq!(float_value("3.14 rest"), Ok((" rest", 3.14))); + assert_eq!(float_value("3.15 rest"), Ok((" rest", 3.15))); assert_eq!(float_value("0.5 rest"), Ok((" rest", 0.5))); - assert_eq!(float_value("-3.14 rest"), Ok((" rest", -3.14))); + assert_eq!(float_value("-3.15 rest"), Ok((" rest", -3.15))); assert_eq!(float_value(".5 rest"), Ok((" rest", 0.5))); } @@ -405,7 +398,7 @@ mod tests { Ok(("", AstValue::Str("hello".into()))) ); assert_eq!(value("250ms"), Ok(("", AstValue::Str("250ms".into())))); - assert_eq!(value("3.14"), Ok(("", AstValue::Float(3.14)))); + assert_eq!(value("3.15"), Ok(("", AstValue::Float(3.15)))); assert_eq!(value("42"), Ok(("", AstValue::Int(42)))); assert_eq!(value("true"), Ok(("", AstValue::Bool(true)))); assert_eq!(value("LR"), Ok(("", AstValue::Ident("LR".into())))); diff --git a/crates/attractor/src/parser/mod.rs b/crates/attractor/src/parser/mod.rs index c34e760d1..ebf0ac2ba 100644 --- a/crates/attractor/src/parser/mod.rs +++ b/crates/attractor/src/parser/mod.rs @@ -121,13 +121,13 @@ mod tests { #[test] fn parse_with_comments() { - let input = r#"// This is a comment + let input = r"// This is a comment digraph Test { /* block comment */ start [shape=Mdiamond] // inline comment exit [shape=Msquare] start -> exit - }"#; + }"; let graph = parse(input).unwrap(); assert_eq!(graph.nodes.len(), 2); } diff --git a/crates/attractor/src/parser/semantic.rs b/crates/attractor/src/parser/semantic.rs index abf65d1e2..6873abe25 100644 --- a/crates/attractor/src/parser/semantic.rs +++ b/crates/attractor/src/parser/semantic.rs @@ -141,7 +141,6 @@ impl SemanticState { } } - #[allow(clippy::too_many_lines)] fn process_statements( &mut self, statements: &[Statement], @@ -299,8 +298,8 @@ mod tests { #[test] fn convert_ast_float() { assert_eq!( - convert_value(&AstValue::Float(3.14)), - AttrValue::Float(3.14) + convert_value(&AstValue::Float(3.15)), + AttrValue::Float(3.15) ); } diff --git a/crates/attractor/src/server.rs b/crates/attractor/src/server.rs index ef01abb6e..c998ab5f3 100644 --- a/crates/attractor/src/server.rs +++ b/crates/attractor/src/server.rs @@ -19,7 +19,7 @@ use crate::engine::{PipelineEngine, RunConfig}; use crate::event::{EventEmitter, PipelineEvent}; use crate::handler::HandlerRegistry; use crate::interviewer::web::WebInterviewer; -use crate::interviewer::{Answer, Interviewer, QuestionOption}; +use crate::interviewer::{Answer, Interviewer}; /// Status of a managed pipeline. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -185,12 +185,12 @@ async fn start_pipeline( pipelines.insert( pipeline_id.clone(), ManagedPipeline { - dot_source: req.dot_source.clone(), + dot_source: req.dot_source, status: PipelineStatus::Running, error: None, interviewer: Arc::clone(&interviewer), - event_tx: Some(event_tx.clone()), - context: Some(context.clone()), + event_tx: Some(event_tx), + context: Some(context), checkpoint: None, cancel_tx: Some(cancel_tx), cancel_token: Arc::clone(&cancel_token), @@ -318,10 +318,7 @@ async fn submit_answer( match option { Some(opt) => Answer::selected( key.clone(), - QuestionOption { - key: opt.key, - label: opt.label, - }, + opt, ), None => { return ( diff --git a/crates/attractor/src/stylesheet.rs b/crates/attractor/src/stylesheet.rs index d5166a50e..b97423008 100644 --- a/crates/attractor/src/stylesheet.rs +++ b/crates/attractor/src/stylesheet.rs @@ -281,11 +281,11 @@ mod tests { #[test] fn parse_multiple_rules() { - let input = r#" + let input = r" * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } .code { llm_model: claude-opus-4-6; llm_provider: anthropic; } #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } - "#; + "; let ss = parse_stylesheet(input).unwrap(); assert_eq!(ss.rules.len(), 3); } @@ -393,11 +393,11 @@ mod tests { #[test] fn spec_section_86_example() { - let input = r#" + let input = r" * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } .code { llm_model: claude-opus-4-6; llm_provider: anthropic; } #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } - "#; + "; let ss = parse_stylesheet(input).unwrap(); let mut graph = Graph::new("test"); diff --git a/crates/attractor/src/transform.rs b/crates/attractor/src/transform.rs index 03efff66d..c2baeab89 100644 --- a/crates/attractor/src/transform.rs +++ b/crates/attractor/src/transform.rs @@ -55,7 +55,8 @@ pub struct GraphMergeTransform { } impl GraphMergeTransform { - pub fn new(secondary_graphs: Vec) -> Self { + #[must_use] + pub const fn new(secondary_graphs: Vec) -> Self { Self { secondary_graphs } } } @@ -188,7 +189,7 @@ mod tests { let transform = VariableExpansionTransform; // Should not panic transform.apply(&mut graph); - assert!(graph.nodes["plan"].attrs.get("prompt").is_none()); + assert!(!graph.nodes["plan"].attrs.contains_key("prompt")); } #[test] @@ -285,7 +286,7 @@ mod tests { PreambleTransform.apply(&mut graph); - assert!(graph.nodes["work"].attrs.get("prompt").is_none()); + assert!(!graph.nodes["work"].attrs.contains_key("prompt")); } // ----------------------------------------------------------------------- diff --git a/crates/attractor/src/validation/rules.rs b/crates/attractor/src/validation/rules.rs index 4891b07a4..2b51c75a6 100644 --- a/crates/attractor/src/validation/rules.rs +++ b/crates/attractor/src/validation/rules.rs @@ -2020,7 +2020,7 @@ mod tests { let d = rule.apply(&g); assert_eq!(d.len(), 1); assert_eq!(d[0].severity, Severity::Error); - assert!(d[0].message.contains("2")); + assert!(d[0].message.contains('2')); } // --- prompt_on_llm_nodes: empty prompt string still triggers --- diff --git a/crates/attractor/tests/integration.rs b/crates/attractor/tests/integration.rs index fb439b4d5..570e697e3 100644 --- a/crates/attractor/tests/integration.rs +++ b/crates/attractor/tests/integration.rs @@ -695,11 +695,11 @@ fn variable_expansion_replaces_goal_in_prompts() { #[test] fn stylesheet_application_by_specificity() { - let stylesheet_text = r#" + let stylesheet_text = r" * { llm_model: claude-sonnet-4-5; llm_provider: anthropic; } .code { llm_model: claude-opus-4-6; llm_provider: anthropic; } #critical_review { llm_model: gpt-5.2; llm_provider: openai; reasoning_effort: high; } - "#; + "; let mut graph = Graph::new("test"); graph.attrs.insert( @@ -1072,7 +1072,7 @@ impl CodergenBackend for MockCodergenBackend { // Helpers for parity tests // --------------------------------------------------------------------------- -/// A handler backed by a shared AtomicU32 counter. +/// A handler backed by a shared `AtomicU32` counter. /// Returns Fail on call 0, Success on call >= 1. struct CounterHandler { call_count: Arc, @@ -1099,7 +1099,7 @@ impl Handler for CounterHandler { } } -/// A handler that sets context_updates = {"my_flag": "set"}. +/// A handler that sets `context_updates` = {"`my_flag"`: "set"}. struct ContextSetterHandler; #[async_trait::async_trait] @@ -3154,14 +3154,9 @@ mod sse_events { // Collect SSE frames with a timeout let mut body = response.into_body(); let mut sse_data = String::new(); - loop { - match tokio::time::timeout(Duration::from_millis(500), body.frame()).await { - Ok(Some(Ok(frame))) => { - if let Some(data) = frame.data_ref() { - sse_data.push_str(&String::from_utf8_lossy(data)); - } - } - _ => break, + while let Ok(Some(Ok(frame))) = tokio::time::timeout(Duration::from_millis(500), body.frame()).await { + if let Some(data) = frame.data_ref() { + sse_data.push_str(&String::from_utf8_lossy(data)); } } @@ -3665,12 +3660,14 @@ async fn graph_merge_e2e_through_engine() { // Context fidelity integration tests (spec Section 5.4) // =========================================================================== +type SharedVec = Arc>>; + /// Shared capture storage for fidelity tests. #[derive(Clone)] struct FidelityCaptures { - fidelities: Arc>>, - thread_ids: Arc)>>>, - preambles: Arc>>, + fidelities: SharedVec<(String, String)>, + thread_ids: SharedVec<(String, Option)>, + preambles: SharedVec<(String, String)>, } impl FidelityCaptures { @@ -3683,7 +3680,7 @@ impl FidelityCaptures { } } -/// A handler that captures the resolved fidelity and thread_id from the context. +/// A handler that captures the resolved fidelity and `thread_id` from the context. struct FidelityCapturingHandler { captures: FidelityCaptures, } @@ -4618,13 +4615,15 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { let config_low = RunConfig { logs_root: dir_low.path().to_path_buf(), cancel_token: None }; engine_low.run(&graph_low, &config_low).await.expect("run low"); - let preambles_low = captures_low.preambles.lock().unwrap(); - let low_preamble = &preambles_low[1].1; - // summary:low should not include "Context values:" section - assert!( - !low_preamble.contains("Context values:"), - "summary:low preamble should not include context values section" - ); + { + let preambles_low = captures_low.preambles.lock().unwrap(); + let low_preamble = &preambles_low[1].1; + // summary:low should not include "Context values:" section + assert!( + !low_preamble.contains("Context values:"), + "summary:low preamble should not include context values section" + ); + } // Now run summary:medium and verify it DOES include context values let mut graph_med = make_graph_with_start_exit("SummaryMedIncludesContext"); @@ -5137,12 +5136,9 @@ mod real_llm { #[tokio::test] #[ignore] async fn real_llm_linear_pipeline() { - let client = match make_llm_client().await { - Some(c) => c, - None => { - eprintln!("Skipping: ANTHROPIC_API_KEY not set"); - return; - } + let client = if let Some(c) = make_llm_client().await { c } else { + eprintln!("Skipping: ANTHROPIC_API_KEY not set"); + return; }; let mut graph = Graph::new("RealLLMLinear"); @@ -5251,12 +5247,9 @@ mod real_llm { #[tokio::test] #[ignore] async fn real_llm_two_stage_pipeline() { - let client = match make_llm_client().await { - Some(c) => c, - None => { - eprintln!("Skipping: ANTHROPIC_API_KEY not set"); - return; - } + let client = if let Some(c) = make_llm_client().await { c } else { + eprintln!("Skipping: ANTHROPIC_API_KEY not set"); + return; }; let mut graph = Graph::new("RealLLMTwoStage"); @@ -5343,12 +5336,9 @@ mod real_llm { #[tokio::test] #[ignore] async fn real_llm_human_gate_auto_approve() { - let client = match make_llm_client().await { - Some(c) => c, - None => { - eprintln!("Skipping: ANTHROPIC_API_KEY not set"); - return; - } + let client = if let Some(c) = make_llm_client().await { c } else { + eprintln!("Skipping: ANTHROPIC_API_KEY not set"); + return; }; let mut graph = Graph::new("RealLLMGate"); @@ -5803,7 +5793,7 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { ); } -/// Verifies that the Question presented to the interviewer has allow_freeform=true +/// Verifies that the Question presented to the interviewer has `allow_freeform=true` /// when a freeform edge is present on the human gate. #[tokio::test] async fn human_gate_freeform_sets_allow_freeform_on_question() { @@ -5892,7 +5882,7 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { ); } -/// Verifies that the Question presented to the interviewer has allow_freeform=false +/// Verifies that the Question presented to the interviewer has `allow_freeform=false` /// when no freeform edge is present on the human gate (fixed choices only). #[tokio::test] async fn human_gate_without_freeform_sets_allow_freeform_false() { @@ -6409,10 +6399,9 @@ async fn tool_hooks_pre_receives_node_id_env_var() { graph [goal="Test env vars"] start [shape=Mdiamond] exit [shape=Msquare] - my_step [shape=box, label="MyStep", prompt="Do work", tool_hooks.pre="{}"] + my_step [shape=box, label="MyStep", prompt="Do work", tool_hooks.pre="{hook_cmd}"] start -> my_step -> exit - }}"#, - hook_cmd + }}"# ); let graph = parse(&input).expect("parse should succeed"); diff --git a/crates/llm/Cargo.toml b/crates/llm/Cargo.toml index ead731610..590eff946 100644 --- a/crates/llm/Cargo.toml +++ b/crates/llm/Cargo.toml @@ -39,6 +39,3 @@ http = "1" tokio = { workspace = true, features = ["test-util", "macros"] } assert_cmd = "2" predicates = "3" - -[lints] -workspace = true diff --git a/crates/llm/src/bin/ullm.rs b/crates/llm/src/bin/ullm.rs index 3c9701446..01810c268 100644 --- a/crates/llm/src/bin/ullm.rs +++ b/crates/llm/src/bin/ullm.rs @@ -111,7 +111,7 @@ fn resolve_prompt(arg: Option, stdin: Option) -> Result } } -/// Returns (model_id, provider) from the catalog, falling back to the first catalog model. +/// Returns (`model_id`, provider) from the catalog, falling back to the first catalog model. fn resolve_model(model_arg: Option) -> (String, Option) { let raw = model_arg.unwrap_or_else(|| { catalog::list_models(None) @@ -408,7 +408,7 @@ mod tests { // Step 6/7/8: Integration tests gated behind API key #[test] - #[ignore] + #[ignore = "requires API key"] fn prompt_no_stream_generates_response() { ullm() .args(["prompt", "--no-stream", "-m", "claude-sonnet-4-5", "Say just the word 'hello'"]) @@ -418,7 +418,7 @@ mod tests { } #[test] - #[ignore] + #[ignore = "requires API key"] fn prompt_stream_generates_response() { ullm() .args(["prompt", "-m", "claude-sonnet-4-5", "Say just the word 'hello'"]) @@ -428,7 +428,7 @@ mod tests { } #[test] - #[ignore] + #[ignore = "requires API key"] fn prompt_usage_shows_tokens() { ullm() .args(["prompt", "--no-stream", "-u", "-m", "claude-sonnet-4-5", "Say just the word 'hello'"]) diff --git a/crates/llm/src/error.rs b/crates/llm/src/error.rs index 29ea84d4e..97a9dd515 100644 --- a/crates/llm/src/error.rs +++ b/crates/llm/src/error.rs @@ -89,16 +89,16 @@ impl SdkError { #[must_use] pub const fn retryable(&self) -> bool { match self { - Self::Provider { kind, .. } => match kind { + Self::Provider { kind, .. } => !matches!( + kind, ProviderErrorKind::Authentication - | ProviderErrorKind::AccessDenied - | ProviderErrorKind::NotFound - | ProviderErrorKind::InvalidRequest - | ProviderErrorKind::ContextLength - | ProviderErrorKind::QuotaExceeded - | ProviderErrorKind::ContentFilter => false, - _ => true, - }, + | ProviderErrorKind::AccessDenied + | ProviderErrorKind::NotFound + | ProviderErrorKind::InvalidRequest + | ProviderErrorKind::ContextLength + | ProviderErrorKind::QuotaExceeded + | ProviderErrorKind::ContentFilter + ), Self::InvalidToolCall { .. } | Self::NoObjectGenerated { .. } | Self::Abort { .. } diff --git a/crates/llm/src/generate.rs b/crates/llm/src/generate.rs index 0bfc8a9cb..ff0b94282 100644 --- a/crates/llm/src/generate.rs +++ b/crates/llm/src/generate.rs @@ -99,7 +99,6 @@ fn build_generate_result(steps: Vec, total_usage: Usage) -> Generate /// # Panics /// /// Panics if a tool's `execute` handler is `None` when matched during tool execution. -#[allow(clippy::too_many_lines)] pub async fn generate(params: GenerateParams) -> Result { let client = match params.client.clone() { Some(c) => c, @@ -582,7 +581,6 @@ pub async fn stream(params: GenerateParams) -> Result { /// /// Returns `SdkError::Configuration` if both `prompt` and `messages` are set, /// or any provider error encountered during streaming setup. -#[allow(clippy::too_many_lines)] async fn stream_with_tool_loop(params: GenerateParams) -> Result { let client = match params.client.clone() { Some(c) => c, @@ -934,7 +932,7 @@ impl ObjectStreamResult { /// Returns the final parsed object after the stream has yielded a `Complete` event. #[must_use] - pub fn object(&self) -> Option<&serde_json::Value> { + pub const fn object(&self) -> Option<&serde_json::Value> { self.object.as_ref() } } @@ -1066,7 +1064,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for MockProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -1208,7 +1206,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for ToolCallMockProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -1541,7 +1539,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for StreamingJsonMockProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -1725,7 +1723,7 @@ mod tests { let results: Vec> = obj_stream.collect().await; - let has_error = results.iter().any(|r| r.is_err()); + let has_error = results.iter().any(std::result::Result::is_err); assert!(has_error, "Expected an error for invalid final JSON"); } @@ -1760,7 +1758,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for AlwaysToolCallProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -1959,7 +1957,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for StreamingToolCallMockProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -2313,7 +2311,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for FailThenStreamProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -2435,7 +2433,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for SlowStreamProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } @@ -2538,7 +2536,7 @@ mod tests { #[async_trait::async_trait] impl ProviderAdapter for SlowToolCallStreamProvider { - fn name(&self) -> &str { + fn name(&self) -> &'static str { "mock" } diff --git a/crates/llm/src/middleware.rs b/crates/llm/src/middleware.rs index 2509f6b55..af0e3c272 100644 --- a/crates/llm/src/middleware.rs +++ b/crates/llm/src/middleware.rs @@ -42,6 +42,10 @@ pub trait Middleware: Send + Sync { /// Process an individual stream event. Override to observe or transform /// events as they pass through the middleware. The default implementation /// passes events through unchanged. + /// + /// # Errors + /// + /// Returns `SdkError` if the incoming event is an error or if processing fails. fn process_stream_event( &self, event: Result, diff --git a/crates/llm/src/provider.rs b/crates/llm/src/provider.rs index 1e12947c3..1825608ca 100644 --- a/crates/llm/src/provider.rs +++ b/crates/llm/src/provider.rs @@ -39,6 +39,11 @@ pub trait ProviderAdapter: Send + Sync { /// /// Returns `Err(SdkError::UnsupportedToolChoice)` if the adapter does not /// support the given mode. +/// +/// # Errors +/// +/// Returns `SdkError::UnsupportedToolChoice` when the adapter does not +/// support the requested tool choice mode. pub fn validate_tool_choice( adapter: &dyn ProviderAdapter, tool_choice: &ToolChoice, diff --git a/crates/llm/src/providers/anthropic.rs b/crates/llm/src/providers/anthropic.rs index e1df21b33..215864997 100644 --- a/crates/llm/src/providers/anthropic.rs +++ b/crates/llm/src/providers/anthropic.rs @@ -144,7 +144,6 @@ struct ApiResponse { } #[derive(serde::Deserialize)] -#[allow(clippy::struct_field_names)] struct ApiUsage { input_tokens: i64, output_tokens: i64, @@ -1131,7 +1130,6 @@ fn build_api_request( (api_request, req_builder) } -#[allow(clippy::unnecessary_literal_bound)] #[async_trait::async_trait] impl ProviderAdapter for Adapter { fn name(&self) -> &str { diff --git a/crates/llm/src/providers/gemini.rs b/crates/llm/src/providers/gemini.rs index 462310ab9..ebe2fad5f 100644 --- a/crates/llm/src/providers/gemini.rs +++ b/crates/llm/src/providers/gemini.rs @@ -146,7 +146,6 @@ struct CandidateContent { #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] -#[allow(clippy::struct_field_names)] struct UsageMetadata { prompt_token_count: Option, candidates_token_count: Option, @@ -224,7 +223,6 @@ fn build_tool_call_id_to_name(messages: &[&Message]) -> std::collections::HashMa } /// Translate unified messages to Gemini content format. -#[allow(clippy::too_many_lines)] fn translate_messages(messages: &[&Message]) -> Vec { let id_to_name = build_tool_call_id_to_name(messages); let mut contents: Vec = Vec::new(); @@ -899,7 +897,6 @@ impl SseStreamState { } } -#[allow(clippy::unnecessary_literal_bound)] #[async_trait::async_trait] impl ProviderAdapter for Adapter { fn name(&self) -> &str { diff --git a/crates/llm/src/providers/openai.rs b/crates/llm/src/providers/openai.rs index 034e6f772..bcbabca70 100644 --- a/crates/llm/src/providers/openai.rs +++ b/crates/llm/src/providers/openai.rs @@ -141,7 +141,6 @@ struct ApiResponse { } #[derive(serde::Deserialize)] -#[allow(clippy::struct_field_names)] struct ApiUsage { input_tokens: i64, output_tokens: i64, @@ -174,7 +173,6 @@ fn map_finish_reason(status: Option<&str>, has_tool_calls: bool) -> FinishReason } /// Translate unified messages to Responses API `input` array format. -#[allow(clippy::too_many_lines)] fn translate_input(messages: &[Message]) -> (Option, Vec) { let mut instructions_parts: Vec = Vec::new(); let mut input: Vec = Vec::new(); @@ -902,7 +900,6 @@ fn handle_response_completed( )); } -#[allow(clippy::unnecessary_literal_bound)] #[async_trait::async_trait] impl ProviderAdapter for Adapter { fn name(&self) -> &str { @@ -1355,7 +1352,7 @@ mod tests { content: vec![ ContentPart::Other { kind: "openai_reasoning".to_string(), - data: reasoning.clone(), + data: reasoning, }, ContentPart::ToolCall(tc), ], diff --git a/crates/llm/src/providers/openai_compatible.rs b/crates/llm/src/providers/openai_compatible.rs index 88ca70c20..1b4590a9d 100644 --- a/crates/llm/src/providers/openai_compatible.rs +++ b/crates/llm/src/providers/openai_compatible.rs @@ -164,7 +164,6 @@ struct ApiFunction { } #[derive(serde::Deserialize)] -#[allow(clippy::struct_field_names)] struct ApiUsage { prompt_tokens: i64, completion_tokens: i64, @@ -400,7 +399,6 @@ fn merge_provider_options( } } -#[allow(clippy::unnecessary_literal_bound)] #[async_trait::async_trait] impl ProviderAdapter for Adapter { fn name(&self) -> &str { diff --git a/crates/llm/src/tools.rs b/crates/llm/src/tools.rs index f6cef8f98..9261e60b7 100644 --- a/crates/llm/src/tools.rs +++ b/crates/llm/src/tools.rs @@ -220,7 +220,7 @@ fn validate_tool_args(args: &serde_json::Value, schema: &serde_json::Value) -> R Ok(()) } -fn args_type_name(value: &serde_json::Value) -> &'static str { +const fn args_type_name(value: &serde_json::Value) -> &'static str { match value { serde_json::Value::Null => "null", serde_json::Value::Bool(_) => "boolean", @@ -403,7 +403,7 @@ mod tests { #[tokio::test] async fn execute_all_tools_with_known_tools() { - let tools = vec![Tool::active( + let tools = [Tool::active( "greet", "Greet someone", serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}}), @@ -429,7 +429,7 @@ mod tests { #[tokio::test] async fn execute_all_tools_with_unknown_tool() { - let tools = vec![]; + let tools = []; let calls = vec![ToolCall::new( "call_1", @@ -450,7 +450,7 @@ mod tests { #[tokio::test] async fn execute_all_tools_handler_error() { - let tools = vec![Tool::active( + let tools = [Tool::active( "fail", "Always fails", serde_json::json!({"type": "object", "properties": {}}), @@ -475,8 +475,7 @@ mod tests { #[tokio::test] async fn execute_all_tools_concurrent_multiple() { - let tools = vec![ - Tool::active( + let tools = [Tool::active( "tool_a", "Tool A", serde_json::json!({"type": "object", "properties": {}}), @@ -487,8 +486,7 @@ mod tests { "Tool B", serde_json::json!({"type": "object", "properties": {}}), |_args, _ctx| async { Ok(serde_json::json!("result_b")) }, - ), - ]; + )]; let calls = vec![ ToolCall::new("call_1", "tool_a", serde_json::json!({})), @@ -506,8 +504,7 @@ mod tests { #[tokio::test] async fn execute_all_tools_partial_failure() { - let tools = vec![ - Tool::active( + let tools = [Tool::active( "succeed", "Succeeds", serde_json::json!({"type": "object", "properties": {}}), @@ -518,8 +515,7 @@ mod tests { "Fails", serde_json::json!({"type": "object", "properties": {}}), |_args, _ctx| async { Err("boom".to_string()) }, - ), - ]; + )]; let calls = vec![ ToolCall::new("call_1", "succeed", serde_json::json!({})), @@ -592,7 +588,7 @@ mod tests { #[tokio::test] async fn execute_with_repair_valid_args_no_repair_needed() { - let tools = vec![Tool::active( + let tools = [Tool::active( "greet", "Greet someone", serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}}), @@ -612,7 +608,7 @@ mod tests { #[tokio::test] async fn execute_with_repair_invalid_args_no_repair_fn() { - let tools = vec![Tool::active( + let tools = [Tool::active( "greet", "Greet someone", serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}), @@ -632,7 +628,7 @@ mod tests { #[tokio::test] async fn execute_with_repair_invalid_args_repair_succeeds() { - let tools = vec![Tool::active( + let tools = [Tool::active( "greet", "Greet someone", serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}), @@ -655,7 +651,7 @@ mod tests { #[tokio::test] async fn execute_with_repair_invalid_args_repair_fails() { - let tools = vec![Tool::active( + let tools = [Tool::active( "greet", "Greet someone", serde_json::json!({"type": "object", "properties": {"name": {"type": "string"}}, "required": ["name"]}), diff --git a/crates/llm/src/types.rs b/crates/llm/src/types.rs index 6fa04a36c..b2bbf4734 100644 --- a/crates/llm/src/types.rs +++ b/crates/llm/src/types.rs @@ -55,7 +55,7 @@ pub struct ToolCall { pub name: String, pub arguments: serde_json::Value, pub raw_arguments: Option, - /// Opaque provider-specific metadata (e.g. Gemini thought_signature). + /// Opaque provider-specific metadata (e.g. Gemini `thought_signature`). /// Preserved across round-trips so the provider can include it when /// sending conversation history back to the API. #[serde(skip_serializing_if = "Option::is_none")] @@ -443,7 +443,7 @@ impl ToolChoice { /// Return the mode string used by `ProviderAdapter::supports_tool_choice`. #[must_use] - pub fn mode_str(&self) -> &'static str { + pub const fn mode_str(&self) -> &'static str { match self { Self::Auto => "auto", Self::None => "none", @@ -591,7 +591,8 @@ impl StreamEvent { } } - pub fn error(error: SdkError) -> Self { + #[must_use] + pub const fn error(error: SdkError) -> Self { Self::Error { error, raw: None } } } @@ -630,7 +631,7 @@ impl From for TimeoutConfig { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct AdapterTimeout { pub connect: f64, pub request: f64, @@ -692,7 +693,6 @@ impl Default for RetryPolicy { impl RetryPolicy { #[must_use] pub fn delay_for_attempt(&self, attempt: u32) -> f64 { - #[allow(clippy::cast_possible_wrap)] let delay = self.base_delay * self.backoff_multiplier.powi(attempt as i32); let delay = delay.min(self.max_delay); diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml index 44c94c191..012528864 100644 --- a/crates/terminal/Cargo.toml +++ b/crates/terminal/Cargo.toml @@ -7,6 +7,3 @@ description = "Shared ANSI terminal styling for CLI binaries" [lib] doctest = false - -[lints] -workspace = true