From 9338c045460eb19bc5bcc52bf2f75096482b9111 Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Sun, 28 Jun 2026 09:55:07 -0400 Subject: [PATCH 1/7] style: format the workspace with cargo fmt --all First pass of CI foundation work. No behavior change: rustfmt reflows only (import ordering, line wrapping, multi-line assert/expression formatting). Makes the new fmt --check CI job green. --- crates/frameshift-capabilities/src/tracker.rs | 11 +- .../src/backend.rs | 22 +-- .../frameshift-catalog-postgres/src/schema.rs | 8 +- crates/frameshift-catalog/src/lib.rs | 2 +- crates/frameshift-cli/src/cmd/diff.rs | 5 +- crates/frameshift-cli/src/cmd/feedback.rs | 13 +- crates/frameshift-cli/src/cmd/grow.rs | 5 +- crates/frameshift-cli/src/cmd/publish.rs | 10 +- crates/frameshift-cli/src/cmd/render.rs | 5 +- crates/frameshift-cli/src/cmd/rule.rs | 15 +- crates/frameshift-cli/src/cmd/skill.rs | 15 +- crates/frameshift-client/src/lib.rs | 67 ++++--- crates/frameshift-client/src/publish.rs | 62 ++++--- crates/frameshift-client/src/registry.rs | 20 +- crates/frameshift-client/src/selection.rs | 3 +- .../frameshift-client/tests/project_id_env.rs | 5 +- crates/frameshift-compose/src/composer.rs | 11 +- crates/frameshift-compose/src/conflict.rs | 10 +- crates/frameshift-compose/src/merge.rs | 8 +- crates/frameshift-conformance/src/score.rs | 78 +++++--- crates/frameshift-daemon/src/orchestrator.rs | 23 ++- crates/frameshift-growth/src/lib.rs | 49 +++-- crates/frameshift-mcp/src/lib.rs | 4 +- crates/frameshift-mcp/src/prompts.rs | 52 +++--- crates/frameshift-mcp/src/protocol.rs | 16 +- crates/frameshift-mcp/src/tools.rs | 37 ++-- .../src/adapter.rs | 9 +- crates/frameshift-orchestrator/src/audit.rs | 12 +- crates/frameshift-orchestrator/src/context.rs | 173 +++++++++++++++--- .../frameshift-orchestrator/src/controller.rs | 27 ++- .../frameshift-orchestrator/src/feedback.rs | 4 +- crates/frameshift-orchestrator/src/index.rs | 128 ++++++++----- crates/frameshift-orchestrator/src/intent.rs | 12 +- crates/frameshift-orchestrator/src/lib.rs | 9 +- crates/frameshift-orchestrator/src/mode.rs | 15 +- crates/frameshift-orchestrator/src/policy.rs | 21 ++- crates/frameshift-orchestrator/src/run.rs | 6 +- crates/frameshift-pack/src/manifest.rs | 6 +- crates/frameshift-pack/src/pack.rs | 7 +- .../frameshift-runtime/tests/integration.rs | 4 +- crates/frameshift-seed/src/main.rs | 56 +++--- crates/frameshift-server/src/auth.rs | 16 +- crates/frameshift-server/src/main.rs | 9 +- .../frameshift-server/src/middleware/auth.rs | 3 +- .../src/middleware/metrics.rs | 5 +- .../frameshift-server/src/routes/handles.rs | 4 +- crates/frameshift-server/src/routes/packs.rs | 6 +- .../frameshift-server/tests/authors_write.rs | 6 +- crates/frameshift-server/tests/integration.rs | 19 +- crates/frameshift-server/tests/publish.rs | 29 ++- crates/frameshift-source/src/validate.rs | 6 +- crates/frameshift-vault/src/validate.rs | 15 +- 52 files changed, 777 insertions(+), 386 deletions(-) diff --git a/crates/frameshift-capabilities/src/tracker.rs b/crates/frameshift-capabilities/src/tracker.rs index 30b1f30..ccdb4a2 100644 --- a/crates/frameshift-capabilities/src/tracker.rs +++ b/crates/frameshift-capabilities/src/tracker.rs @@ -52,8 +52,15 @@ mod tests { .map(String::from) .collect() ); - assert_eq!(report.used, ["Read"].into_iter().map(String::from).collect()); - let unused: Vec<&str> = report.unused_capabilities().iter().map(|s| s.as_str()).collect(); + assert_eq!( + report.used, + ["Read"].into_iter().map(String::from).collect() + ); + let unused: Vec<&str> = report + .unused_capabilities() + .iter() + .map(|s| s.as_str()) + .collect(); assert_eq!(unused, vec!["Bash", "Edit"]); } diff --git a/crates/frameshift-catalog-postgres/src/backend.rs b/crates/frameshift-catalog-postgres/src/backend.rs index 13e8575..946d859 100644 --- a/crates/frameshift-catalog-postgres/src/backend.rs +++ b/crates/frameshift-catalog-postgres/src/backend.rs @@ -31,8 +31,8 @@ use frameshift_catalog::{ use crate::config::PostgresCatalogConfig; use crate::errors::{map_diesel_error, map_migration_error, map_pool_error}; use crate::models::{ - vec_to_pubkey, AuthorRow, HandleRow, NewAuthorRow, NewHandleRow, NewPackDownloadRow, NewPackRow, - NewPackVersionRow, PackRow, PackVersionRow, + vec_to_pubkey, AuthorRow, HandleRow, NewAuthorRow, NewHandleRow, NewPackDownloadRow, + NewPackRow, NewPackVersionRow, PackRow, PackVersionRow, }; use crate::pool::{build_pool, PgPool}; use crate::schema::{authors, handles, pack_downloads, pack_versions, packs}; @@ -861,16 +861,14 @@ impl CatalogBackend for PostgresCatalog { ) -> Result<(), CatalogError> { let mut conn = self.pool.get().await.map_err(map_pool_error)?; - let rows_affected = diesel::sql_query( - "UPDATE packs SET extends = $1 WHERE name = $2", - ) - .bind::, _>( - extends.map(str::to_string), - ) - .bind::(pack_name.to_string()) - .execute(&mut *conn) - .await - .map_err(|e| map_diesel_error(e, "pack", pack_name.to_string()))?; + let rows_affected = diesel::sql_query("UPDATE packs SET extends = $1 WHERE name = $2") + .bind::, _>( + extends.map(str::to_string), + ) + .bind::(pack_name.to_string()) + .execute(&mut *conn) + .await + .map_err(|e| map_diesel_error(e, "pack", pack_name.to_string()))?; if rows_affected == 0 { return Err(CatalogError::NotFound { diff --git a/crates/frameshift-catalog-postgres/src/schema.rs b/crates/frameshift-catalog-postgres/src/schema.rs index 72e65ee..194c524 100644 --- a/crates/frameshift-catalog-postgres/src/schema.rs +++ b/crates/frameshift-catalog-postgres/src/schema.rs @@ -137,4 +137,10 @@ diesel::joinable!(pack_versions -> authors (author_pubkey)); // Allow Diesel join inference between handles and authors. diesel::joinable!(handles -> authors (pubkey)); -diesel::allow_tables_to_appear_in_same_query!(authors, packs, pack_versions, handles, pack_downloads,); +diesel::allow_tables_to_appear_in_same_query!( + authors, + packs, + pack_versions, + handles, + pack_downloads, +); diff --git a/crates/frameshift-catalog/src/lib.rs b/crates/frameshift-catalog/src/lib.rs index 4890347..431f871 100644 --- a/crates/frameshift-catalog/src/lib.rs +++ b/crates/frameshift-catalog/src/lib.rs @@ -58,7 +58,7 @@ pub(crate) mod serde_helpers { pub use backend::CatalogBackend; pub use error::{CatalogError, HealthStatus}; pub use filters::{PackSearchFilters, PackSearchResult, SortMode}; -pub use identity::Ed25519PublicKey; pub use frameshift_pack::ObjectHash; +pub use identity::Ed25519PublicKey; pub use records::{AuthorRecord, OauthLink, PackRecord, PackVersionRecord}; pub use status::{PackStatus, TombstoneReason, TombstoneRecord}; diff --git a/crates/frameshift-cli/src/cmd/diff.rs b/crates/frameshift-cli/src/cmd/diff.rs index 238f501..1f195cb 100644 --- a/crates/frameshift-cli/src/cmd/diff.rs +++ b/crates/frameshift-cli/src/cmd/diff.rs @@ -171,7 +171,10 @@ mod tests { }; src_b.write_to_dir(&dir_b).expect("write pb"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = DiffArgs { persona_a: "pa".to_string(), persona_b: "pb".to_string(), diff --git a/crates/frameshift-cli/src/cmd/feedback.rs b/crates/frameshift-cli/src/cmd/feedback.rs index e6c8c39..119cac3 100644 --- a/crates/frameshift-cli/src/cmd/feedback.rs +++ b/crates/frameshift-cli/src/cmd/feedback.rs @@ -58,20 +58,19 @@ pub fn run_feedback(client: &Client, args: FeedbackArgs) -> Result<(), CliError> } }); - prefs.record_override_with_intent( - args.auto_pick.as_deref(), - &args.chosen, - intent, - ); + prefs.record_override_with_intent(args.auto_pick.as_deref(), &args.chosen, intent); - prefs.save(&prefs_path) + prefs + .save(&prefs_path) .map_err(|e| CliError::Orchestrator(e.to_string()))?; println!( "recorded override: {} -> {}{}", args.auto_pick.as_deref().unwrap_or("(none)"), args.chosen, - args.intent.as_deref().map_or(String::new(), |i| format!(" (intent: {i})")), + args.intent + .as_deref() + .map_or(String::new(), |i| format!(" (intent: {i})")), ); Ok(()) diff --git a/crates/frameshift-cli/src/cmd/grow.rs b/crates/frameshift-cli/src/cmd/grow.rs index cbc412f..6fc533e 100644 --- a/crates/frameshift-cli/src/cmd/grow.rs +++ b/crates/frameshift-cli/src/cmd/grow.rs @@ -40,9 +40,8 @@ pub fn run(args: GrowArgs) -> Result<(), CliError> { /// Execute grow append -- write a timestamped entry to the persona's growth.md. fn run_append(args: AppendArgs) -> Result<(), CliError> { let client = frameshift_client::Client::with_default_data_root()?; - let project_root = std::env::current_dir().map_err(|e| { - CliError::Growth(format!("cannot determine current directory: {}", e)) - })?; + let project_root = std::env::current_dir() + .map_err(|e| CliError::Growth(format!("cannot determine current directory: {}", e)))?; let project_id = client.project_id(&project_root)?; frameshift_growth::append(client.data_root(), &project_id, &args.persona, &args.text) diff --git a/crates/frameshift-cli/src/cmd/publish.rs b/crates/frameshift-cli/src/cmd/publish.rs index 2932cd5..aceb53a 100644 --- a/crates/frameshift-cli/src/cmd/publish.rs +++ b/crates/frameshift-cli/src/cmd/publish.rs @@ -11,7 +11,7 @@ use clap::Args; use frameshift_client::{Client, ClientError}; use frameshift_source::render::{render_to_markdown, RenderTarget}; -use crate::util::{CliError, load_persona_by_name, validate_server_url}; +use crate::util::{load_persona_by_name, validate_server_url, CliError}; /// Default pack version when a persona source declares none. const DEFAULT_PACK_VERSION: &str = "0.1.0"; @@ -138,7 +138,10 @@ fn write_pack_toml( ("handle", handle), ("author_pubkey", author_pubkey_hex), ] { - if value.chars().any(|c| c == '"' || c == '\\' || c.is_control()) { + if value + .chars() + .any(|c| c == '"' || c == '\\' || c.is_control()) + { return Err(CliError::Publish(format!( "{field} contains characters not allowed in a pack manifest \ (quotes, backslashes, or control characters): {value:?}" @@ -250,7 +253,8 @@ tone = "neutral" // A clean set of fields writes a loadable pack.toml. write_pack_toml(dir.path(), "demo", "0.1.0", "alice", "deadbeef") .expect("clean fields must succeed"); - let written = std::fs::read_to_string(dir.path().join("pack.toml")).expect("read pack.toml"); + let written = + std::fs::read_to_string(dir.path().join("pack.toml")).expect("read pack.toml"); assert!(written.contains("author_handle = \"alice\"")); } } diff --git a/crates/frameshift-cli/src/cmd/render.rs b/crates/frameshift-cli/src/cmd/render.rs index 8dbdd98..4af005d 100644 --- a/crates/frameshift-cli/src/cmd/render.rs +++ b/crates/frameshift-cli/src/cmd/render.rs @@ -109,7 +109,10 @@ mod tests { // Verify the source loads; we check the rendered content indirectly // by ensuring run_render does not error. - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = RenderArgs { persona: "render-test".to_string(), target: RenderTargetArg(RenderTarget::Generic), diff --git a/crates/frameshift-cli/src/cmd/rule.rs b/crates/frameshift-cli/src/cmd/rule.rs index ea16dfd..45ab9d2 100644 --- a/crates/frameshift-cli/src/cmd/rule.rs +++ b/crates/frameshift-cli/src/cmd/rule.rs @@ -174,7 +174,10 @@ mod tests { src.write_to_dir(&persona_dir) .expect("write initial source"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = RuleAddArgs { persona: "test-persona".to_string(), id: "no-panic".to_string(), @@ -220,7 +223,10 @@ mod tests { }; src.write_to_dir(&persona_dir).expect("write"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = RuleRemoveArgs { persona: "p2".to_string(), id: "r1".to_string(), @@ -260,7 +266,10 @@ mod tests { }; src.write_to_dir(&persona_dir).expect("write"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = RuleAddArgs { persona: "dup".to_string(), id: "existing".to_string(), diff --git a/crates/frameshift-cli/src/cmd/skill.rs b/crates/frameshift-cli/src/cmd/skill.rs index 0036867..2bc87f3 100644 --- a/crates/frameshift-cli/src/cmd/skill.rs +++ b/crates/frameshift-cli/src/cmd/skill.rs @@ -116,7 +116,10 @@ mod tests { let src = PersonaSource::new(Persona::new("sp1")); src.write_to_dir(&persona_dir).expect("write"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = SkillAddArgs { persona: "sp1".to_string(), id: "brainstorming".to_string(), @@ -160,7 +163,10 @@ mod tests { }; src.write_to_dir(&persona_dir).expect("write"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = SkillRemoveArgs { persona: "sp2".to_string(), id: "brainstorming".to_string(), @@ -198,7 +204,10 @@ mod tests { }; src.write_to_dir(&persona_dir).expect("write"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let args = SkillAddArgs { persona: "dup-skill".to_string(), id: "brainstorming".to_string(), diff --git a/crates/frameshift-client/src/lib.rs b/crates/frameshift-client/src/lib.rs index 620984a..47c20d2 100644 --- a/crates/frameshift-client/src/lib.rs +++ b/crates/frameshift-client/src/lib.rs @@ -13,7 +13,9 @@ pub use model::{ PersonaSpec, ProjectConfig, ProjectPaths, SyncReport, SCHEMA_VERSION, }; pub use publish::PublishOutcome; -pub use selection::{SelectionEvent, SelectionTelemetry, SELECTION_HISTORY_FILENAME, TELEMETRY_URL_ENV}; +pub use selection::{ + SelectionEvent, SelectionTelemetry, SELECTION_HISTORY_FILENAME, TELEMETRY_URL_ENV, +}; use base64::{engine::general_purpose, Engine as _}; use ed25519_dalek::VerifyingKey; @@ -1043,10 +1045,7 @@ mod tests { } /// Helper: set up a minimal pack and install it, returning the client and project root. - fn install_test_persona( - tmp: &tempfile::TempDir, - name: &str, - ) -> (Client, std::path::PathBuf) { + fn install_test_persona(tmp: &tempfile::TempDir, name: &str) -> (Client, std::path::PathBuf) { let pack_dir = tmp.path().join("pack"); fs::create_dir_all(&pack_dir).unwrap(); fs::write( @@ -1086,11 +1085,12 @@ mod tests { fn installed_persona_source_dirs_returns_entries() { let tmp = tempfile::tempdir().unwrap(); let (client, project_root) = install_test_persona(&tmp, "mypersona"); - let dirs = client - .installed_persona_source_dirs(&project_root) - .unwrap(); + let dirs = client.installed_persona_source_dirs(&project_root).unwrap(); assert_eq!(dirs.len(), 1, "expected exactly one source dir"); - assert!(dirs[0].ends_with("source"), "source dir should end with 'source'"); + assert!( + dirs[0].ends_with("source"), + "source dir should end with 'source'" + ); } /// installed_persona_source_dirs returns empty vec when no personas installed. @@ -1103,9 +1103,7 @@ mod tests { data_root: tmp.path().join("data"), config_root: None, }); - let dirs = client - .installed_persona_source_dirs(&project_root) - .unwrap(); + let dirs = client.installed_persona_source_dirs(&project_root).unwrap(); assert!(dirs.is_empty()); } @@ -1117,7 +1115,9 @@ mod tests { let content = client .rendered_persona(&project_root, "rendtest", "claude") .unwrap(); - assert!(content.contains("rendtest") || content.contains("Rendtest") || !content.is_empty()); + assert!( + content.contains("rendtest") || content.contains("Rendtest") || !content.is_empty() + ); } /// rendered_persona returns an error for an unknown render target. @@ -1163,7 +1163,10 @@ mod tests { assert!(state_dir.exists(), "state dir should exist after install"); // The path should contain "projects" and the project id. let s = state_dir.to_string_lossy(); - assert!(s.contains("projects"), "state dir path must contain 'projects'"); + assert!( + s.contains("projects"), + "state dir path must contain 'projects'" + ); } #[test] @@ -1203,7 +1206,11 @@ mod tests { "schema_version = 1\nname = \"testpersona\"\nauthor_handle = \"test\"\nauthor_pubkey = \"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef\"\nversion = \"0.1.0\"\n", ) .unwrap(); - fs::write(pack_dir.join("AGENTS.md"), "# Test Persona\n\nBehavior rules here.\n").unwrap(); + fs::write( + pack_dir.join("AGENTS.md"), + "# Test Persona\n\nBehavior rules here.\n", + ) + .unwrap(); let project_root = tmp.path().join("project"); fs::create_dir_all(&project_root).unwrap(); @@ -1230,15 +1237,27 @@ mod tests { .join("personas/testpersona/rendered/claude/CLAUDE.md"); let content = fs::read_to_string(&rendered).unwrap(); - assert!(content.contains("# Infrastructure"), "missing infra overlay"); + assert!( + content.contains("# Infrastructure"), + "missing infra overlay" + ); assert!(content.contains("Test infra content"), "missing infra body"); - assert!(content.contains("Active persona: testpersona"), "missing persona context header"); - assert!(content.contains("# Test Persona"), "missing persona content"); + assert!( + content.contains("Active persona: testpersona"), + "missing persona context header" + ); + assert!( + content.contains("# Test Persona"), + "missing persona content" + ); // Infra must come before persona content let infra_pos = content.find("# Infrastructure").unwrap(); let persona_pos = content.find("# Test Persona").unwrap(); - assert!(infra_pos < persona_pos, "infra overlay must precede persona content"); + assert!( + infra_pos < persona_pos, + "infra overlay must precede persona content" + ); } #[test] @@ -1280,7 +1299,13 @@ mod tests { .join("personas/noinfratestp/rendered/claude/CLAUDE.md"); let content = fs::read_to_string(&rendered).unwrap(); - assert!(content.contains("# Bare Persona"), "persona content must be present"); - assert!(!content.contains("Infrastructure"), "no infra overlay expected"); + assert!( + content.contains("# Bare Persona"), + "persona content must be present" + ); + assert!( + !content.contains("Infrastructure"), + "no infra overlay expected" + ); } } diff --git a/crates/frameshift-client/src/publish.rs b/crates/frameshift-client/src/publish.rs index b9d9923..72bc18c 100644 --- a/crates/frameshift-client/src/publish.rs +++ b/crates/frameshift-client/src/publish.rs @@ -74,12 +74,14 @@ pub fn load_or_create_signing_key(data_root: &Path) -> Result Result { use std::io::Write as _; - file.write_all(&key.to_bytes()).map_err(|source| ClientError::Io { - path: path.clone(), - source, - })?; + file.write_all(&key.to_bytes()) + .map_err(|source| ClientError::Io { + path: path.clone(), + source, + })?; Ok(key) } // Another process created the key between our `exists` check and here; @@ -122,12 +125,14 @@ pub fn load_or_create_signing_key(data_root: &Path) -> Result Err(ClientError::Io { @@ -165,8 +170,8 @@ pub fn register_author( "handle": handle, "display_name": display_name, }); - let body = serde_json::to_vec(&body_value) - .map_err(|e| ClientError::JsonSerialize(e.to_string()))?; + let body = + serde_json::to_vec(&body_value).map_err(|e| ClientError::JsonSerialize(e.to_string()))?; let base = server_url.trim_end_matches('/'); let url = format!("{base}/v1/authors"); @@ -222,11 +227,7 @@ pub fn publish_pack_dir( /// Send a prepared signed request body, mapping non-2xx statuses to /// [`ClientError::RegistryRejected`] (with the status code preserved) and /// transport errors to [`ClientError::RegistryHttp`]. -fn send_signed( - req: ureq::Request, - url: &str, - body: &[u8], -) -> Result { +fn send_signed(req: ureq::Request, url: &str, body: &[u8]) -> Result { match req.send_bytes(body) { Ok(response) => Ok(response), Err(ureq::Error::Status(status, response)) => { @@ -572,9 +573,8 @@ mod tests { let timestamp = hdr.get("x-frameshift-timestamp").unwrap(); let nonce = hdr.get("x-frameshift-nonce").unwrap(); let body_hex = hex::encode(Sha256::digest(&body)); - let message = format!( - "{SIGNING_DOMAIN}\nPOST\n/v1/packs\n{body_hex}\n{timestamp}\n{nonce}" - ); + let message = + format!("{SIGNING_DOMAIN}\nPOST\n/v1/packs\n{body_hex}\n{timestamp}\n{nonce}"); let pk_bytes: [u8; 32] = URL_SAFE_NO_PAD .decode(hdr.get("x-frameshift-pubkey").unwrap()) .unwrap() @@ -617,7 +617,13 @@ mod tests { assert_eq!(outcome.pack_hash, "abc123"); let (envelope_ok, fields_ok) = handle.join().unwrap(); - assert!(envelope_ok, "signed-request envelope must verify on the server side"); - assert!(fields_ok, "all three multipart fields must reach the server"); + assert!( + envelope_ok, + "signed-request envelope must verify on the server side" + ); + assert!( + fields_ok, + "all three multipart fields must reach the server" + ); } } diff --git a/crates/frameshift-client/src/registry.rs b/crates/frameshift-client/src/registry.rs index da11a16..b4fa122 100644 --- a/crates/frameshift-client/src/registry.rs +++ b/crates/frameshift-client/src/registry.rs @@ -206,10 +206,12 @@ pub fn fetch_and_install( /// Returns a structured [`ClientError::RegistryHttp`] on HTTP errors or /// deserialization failures. fn ureq_get_json(url: &str) -> Result { - let response = ureq::get(url).call().map_err(|err| ClientError::RegistryHttp { - url: url.to_string(), - detail: err.to_string(), - })?; + let response = ureq::get(url) + .call() + .map_err(|err| ClientError::RegistryHttp { + url: url.to_string(), + detail: err.to_string(), + })?; if response.status() != 200 { return Err(ClientError::RegistryHttp { @@ -231,10 +233,12 @@ fn ureq_get_json(url: &str) -> Result Result, ClientError> { - let response = ureq::get(url).call().map_err(|err| ClientError::RegistryHttp { - url: url.to_string(), - detail: err.to_string(), - })?; + let response = ureq::get(url) + .call() + .map_err(|err| ClientError::RegistryHttp { + url: url.to_string(), + detail: err.to_string(), + })?; if response.status() != 200 { return Err(ClientError::RegistryHttp { diff --git a/crates/frameshift-client/src/selection.rs b/crates/frameshift-client/src/selection.rs index d23f0b0..7ba9406 100644 --- a/crates/frameshift-client/src/selection.rs +++ b/crates/frameshift-client/src/selection.rs @@ -78,7 +78,8 @@ pub fn append_selection_event( ) -> Result<(), ClientError> { use std::io::Write as _; // Serialize to a single line; JSONL requires exactly one object per line. - let line = serde_json::to_string(event).map_err(|e| ClientError::JsonSerialize(e.to_string()))?; + let line = + serde_json::to_string(event).map_err(|e| ClientError::JsonSerialize(e.to_string()))?; let mut file = std::fs::OpenOptions::new() .create(true) .append(true) diff --git a/crates/frameshift-client/tests/project_id_env.rs b/crates/frameshift-client/tests/project_id_env.rs index 361523c..9231837 100644 --- a/crates/frameshift-client/tests/project_id_env.rs +++ b/crates/frameshift-client/tests/project_id_env.rs @@ -14,7 +14,10 @@ fn project_id_env_override_is_used_verbatim() { fs::create_dir_all(&project_root).expect("create project"); std::env::set_var("FRAMESHIFT_PROJECT_ID", "team-alpha"); - let client = Client::new(ClientOptions { data_root, config_root: None }); + let client = Client::new(ClientOptions { + data_root, + config_root: None, + }); let result = client.project_id(&project_root); std::env::remove_var("FRAMESHIFT_PROJECT_ID"); diff --git a/crates/frameshift-compose/src/composer.rs b/crates/frameshift-compose/src/composer.rs index 6228080..20c24f4 100644 --- a/crates/frameshift-compose/src/composer.rs +++ b/crates/frameshift-compose/src/composer.rs @@ -186,14 +186,21 @@ mod tests { ); resolver.insert( "strict-mixin", - source_with_rule("strict-mixin", make_rule("no-panic", SourceLayer::L1, false)), + source_with_rule( + "strict-mixin", + make_rule("no-panic", SourceLayer::L1, false), + ), ); let composer = Composer::new(resolver); let root = PersonaSource::new(Persona::new("root")); let err = composer - .compose(root, Some("base".to_string()), &["strict-mixin".to_string()]) + .compose( + root, + Some("base".to_string()), + &["strict-mixin".to_string()], + ) .expect_err("mixin L1 override should fail"); assert!( diff --git a/crates/frameshift-compose/src/conflict.rs b/crates/frameshift-compose/src/conflict.rs index 7813b25..cc8582e 100644 --- a/crates/frameshift-compose/src/conflict.rs +++ b/crates/frameshift-compose/src/conflict.rs @@ -152,10 +152,7 @@ mod tests { }], skill_collisions: vec![IdCollision { id: "tdd".to_string(), - layers: vec![ - Layer::Mixin("a".to_string()), - Layer::Mixin("b".to_string()), - ], + layers: vec![Layer::Mixin("a".to_string()), Layer::Mixin("b".to_string())], }], }; @@ -166,10 +163,7 @@ mod tests { })); assert!(conflicts.contains(&Conflict::SkillIdCollision { id: "tdd".to_string(), - layers: vec![ - Layer::Mixin("a".to_string()), - Layer::Mixin("b".to_string()), - ], + layers: vec![Layer::Mixin("a".to_string()), Layer::Mixin("b".to_string()),], })); } diff --git a/crates/frameshift-compose/src/merge.rs b/crates/frameshift-compose/src/merge.rs index ba6d55f..b10c183 100644 --- a/crates/frameshift-compose/src/merge.rs +++ b/crates/frameshift-compose/src/merge.rs @@ -264,8 +264,8 @@ mod tests { fn merge_two_empty_sources_yields_empty_composed() { let a = empty_fixture("base"); let b = empty_fixture("root"); - let composed = merge_sources(MergeOrder::BaseFirst, &[&a, &b]) - .expect("empty merge should succeed"); + let composed = + merge_sources(MergeOrder::BaseFirst, &[&a, &b]).expect("empty merge should succeed"); assert!(composed.rules.is_empty()); assert!(composed.skills.is_empty()); assert!(composed.patterns.stack.is_empty()); @@ -335,8 +335,8 @@ mod tests { }, ]; - let err = merge_layers(&layers) - .expect_err("root without override_inherited should fail for L1"); + let err = + merge_layers(&layers).expect_err("root without override_inherited should fail for L1"); assert!( matches!(&err, ComposeError::L1Override { rule_id, .. } if rule_id == "no-panic"), "unexpected error: {err}" diff --git a/crates/frameshift-conformance/src/score.rs b/crates/frameshift-conformance/src/score.rs index f54a293..b110132 100644 --- a/crates/frameshift-conformance/src/score.rs +++ b/crates/frameshift-conformance/src/score.rs @@ -32,25 +32,23 @@ pub fn score_test(test: &TestCase, response: &str) -> Score { _ => Score::ZERO, }, ScorerKind::Regex => match &test.expected { - ExpectedBehavior::Matches { pattern } => { - match regex::Regex::new(pattern) { - Ok(re) => { - if re.is_match(response) { - Score::PERFECT - } else { - Score::ZERO - } - } - Err(_) => { - tracing::warn!( - pattern = %pattern, - "invalid regex in test case {}", - test.id - ); + ExpectedBehavior::Matches { pattern } => match regex::Regex::new(pattern) { + Ok(re) => { + if re.is_match(response) { + Score::PERFECT + } else { Score::ZERO } } - } + Err(_) => { + tracing::warn!( + pattern = %pattern, + "invalid regex in test case {}", + test.id + ); + Score::ZERO + } + }, _ => Score::ZERO, }, ScorerKind::ExactJson => match &test.expected { @@ -113,10 +111,14 @@ pub fn bundle_score(bundle: &TestBundle, results: &[(TestCase, String)]) -> Scor /// response seen for each test id. Shared by [`bundle_score`] and /// [`crate::caller::score_bundle_with_caller`] so both resist omitted and /// duplicated result entries identically. -pub(crate) fn first_response_per_id(results: &[(TestCase, String)]) -> std::collections::HashMap<&str, &str> { +pub(crate) fn first_response_per_id( + results: &[(TestCase, String)], +) -> std::collections::HashMap<&str, &str> { let mut responses: std::collections::HashMap<&str, &str> = std::collections::HashMap::new(); for (case, response) in results { - responses.entry(case.id.as_str()).or_insert(response.as_str()); + responses + .entry(case.id.as_str()) + .or_insert(response.as_str()); } responses } @@ -154,7 +156,9 @@ mod tests { fn regex_scorer_matches_substring() { let case = make_case( "t1", - ExpectedBehavior::Matches { pattern: "hello".to_string() }, + ExpectedBehavior::Matches { + pattern: "hello".to_string(), + }, ScorerKind::Regex, ); assert_eq!(score_test(&case, "say hello"), Score::PERFECT); @@ -165,7 +169,9 @@ mod tests { fn regex_scorer_no_match() { let case = make_case( "t2", - ExpectedBehavior::Matches { pattern: "goodbye".to_string() }, + ExpectedBehavior::Matches { + pattern: "goodbye".to_string(), + }, ScorerKind::Regex, ); assert_eq!(score_test(&case, "hello"), Score::ZERO); @@ -176,7 +182,9 @@ mod tests { fn regex_scorer_anchored() { let case = make_case( "t3", - ExpectedBehavior::Matches { pattern: "^hello".to_string() }, + ExpectedBehavior::Matches { + pattern: "^hello".to_string(), + }, ScorerKind::Regex, ); assert_eq!(score_test(&case, "say hello"), Score::ZERO); @@ -187,7 +195,9 @@ mod tests { fn regex_scorer_invalid_pattern() { let case = make_case( "t4", - ExpectedBehavior::Matches { pattern: "[unclosed".to_string() }, + ExpectedBehavior::Matches { + pattern: "[unclosed".to_string(), + }, ScorerKind::Regex, ); assert_eq!(score_test(&case, "anything"), Score::ZERO); @@ -250,7 +260,9 @@ mod tests { fn caller_returns_zero_in_score_test() { let case = make_case( "c1", - ExpectedBehavior::Custom { id: "my-judge".to_string() }, + ExpectedBehavior::Custom { + id: "my-judge".to_string(), + }, ScorerKind::Caller, ); assert_eq!(score_test(&case, "any response"), Score::ZERO); @@ -273,13 +285,17 @@ mod tests { let mut bundle = make_bundle(); bundle.tests.push(make_case( "c2", - ExpectedBehavior::Custom { id: "judge".to_string() }, + ExpectedBehavior::Custom { + id: "judge".to_string(), + }, ScorerKind::Caller, )); let results = vec![( make_case( "c2", - ExpectedBehavior::Custom { id: "judge".to_string() }, + ExpectedBehavior::Custom { + id: "judge".to_string(), + }, ScorerKind::Caller, ), "response".to_string(), @@ -296,12 +312,16 @@ mod tests { fn omitted_result_scores_zero() { let pass = make_case( "p", - ExpectedBehavior::Contains { value: "ok".to_string() }, + ExpectedBehavior::Contains { + value: "ok".to_string(), + }, ScorerKind::Substring, ); let fail = make_case( "f", - ExpectedBehavior::Contains { value: "ok".to_string() }, + ExpectedBehavior::Contains { + value: "ok".to_string(), + }, ScorerKind::Substring, ); let mut bundle = make_bundle(); @@ -319,7 +339,9 @@ mod tests { fn duplicated_results_counted_once() { let pass = make_case( "p", - ExpectedBehavior::Contains { value: "ok".to_string() }, + ExpectedBehavior::Contains { + value: "ok".to_string(), + }, ScorerKind::Substring, ); let mut bundle = make_bundle(); diff --git a/crates/frameshift-daemon/src/orchestrator.rs b/crates/frameshift-daemon/src/orchestrator.rs index 5eb92f6..0a43985 100644 --- a/crates/frameshift-daemon/src/orchestrator.rs +++ b/crates/frameshift-daemon/src/orchestrator.rs @@ -191,7 +191,10 @@ mod tests { // Enable mode. let state_dir = client.orchestrator_state_dir(&project_root).unwrap(); fs::create_dir_all(&state_dir).unwrap(); - let mode = ModeState { mode: Mode::On, sensitivity: 0.5 }; + let mode = ModeState { + mode: Mode::On, + sensitivity: 0.5, + }; mode.save(&state_dir.join("automate.json")).unwrap(); // Write lock marker. @@ -224,7 +227,10 @@ mod tests { // Enable mode, no lock. let state_dir = client.orchestrator_state_dir(&project_root).unwrap(); fs::create_dir_all(&state_dir).unwrap(); - let mode = ModeState { mode: Mode::On, sensitivity: 0.5 }; + let mode = ModeState { + mode: Mode::On, + sensitivity: 0.5, + }; mode.save(&state_dir.join("automate.json")).unwrap(); let policy = SwitchPolicy::default(); @@ -268,7 +274,10 @@ mod tests { // Enable mode. let state_dir = client.orchestrator_state_dir(&project_root).unwrap(); - let mode = ModeState { mode: Mode::On, sensitivity: 0.5 }; + let mode = ModeState { + mode: Mode::On, + sensitivity: 0.5, + }; mode.save(&state_dir.join("automate.json")).unwrap(); // Use a lenient policy so the single persona will pass the confidence threshold. @@ -338,7 +347,10 @@ mod tests { // Enable mode. let state_dir = client.orchestrator_state_dir(&project_root).unwrap(); - let mode = ModeState { mode: Mode::On, sensitivity: 0.5 }; + let mode = ModeState { + mode: Mode::On, + sensitivity: 0.5, + }; mode.save(&state_dir.join("automate.json")).unwrap(); // Lenient policy so the single installed persona crosses the @@ -359,8 +371,7 @@ mod tests { // an entry whose `from` is the pre-seeded persona name. let audit_path = state_dir.join("automate-audit.jsonl"); if audit_path.exists() { - let log = - AuditLog::load(&audit_path).expect("audit log should load if it exists"); + let log = AuditLog::load(&audit_path).expect("audit log should load if it exists"); let recent = log.recent(1); if !recent.is_empty() { assert_eq!( diff --git a/crates/frameshift-growth/src/lib.rs b/crates/frameshift-growth/src/lib.rs index cdb58cf..0b12815 100644 --- a/crates/frameshift-growth/src/lib.rs +++ b/crates/frameshift-growth/src/lib.rs @@ -103,11 +103,15 @@ pub fn append_with_timestamp( let _ = file.set_permissions(fs::Permissions::from_mode(0o600)); } - writeln!(file, "---\n\n\n{}\n", timestamp, entry_text) - .map_err(|source| GrowthError::Io { - path: growth_path, - source, - })?; + writeln!( + file, + "---\n\n\n{}\n", + timestamp, entry_text + ) + .map_err(|source| GrowthError::Io { + path: growth_path, + source, + })?; Ok(()) } @@ -234,7 +238,10 @@ pub fn append_jsonl( let mut file = growth_open_options() .open(&path) - .map_err(|source| GrowthError::Io { path: path.clone(), source })?; + .map_err(|source| GrowthError::Io { + path: path.clone(), + source, + })?; file.write_all(line.as_bytes()) .map_err(|source| GrowthError::Io { path, source })?; @@ -427,7 +434,8 @@ pub fn summarize( } // Most recent entry per intent category. - let mut by_intent: std::collections::BTreeMap = std::collections::BTreeMap::new(); + let mut by_intent: std::collections::BTreeMap = + std::collections::BTreeMap::new(); let mut no_intent: Vec<&GrowthEntry> = Vec::new(); // Process entries in reverse chronological order. @@ -444,14 +452,20 @@ pub fn summarize( // more than 10 unique intents cannot underflow `10 - len` (debug panic / // release over-return). selected.truncate(10); - for entry in no_intent.iter().take(10usize.saturating_sub(selected.len())) { + for entry in no_intent + .iter() + .take(10usize.saturating_sub(selected.len())) + { // Simple Jaccard dedup: skip if > 50% token overlap with any selected entry. let tokens: std::collections::HashSet<&str> = entry.text.split_whitespace().collect(); let is_dup = selected.iter().any(|existing| { - let existing_tokens: std::collections::HashSet<&str> = existing.split_whitespace().collect(); + let existing_tokens: std::collections::HashSet<&str> = + existing.split_whitespace().collect(); let intersection = tokens.intersection(&existing_tokens).count(); let union = tokens.union(&existing_tokens).count(); - if union == 0 { return true; } + if union == 0 { + return true; + } (intersection as f32 / union as f32) > 0.5 }); if !is_dup { @@ -607,7 +621,9 @@ mod tests { }; append_jsonl(tmp.path(), "abc123", "rust", &entry).unwrap(); - let path = tmp.path().join("projects/abc123/personas/rust/growth.jsonl"); + let path = tmp + .path() + .join("projects/abc123/personas/rust/growth.jsonl"); assert!(path.exists()); let content = fs::read_to_string(&path).unwrap(); let parsed: GrowthEntry = serde_json::from_str(content.trim()).unwrap(); @@ -682,7 +698,11 @@ mod tests { persona: "rust".to_string(), auto_selected: false, task: None, - intent: if i % 3 == 0 { Some("debugging".to_string()) } else { None }, + intent: if i % 3 == 0 { + Some("debugging".to_string()) + } else { + None + }, text: format!("learning number {i}"), scope: Scope::Project, }; @@ -691,7 +711,10 @@ mod tests { let summary = summarize(tmp.path(), "p1", "rust", Scope::Project).unwrap(); let line_count = summary.lines().filter(|l| !l.is_empty()).count(); - assert!(line_count <= 10, "summary should cap at 10 entries, got {line_count}"); + assert!( + line_count <= 10, + "summary should cap at 10 entries, got {line_count}" + ); assert!(!summary.is_empty()); } } diff --git a/crates/frameshift-mcp/src/lib.rs b/crates/frameshift-mcp/src/lib.rs index cc7e055..8b7aee6 100644 --- a/crates/frameshift-mcp/src/lib.rs +++ b/crates/frameshift-mcp/src/lib.rs @@ -5,9 +5,9 @@ /// MCP server error types. pub mod error; -/// JSON-RPC 2.0 protocol types and response helpers. -pub mod protocol; /// Prompt definitions and dispatch logic (`prompts/list`, `prompts/get`). pub mod prompts; +/// JSON-RPC 2.0 protocol types and response helpers. +pub mod protocol; /// Tool definitions and dispatch logic (`tools/list`, `tools/call`). pub mod tools; diff --git a/crates/frameshift-mcp/src/prompts.rs b/crates/frameshift-mcp/src/prompts.rs index ff17837..ae9fd24 100644 --- a/crates/frameshift-mcp/src/prompts.rs +++ b/crates/frameshift-mcp/src/prompts.rs @@ -12,9 +12,7 @@ use std::path::{Path, PathBuf}; use frameshift_client::Client; -use frameshift_orchestrator::{ - feedback::Preferences, policy::PolicyWeights, run::SelectionInputs, -}; +use frameshift_orchestrator::{feedback::Preferences, policy::PolicyWeights, run::SelectionInputs}; use crate::protocol::{PromptArgDef, PromptContent, PromptDef, PromptMessage, PromptResult}; @@ -26,10 +24,9 @@ pub fn prompt_definitions() -> Vec { vec![ PromptDef { name: "active_persona".to_string(), - description: - "Insert the project's active Frameshift persona into the conversation. \ + description: "Insert the project's active Frameshift persona into the conversation. \ Use this at session start to load the persona without any hook glue." - .to_string(), + .to_string(), arguments: vec![PromptArgDef { name: "project_root".to_string(), description: "Absolute path to the project root.".to_string(), @@ -38,11 +35,10 @@ pub fn prompt_definitions() -> Vec { }, PromptDef { name: "select_persona".to_string(), - description: - "Rank Frameshift personas for the current project context and return the \ + description: "Rank Frameshift personas for the current project context and return the \ top candidates with score, confidence, and rationale. Activate the chosen \ one with the `frameshift_use` tool." - .to_string(), + .to_string(), arguments: vec![ PromptArgDef { name: "project_root".to_string(), @@ -51,27 +47,24 @@ pub fn prompt_definitions() -> Vec { }, PromptArgDef { name: "task".to_string(), - description: - "Optional one-line description of the task to steer the ranker." - .to_string(), + description: "Optional one-line description of the task to steer the ranker." + .to_string(), required: false, }, PromptArgDef { name: "library".to_string(), - description: - "Optional path to a persona library directory to rank from \ + description: "Optional path to a persona library directory to rank from \ instead of the project-installed personas." - .to_string(), + .to_string(), required: false, }, ], }, PromptDef { name: "automate_status".to_string(), - description: - "Report Frameshift automate-mode state for this project: mode (On/Off), \ + description: "Report Frameshift automate-mode state for this project: mode (On/Off), \ active persona, and recent persona transitions from the audit log." - .to_string(), + .to_string(), arguments: vec![PromptArgDef { name: "project_root".to_string(), description: "Absolute path to the project root.".to_string(), @@ -192,8 +185,8 @@ fn call_select_persona( weights: PolicyWeights::default(), }; - let ranked = frameshift_orchestrator::select(&inputs) - .map_err(|e| format!("selection failed: {e}"))?; + let ranked = + frameshift_orchestrator::select(&inputs).map_err(|e| format!("selection failed: {e}"))?; if ranked.is_empty() { return Ok(text_message_result( @@ -219,7 +212,10 @@ fn call_select_persona( )); } - Ok(text_message_result(Some("Frameshift persona ranking".to_string()), body)) + Ok(text_message_result( + Some("Frameshift persona ranking".to_string()), + body, + )) } /// Handle the `automate_status` prompt. @@ -302,10 +298,7 @@ fn call_automate_status( } /// Extract a required string argument and convert it to a PathBuf. -fn get_required_path( - arguments: &serde_json::Value, - key: &str, -) -> Result { +fn get_required_path(arguments: &serde_json::Value, key: &str) -> Result { let s = arguments .get(key) .and_then(|v| v.as_str()) @@ -355,9 +348,7 @@ fn project_root_arg_from_path(p: &Path) -> serde_json::Value { #[cfg(test)] mod tests { use super::*; - use frameshift_client::{ - Client, ClientOptions, InstallRequest, InstallSource, PersonaSpec, - }; + use frameshift_client::{Client, ClientOptions, InstallRequest, InstallSource, PersonaSpec}; use std::fs; /// Build a Client backed by a temporary data root. @@ -529,7 +520,10 @@ mod tests { ) .unwrap(); let text = &result.messages[0].content.text; - assert!(text.contains("mode: off"), "expected mode: off, got: {text}"); + assert!( + text.contains("mode: off"), + "expected mode: off, got: {text}" + ); } /// All prompts reject missing project_root with a clear error message. diff --git a/crates/frameshift-mcp/src/protocol.rs b/crates/frameshift-mcp/src/protocol.rs index 42c21cf..037d632 100644 --- a/crates/frameshift-mcp/src/protocol.rs +++ b/crates/frameshift-mcp/src/protocol.rs @@ -152,7 +152,10 @@ pub struct PromptContent { } /// Construct a successful JSON-RPC response with the given id and result value. -pub fn success_response(id: Option, result: serde_json::Value) -> JsonRpcResponse { +pub fn success_response( + id: Option, + result: serde_json::Value, +) -> JsonRpcResponse { JsonRpcResponse { jsonrpc: "2.0", id, @@ -190,17 +193,18 @@ mod tests { }); let response = success_response(Some(serde_json::json!(1)), result); let serialized = serde_json::to_value(&response).unwrap(); - assert_eq!( - serialized["result"]["serverInfo"]["name"], - "frameshift-mcp" - ); + assert_eq!(serialized["result"]["serverInfo"]["name"], "frameshift-mcp"); } /// Verify the shape of an error response: must have error.code and error.message, /// no result field. #[test] fn error_response_format() { - let response = error_response(Some(serde_json::json!(42)), -32601, "method not found".to_string()); + let response = error_response( + Some(serde_json::json!(42)), + -32601, + "method not found".to_string(), + ); let serialized = serde_json::to_value(&response).unwrap(); assert_eq!(serialized["jsonrpc"], "2.0"); assert_eq!(serialized["id"], 42); diff --git a/crates/frameshift-mcp/src/tools.rs b/crates/frameshift-mcp/src/tools.rs index f57d63a..5195dbe 100644 --- a/crates/frameshift-mcp/src/tools.rs +++ b/crates/frameshift-mcp/src/tools.rs @@ -459,15 +459,23 @@ fn call_automate(arguments: &serde_json::Value, client: &Client) -> ToolResult { match action { "on" => { - let state = ModeState { mode: Mode::On, sensitivity: 0.5 }; + let state = ModeState { + mode: Mode::On, + sensitivity: 0.5, + }; if let Err(e) = state.save(&mode_path) { return err_result(format!("failed to save mode: {}", e)); } - ok_result(serde_json::json!({ "mode": "on", "sensitivity": state.sensitivity }).to_string()) + ok_result( + serde_json::json!({ "mode": "on", "sensitivity": state.sensitivity }).to_string(), + ) } "off" => { - let state = ModeState { mode: Mode::Off, sensitivity: 0.5 }; + let state = ModeState { + mode: Mode::Off, + sensitivity: 0.5, + }; if let Err(e) = state.save(&mode_path) { return err_result(format!("failed to save mode: {}", e)); } @@ -860,7 +868,10 @@ mod tests { result.content[0].text ); let parsed: serde_json::Value = serde_json::from_str(&result.content[0].text).unwrap(); - assert!(parsed["bias"].is_object(), "result must have a 'bias' object"); + assert!( + parsed["bias"].is_object(), + "result must have a 'bias' object" + ); assert_eq!( parsed["bias"].as_object().unwrap().len(), 0, @@ -888,9 +899,12 @@ mod tests { }), &client, ); - assert!(bump.is_error.is_none(), "bump failed: {:?}", bump.content[0].text); - let bump_parsed: serde_json::Value = - serde_json::from_str(&bump.content[0].text).unwrap(); + assert!( + bump.is_error.is_none(), + "bump failed: {:?}", + bump.content[0].text + ); + let bump_parsed: serde_json::Value = serde_json::from_str(&bump.content[0].text).unwrap(); let bumped_bias = bump_parsed["bias"].as_f64().unwrap(); assert!(bumped_bias > 0.0, "bump must produce a positive bias"); @@ -900,8 +914,7 @@ mod tests { &serde_json::json!({"project_root": root_str, "action": "show"}), &client, ); - let show_parsed: serde_json::Value = - serde_json::from_str(&show.content[0].text).unwrap(); + let show_parsed: serde_json::Value = serde_json::from_str(&show.content[0].text).unwrap(); assert_eq!( show_parsed["bias"]["rust"].as_f64().unwrap(), bumped_bias, @@ -937,8 +950,7 @@ mod tests { &client, ); assert!(reset.is_error.is_none()); - let reset_parsed: serde_json::Value = - serde_json::from_str(&reset.content[0].text).unwrap(); + let reset_parsed: serde_json::Value = serde_json::from_str(&reset.content[0].text).unwrap(); assert_eq!(reset_parsed["reset"], true); // Show must now be empty. @@ -947,8 +959,7 @@ mod tests { &serde_json::json!({"project_root": root_str, "action": "show"}), &client, ); - let show_parsed: serde_json::Value = - serde_json::from_str(&show.content[0].text).unwrap(); + let show_parsed: serde_json::Value = serde_json::from_str(&show.content[0].text).unwrap(); assert_eq!( show_parsed["bias"].as_object().unwrap().len(), 0, diff --git a/crates/frameshift-memory-sqlite-fts/src/adapter.rs b/crates/frameshift-memory-sqlite-fts/src/adapter.rs index a694127..73856d0 100644 --- a/crates/frameshift-memory-sqlite-fts/src/adapter.rs +++ b/crates/frameshift-memory-sqlite-fts/src/adapter.rs @@ -605,12 +605,9 @@ mod tests { async fn make_adapter() -> (SqliteFtsAdapter, TempDir) { let dir = TempDir::new().expect("tempdir"); let path = dir.path().join("test.db"); - let adapter = SqliteFtsAdapter::new(SqliteFtsConfig { - path, - pool_size: 2, - }) - .await - .expect("adapter init"); + let adapter = SqliteFtsAdapter::new(SqliteFtsConfig { path, pool_size: 2 }) + .await + .expect("adapter init"); (adapter, dir) } diff --git a/crates/frameshift-orchestrator/src/audit.rs b/crates/frameshift-orchestrator/src/audit.rs index 79cf6fa..fc1cb30 100644 --- a/crates/frameshift-orchestrator/src/audit.rs +++ b/crates/frameshift-orchestrator/src/audit.rs @@ -124,7 +124,8 @@ mod tests { let path = tmp.path().join("audit.jsonl"); let mut log = AuditLog::load(&path).unwrap(); - log.append(&path, make_transition(None, "rust-expert")).unwrap(); + log.append(&path, make_transition(None, "rust-expert")) + .unwrap(); assert_eq!(log.entries.len(), 1); // Reload and verify persistence. @@ -141,8 +142,10 @@ mod tests { let mut log = AuditLog::load(&path).unwrap(); log.append(&path, make_transition(None, "alpha")).unwrap(); - log.append(&path, make_transition(Some("alpha"), "beta")).unwrap(); - log.append(&path, make_transition(Some("beta"), "gamma")).unwrap(); + log.append(&path, make_transition(Some("alpha"), "beta")) + .unwrap(); + log.append(&path, make_transition(Some("beta"), "gamma")) + .unwrap(); let loaded = AuditLog::load(&path).unwrap(); assert_eq!(loaded.entries.len(), 3); @@ -157,7 +160,8 @@ mod tests { let mut log = AuditLog::load(&path).unwrap(); for i in 0..5 { - log.append(&path, make_transition(None, &format!("p{i}"))).unwrap(); + log.append(&path, make_transition(None, &format!("p{i}"))) + .unwrap(); } let recent = log.recent(2); diff --git a/crates/frameshift-orchestrator/src/context.rs b/crates/frameshift-orchestrator/src/context.rs index d3c534c..ce85a1a 100644 --- a/crates/frameshift-orchestrator/src/context.rs +++ b/crates/frameshift-orchestrator/src/context.rs @@ -10,7 +10,15 @@ const MAX_FILES: usize = 2000; const MAX_DEPTH: usize = 6; /// Directories to skip entirely during the walk. -const SKIP_DIRS: &[&str] = &[".git", "target", "node_modules", ".cache", "__pycache__", ".hg", ".svn"]; +const SKIP_DIRS: &[&str] = &[ + ".git", + "target", + "node_modules", + ".cache", + "__pycache__", + ".hg", + ".svn", +]; /// A snapshot of the inferred work context for a project directory. #[derive(Debug, Clone, PartialEq)] @@ -53,7 +61,13 @@ pub fn sense(project_root: &Path, task_hint: Option<&str>) -> ContextSignal { let mut frameworks: Vec = Vec::new(); let mut file_count = 0usize; - walk(project_root, 0, &mut raw_counts, &mut frameworks, &mut file_count); + walk( + project_root, + 0, + &mut raw_counts, + &mut frameworks, + &mut file_count, + ); // Deduplicate frameworks (marker files may appear at multiple levels). frameworks.sort(); @@ -173,25 +187,111 @@ fn detect_markers( /// cluster become available for matching. This closes vocabulary gaps /// between task descriptions and persona keyword sets. const DOMAIN_CLUSTERS: &[&[&str]] = &[ - &["debug", "debugging", "error", "crash", "panic", "backtrace", - "stacktrace", "fix", "trace", "segfault", "coredump"], - &["security", "vulnerability", "cve", "audit", "pentest", "exploit", - "hardening", "threat", "compliance", "owasp"], - &["test", "testing", "unittest", "integration", "coverage", "assertion", - "mock", "fixture", "snapshot", "e2e"], - &["docs", "documentation", "readme", "tutorial", "changelog", "prose", - "copywriting", "draft", "publish", "article"], - &["perf", "performance", "benchmark", "profiling", "flamegraph", - "latency", "throughput", "optimization", "hotpath"], - &["deploy", "deployment", "infrastructure", "ci", "cd", "pipeline", - "container", "docker", "kubernetes", "systemd", "nginx"], - &["refactor", "refactoring", "cleanup", "restructure", "extract", - "inline", "rename", "decompose"], + &[ + "debug", + "debugging", + "error", + "crash", + "panic", + "backtrace", + "stacktrace", + "fix", + "trace", + "segfault", + "coredump", + ], + &[ + "security", + "vulnerability", + "cve", + "audit", + "pentest", + "exploit", + "hardening", + "threat", + "compliance", + "owasp", + ], + &[ + "test", + "testing", + "unittest", + "integration", + "coverage", + "assertion", + "mock", + "fixture", + "snapshot", + "e2e", + ], + &[ + "docs", + "documentation", + "readme", + "tutorial", + "changelog", + "prose", + "copywriting", + "draft", + "publish", + "article", + ], + &[ + "perf", + "performance", + "benchmark", + "profiling", + "flamegraph", + "latency", + "throughput", + "optimization", + "hotpath", + ], + &[ + "deploy", + "deployment", + "infrastructure", + "ci", + "cd", + "pipeline", + "container", + "docker", + "kubernetes", + "systemd", + "nginx", + ], + &[ + "refactor", + "refactoring", + "cleanup", + "restructure", + "extract", + "inline", + "rename", + "decompose", + ], &["review", "reviewing", "pullrequest", "approve", "critique"], - &["implement", "implementing", "build", "create", "feature", - "scaffold", "wire", "integrate"], - &["design", "architect", "architecture", "plan", "planning", - "spec", "specification", "rfc", "proposal"], + &[ + "implement", + "implementing", + "build", + "create", + "feature", + "scaffold", + "wire", + "integrate", + ], + &[ + "design", + "architect", + "architecture", + "plan", + "planning", + "spec", + "specification", + "rfc", + "proposal", + ], ]; /// Expand task tokens with domain cluster members. @@ -223,10 +323,25 @@ fn expand_task_tokens(tokens: &mut Vec) { /// These terms must be specific enough to identify a writing task without /// triggering for incidental mentions in code-focused personas. const PROSE_TASK_TRIGGERS: &[&str] = &[ - "docs", "doc", "documentation", "changelog", "changelogs", - "readme", "tutorial", "tutorials", "release", "notes", - "prose", "writing", "copywriting", "blog", "post", - "draft", "publish", "article", "essay", + "docs", + "doc", + "documentation", + "changelog", + "changelogs", + "readme", + "tutorial", + "tutorials", + "release", + "notes", + "prose", + "writing", + "copywriting", + "blog", + "post", + "draft", + "publish", + "article", + "essay", ]; /// Augment `languages` with a `prose` entry if the task tokens mention writing @@ -307,8 +422,14 @@ mod tests { fn sense_rust_project() { let tmp = make_rust_project(); let sig = sense(tmp.path(), None); - assert!(sig.languages.contains_key("rust"), "expected rust in languages"); - assert!(sig.frameworks.contains(&"cargo".to_string()), "expected cargo framework"); + assert!( + sig.languages.contains_key("rust"), + "expected rust in languages" + ); + assert!( + sig.frameworks.contains(&"cargo".to_string()), + "expected cargo framework" + ); } /// Task hint tokens are normalized and deduplicated. diff --git a/crates/frameshift-orchestrator/src/controller.rs b/crates/frameshift-orchestrator/src/controller.rs index bc5ddd4..95a108c 100644 --- a/crates/frameshift-orchestrator/src/controller.rs +++ b/crates/frameshift-orchestrator/src/controller.rs @@ -154,7 +154,9 @@ impl SwitchController { AutomateState::Locked { persona } => persona.clone(), _ => String::new(), }; - self.state = AutomateState::Locked { persona: locked_name }; + self.state = AutomateState::Locked { + persona: locked_name, + }; self.debounce_count = 0; self.challenger = None; } @@ -216,7 +218,9 @@ impl SwitchController { } } - AutomateState::Active { persona: current, .. } => { + AutomateState::Active { + persona: current, .. + } => { if top.persona == *current { // Same persona still at top -- reset challenger tracking, hold. self.debounce_count = 0; @@ -227,7 +231,8 @@ impl SwitchController { // Distribution-aware switching: compute mean and stddev of all scores. let scores: Vec = ranked.iter().map(|s| s.score).collect(); let mean = scores.iter().sum::() / scores.len() as f32; - let variance = scores.iter().map(|s| (s - mean).powi(2)).sum::() / scores.len() as f32; + let variance = + scores.iter().map(|s| (s - mean).powi(2)).sum::() / scores.len() as f32; let stddev = variance.sqrt().max(f32::EPSILON); // Z-score confidence: is the top score a statistical outlier above the mean? @@ -343,7 +348,11 @@ mod tests { scored("gamma", 0.800, 0.4), ]; let d2 = ctrl.decide(&ranked_b); - assert_eq!(d2, Decision::Hold, "small normalized gap must not trigger switch"); + assert_eq!( + d2, + Decision::Hold, + "small normalized gap must not trigger switch" + ); } /// A sustained clearly-stronger competitor DOES switch after debounce. @@ -362,7 +371,9 @@ mod tests { // Establish alpha as active. let ranked_a = vec![scored("alpha", 0.7, 0.65), scored("beta", 0.3, 0.2)]; ctrl.decide(&ranked_a); - assert!(matches!(ctrl.state(), AutomateState::Active { persona, .. } if persona == "alpha")); + assert!( + matches!(ctrl.state(), AutomateState::Active { persona, .. } if persona == "alpha") + ); // Beta clearly stronger: gap = 0.9 - 0.3 = 0.6; range = 0.6; normalized_gap = 1.0 > 0.09. // stddev will be large, so z-score may or may not be confident -- but gap is meaningful. @@ -490,7 +501,11 @@ mod tests { assert_eq!(d1, Decision::Hold, "low sensitivity first tick should hold"); let d2 = ctrl.decide(&ranked_b); - assert_eq!(d2, Decision::Hold, "low sensitivity second tick should hold"); + assert_eq!( + d2, + Decision::Hold, + "low sensitivity second tick should hold" + ); let d3 = ctrl.decide(&ranked_b); assert!( diff --git a/crates/frameshift-orchestrator/src/feedback.rs b/crates/frameshift-orchestrator/src/feedback.rs index e339ee1..25b57de 100644 --- a/crates/frameshift-orchestrator/src/feedback.rs +++ b/crates/frameshift-orchestrator/src/feedback.rs @@ -170,8 +170,8 @@ impl Preferences { days_since_override: u32, ) -> f32 { let raw = self.bias_for_intent(persona, intent); - let decay_multiplier = (1.0 - days_since_override as f32 * DECAY_RATE_PER_DAY) - .max(DECAY_FLOOR); + let decay_multiplier = + (1.0 - days_since_override as f32 * DECAY_RATE_PER_DAY).max(DECAY_FLOOR); raw * decay_multiplier } diff --git a/crates/frameshift-orchestrator/src/index.rs b/crates/frameshift-orchestrator/src/index.rs index 7fab8a7..1c65969 100644 --- a/crates/frameshift-orchestrator/src/index.rs +++ b/crates/frameshift-orchestrator/src/index.rs @@ -10,9 +10,9 @@ use crate::error::OrchestratorError; /// Stopwords excluded from persona keyword extraction. const STOPWORDS: &[&str] = &[ - "the", "and", "for", "with", "that", "this", "you", "are", "not", "its", - "use", "all", "can", "has", "was", "will", "any", "but", "our", "have", - "from", "they", "when", "your", "how", "what", "who", + "the", "and", "for", "with", "that", "this", "you", "are", "not", "its", "use", "all", "can", + "has", "was", "will", "any", "but", "our", "have", "from", "they", "when", "your", "how", + "what", "who", ]; /// A matchable, pre-processed representation of a single persona. @@ -214,14 +214,11 @@ impl PersonaProfile { }; // Name: pack.toml `name` > directory file_name. - let name = pack - .name - .filter(|n| !n.is_empty()) - .unwrap_or_else(|| { - dir.file_name() - .map(|n| n.to_string_lossy().into_owned()) - .unwrap_or_else(|| "unknown".to_string()) - }); + let name = pack.name.filter(|n| !n.is_empty()).unwrap_or_else(|| { + dir.file_name() + .map(|n| n.to_string_lossy().into_owned()) + .unwrap_or_else(|| "unknown".to_string()) + }); // Build keyword corpus: always include the full body. // High-signal sections are included twice for weighting. @@ -263,21 +260,32 @@ impl PersonaProfile { } // Capability manifest from pack.toml. - let (required_tools, network_egress, primary_intents, anti_keywords) = if let Some(cm) = pack.capability_manifest { - let intents: Vec = cm - .primary_intents - .iter() - .filter_map(|s| parse_intent(s)) - .collect(); - let resolved_intents = if intents.is_empty() { - infer_intents_from_keywords(&keywords) + let (required_tools, network_egress, primary_intents, anti_keywords) = + if let Some(cm) = pack.capability_manifest { + let intents: Vec = cm + .primary_intents + .iter() + .filter_map(|s| parse_intent(s)) + .collect(); + let resolved_intents = if intents.is_empty() { + infer_intents_from_keywords(&keywords) + } else { + intents + }; + ( + cm.required_tools, + cm.network_egress, + resolved_intents, + cm.anti_keywords, + ) } else { - intents + ( + Vec::new(), + false, + infer_intents_from_keywords(&keywords), + Vec::new(), + ) }; - (cm.required_tools, cm.network_egress, resolved_intents, cm.anti_keywords) - } else { - (Vec::new(), false, infer_intents_from_keywords(&keywords), Vec::new()) - }; Ok(PersonaProfile { name, @@ -378,9 +386,28 @@ const LANGUAGE_LEXICON: &[(&str, &str)] = &[ /// Known language identifiers used for keyword-based language detection. const KNOWN_LANGUAGES: &[&str] = &[ - "rust", "typescript", "javascript", "python", "go", "java", "ruby", - "c", "cpp", "markdown", "toml", "shell", "bash", "sql", "yaml", - "haskell", "kotlin", "swift", "scala", "elixir", "erlang", "clojure", + "rust", + "typescript", + "javascript", + "python", + "go", + "java", + "ruby", + "c", + "cpp", + "markdown", + "toml", + "shell", + "bash", + "sql", + "yaml", + "haskell", + "kotlin", + "swift", + "scala", + "elixir", + "erlang", + "clojure", "prose", ]; @@ -391,8 +418,14 @@ const KNOWN_LANGUAGES: &[&str] = &[ /// next heading of the same or higher level. fn extract_high_signal_sections(body: &str) -> Vec { const HIGH_SIGNAL: &[&str] = &[ - "l2 anchor", "tech stack", "concrete patterns", "operating frame", - "who you are", "language", "stack", "tools", + "l2 anchor", + "tech stack", + "concrete patterns", + "operating frame", + "who you are", + "language", + "stack", + "tools", ]; let mut sections: Vec = Vec::new(); @@ -498,9 +531,7 @@ impl PersonaIndex { for dir in dirs { match PersonaProfile::from_persona_dir(dir) { Ok(profile) => profiles.push(profile), - Err(OrchestratorError::Io(e)) - if e.kind() == std::io::ErrorKind::NotFound => - { + Err(OrchestratorError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => { tracing::warn!( dir = %dir.display(), "skipping persona dir: no persona.toml or AGENTS.md found" @@ -545,9 +576,7 @@ impl PersonaIndex { match PersonaProfile::from_persona_dir(&path) { Ok(profile) => profiles.push(profile), - Err(OrchestratorError::Io(e)) - if e.kind() == std::io::ErrorKind::NotFound => - { + Err(OrchestratorError::Io(e)) if e.kind() == std::io::ErrorKind::NotFound => { tracing::warn!( dir = %path.display(), "skipping catalog dir: no persona.toml or AGENTS.md found" @@ -623,7 +652,10 @@ mod tests { fn profile_detects_language_from_name() { let src = minimal_source("rust-expert", "precise and performant"); let profile = PersonaProfile::from_source(&src); - assert!(profile.keywords.iter().any(|k| k == "rust" || k == "expert")); + assert!(profile + .keywords + .iter() + .any(|k| k == "rust" || k == "expert")); } /// PersonaIndex::build creates one profile per source. @@ -662,7 +694,10 @@ mod tests { profile.languages ); assert!( - profile.keywords.iter().any(|k| k == "rust" || k == "cargo" || k == "clippy"), + profile + .keywords + .iter() + .any(|k| k == "rust" || k == "cargo" || k == "clippy"), "expected rust/cargo/clippy in keywords; got: {:?}", profile.keywords ); @@ -707,7 +742,10 @@ mod tests { let profile = PersonaProfile::from_persona_dir(&dir).unwrap(); assert_eq!(profile.name, "writer"); assert!( - profile.keywords.iter().any(|k| k == "documentation" || k == "docs" || k == "writer" || k == "prose"), + profile + .keywords + .iter() + .any(|k| k == "documentation" || k == "docs" || k == "writer" || k == "prose"), "expected writing-related keywords; got: {:?}", profile.keywords ); @@ -728,8 +766,12 @@ mod tests { }); let profile = PersonaProfile::from_source(&src); assert_eq!(profile.primary_intents.len(), 2); - assert!(profile.primary_intents.contains(&crate::intent::Intent::Implementation)); - assert!(profile.primary_intents.contains(&crate::intent::Intent::Debugging)); + assert!(profile + .primary_intents + .contains(&crate::intent::Intent::Implementation)); + assert!(profile + .primary_intents + .contains(&crate::intent::Intent::Debugging)); } /// profile_extracts_anti_keywords verifies that anti_keywords from the @@ -758,7 +800,11 @@ mod tests { // Valid freeform persona. let rust_dir = catalog.join("rust"); fs::create_dir_all(&rust_dir).unwrap(); - fs::write(rust_dir.join("AGENTS.md"), "# Rust\n\ncargo clippy rustc ownership\n").unwrap(); + fs::write( + rust_dir.join("AGENTS.md"), + "# Rust\n\ncargo clippy rustc ownership\n", + ) + .unwrap(); // Valid typed persona. let typed_dir = catalog.join("typed"); diff --git a/crates/frameshift-orchestrator/src/intent.rs b/crates/frameshift-orchestrator/src/intent.rs index 23725a2..0b9861d 100644 --- a/crates/frameshift-orchestrator/src/intent.rs +++ b/crates/frameshift-orchestrator/src/intent.rs @@ -297,14 +297,22 @@ mod tests { /// Debugging tokens classify to Debugging intent. #[test] fn classifies_debugging_intent() { - let tokens = vec!["debugging".to_string(), "rust".to_string(), "error".to_string()]; + let tokens = vec![ + "debugging".to_string(), + "rust".to_string(), + "error".to_string(), + ]; assert_eq!(classify(&tokens), Some(Intent::Debugging)); } /// Mixed security and review tokens classify to the higher-hit intent (Security). #[test] fn classifies_security_intent() { - let tokens = vec!["reviewing".to_string(), "security".to_string(), "vulnerability".to_string()]; + let tokens = vec![ + "reviewing".to_string(), + "security".to_string(), + "vulnerability".to_string(), + ]; assert_eq!(classify(&tokens), Some(Intent::Security)); } diff --git a/crates/frameshift-orchestrator/src/lib.rs b/crates/frameshift-orchestrator/src/lib.rs index d3999b1..3360a2c 100644 --- a/crates/frameshift-orchestrator/src/lib.rs +++ b/crates/frameshift-orchestrator/src/lib.rs @@ -33,7 +33,9 @@ pub use index::{PersonaIndex, PersonaProfile}; pub use intent::{classify as classify_intent, Intent}; pub use mode::{Mode, ModeState}; pub use policy::{rank, PolicyWeights, ScoreComponents, Scored}; -pub use run::{select, select_rich, CandidateOutput, ContextSnapshot, SelectionInputs, SelectionOutput}; +pub use run::{ + select, select_rich, CandidateOutput, ContextSnapshot, SelectionInputs, SelectionOutput, +}; /// Facade that wires together the index, weights, policy, preferences, and controller. /// @@ -91,7 +93,10 @@ mod tests { PersonaProfile { name: name.to_string(), description: None, - languages: languages.iter().map(|l| l.to_string()).collect::>(), + languages: languages + .iter() + .map(|l| l.to_string()) + .collect::>(), keywords: languages.iter().map(|l| l.to_string()).collect(), required_tools: vec![], network_egress: false, diff --git a/crates/frameshift-orchestrator/src/mode.rs b/crates/frameshift-orchestrator/src/mode.rs index a560918..be9b5e4 100644 --- a/crates/frameshift-orchestrator/src/mode.rs +++ b/crates/frameshift-orchestrator/src/mode.rs @@ -44,7 +44,10 @@ impl ModeState { /// Returns `ModeState { mode: Mode::Off }` if the file does not exist. pub fn load(path: &Path) -> Result { if !path.exists() { - return Ok(ModeState { mode: Mode::Off, sensitivity: default_sensitivity() }); + return Ok(ModeState { + mode: Mode::Off, + sensitivity: default_sensitivity(), + }); } let data = std::fs::read_to_string(path)?; let state = serde_json::from_str(&data)?; @@ -81,7 +84,10 @@ mod tests { fn save_load_roundtrip_on() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("mode.json"); - let state = ModeState { mode: Mode::On, sensitivity: 0.5 }; + let state = ModeState { + mode: Mode::On, + sensitivity: 0.5, + }; state.save(&path).unwrap(); let loaded = ModeState::load(&path).unwrap(); assert_eq!(loaded.mode, Mode::On); @@ -93,7 +99,10 @@ mod tests { fn save_load_roundtrip_off() { let tmp = TempDir::new().unwrap(); let path = tmp.path().join("mode.json"); - let state = ModeState { mode: Mode::Off, sensitivity: 0.5 }; + let state = ModeState { + mode: Mode::Off, + sensitivity: 0.5, + }; state.save(&path).unwrap(); let loaded = ModeState::load(&path).unwrap(); assert_eq!(loaded.mode, Mode::Off); diff --git a/crates/frameshift-orchestrator/src/policy.rs b/crates/frameshift-orchestrator/src/policy.rs index a1f387d..0f68c4e 100644 --- a/crates/frameshift-orchestrator/src/policy.rs +++ b/crates/frameshift-orchestrator/src/policy.rs @@ -160,7 +160,8 @@ pub fn rank( if profile.primary_intents.is_empty() { 0.0 } else { - profile.primary_intents + profile + .primary_intents .iter() .map(|pi| crate::intent::relatedness(*pi, task_intent)) .fold(0.0_f32, f32::max) @@ -421,7 +422,9 @@ mod tests { let mut perf = make_profile("perf-expert", &["rust"], &["rust", "profiling"]); perf.primary_intents = vec![Intent::Performance]; - let index = PersonaIndex { profiles: vec![rust, perf] }; + let index = PersonaIndex { + profiles: vec![rust, perf], + }; let ctx = ContextSignal { project_name: "test".to_string(), languages: { @@ -435,7 +438,10 @@ mod tests { }; let ranked = rank(&ctx, &index, &PolicyWeights::default(), &Preferences::new()); - assert_eq!(ranked[0].persona, "rust-expert", "debugging intent should boost rust-expert"); + assert_eq!( + ranked[0].persona, "rust-expert", + "debugging intent should boost rust-expert" + ); } /// Anti-keywords penalize the lexical score when task tokens match the blacklist. @@ -446,11 +452,16 @@ mod tests { let persona_b = make_profile("frontend", &["typescript"], &["react", "css"]); - let index = PersonaIndex { profiles: vec![persona_a, persona_b] }; + let index = PersonaIndex { + profiles: vec![persona_a, persona_b], + }; let ctx = make_ctx(&[("rust", 1.0)], &["css", "styling", "frontend"]); let ranked = rank(&ctx, &index, &PolicyWeights::default(), &Preferences::new()); let backend_entry = ranked.iter().find(|s| s.persona == "backend").unwrap(); - assert!(backend_entry.components.lexical < 0.3, "anti-keywords should penalize lexical score"); + assert!( + backend_entry.components.lexical < 0.3, + "anti-keywords should penalize lexical score" + ); } } diff --git a/crates/frameshift-orchestrator/src/run.rs b/crates/frameshift-orchestrator/src/run.rs index c1d595b..7ca3bfd 100644 --- a/crates/frameshift-orchestrator/src/run.rs +++ b/crates/frameshift-orchestrator/src/run.rs @@ -363,7 +363,11 @@ tone = "precise" fn selection_output_serializes_to_json() { let tmp = TempDir::new().unwrap(); let dir_a = tmp.path().join("alpha"); - make_freeform_persona(&dir_a, "alpha", "# Alpha\n\nRust cargo clippy. Debugging and implementation.\n"); + make_freeform_persona( + &dir_a, + "alpha", + "# Alpha\n\nRust cargo clippy. Debugging and implementation.\n", + ); let project = TempDir::new().unwrap(); let inputs = SelectionInputs { diff --git a/crates/frameshift-pack/src/manifest.rs b/crates/frameshift-pack/src/manifest.rs index f42a2ef..25fc7fe 100644 --- a/crates/frameshift-pack/src/manifest.rs +++ b/crates/frameshift-pack/src/manifest.rs @@ -197,7 +197,8 @@ version = "0.1.0" schema_version: 1, name: "child".to_string(), author_handle: "alice".to_string(), - author_pubkey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(), + author_pubkey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + .to_string(), version: "1.0.0".to_string(), parent_hash: None, license: None, @@ -223,7 +224,8 @@ version = "0.1.0" schema_version: 1, name: "minimal".to_string(), author_handle: "t".to_string(), - author_pubkey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef".to_string(), + author_pubkey: "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + .to_string(), version: "0.1.0".to_string(), parent_hash: None, license: None, diff --git a/crates/frameshift-pack/src/pack.rs b/crates/frameshift-pack/src/pack.rs index 273b909..424995a 100644 --- a/crates/frameshift-pack/src/pack.rs +++ b/crates/frameshift-pack/src/pack.rs @@ -99,7 +99,12 @@ fn load_signature(dir: &Path) -> Result, PackError> { // File absent: pack is simply unsigned. Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), // Any other I/O error (permissions, device error, etc.) is hard. - Err(e) => return Err(PackError::Io { path: sig_path, source: e }), + Err(e) => { + return Err(PackError::Io { + path: sig_path, + source: e, + }) + } }; // File present: must be exactly 64 bytes; wrong length is an explicit error. let found = bytes.len(); diff --git a/crates/frameshift-runtime/tests/integration.rs b/crates/frameshift-runtime/tests/integration.rs index b0d188a..c260efe 100644 --- a/crates/frameshift-runtime/tests/integration.rs +++ b/crates/frameshift-runtime/tests/integration.rs @@ -6,7 +6,9 @@ use std::collections::BTreeMap; use std::sync::Mutex; -use frameshift_runtime::{CapabilityManifest, Runtime, RuntimeConfig, RuntimeError, TemplateSource}; +use frameshift_runtime::{ + CapabilityManifest, Runtime, RuntimeConfig, RuntimeError, TemplateSource, +}; use frameshift_vault::{ Auth, Identity, Preferences, RuntimeMode, VaultBackend, VaultData, VaultError, }; diff --git a/crates/frameshift-seed/src/main.rs b/crates/frameshift-seed/src/main.rs index 2a51798..8a07036 100644 --- a/crates/frameshift-seed/src/main.rs +++ b/crates/frameshift-seed/src/main.rs @@ -175,15 +175,7 @@ async fn run() -> Result<(), SeedError> { )?; } - match seed_persona( - &path, - &catalog, - &objects, - &signing_key, - author_pubkey, - ) - .await - { + match seed_persona(&path, &catalog, &objects, &signing_key, author_pubkey).await { Ok(()) => { info!("seeded persona: {dir_name}"); seeded += 1; @@ -213,16 +205,16 @@ async fn run() -> Result<(), SeedError> { impl SeedConfig { /// Build a validated configuration from the current process environment. fn from_env() -> Result { - let postgres_url = std::env::var("POSTGRES_URL") - .map_err(|_| SeedError::MissingEnv("POSTGRES_URL"))?; + let postgres_url = + std::env::var("POSTGRES_URL").map_err(|_| SeedError::MissingEnv("POSTGRES_URL"))?; let object_store_root = std::env::var("OBJECT_STORE_ROOT") .unwrap_or_else(|_| "/tmp/frameshift-objects".to_string()); - let personas_root = std::env::var("PERSONAS_ROOT") - .map_err(|_| SeedError::MissingEnv("PERSONAS_ROOT"))?; + let personas_root = + std::env::var("PERSONAS_ROOT").map_err(|_| SeedError::MissingEnv("PERSONAS_ROOT"))?; let author_handle = std::env::var("SEED_AUTHOR_HANDLE").unwrap_or_else(|_| "seed-author".to_string()); - let author_display_name = std::env::var("SEED_AUTHOR_DISPLAY_NAME") - .unwrap_or_else(|_| "Seed Author".to_string()); + let author_display_name = + std::env::var("SEED_AUTHOR_DISPLAY_NAME").unwrap_or_else(|_| "Seed Author".to_string()); let signing_key_path = match std::env::var("SEED_SIGNING_KEY_PATH") { Ok(path) => PathBuf::from(path), Err(_) => default_signing_key_path(&object_store_root, &author_handle), @@ -327,7 +319,10 @@ async fn seed_persona( parent_hash: None, capability_manifest_json: cap_json, schema_version: manifest.schema_version, - license: manifest.license.clone().unwrap_or_else(|| "UNKNOWN".to_string()), + license: manifest + .license + .clone() + .unwrap_or_else(|| "UNKNOWN".to_string()), published_at: Utc::now(), status: PackStatus::Active, size_bytes: canonical_bytes.len() as u64, @@ -476,7 +471,10 @@ fn load_or_create_signing_key(path: &Path) -> Result { "signing key file must be exactly 32 bytes", )) })?; - info!("adopted concurrently-created signing key at {}", path.display()); + info!( + "adopted concurrently-created signing key at {}", + path.display() + ); Ok(SigningKey::from_bytes(&seed)) } Err(e) => Err(SeedError::Io(e)), @@ -506,7 +504,10 @@ fn write_synthetic_pack_toml( // are interpolated into quoted TOML strings below. A value containing a quote, // backslash, or control character could inject arbitrary manifest keys. for (field, value) in [("dir_name", dir_name), ("author_handle", author_handle)] { - if value.chars().any(|c| c == '"' || c == '\\' || c.is_control()) { + if value + .chars() + .any(|c| c == '"' || c == '\\' || c.is_control()) + { return Err(SeedError::Io(std::io::Error::other(format!( "{field} contains characters not allowed in a pack manifest: {value:?}" )))); @@ -605,14 +606,13 @@ async fn update_pack_metadata( // Raw UPDATE: the catalog trait has no update-metadata method, so we // issue the statement directly via diesel's sql_query API. - let result = diesel::sql_query( - "UPDATE packs SET description = $1, tags = $2 WHERE name = $3", - ) - .bind::(&metadata.description) - .bind::, _>(&metadata.tags) - .bind::(&metadata.name) - .execute(&mut *conn) - .await; + let result = + diesel::sql_query("UPDATE packs SET description = $1, tags = $2 WHERE name = $3") + .bind::(&metadata.description) + .bind::, _>(&metadata.tags) + .bind::(&metadata.name) + .execute(&mut *conn) + .await; match result { Ok(rows) if rows > 0 => { @@ -667,8 +667,8 @@ fn derive_pack_metadata(path: &Path, dir_name: &str) -> Result Result MAX_NONCE_LEN { - tracing::warn!(len = nonce_raw.len(), "signed request: nonce length out of bounds"); + tracing::warn!( + len = nonce_raw.len(), + "signed request: nonce length out of bounds" + ); return Err(auth_failed()); } if !nonce_raw @@ -222,7 +225,10 @@ pub fn parse_headers(headers: &HeaderMap) -> Result(headers: &'a HeaderMap, name: &str) -> Result<&'a str, AppErro auth_failed() })?; if s.len() > MAX_HEADER_LEN { - tracing::warn!(header = name, len = s.len(), "signed request: header too long"); + tracing::warn!( + header = name, + len = s.len(), + "signed request: header too long" + ); return Err(auth_failed()); } Ok(s) diff --git a/crates/frameshift-server/src/main.rs b/crates/frameshift-server/src/main.rs index 00c2c8e..64dda36 100644 --- a/crates/frameshift-server/src/main.rs +++ b/crates/frameshift-server/src/main.rs @@ -101,9 +101,7 @@ async fn build_state(config: Arc) -> Result /// /// Unknown values produce a [`ServerError::Startup`] so a typo in the env /// fails fast rather than silently defaulting. -async fn build_object_store( - config: &ServerConfig, -) -> Result, ServerError> { +async fn build_object_store(config: &ServerConfig) -> Result, ServerError> { match config.object_store_backend.as_str() { "fs" => { let fs_cfg = FsPackStoreConfig { @@ -217,9 +215,7 @@ async fn build_memory_adapter( /// Accepted formats: /// - `"none"` -> `HttpAuth::None` /// - `"bearer:"` -> `HttpAuth::Bearer()` -fn parse_memory_http_auth( - raw: &str, -) -> Result { +fn parse_memory_http_auth(raw: &str) -> Result { use frameshift_memory_http::HttpAuth; if raw == "none" || raw.is_empty() { @@ -267,4 +263,3 @@ async fn main() { std::process::exit(code); } } - diff --git a/crates/frameshift-server/src/middleware/auth.rs b/crates/frameshift-server/src/middleware/auth.rs index 14f33c2..42c200c 100644 --- a/crates/frameshift-server/src/middleware/auth.rs +++ b/crates/frameshift-server/src/middleware/auth.rs @@ -69,8 +69,7 @@ pub async fn require_signed_request( // can read it; the content-type header (with multipart boundary) survives // because we preserve `parts` verbatim. let mut req = Request::from_parts(parts, Body::from(bytes)); - req.extensions_mut() - .insert(auth::VerifiedSigner { pubkey }); + req.extensions_mut().insert(auth::VerifiedSigner { pubkey }); Ok(next.run(req).await) } diff --git a/crates/frameshift-server/src/middleware/metrics.rs b/crates/frameshift-server/src/middleware/metrics.rs index 78bf712..b208a9b 100644 --- a/crates/frameshift-server/src/middleware/metrics.rs +++ b/crates/frameshift-server/src/middleware/metrics.rs @@ -82,10 +82,7 @@ type BoxFuture = Pin + Send + 'static>>; impl Service> for MetricsService where - S: Service, Response = axum::http::Response> - + Clone - + Send - + 'static, + S: Service, Response = axum::http::Response> + Clone + Send + 'static, S::Future: Send + 'static, S::Error: Send + 'static, ReqBody: Send + 'static, diff --git a/crates/frameshift-server/src/routes/handles.rs b/crates/frameshift-server/src/routes/handles.rs index 878626e..cede106 100644 --- a/crates/frameshift-server/src/routes/handles.rs +++ b/crates/frameshift-server/src/routes/handles.rs @@ -47,7 +47,9 @@ pub async fn get_handle( ) -> Result, AppError> { // Reject unreasonably long values before hitting the catalog. if handle.len() > 256 { - return Err(AppError::BadRequest("handle exceeds maximum length".to_string())); + return Err(AppError::BadRequest( + "handle exceeds maximum length".to_string(), + )); } let author = state .catalog diff --git a/crates/frameshift-server/src/routes/packs.rs b/crates/frameshift-server/src/routes/packs.rs index f7335af..a7a9d74 100644 --- a/crates/frameshift-server/src/routes/packs.rs +++ b/crates/frameshift-server/src/routes/packs.rs @@ -204,8 +204,7 @@ async fn extract_targz(archive_bytes: Vec, dir: std::path::PathBuf) -> Resul .entries() .map_err(|e| AppError::BadRequest(format!("tar entries: {e}")))?; for entry in entries { - let mut entry = - entry.map_err(|e| AppError::BadRequest(format!("tar entry: {e}")))?; + let mut entry = entry.map_err(|e| AppError::BadRequest(format!("tar entry: {e}")))?; // Reject any entry that is not a regular file or directory. Symlinks, // hardlinks, and device nodes have no legitimate place in a pack and // could be used to plant a link that escapes the extraction dir or @@ -362,8 +361,7 @@ pub async fn publish_pack( // Extract tar.gz into a tempdir, then load the pack from the extracted // directory. The TempDir is dropped at the end of the function and the // bytes are moved into the object store before that point. - let tmp = tempfile::TempDir::new() - .map_err(|e| AppError::Internal(format!("tempdir: {e}")))?; + let tmp = tempfile::TempDir::new().map_err(|e| AppError::Internal(format!("tempdir: {e}")))?; extract_targz(pack_archive.clone(), tmp.path().to_path_buf()).await?; let pack_root = find_pack_root(tmp.path())?; diff --git a/crates/frameshift-server/tests/authors_write.rs b/crates/frameshift-server/tests/authors_write.rs index 08118f4..ff88ac4 100644 --- a/crates/frameshift-server/tests/authors_write.rs +++ b/crates/frameshift-server/tests/authors_write.rs @@ -69,9 +69,9 @@ fn mk_state(catalog: MockCatalog) -> AppState { memory: None, config: test_config(), metrics: Arc::new(Metrics::new()), - auth_nonces: Arc::new(frameshift_server::auth::NonceCache::new(Duration::from_secs( - 600, - ))), + auth_nonces: Arc::new(frameshift_server::auth::NonceCache::new( + Duration::from_secs(600), + )), } } diff --git a/crates/frameshift-server/tests/integration.rs b/crates/frameshift-server/tests/integration.rs index 217694b..638ed82 100644 --- a/crates/frameshift-server/tests/integration.rs +++ b/crates/frameshift-server/tests/integration.rs @@ -29,8 +29,8 @@ use std::sync::Arc; use std::time::Duration; use axum::http::{Request, StatusCode}; -use http_body_util::BodyExt as _; use frameshift_catalog::CatalogBackend as _; +use http_body_util::BodyExt as _; use secrecy::SecretString; use tower::ServiceExt as _; @@ -613,10 +613,7 @@ fn dl_state_with_rate(catalog: MockCatalog, objects: MockPackStore, rate: u32) - } /// Issue a one-shot POST request with empty body. -async fn oneshot_post_empty( - state: AppState, - path: &str, -) -> axum::http::Response { +async fn oneshot_post_empty(state: AppState, path: &str) -> axum::http::Response { let router = app(state); let request = Request::builder() .method("POST") @@ -649,8 +646,11 @@ async fn download_url_mint_then_stream_succeeds() { let state = dl_state(catalog, objects); // Step 1: mint the URL. - let resp = oneshot_post_empty(state.clone(), "/v1/packs/dl-pack/versions/1.0.0/download-url") - .await; + let resp = oneshot_post_empty( + state.clone(), + "/v1/packs/dl-pack/versions/1.0.0/download-url", + ) + .await; assert_eq!(resp.status(), StatusCode::OK); let body = body_json(resp).await; let url = body["url"].as_str().expect("url field present").to_string(); @@ -779,10 +779,7 @@ async fn download_url_rate_limited_returns_429() { statuses.push(resp.status()); } - let ok = statuses - .iter() - .filter(|s| **s == StatusCode::OK) - .count(); + let ok = statuses.iter().filter(|s| **s == StatusCode::OK).count(); let limited = statuses .iter() .filter(|s| **s == StatusCode::TOO_MANY_REQUESTS) diff --git a/crates/frameshift-server/tests/publish.rs b/crates/frameshift-server/tests/publish.rs index 706fd3f..63ffa2d 100644 --- a/crates/frameshift-server/tests/publish.rs +++ b/crates/frameshift-server/tests/publish.rs @@ -173,13 +173,10 @@ async fn post_publish( ) -> axum::http::Response { let boundary = "frameshifttestboundary"; let body = build_multipart(boundary, pack_bytes, signature, author_handle); - let mut req = Request::builder() - .method("POST") - .uri("/v1/packs") - .header( - "content-type", - format!("multipart/form-data; boundary={boundary}"), - ); + let mut req = Request::builder().method("POST").uri("/v1/packs").header( + "content-type", + format!("multipart/form-data; boundary={boundary}"), + ); if let Some(key) = request_key { for h in mocks::signing::signed_headers(key, "POST", "/v1/packs", &body) { req = req.header(h.name, h.value); @@ -281,14 +278,22 @@ async fn publish_happy_path_returns_200_and_pack_is_fetchable() { Some(&signing), ) .await; - assert_eq!(resp.status(), StatusCode::OK, "expected 200, got {}", resp.status()); + assert_eq!( + resp.status(), + StatusCode::OK, + "expected 200, got {}", + resp.status() + ); let body = body_json(resp).await; assert_eq!(body["name"], "demo-pack"); assert_eq!(body["version"], "0.1.0"); assert_eq!(body["author_handle"], "alice"); assert!( - body["pack_hash"].as_str().map(|s| s.len() == 64).unwrap_or(false), + body["pack_hash"] + .as_str() + .map(|s| s.len() == 64) + .unwrap_or(false), "pack_hash must be a 64-char hex string" ); @@ -476,7 +481,11 @@ async fn publish_replayed_request_returns_401() { }; let resp1 = app(state.clone()).oneshot(build_req()).await.unwrap(); - assert_eq!(resp1.status(), StatusCode::OK, "first publish should succeed"); + assert_eq!( + resp1.status(), + StatusCode::OK, + "first publish should succeed" + ); let resp2 = app(state).oneshot(build_req()).await.unwrap(); assert_eq!( diff --git a/crates/frameshift-source/src/validate.rs b/crates/frameshift-source/src/validate.rs index 9268ed8..781d7ec 100644 --- a/crates/frameshift-source/src/validate.rs +++ b/crates/frameshift-source/src/validate.rs @@ -495,7 +495,11 @@ fn scan_persona_fields(src: &PersonaSource, warnings: &mut Vec) // `default_questions[].question` field: user-supplied question text surfaced to agents. for dq in &p.default_questions { - check_text(&dq.question, "persona.default_questions[].question", warnings); + check_text( + &dq.question, + "persona.default_questions[].question", + warnings, + ); } } diff --git a/crates/frameshift-vault/src/validate.rs b/crates/frameshift-vault/src/validate.rs index d77508a..d03ed21 100644 --- a/crates/frameshift-vault/src/validate.rs +++ b/crates/frameshift-vault/src/validate.rs @@ -118,7 +118,10 @@ mod tests { !rendered.contains("super-secret-token"), "secret value must not appear: {rendered}" ); - assert!(!rendered.contains("hidden prose"), "overlay value must not appear"); + assert!( + !rendered.contains("hidden prose"), + "overlay value must not appear" + ); } /// A non-http(s) memory endpoint is rejected. @@ -132,7 +135,10 @@ mod tests { auth_method: "api-key".to_owned(), auth_value_vault_ref: "memory_api_key".to_owned(), }); - assert!(super::validate(&v).is_err(), "file:// endpoint must be rejected"); + assert!( + super::validate(&v).is_err(), + "file:// endpoint must be rejected" + ); } /// An over-cap overlay value is rejected. @@ -140,6 +146,9 @@ mod tests { fn rejects_oversized_overlay() { let mut v = base(); v.set_overlay("agent.slot".to_owned(), "x".repeat(64 * 1024 + 1)); - assert!(super::validate(&v).is_err(), "oversized overlay must be rejected"); + assert!( + super::validate(&v).is_err(), + "oversized overlay must be rejected" + ); } } From 7620fe8b94c62364029a261cd1b3c8844ed65f9b Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Sun, 28 Jun 2026 10:00:32 -0400 Subject: [PATCH 2/7] ci: gate PRs on fmt, clippy -D warnings, tests, postgres, and audit The only prior workflow mirrored the repo; nothing ran the 600+ tests or clippy on a PR. Adds .github/workflows/ci.yml with five jobs: rustfmt --check, clippy --workspace --all-targets -D warnings, cargo test --workspace (the Docker-gated postgres tests stay #[ignore]), a scoped postgres-integration job that lets testcontainers self-start its database, and a non-blocking RustSec advisory scan. Jobs run on stable to match the local toolchain; libpq-dev is installed on the compiling jobs for diesel/pq-sys. Workspace is already fmt- and clippy-clean, so the pipeline is green on first run. --- .github/workflows/ci.yml | 101 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..72d18da --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,101 @@ +# Continuous integration for the Frameshift workspace. +# +# Gates every pull request (and pushes to the default branch) on formatting, +# lints, the unit/integration test suite, the Docker-gated Postgres tests, and +# a supply-chain advisory scan. Jobs run on the stable toolchain to match the +# local development environment; the workspace declares rust-version 1.75 but is +# built on current stable, so a dedicated 1.75 MSRV-verification job is deferred +# to the later tier rather than risking a spurious MSRV failure. +name: CI + +on: + push: + branches: ["main", "master"] + pull_request: + +env: + CARGO_TERM_COLOR: always + RUST_BACKTRACE: 1 + +# Only keep the latest run per ref alive; cancel superseded runs to save minutes. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + # Formatting check. Does not compile anything, so it needs no system libs and + # fails fast on style drift. + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - run: cargo fmt --all -- --check + + # Lints. clippy compiles the whole workspace (all targets), so it needs + # libpq-dev for the diesel/pq-sys-backed catalog crate. -D warnings makes any + # lint a hard failure. + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Install libpq + run: sudo apt-get update -q && sudo apt-get install -y libpq-dev + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --workspace --all-targets -- -D warnings + + # Unit and integration tests. The Postgres integration tests are gated behind + # #[ignore = "requires Docker"], so plain `cargo test --workspace` skips them + # and runs fast without a database. + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install libpq + run: sudo apt-get update -q && sudo apt-get install -y libpq-dev + - uses: Swatinem/rust-cache@v2 + - run: cargo test --workspace + + # The Docker-gated Postgres integration tests. testcontainers starts its own + # postgres container against the runner's Docker daemon (no service block or + # DATABASE_URL needed); migrations run inside PostgresCatalog::new(). Scoped to + # the one crate that owns these tests so --include-ignored stays narrow. + test-postgres: + name: Test (Postgres integration) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install libpq + run: sudo apt-get update -q && sudo apt-get install -y libpq-dev + - uses: Swatinem/rust-cache@v2 + - run: cargo test -p frameshift-catalog-postgres -- --include-ignored + + # Supply-chain advisory scan against the RustSec database. Non-blocking for + # now (continue-on-error) so a pre-existing transitive advisory cannot wedge + # the pipeline before the backlog is triaged; ratchet to blocking once clean. + audit: + name: Security audit + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v4 + - uses: rustsec/audit-check@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + +# Later tier (intentionally not yet wired, tracked in the roadmap): +# - cargo-deny (needs a deny.toml: license allowlist, ban list, advisory policy) +# - cargo doc --no-deps --workspace to catch broken intra-doc links +# - an MSRV verification job pinned to 1.75 (or reconcile the declared rust-version) +# - coverage (cargo-llvm-cov) once the currently-untested crates gain tests +# - .github/dependabot.yml for weekly Cargo updates From 59c5cdc8dbc6779299fd4372e120e03c649efd00 Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Sun, 28 Jun 2026 10:14:16 -0400 Subject: [PATCH 3/7] hardening: bound MCP stdin line length; graceful startup; boundary persona checks - Replace the unbounded lines() reader with read_capped_line: a single JSON-RPC line is capped at 8 MiB and an over-long line is rejected with a -32700 (the next line still processes), so a client that never sends a newline cannot drive unbounded memory growth. - Replace the startup Client init .expect() with a clean stderr diagnostic and nonzero exit instead of a panic. - Validate the persona name at the MCP boundary in call_activate and call_use, mirroring call_grow_append. Defense-in-depth: the client layer already validates in activate()/rendered_persona(), this rejects earlier with a clear error. Regression tests cover cap-then-recover and boundary rejection for both activate and use. clippy -D warnings clean; cargo test -p frameshift-mcp green. --- crates/frameshift-mcp/src/main.rs | 209 +++++++++++++++++++++++++++-- crates/frameshift-mcp/src/tools.rs | 9 ++ 2 files changed, 207 insertions(+), 11 deletions(-) diff --git a/crates/frameshift-mcp/src/main.rs b/crates/frameshift-mcp/src/main.rs index d07f55c..fd9b1fe 100644 --- a/crates/frameshift-mcp/src/main.rs +++ b/crates/frameshift-mcp/src/main.rs @@ -4,7 +4,7 @@ use frameshift_client::Client; use frameshift_mcp::protocol::{error_response, success_response, JsonRpcMessage, JsonRpcResponse}; use frameshift_mcp::{prompts, tools}; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, BufWriter}; +use tokio::io::{AsyncReadExt, AsyncWriteExt, BufWriter}; /// Main entry point. Initializes tracing, creates the client, then /// runs the stdin JSON-RPC read loop writing responses to stdout. @@ -16,19 +16,134 @@ async fn main() { .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); - let client = Client::with_default_data_root().expect("failed to initialize client"); + let client = match Client::with_default_data_root() { + Ok(c) => c, + Err(e) => { + // stdout is the protocol channel and no request id exists yet, so the + // only useful signal is a clear stderr diagnostic and a nonzero exit + // rather than a panic that gives the client an unexplained EOF. + eprintln!("frameshift-mcp: failed to initialize client: {e}"); + std::process::exit(1); + } + }; - let reader = BufReader::new(tokio::io::stdin()); + let mut stdin = tokio::io::stdin(); let mut stdout = BufWriter::new(tokio::io::stdout()); - let mut lines = reader.lines(); - - while let Ok(Some(line)) = lines.next_line().await { - if let Some(response) = handle_message(&line, &client) { - let json = serde_json::to_string(&response).unwrap_or_default(); - let _ = stdout.write_all(json.as_bytes()).await; - let _ = stdout.write_all(b"\n").await; - let _ = stdout.flush().await; + let mut pending: Vec = Vec::new(); + let mut line: Vec = Vec::new(); + + loop { + match read_capped_line(&mut stdin, &mut pending, &mut line, MAX_LINE_BYTES).await { + Ok(LineRead::Eof) | Err(_) => break, + Ok(LineRead::TooLong) => { + let response = + error_response(None, -32700, "request line exceeds maximum size".to_string()); + write_response(&mut stdout, &response).await; + } + Ok(LineRead::Line) => { + let text = String::from_utf8_lossy(&line); + if let Some(response) = handle_message(&text, &client) { + write_response(&mut stdout, &response).await; + } + } + } + } +} + +/// Maximum size in bytes of a single JSON-RPC line read from stdin. Larger lines +/// are rejected rather than buffered, bounding memory against a client that never +/// sends a newline. +const MAX_LINE_BYTES: usize = 8 * 1024 * 1024; + +/// Outcome of one capped read from the input stream. +enum LineRead { + /// A complete line (trailing newline stripped) was read into the output buffer. + Line, + /// The line exceeded the cap and was discarded; the stream is positioned just + /// past its terminating newline. + TooLong, + /// End of input. + Eof, +} + +/// Serialize a JSON-RPC response and write it to stdout as one newline-terminated line. +async fn write_response(stdout: &mut BufWriter, response: &JsonRpcResponse) { + let json = serde_json::to_string(response).unwrap_or_default(); + let _ = stdout.write_all(json.as_bytes()).await; + let _ = stdout.write_all(b"\n").await; + let _ = stdout.flush().await; +} + +/// Read one newline-delimited line into `out`, capping its length at `max` bytes. +/// +/// `pending` carries bytes read past the previous line between calls. If a line +/// exceeds `max` before a newline arrives, the rest of that line is discarded and +/// `TooLong` is returned, so a client that never sends a newline cannot drive +/// unbounded memory growth. +async fn read_capped_line( + reader: &mut R, + pending: &mut Vec, + out: &mut Vec, + max: usize, +) -> std::io::Result +where + R: AsyncReadExt + Unpin, +{ + out.clear(); + let mut chunk = [0u8; 64 * 1024]; + loop { + if let Some(pos) = pending.iter().position(|&b| b == b'\n') { + if pos > max { + // The completed line is over the cap: discard it, newline included. + pending.drain(..=pos); + return Ok(LineRead::TooLong); + } + out.extend_from_slice(&pending[..pos]); + pending.drain(..=pos); + return Ok(LineRead::Line); + } + if pending.len() > max { + // Over the cap with no newline yet: discard and skip to the next one. + return drain_over_long(reader, pending, &mut chunk).await; } + let n = reader.read(&mut chunk).await?; + if n == 0 { + if pending.is_empty() { + return Ok(LineRead::Eof); + } + // Final line with no trailing newline at end of input. + if pending.len() > max { + pending.clear(); + return Ok(LineRead::TooLong); + } + out.extend_from_slice(pending); + pending.clear(); + return Ok(LineRead::Line); + } + pending.extend_from_slice(&chunk[..n]); + } +} + +/// Discard input up to and including the next newline, then report `TooLong`. +async fn drain_over_long( + reader: &mut R, + pending: &mut Vec, + chunk: &mut [u8], +) -> std::io::Result +where + R: AsyncReadExt + Unpin, +{ + loop { + if let Some(pos) = pending.iter().position(|&b| b == b'\n') { + pending.drain(..=pos); + return Ok(LineRead::TooLong); + } + pending.clear(); + let n = reader.read(chunk).await?; + if n == 0 { + return Ok(LineRead::TooLong); + } + pending.extend_from_slice(&chunk[..n]); } } @@ -370,4 +485,76 @@ mod tests { let parsed: serde_json::Value = serde_json::from_str(content_text).unwrap(); assert_eq!(parsed["appended"], true); } + + /// The capped line reader rejects an over-long line and recovers on the next one. + #[tokio::test] + async fn read_capped_line_rejects_oversized_then_recovers() { + // With max = 8, "toolongline" (11 bytes) exceeds the cap; "ok" does not. + let input: &[u8] = b"toolongline\nok\n"; + let mut reader = input; + let mut pending = Vec::new(); + let mut out = Vec::new(); + let r1 = read_capped_line(&mut reader, &mut pending, &mut out, 8) + .await + .unwrap(); + assert!( + matches!(r1, LineRead::TooLong), + "over-long line must be rejected" + ); + let r2 = read_capped_line(&mut reader, &mut pending, &mut out, 8) + .await + .unwrap(); + assert!(matches!(r2, LineRead::Line)); + assert_eq!(out, b"ok"); + let r3 = read_capped_line(&mut reader, &mut pending, &mut out, 8) + .await + .unwrap(); + assert!(matches!(r3, LineRead::Eof)); + } + + /// frameshift_activate rejects a traversal persona name at the MCP boundary. + #[test] + fn tools_call_activate_rejects_traversal_persona() { + let tmp = tempfile::tempdir().unwrap(); + let client = make_client(&tmp.path().join("data")); + let project_root = tmp.path().join("project"); + fs::create_dir_all(&project_root).unwrap(); + let msg = serde_json::json!({ + "jsonrpc": "2.0", "id": 30, "method": "tools/call", + "params": { "name": "frameshift_activate", "arguments": { + "persona": "../evil", + "project_root": project_root.to_str().unwrap() + }} + }); + let response = handle_message(&msg.to_string(), &client).expect("response"); + let serialized = serde_json::to_value(&response).unwrap(); + let text = serialized["result"]["content"][0]["text"].as_str().unwrap(); + assert!( + text.starts_with("invalid persona name"), + "expected boundary rejection, got: {text}" + ); + } + + /// frameshift_use rejects a traversal persona name at the MCP boundary. + #[test] + fn tools_call_use_rejects_traversal_persona() { + let tmp = tempfile::tempdir().unwrap(); + let client = make_client(&tmp.path().join("data")); + let project_root = tmp.path().join("project"); + fs::create_dir_all(&project_root).unwrap(); + let msg = serde_json::json!({ + "jsonrpc": "2.0", "id": 31, "method": "tools/call", + "params": { "name": "frameshift_use", "arguments": { + "project_root": project_root.to_str().unwrap(), + "persona": "../evil" + }} + }); + let response = handle_message(&msg.to_string(), &client).expect("response"); + let serialized = serde_json::to_value(&response).unwrap(); + let text = serialized["result"]["content"][0]["text"].as_str().unwrap(); + assert!( + text.starts_with("invalid persona name"), + "expected boundary rejection, got: {text}" + ); + } } diff --git a/crates/frameshift-mcp/src/tools.rs b/crates/frameshift-mcp/src/tools.rs index 5195dbe..905f450 100644 --- a/crates/frameshift-mcp/src/tools.rs +++ b/crates/frameshift-mcp/src/tools.rs @@ -235,6 +235,11 @@ fn call_activate(arguments: &serde_json::Value, client: &Client) -> ToolResult { Some(s) => s, None => return err_result("missing required argument: persona".to_string()), }; + // Validate at the MCP boundary so a traversal name is rejected here with a + // clear error, mirroring call_grow_append (the client layer also guards it). + if let Err(e) = frameshift_client::validate_persona_name(persona) { + return err_result(format!("invalid persona name: {e}")); + } let project_root_str = match arguments.get("project_root").and_then(|v| v.as_str()) { Some(s) => s, @@ -410,6 +415,10 @@ fn call_use(arguments: &serde_json::Value, client: &Client) -> ToolResult { Some(s) => s, None => return err_result("missing required argument: persona".to_string()), }; + // Validate at the MCP boundary, mirroring call_grow_append/call_activate. + if let Err(e) = frameshift_client::validate_persona_name(persona) { + return err_result(format!("invalid persona name: {e}")); + } let project_root = match validate_path_arg(project_root_str) { Ok(p) => p, From a0c80669f95079a520456ec702208ee4e433e599 Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Sun, 28 Jun 2026 15:29:14 -0400 Subject: [PATCH 4/7] style: rephrase MCP TooLong response so upstream rustfmt agrees The local Arch rustfmt (1.9.0) and CI's upstream rustfmt (rust 1.96.0) format a wrapped 3-arg error_response() call differently -- Arch keeps it on two lines, upstream explodes the args. Binding the message to a temp var keeps both statements short enough that neither rustfmt reformats them, so the fmt job is reproducible across both. --- crates/frameshift-mcp/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/frameshift-mcp/src/main.rs b/crates/frameshift-mcp/src/main.rs index fd9b1fe..fd5b4ad 100644 --- a/crates/frameshift-mcp/src/main.rs +++ b/crates/frameshift-mcp/src/main.rs @@ -36,8 +36,8 @@ async fn main() { match read_capped_line(&mut stdin, &mut pending, &mut line, MAX_LINE_BYTES).await { Ok(LineRead::Eof) | Err(_) => break, Ok(LineRead::TooLong) => { - let response = - error_response(None, -32700, "request line exceeds maximum size".to_string()); + let msg = "request line exceeds maximum size".to_string(); + let response = error_response(None, -32700, msg); write_response(&mut stdout, &response).await; } Ok(LineRead::Line) => { From 4d1ac71732f9eec850ad067bc96c89cf131b1c2e Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Mon, 29 Jun 2026 00:45:24 -0400 Subject: [PATCH 5/7] chore: ignore .audit-fleet local audit working directories These hold raw multi-model audit findings with local filesystem paths, unpublished vulnerability locations, and internal tool identifiers that do not belong in a public repo. --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e9c702d..2b919c8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ data/ # Secrets bin/leak-patterns.txt + +# Local audit working directories (raw findings, local paths, tool identifiers) +.audit-fleet/ +.audit-fleet-web/ From 70dd3492217a190f4679c08105e9ba01eeb8ca77 Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Mon, 29 Jun 2026 00:45:38 -0400 Subject: [PATCH 6/7] deps: bump vulnerable dependencies to clear RUSTSEC advisories Clears all 8 cargo-audit vulnerabilities that were gating the Security audit job, and ratchets that job from continue-on-error to blocking. - diesel 2.2 -> 2.3.10, diesel-async 0.5 -> 0.9.2, diesel_migrations 2.2 -> 2.3, bb8 0.8 -> 0.9 (diesel-async 0.9 implements bb8::ManageConnection only for bb8 0.9). Clears RUSTSEC-2026-0136 and -0137 plus the diesel/diesel-async unsound advisories. - cargo update pulls postgres-protocol 0.6.12 (RUSTSEC-2026-0179/-0180), tokio-postgres 0.7.18 (RUSTSEC-2026-0178), quinn-proto 0.11.15 (RUSTSEC-2026-0185). - frameshift-server: prometheus default-features=false drops protobuf 2.28 (RUSTSEC-2024-0437); the server only emits the text format. - dev: testcontainers 0.23 -> 0.27, testcontainers-modules 0.11 -> 0.15; drops unmaintained tokio-tar (RUSTSEC-2025-0111, no upstream fix) for the maintained astral-tokio-tar fork, and rustls-pemfile. - ci: audit job now blocking, ignoring only RUSTSEC-2024-0370 (proc-macro-error, build-only via age, removable only by the age 0.11 beta that backs vault crypto). diesel-async 0.9 migration: transaction() now takes an AsyncFnOnce, so register_pack_version uses an `async move |conn|` closure instead of the removed `|conn| Box::pin(async move { .. })` form; ManagerConfig is now generic, so pool.rs names AsyncPgConnection explicitly. rust-version 1.75 -> 1.86 to match the new floor (diesel 2.3 + async closures). clippy -D warnings clean; cargo test --workspace 616 passed, 0 failed. --- .github/workflows/ci.yml | 14 +- Cargo.lock | 975 +++++++++--------- Cargo.toml | 16 +- .../src/backend.rs | 157 +-- .../frameshift-catalog-postgres/src/pool.rs | 6 +- crates/frameshift-server/Cargo.toml | 5 +- 6 files changed, 601 insertions(+), 572 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72d18da..8d3c019 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,18 +80,24 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo test -p frameshift-catalog-postgres -- --include-ignored - # Supply-chain advisory scan against the RustSec database. Non-blocking for - # now (continue-on-error) so a pre-existing transitive advisory cannot wedge - # the pipeline before the backlog is triaged; ratchet to blocking once clean. + # Supply-chain advisory scan against the RustSec database. Now blocking: the + # advisory backlog has been triaged and the vulnerable dependencies bumped, so + # a new advisory should fail the pipeline rather than slip in silently. + # + # The single ignore is RUSTSEC-2024-0370 (proc-macro-error unmaintained). It is + # a build-time-only proc-macro pulled transitively by age -> i18n-embed-fl; it + # never ships in any runtime artifact, and the only way to drop it is to bump + # age from 0.10 to the 0.11 BETA, which is unacceptable for the crate that + # backs local vault encryption. Re-evaluate when age ships a stable 0.11. audit: name: Security audit runs-on: ubuntu-latest - continue-on-error: true steps: - uses: actions/checkout@v4 - uses: rustsec/audit-check@v2 with: token: ${{ secrets.GITHUB_TOKEN }} + ignore: RUSTSEC-2024-0370 # Later tier (intentionally not yet wired, tracked in the roadmap): # - cargo-deny (needs a deny.toml: license allowlist, ban list, advisory policy) diff --git a/Cargo.lock b/Cargo.lock index ec7e2d9..2be7a0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -149,15 +149,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.102" +version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "arc-swap" -version = "1.9.1" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +checksum = "c049c0be4daef0b145cb3555416b3b8ef5b7888a38aea1a3a155801fe7b0810b" dependencies = [ "rustversion", ] @@ -172,6 +172,22 @@ dependencies = [ "serde_json", ] +[[package]] +name = "astral-tokio-tar" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08648fef353ab39a9d26f909ad53fc4f071be4c91853b78523f5cc3d9e5ebffd" +dependencies = [ + "futures-core", + "libc", + "portable-atomic", + "rustc-hash 2.1.2", + "rustix 0.38.44", + "tokio", + "tokio-stream", + "xattr", +] + [[package]] name = "async-compression" version = "0.4.42" @@ -184,6 +200,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -192,7 +230,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -212,9 +250,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "axum" @@ -298,13 +336,13 @@ dependencies = [ [[package]] name = "bb8" -version = "0.8.6" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89aabfae550a5c44b43ab941844ffcd2e993cb6900b342debf59e9ea74acdb8" +checksum = "457d7ed3f888dfd2c7af56d4975cade43c622f74bdcddfed6d4352f57acc6310" dependencies = [ - "async-trait", "futures-util", "parking_lot", + "portable-atomic", "tokio", ] @@ -322,9 +360,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "block-buffer" @@ -337,20 +375,23 @@ dependencies = [ [[package]] name = "block-buffer" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" dependencies = [ "hybrid-array", ] [[package]] name = "bollard" -version = "0.18.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ccca1260af6a459d75994ad5acc1651bcabcbdbc41467cc9786519ab854c30" +checksum = "ee04c4c84f1f811b017f2fbb7dd8815c976e7ca98593de9c1e2afad0f636bff4" dependencies = [ + "async-stream", "base64 0.22.1", + "bitflags 2.13.0", + "bollard-buildkit-proto", "bollard-stubs", "bytes", "futures-core", @@ -365,33 +406,54 @@ dependencies = [ "hyper-util", "hyperlocal", "log", + "num", "pin-project-lite", + "rand 0.9.4", "rustls", "rustls-native-certs", - "rustls-pemfile", "rustls-pki-types", "serde", "serde_derive", "serde_json", - "serde_repr", "serde_urlencoded", "thiserror 2.0.18", + "time", "tokio", + "tokio-stream", "tokio-util", + "tonic", "tower-service", "url", "winapi", ] +[[package]] +name = "bollard-buildkit-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a885520bf6249ab931a764ffdb87b0ceef48e6e7d807cfdb21b751e086e1ad" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-prost", + "ureq 3.3.0", +] + [[package]] name = "bollard-stubs" -version = "1.47.1-rc.27.3.1" +version = "1.52.1-rc.29.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f179cfbddb6e77a5472703d4b30436bff32929c0aa8a9008ecf23d1d3cdd0da" +checksum = "0f0a8ca8799131c1837d1282c3f81f31e76ceb0ce426e04a7fe1ccee3287c066" dependencies = [ + "base64 0.22.1", + "bollard-buildkit-proto", + "bytes", + "prost", "serde", + "serde_json", "serde_repr", - "serde_with", + "time", ] [[package]] @@ -405,9 +467,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "bytemuck" @@ -423,15 +485,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" [[package]] name = "cc" -version = "1.2.62" +version = "1.2.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" dependencies = [ "find-msvc-tools", "shlex", @@ -462,9 +524,9 @@ dependencies = [ [[package]] name = "chacha20" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", "cpufeatures 0.3.0", @@ -486,9 +548,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.44" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" dependencies = [ "iana-time-zone", "js-sys", @@ -540,7 +602,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -551,9 +613,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmov" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" [[package]] name = "colorchoice" @@ -660,9 +722,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" dependencies = [ "hybrid-array", ] @@ -700,17 +762,17 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", + "darling_core 0.21.3", + "darling_macro 0.21.3", ] [[package]] @@ -725,16 +787,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -747,18 +809,18 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ - "darling_core 0.20.11", + "darling_core 0.21.3", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -769,7 +831,7 @@ checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core 0.23.0", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -856,20 +918,20 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ - "powerfmt", "serde_core", ] [[package]] name = "diesel" -version = "2.2.12" +version = "2.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229850a212cd9b84d4f0290ad9d294afc0ae70fccaa8949dbe8b43ffafa1e20c" +checksum = "29fe29a87fb84c631ffb3ba21798c4b1f3a964701ba78f0dce4bf8668562ec88" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "byteorder", "chrono", "diesel_derives", + "downcast-rs", "itoa", "pq-sys", "serde_json", @@ -878,37 +940,37 @@ dependencies = [ [[package]] name = "diesel-async" -version = "0.5.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51a307ac00f7c23f526a04a77761a0519b9f0eb2838ebf5b905a58580095bdcb" +checksum = "dd39af30158d444884f166fe4c58f35dc40ad71ad017bb59408a3448526ff4bd" dependencies = [ - "async-trait", "bb8", "diesel", + "futures-core", "futures-util", - "scoped-futures", + "pin-project-lite", "tokio", "tokio-postgres", ] [[package]] name = "diesel_derives" -version = "2.2.7" +version = "2.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b96984c469425cb577bf6f17121ecb3e4fe1e81de5d8f780dd372802858d756" +checksum = "d1817b7f4279b947fc4cafddec12b0e5f8727141706561ce3ac94a60bddd1cf5" dependencies = [ "diesel_table_macro_syntax", "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "diesel_migrations" -version = "2.2.0" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a73ce704bad4231f001bff3314d91dce4aba0770cee8b233991859abc15c1f6" +checksum = "28d0f4a98124ba6d4ca75da535f65984badec16a003b6e2f94a01e31a79490b8" dependencies = [ "diesel", "migrations_internals", @@ -917,11 +979,11 @@ dependencies = [ [[package]] name = "diesel_table_macro_syntax" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" +checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -941,46 +1003,52 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.0", + "block-buffer 0.12.1", "const-oid 0.10.2", - "crypto-common 0.2.1", + "crypto-common 0.2.2", "ctutils", ] [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "docker_credential" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4564c274ebf369f501de192b02a0b81a5c4bda375abfe526aa70fc702fa6fa0" +checksum = "29547a1dc60885a552306986316bc9701ba120c1a8db6769fa68691529ad373d" dependencies = [ "base64 0.22.1", "serde", "serde_json", ] +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + [[package]] name = "dsl_auto_type" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" +checksum = "dd122633e4bef06db27737f21d3738fb89c8f6d5360d6d9d7635dda142a7757e" dependencies = [ - "darling 0.20.11", + "darling 0.21.3", "either", "heck", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1016,9 +1084,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "encoding_rs" @@ -1047,13 +1115,12 @@ dependencies = [ [[package]] name = "etcetera" -version = "0.8.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" dependencies = [ "cfg-if", - "home", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -1080,6 +1147,17 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ferroid" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee93edf3c501f0035bbeffeccfed0b79e14c311f12195ec0e661e114a0f60da4" +dependencies = [ + "portable-atomic", + "rand 0.10.1", + "web-time", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1185,12 +1263,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - [[package]] name = "foldhash" version = "0.2.0" @@ -1296,7 +1368,7 @@ dependencies = [ "thiserror 2.0.18", "toml 0.8.23", "tracing", - "ureq", + "ureq 2.12.1", ] [[package]] @@ -1667,7 +1739,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -1744,16 +1816,14 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "rand_core 0.10.1", - "wasip2", - "wasip3", ] [[package]] @@ -1781,9 +1851,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" dependencies = [ "atomic-waker", "bytes", @@ -1813,15 +1883,6 @@ dependencies = [ "ahash", ] -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash 0.1.5", -] - [[package]] name = "hashbrown" version = "0.16.1" @@ -1830,7 +1891,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ "allocator-api2", "equivalent", - "foldhash 0.2.0", + "foldhash", ] [[package]] @@ -1904,9 +1965,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.0" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" dependencies = [ "bytes", "itoa", @@ -1955,18 +2016,18 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hybrid-array" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" dependencies = [ "typenum", ] [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" dependencies = [ "atomic-waker", "bytes", @@ -2013,7 +2074,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] @@ -2119,7 +2180,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.10.0", - "syn 2.0.117", + "syn 2.0.118", "unic-langid", ] @@ -2133,7 +2194,7 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2242,12 +2303,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - [[package]] name = "ident_case" version = "1.0.1" @@ -2387,21 +2442,20 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" dependencies = [ "cfg-if", "futures-util", - "once_cell", "wasm-bindgen", ] [[package]] name = "kqueue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" dependencies = [ "kqueue-sys", "libc", @@ -2413,7 +2467,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "libc", ] @@ -2423,12 +2477,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - [[package]] name = "libc" version = "0.2.186" @@ -2437,18 +2485,18 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libmimalloc-sys" -version = "0.1.47" +version = "0.1.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1eacfa31c33ec25e873c136ba5669f00f9866d0688bea7be4d3f7e43067df6" +checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9" dependencies = [ "cc", ] [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -2464,6 +2512,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -2487,9 +2541,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" [[package]] name = "lru-slab" @@ -2534,15 +2588,15 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" [[package]] name = "migrations_internals" -version = "2.2.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bda1634d70d5bd53553cf15dca9842a396e8c799982a3ad22998dc44d961f24" +checksum = "36c791ecdf977c99f45f23280405d7723727470f6689a5e6dbf513ac547ae10d" dependencies = [ "serde", "toml 0.9.12+spec-1.1.0", @@ -2550,9 +2604,9 @@ dependencies = [ [[package]] name = "migrations_macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb161cc72176cb37aa47f1fc520d3ef02263d67d661f44f05d05a079e1237fd" +checksum = "36fc5ac76be324cfd2d3f2cf0fdf5d5d3c4f14ed8aaebadb09e304ba42282703" dependencies = [ "migrations_internals", "proc-macro2", @@ -2561,9 +2615,9 @@ dependencies = [ [[package]] name = "mimalloc" -version = "0.1.50" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3627c4272df786b9260cabaa46aec1d59c93ede723d4c3ef646c503816b0640" +checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862" dependencies = [ "libmimalloc-sys", ] @@ -2604,9 +2658,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", @@ -2658,7 +2712,7 @@ version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "filetime", "inotify", "kqueue", @@ -2678,11 +2732,75 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + [[package]] name = "num-conv" -version = "0.2.1" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] [[package]] name = "num-traits" @@ -2709,7 +2827,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -2801,7 +2919,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.18", + "redox_syscall", "smallvec", "windows-link", ] @@ -2828,7 +2946,7 @@ dependencies = [ "regex", "regex-syntax", "structmeta", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2861,7 +2979,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2906,7 +3024,7 @@ checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -2950,9 +3068,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postgres-protocol" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56201207dac53e2f38e848e31b4b91616a6bb6e0c7205b77718994a7f49e70fc" +checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" dependencies = [ "base64 0.22.1", "byteorder", @@ -2968,9 +3086,9 @@ dependencies = [ [[package]] name = "postgres-types" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc729a129e682e8d24170cd30ae1aa01b336b096cbb56df6d534ffec133d186" +checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" dependencies = [ "bytes", "fallible-iterator 0.2.0", @@ -3012,16 +3130,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3063,7 +3171,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "version_check", "yansi", ] @@ -3079,15 +3187,40 @@ dependencies = [ "lazy_static", "memchr", "parking_lot", - "protobuf", "thiserror 1.0.69", ] [[package]] -name = "protobuf" -version = "2.28.0" +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "prost-types" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] [[package]] name = "quanta" @@ -3116,9 +3249,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.9" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" dependencies = [ "bytes", "cfg_aliases", @@ -3136,9 +3269,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.14" +version = "0.11.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" dependencies = [ "bytes", "getrandom 0.3.4", @@ -3171,9 +3304,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.45" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" dependencies = [ "proc-macro2", ] @@ -3217,8 +3350,8 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20 0.10.0", - "getrandom 0.4.2", + "chacha20 0.10.1", + "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3272,16 +3405,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.11.1", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" -dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.0", ] [[package]] @@ -3290,7 +3414,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", ] [[package]] @@ -3310,14 +3434,14 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "regex" -version = "1.12.3" +version = "1.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" dependencies = [ "aho-corasick", "memchr", @@ -3338,9 +3462,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "reqwest" @@ -3382,7 +3506,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-streams", "web-sys", - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] @@ -3405,7 +3529,7 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "fallible-iterator 0.3.0", "fallible-streaming-iterator", "hashlink", @@ -3433,7 +3557,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.117", + "syn 2.0.118", "walkdir", ] @@ -3468,24 +3592,37 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.12.1", "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.40" +version = "0.23.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" dependencies = [ "log", "once_cell", @@ -3498,9 +3635,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" dependencies = [ "openssl-probe", "rustls-pki-types", @@ -3508,15 +3645,6 @@ dependencies = [ "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" -dependencies = [ - "rustls-pki-types", -] - [[package]] name = "rustls-pki-types" version = "1.14.1" @@ -3601,15 +3729,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "scoped-futures" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b24aae2d0636530f359e9d5ef0c04669d11c5e756699b27a6a6d845d8329091" -dependencies = [ - "pin-project-lite", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3643,7 +3762,7 @@ version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "core-foundation", "core-foundation-sys", "libc", @@ -3708,14 +3827,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "itoa", "memchr", @@ -3743,7 +3862,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3778,9 +3897,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" dependencies = [ "base64 0.22.1", "bs58", @@ -3798,14 +3917,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" dependencies = [ "darling 0.23.0", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3841,9 +3960,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.3.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signal-hook-registry" @@ -3884,15 +4003,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "socket2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3961,7 +4080,7 @@ dependencies = [ "proc-macro2", "quote", "structmeta-derive", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3972,7 +4091,7 @@ checksum = "152a0b65a590ff6c3da95cabe2353ee04e6167c896b28e3b14478c2636c922fc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -3993,9 +4112,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.117" +version = "2.0.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" dependencies = [ "proc-macro2", "quote", @@ -4019,14 +4138,14 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "tar" -version = "0.4.45" +version = "0.4.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840" dependencies = [ "filetime", "libc", @@ -4040,26 +4159,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.2", + "getrandom 0.4.3", "once_cell", - "rustix", + "rustix 1.1.4", "windows-sys 0.61.2", ] [[package]] name = "testcontainers" -version = "0.23.3" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a4f01f39bb10fc2a5ab23eb0d888b1e2bb168c157f61a1b98e6c501c639c74" +checksum = "bfd5785b5483672915ed5fe3cddf9f546802779fc1eceff0a6fb7321fac81c1e" dependencies = [ + "astral-tokio-tar", "async-trait", "bollard", - "bollard-stubs", "bytes", "docker_credential", "either", "etcetera", + "ferroid", "futures", + "http", + "itertools", "log", "memchr", "parse-display", @@ -4070,16 +4192,15 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tokio-stream", - "tokio-tar", "tokio-util", "url", ] [[package]] name = "testcontainers-modules" -version = "0.11.6" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d43ed4e8f58424c3a2c6c56dbea6643c3c23e8666a34df13c54f0a184e6c707" +checksum = "e5985fde5befe4ffa77a052e035e16c2da86e8bae301baa9f9904ad3c494d357" dependencies = [ "testcontainers", ] @@ -4110,7 +4231,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4121,7 +4242,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4135,12 +4256,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.47" +version = "0.3.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +checksum = "85c17d80feb7334b40c484e45ed1a5273dfd8bfda537c3be2e74a06a6686f327" dependencies = [ "deranged", - "itoa", "num-conv", "powerfmt", "serde_core", @@ -4150,15 +4270,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.27" +version = "0.2.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +checksum = "dcef1a61bdb119096e153208ec5cbec23944ce8bca13be5c7f60c634f7403935" dependencies = [ "num-conv", "time-core", @@ -4198,7 +4318,7 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio 1.2.0", + "mio 1.2.1", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -4215,14 +4335,14 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] name = "tokio-postgres" -version = "0.7.17" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd8df5ef180f6364759a6f00f7aadda4fbbac86cdee37480826a6ff9f3574ce" +checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" dependencies = [ "async-trait", "byteorder", @@ -4265,21 +4385,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-tar" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5714c010ca3e5c27114c1cdeb9d14641ace49874aa5626d7149e47aedace75" -dependencies = [ - "filetime", - "futures-core", - "libc", - "redox_syscall 0.3.5", - "tokio", - "tokio-stream", - "xattr", -] - [[package]] name = "tokio-util" version = "0.7.18" @@ -4320,12 +4425,10 @@ version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.14.0", "serde_core", "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "toml_writer", "winnow 0.7.15", ] @@ -4367,7 +4470,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.2", + "winnow 1.0.3", ] [[package]] @@ -4376,12 +4479,6 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - [[package]] name = "tonic" version = "0.14.6" @@ -4411,6 +4508,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "tonic-prost" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +dependencies = [ + "bytes", + "prost", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -4432,12 +4540,12 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "async-compression", - "bitflags 2.11.1", + "bitflags 2.13.0", "bytes", "futures-core", "futures-util", @@ -4504,7 +4612,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -4576,9 +4684,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uncased" @@ -4635,12 +4743,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - [[package]] name = "universal-hash" version = "0.5.1" @@ -4675,6 +4777,33 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "ureq" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" +dependencies = [ + "base64 0.22.1", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "ureq-proto", + "utf8-zero", +] + +[[package]] +name = "ureq-proto" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" +dependencies = [ + "base64 0.22.1", + "http", + "httparse", + "log", +] + [[package]] name = "url" version = "2.5.8" @@ -4688,6 +4817,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4702,11 +4837,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.1" +version = "1.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" dependencies = [ - "getrandom 0.4.2", + "getrandom 0.4.3", "js-sys", "serde_core", "wasm-bindgen", @@ -4766,20 +4901,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +version = "1.0.4+wasi-0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" dependencies = [ - "wit-bindgen 0.51.0", + "wit-bindgen", ] [[package]] @@ -4793,9 +4919,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" dependencies = [ "cfg-if", "once_cell", @@ -4806,9 +4932,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4816,9 +4942,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4826,48 +4952,26 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - [[package]] name = "wasm-streams" version = "0.4.2" @@ -4881,23 +4985,11 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver", -] - [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" dependencies = [ "js-sys", "wasm-bindgen", @@ -4919,14 +5011,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.7", + "webpki-roots 1.0.8", ] [[package]] name = "webpki-roots" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" dependencies = [ "rustls-pki-types", ] @@ -4996,7 +5088,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5007,7 +5099,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5052,6 +5144,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -5267,9 +5368,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wiremock" @@ -5294,100 +5395,12 @@ dependencies = [ "url", ] -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - [[package]] name = "wit-bindgen" version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.1", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - [[package]] name = "writeable" version = "0.6.3" @@ -5413,7 +5426,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.1.4", ] [[package]] @@ -5424,9 +5437,9 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "yoke" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5441,28 +5454,28 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5482,28 +5495,28 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] @@ -5537,7 +5550,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.117", + "syn 2.0.118", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 944aa80..8fe8b2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,9 @@ members = [ [workspace.package] edition = "2021" -rust-version = "1.75" +# diesel 2.3 requires 1.86 and the diesel-async 0.9 transaction API uses async +# closures (stable since 1.85); 1.86 is the real floor after those bumps. +rust-version = "1.86" license = "Elastic-2.0" [workspace.dependencies] @@ -60,12 +62,12 @@ tower = "0.5" tower-http = { version = "0.6", features = ["trace", "compression-gzip", "request-id", "limit", "cors", "set-header"] } tokio = { version = "1", features = ["full"] } http-body-util = "0.1" -diesel = { version = "2.2", features = ["postgres", "chrono", "serde_json", "uuid"] } -diesel-async = { version = "0.5", features = ["postgres", "bb8"] } -diesel_migrations = { version = "2.2", features = ["postgres"] } -bb8 = "0.8" -testcontainers = "0.23" -testcontainers-modules = { version = "0.11", features = ["postgres"] } +diesel = { version = "2.3.8", features = ["postgres", "chrono", "serde_json", "uuid"] } +diesel-async = { version = "0.9", features = ["postgres", "bb8"] } +diesel_migrations = { version = "2.3", features = ["postgres"] } +bb8 = "0.9" +testcontainers = "0.27" +testcontainers-modules = { version = "0.15", features = ["postgres"] } figment = { version = "0.10", features = ["env", "toml"] } mimalloc = "0.1" clap = { version = "4", features = ["derive"] } diff --git a/crates/frameshift-catalog-postgres/src/backend.rs b/crates/frameshift-catalog-postgres/src/backend.rs index 946d859..c814dab 100644 --- a/crates/frameshift-catalog-postgres/src/backend.rs +++ b/crates/frameshift-catalog-postgres/src/backend.rs @@ -379,92 +379,93 @@ impl CatalogBackend for PostgresCatalog { use diesel_async::AsyncConnection as _; let tx_result = conn - .transaction::<(), TxError, _>(|conn| { - let new_pack = new_pack.clone(); - let new_version = new_version.clone(); - let pack_name = pack_name_clone.clone(); - let version = version_clone.clone(); - let incoming_author = incoming_author_bytes.clone(); - Box::pin(async move { - // D5: If the pack head already exists, verify the publishing - // author matches the stored current_author. First-publish - // (no existing row) is always allowed. - let existing_pack: Option = packs::table - .filter(packs::name.eq(&pack_name)) - .select(PackRow::as_select()) - .first(conn) - .await - .optional() - .map_err(|e| { - TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) - })?; - - if let Some(ref existing) = existing_pack { - // Pack already exists -- check ownership. - if existing.current_author != incoming_author { - return Err(TxError::Catalog(CatalogError::Unauthorized { - kind: "pack", - key: pack_name.clone(), - })); - } + .transaction::<(), TxError, _>(async move |conn| { + // diesel-async 0.9 takes an `AsyncFnOnce`, so the old + // `|conn| Box::pin(async move { .. })` wrapper is gone -- the body + // is now the async closure directly. `new_pack` and `new_version` + // are captured by move under their own names; the comparison values + // are rebound (by move, no clone) to the short names used below. + let pack_name = pack_name_clone; + let version = version_clone; + let incoming_author = incoming_author_bytes; + // D5: If the pack head already exists, verify the publishing + // author matches the stored current_author. First-publish + // (no existing row) is always allowed. + let existing_pack: Option = packs::table + .filter(packs::name.eq(&pack_name)) + .select(PackRow::as_select()) + .first(conn) + .await + .optional() + .map_err(|e| { + TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) + })?; + + if let Some(ref existing) = existing_pack { + // Pack already exists -- check ownership. + if existing.current_author != incoming_author { + return Err(TxError::Catalog(CatalogError::Unauthorized { + kind: "pack", + key: pack_name.clone(), + })); } + } - // Upsert the parent pack row; do nothing if it already exists. - diesel::insert_into(packs::table) - .values(&new_pack) - .on_conflict(packs::name) - .do_nothing() - .execute(conn) - .await - .map_err(|e| { - TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) - })?; - - // Insert the version row. - diesel::insert_into(pack_versions::table) - .values(&new_version) + // Upsert the parent pack row; do nothing if it already exists. + diesel::insert_into(packs::table) + .values(&new_pack) + .on_conflict(packs::name) + .do_nothing() + .execute(conn) + .await + .map_err(|e| { + TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) + })?; + + // Insert the version row. + diesel::insert_into(pack_versions::table) + .values(&new_version) + .execute(conn) + .await + .map_err(|e| { + TxError::Catalog(map_diesel_error( + e, + "pack_version", + format!("{pack_name}@{version}"), + )) + })?; + + // D8: Update latest_version using true semver precedence. + // Read the current stored value (may have changed from the + // row we fetched above if this is a first insert), then + // compare using semver_gt before issuing the UPDATE. + let current_latest: Option = packs::table + .filter(packs::name.eq(&pack_name)) + .select(packs::latest_version) + .first(conn) + .await + .map_err(|e| { + TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) + })?; + + // Only update when the new version has strictly higher + // semver precedence than the stored latest. + let should_update = match ¤t_latest { + None => true, + Some(stored) => semver_gt(&version, stored), + }; + + if should_update { + diesel::update(packs::table.filter(packs::name.eq(&pack_name))) + .set(packs::latest_version.eq(Some(&version))) .execute(conn) .await - .map_err(|e| { - TxError::Catalog(map_diesel_error( - e, - "pack_version", - format!("{pack_name}@{version}"), - )) - })?; - - // D8: Update latest_version using true semver precedence. - // Read the current stored value (may have changed from the - // row we fetched above if this is a first insert), then - // compare using semver_gt before issuing the UPDATE. - let current_latest: Option = packs::table - .filter(packs::name.eq(&pack_name)) - .select(packs::latest_version) - .first(conn) - .await .map_err(|e| { TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) })?; + } - // Only update when the new version has strictly higher - // semver precedence than the stored latest. - let should_update = match ¤t_latest { - None => true, - Some(stored) => semver_gt(&version, stored), - }; - - if should_update { - diesel::update(packs::table.filter(packs::name.eq(&pack_name))) - .set(packs::latest_version.eq(Some(&version))) - .execute(conn) - .await - .map_err(|e| { - TxError::Catalog(map_diesel_error(e, "pack", pack_name.clone())) - })?; - } - - Ok(()) - }) + Ok(()) }) .await; diff --git a/crates/frameshift-catalog-postgres/src/pool.rs b/crates/frameshift-catalog-postgres/src/pool.rs index 0606ca9..3de88fe 100644 --- a/crates/frameshift-catalog-postgres/src/pool.rs +++ b/crates/frameshift-catalog-postgres/src/pool.rs @@ -46,7 +46,11 @@ pub async fn build_pool( // Build the manager with a custom connection setup callback. // The callback runs for every new physical connection, before the // connection enters the pool's idle queue. - let mut manager_config = ManagerConfig::default(); + // + // diesel-async 0.6+ made `ManagerConfig` generic over the connection type, + // so the connection type is named explicitly here rather than left to + // inference. + let mut manager_config = ManagerConfig::::default(); manager_config.custom_setup = Box::new(move |url: &str| { let url = url.to_string(); let timeout_ms = statement_timeout_ms; diff --git a/crates/frameshift-server/Cargo.toml b/crates/frameshift-server/Cargo.toml index 3f93c0e..68a98a5 100644 --- a/crates/frameshift-server/Cargo.toml +++ b/crates/frameshift-server/Cargo.toml @@ -46,7 +46,10 @@ flate2 = "1" hmac = { workspace = true } sha2 = { workspace = true } tower_governor = "0.8" -prometheus = "0.13" +# default-features disabled to drop the unmaintained protobuf 2.x dependency +# (RUSTSEC-2024-0437); the server only emits the Prometheus text exposition +# format via TextEncoder, never the protobuf wire format. +prometheus = { version = "0.13", default-features = false } [dev-dependencies] tokio = { workspace = true } From 52a267c9c9892a1706dadfa400a6014a21665809 Mon Sep 17 00:00:00 2001 From: GhostFrame Date: Mon, 29 Jun 2026 00:47:00 -0400 Subject: [PATCH 7/7] ci(mirror): push branches/tags explicitly instead of --mirror `git push --mirror` deletes refs absent from the local checkout. Because actions/checkout only fetches the triggering branch, the mirror push tried to delete every other ref on the target -- and the remote rejects deleting its default branch `main`, so the job failed on every push. Fetch all source branches into remote-tracking refs, then force-push branches and tags with explicit refspecs (additive, no deletion). --- .github/workflows/mirror.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index b0f28c6..9dc7048 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -18,5 +18,13 @@ jobs: env: MIRROR_TOKEN: ${{ secrets.MIRROR_TOKEN }} run: | - git remote add mirror https://x-access-token:${MIRROR_TOKEN}@github.com/Syntheos-Systems/FrameShift.git - git push mirror --mirror \ No newline at end of file + git remote add mirror "https://x-access-token:${MIRROR_TOKEN}@github.com/Syntheos-Systems/FrameShift.git" + # actions/checkout only fetches the triggering ref, so pull every source + # branch into remote-tracking refs before mirroring. + git fetch --prune origin '+refs/heads/*:refs/remotes/origin/*' + # Mirror all branches and tags. We deliberately avoid `git push --mirror`: + # it deletes refs absent from the local checkout, which fails because the + # remote refuses to delete its default branch `main`. Force-push so the + # mirror still follows rebases and force-updates on the source. + git push --force mirror 'refs/remotes/origin/*:refs/heads/*' + git push --force mirror 'refs/tags/*:refs/tags/*' \ No newline at end of file