From aecf88c1c1c30653dbe670f7d5ddd99e82e551a6 Mon Sep 17 00:00:00 2001 From: webdevtodayjason <115739170+webdevtodayjason@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:01:07 -0500 Subject: [PATCH] Plug-ins: whole-number parameters keep their exact value and stay in range --- crates/plugins/src/manifest.rs | 35 ++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/plugins/src/manifest.rs b/crates/plugins/src/manifest.rs index e5d76766..1498d218 100644 --- a/crates/plugins/src/manifest.rs +++ b/crates/plugins/src/manifest.rs @@ -203,8 +203,16 @@ impl Manifest { let resolved = match spec { ParamSpec::Number { min, max, .. } => json!(v.as_f64().filter(|f| f.is_finite()).ok_or_else(|| wrong("a number"))?.clamp(*min, *max)), ParamSpec::Int { min, max, .. } => { - let f = v.as_f64().filter(|f| f.is_finite()).ok_or_else(|| wrong("a number"))?; - json!((f.round().clamp(*min as f64, *max as f64)) as i64) + // Keep JSON integers exact: f64 cannot represent every i64 or its bounds. + let value = if let Some(i) = v.as_i64() { + i.clamp(*min, *max) + } else if let Some(i) = v.as_u64() { + i128::from(i).clamp(i128::from(*min), i128::from(*max)) as i64 + } else { + let f = v.as_f64().filter(|f| f.is_finite()).ok_or_else(|| wrong("a number"))?; + (f.round() as i64).clamp(*min, *max) + }; + json!(value) } ParamSpec::Bool { .. } => json!(v.as_bool().ok_or_else(|| wrong("true or false"))?), ParamSpec::Choice { options, .. } => { @@ -303,6 +311,29 @@ mod tests { assert!(m.resolve_params(&Value::Null).is_ok()); } + #[test] + fn integer_parameters_preserve_precision_and_exact_bounds() { + let m = Manifest::parse( + br#"{"id":"org.example.generative","name":"Generative Pattern","kind":"filter", + "params":{"seed":{"type":"int","min":-9007199254740995,"max":9007199254740995}}}"#, + ) + .unwrap(); + for (given, expected) in [ + (json!(9007199254740992_i64), 9007199254740992_i64), + (json!(9007199254740993_i64), 9007199254740993_i64), + (json!(-9007199254740993_i64), -9007199254740993_i64), + (json!(i64::MAX), 9007199254740995_i64), + (json!(i64::MIN), -9007199254740995_i64), + (json!(u64::MAX), 9007199254740995_i64), + (json!(1e30), 9007199254740995_i64), + (json!(-1e30), -9007199254740995_i64), + (json!(3.6), 4_i64), + ] { + let resolved = m.resolve_params(&json!({"seed": given})).unwrap(); + assert_eq!(resolved["seed"], json!(expected), "input: {given}"); + } + } + #[test] fn rejects_bad_manifests() { for bad in [