From f55cf595f8511fca9afc86cc572f26b017fb79ca Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:09:56 +0000 Subject: [PATCH 1/9] feat(forces): add validated shared authoring snapshots and evaluator --- Cargo.toml | 1 + src/force_field.rs | 445 ++++++++++++++++++++++++++++++++++ src/forces.rs | 4 + src/lib.rs | 5 + tests/force_field_contract.rs | 293 ++++++++++++++++++++++ 5 files changed, 748 insertions(+) create mode 100644 src/force_field.rs create mode 100644 tests/force_field_contract.rs diff --git a/Cargo.toml b/Cargo.toml index 8dbddad3..7d6846d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ eframe = { version = "=0.33.3", optional = true, default-features = false, featu [features] default = ["png-export"] png-export = ["dep:png"] +force-serde = ["dep:serde", "dep:serde_json"] parallel = ["dep:rayon"] work-observation = ["parallel"] desktop = ["dep:serde", "dep:serde_json", "dep:eframe", "png-export", "parallel", "work-observation"] diff --git a/src/force_field.rs b/src/force_field.rs new file mode 100644 index 00000000..e6804a44 --- /dev/null +++ b/src/force_field.rs @@ -0,0 +1,445 @@ +//! Solver-independent, static external fields in SI coordinates. +//! Authoring data is mutable; only a validated, owned snapshot can be evaluated. +use crate::ForceUnits; +use std::fmt; + +pub const FORCE_SCENE_VERSION: u32 = 1; +pub const MAX_FORCE_OBJECTS: usize = 256; + +/// Caller-owned identity, unique across models and forces in an authoring scene. +/// Zero is reserved. The force document checks its own subset for duplicates. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr(any(feature = "force-serde", feature = "desktop"), serde(transparent))] +pub struct SceneObjectId(pub u64); + +/// Translation in metres; unit quaternion [x,y,z,w], local to world. No scale. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + serde(deny_unknown_fields) +)] +pub struct ForceTransform { + pub translation: [f64; 3], + pub rotation: [f64; 4], +} +impl Default for ForceTransform { + fn default() -> Self { + Self { + translation: [0.0; 3], + rotation: [0.0, 0.0, 0.0, 1.0], + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + serde(deny_unknown_fields) +)] +pub enum ForceKind { + /// Local vector in the selected units (m/s² or N/m³). + Directional { vector: [f64; 3] }, + /// Constant magnitude, positive outward; negative attracts. Zero at centre. + Radial { strength: f64 }, + /// Constant tangential magnitude around local +Y, right-hand rule. + /// Positive strength points toward -Z at +X. Zero on the axis. + Vortex { strength: f64 }, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + serde(deny_unknown_fields) +)] +pub enum ForceSupport { + Global, + /// Local sphere, open outer boundary. Edge fraction is in [0,1]. + Sphere { + radius: f64, + edge_fraction: f64, + }, + /// Local box [-half_extent,+half_extent), with an optional smooth edge. + Box { + half_extent: [f64; 3], + edge_fraction: f64, + }, +} + +/// Static activation sampled at accepted interval start, in seconds. +#[derive(Debug, Clone, Copy, PartialEq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + serde(deny_unknown_fields) +)] +pub struct ForceTime { + pub start_seconds: f64, + /// Exclusive end; None has no end. Steps are not split at these endpoints. + pub end_seconds: Option, +} +impl Default for ForceTime { + fn default() -> Self { + Self { + start_seconds: 0.0, + end_seconds: None, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + serde(deny_unknown_fields) +)] +pub struct ForceObject { + pub id: SceneObjectId, + pub name: String, + pub enabled: bool, + pub transform: ForceTransform, + pub units: ForceUnits, + pub kind: ForceKind, + pub support: ForceSupport, + pub time: ForceTime, +} + +/// Serialization carries explicit schema version; unknown fields/variants fail. +/// Deserialized authoring data still requires freeze() before solver use. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + serde(deny_unknown_fields) +)] +pub struct ForceScene { + pub version: u32, + pub objects: Vec, +} +impl Default for ForceScene { + fn default() -> Self { + Self { + version: FORCE_SCENE_VERSION, + objects: Vec::new(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ForceFieldError { + UnsupportedVersion, + TooManyObjects, + InvalidIdentity, + InvalidObject, + InvalidSample, + ArithmeticFailure, +} +impl fmt::Display for ForceFieldError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "external force field: {self:?}") + } +} +impl std::error::Error for ForceFieldError {} + +/// Independent owned run inputs. No mutable access or deserialization bypass. +/// The host retains this with run metadata, separate from later draft edits. +#[derive(Debug, Clone)] +pub struct ForceSnapshot { + scene: ForceScene, +} +impl ForceScene { + pub fn freeze(&self) -> Result { + if self.version != FORCE_SCENE_VERSION { + return Err(ForceFieldError::UnsupportedVersion); + } + if self.objects.len() > MAX_FORCE_OBJECTS { + return Err(ForceFieldError::TooManyObjects); + } + for (i, object) in self.objects.iter().enumerate() { + if object.id.0 == 0 || self.objects[..i].iter().any(|o| o.id == object.id) { + return Err(ForceFieldError::InvalidIdentity); + } + object.validate()?; + } + Ok(ForceSnapshot { + scene: self.clone(), + }) + } +} +impl ForceSnapshot { + pub fn scene(&self) -> &ForceScene { + &self.scene + } + + /// Acceleration at a world position. Force density is divided by the caller's + /// positive local mass density, never by particle mass. No inverse-square law. + pub fn acceleration( + &self, + position: [f64; 3], + time: f64, + density: f64, + ) -> Result<[f64; 3], ForceFieldError> { + if position.iter().any(|v| !v.is_finite()) + || !time.is_finite() + || time < 0.0 + || !density.is_finite() + || density <= 0.0 + { + return Err(ForceFieldError::InvalidSample); + } + let mut sum = [0.0; 3]; + for object in self.scene.objects.iter().filter(|o| o.active(time)) { + let value = object.evaluate(position)?; + for d in 0..3 { + sum[d] += match object.units { + ForceUnits::Acceleration => value[d], + ForceUnits::ForceDensity => value[d] / density, + }; + } + } + finite(sum) + } + + /// Conservative world-component bound for MAC displacement budgeting. + /// Triangle inequality deliberately ignores support and cancellation. + pub(crate) fn acceleration_bound( + &self, + time: f64, + density: f64, + ) -> Result { + let mut bound = 0.0; + for object in self.scene.objects.iter().filter(|o| o.active(time)) { + let magnitude = match object.kind { + ForceKind::Directional { vector } => length(vector), + ForceKind::Radial { strength } | ForceKind::Vortex { strength } => strength.abs(), + }; + // Unit quaternions are checked to finite tolerance; factor covers + // the corresponding rotation matrix norm error conservatively. + // A tiny absolute allowance also covers underflow/rounding during + // normalization, rotation and unit conversion of subnormal values. + let magnitude = magnitude * (1.0 + 1e-10) + f64::MIN_POSITIVE; + bound += match object.units { + ForceUnits::Acceleration => magnitude, + ForceUnits::ForceDensity => magnitude / density + f64::MIN_POSITIVE, + }; + } + if bound.is_finite() { + Ok(bound) + } else { + Err(ForceFieldError::ArithmeticFailure) + } + } +} +fn length(v: [f64; 3]) -> f64 { + v[0].hypot(v[1]).hypot(v[2]) +} +// Scale first: direct hypot can round a subnormal multi-axis norm enough to +// exceed unit magnitude, or overflow for otherwise finite world coordinates. +fn direction(v: [f64; 3]) -> [f64; 3] { + let scale = v.into_iter().map(f64::abs).fold(0.0_f64, f64::max); + if scale == 0.0 { + return [0.0; 3]; + } + let scaled = v.map(|x| x / scale); + let norm = length(scaled); + scaled.map(|x| x / norm) +} +fn finite(v: [f64; 3]) -> Result<[f64; 3], ForceFieldError> { + if v.iter().all(|x| x.is_finite()) { + Ok(v) + } else { + Err(ForceFieldError::ArithmeticFailure) + } +} +fn rotate(q: [f64; 4], v: [f64; 3]) -> [f64; 3] { + let [x, y, z, w] = q; + [ + (1.0 - 2.0 * (y * y + z * z)) * v[0] + + 2.0 * (x * y - z * w) * v[1] + + 2.0 * (x * z + y * w) * v[2], + 2.0 * (x * y + z * w) * v[0] + + (1.0 - 2.0 * (x * x + z * z)) * v[1] + + 2.0 * (y * z - x * w) * v[2], + 2.0 * (x * z - y * w) * v[0] + + 2.0 * (y * z + x * w) * v[1] + + (1.0 - 2.0 * (x * x + y * y)) * v[2], + ] +} +impl ForceObject { + fn validate(&self) -> Result<(), ForceFieldError> { + let q = self.transform.rotation; + let q2: f64 = q.iter().map(|x| x * x).sum(); + let edge_valid = |e: f64| e.is_finite() && (0.0..=1.0).contains(&e); + let support_valid = match self.support { + ForceSupport::Global => true, + ForceSupport::Sphere { + radius, + edge_fraction, + } => radius.is_finite() && radius > 0.0 && edge_valid(edge_fraction), + ForceSupport::Box { + half_extent, + edge_fraction, + } => half_extent.iter().all(|v| v.is_finite() && *v > 0.0) && edge_valid(edge_fraction), + }; + let magnitude = match self.kind { + ForceKind::Directional { vector } => length(vector), + ForceKind::Radial { strength } | ForceKind::Vortex { strength } => strength, + }; + if self.name.len() > 256 + || self.transform.translation.iter().any(|x| !x.is_finite()) + || !q2.is_finite() + || (q2 - 1.0).abs() > 1e-12 + || !magnitude.is_finite() + || !support_valid + || !self.time.start_seconds.is_finite() + || self.time.start_seconds < 0.0 + || self + .time + .end_seconds + .is_some_and(|end| !end.is_finite() || end <= self.time.start_seconds) + { + return Err(ForceFieldError::InvalidObject); + } + Ok(()) + } + fn active(&self, time: f64) -> bool { + self.enabled + && time >= self.time.start_seconds + && self.time.end_seconds.is_none_or(|end| time < end) + } + fn evaluate(&self, position: [f64; 3]) -> Result<[f64; 3], ForceFieldError> { + let delta = finite(std::array::from_fn(|d| { + position[d] - self.transform.translation[d] + }))?; + let [x, y, z, w] = self.transform.rotation; + let local = finite(rotate([-x, -y, -z, w], delta))?; + let (r, edge) = match self.support { + ForceSupport::Global => (0.0, 0.0), + ForceSupport::Sphere { + radius, + edge_fraction, + } => { + let r = length(local.map(|v| v / radius)); + if r >= 1.0 { + return Ok([0.0; 3]); + } + (r, edge_fraction) + } + ForceSupport::Box { + half_extent, + edge_fraction, + } => { + if (0..3).any(|d| local[d] < -half_extent[d] || local[d] >= half_extent[d]) { + return Ok([0.0; 3]); + } + ( + (0..3) + .map(|d| local[d].abs() / half_extent[d]) + .fold(0.0_f64, f64::max), + edge_fraction, + ) + } + }; + let weight = if edge == 0.0 { + 1.0 + } else { + let t = ((1.0 - r) / edge).clamp(0.0, 1.0); + t * t * (3.0 - 2.0 * t) + }; + let local_value = match self.kind { + ForceKind::Directional { vector } => vector, + ForceKind::Radial { strength } => direction(local).map(|v| v * strength), + ForceKind::Vortex { strength } => { + direction([local[2], 0.0, -local[0]]).map(|v| v * strength) + } + }; + finite(rotate( + self.transform.rotation, + local_value.map(|v| v * weight), + )) + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn rotated_subnormal_fields_respect_acceleration_bound() { + let h = 0.5_f64.sqrt(); + for (kind, units) in [ + ( + ForceKind::Radial { strength: 2.0 }, + ForceUnits::Acceleration, + ), + ( + ForceKind::Vortex { strength: -2.0 }, + ForceUnits::Acceleration, + ), + ( + ForceKind::Directional { + vector: [1.0, -2.0, 3.0], + }, + ForceUnits::ForceDensity, + ), + ( + ForceKind::Radial { + strength: f64::from_bits(1), + }, + ForceUnits::Acceleration, + ), + ] { + let fields = ForceScene { + version: FORCE_SCENE_VERSION, + objects: vec![ForceObject { + id: SceneObjectId(1), + name: "subnormal".into(), + enabled: true, + transform: ForceTransform { + translation: [0.0; 3], + rotation: [0.0, 0.0, h, h], + }, + units, + kind, + support: ForceSupport::Global, + time: ForceTime::default(), + }], + } + .freeze() + .unwrap(); + let tiny = f64::from_bits(1); + let actual = fields.acceleration([tiny, tiny, tiny], 0.0, 1.0).unwrap(); + let bound = fields.acceleration_bound(0.0, 1.0).unwrap(); + assert!(length(actual) <= bound); + if matches!( + kind, + ForceKind::Radial { strength: 2.0 } | ForceKind::Vortex { strength: -2.0 } + ) { + assert!((length(actual) - 2.0).abs() < 1e-12); + } + } + } +} diff --git a/src/forces.rs b/src/forces.rs index 2b5d6569..6438dbe6 100644 --- a/src/forces.rs +++ b/src/forces.rs @@ -4,6 +4,10 @@ use crate::{Axis, GridGeometry, SimulationError, StepReport, StepStage}; /// Acceleration is m/s²; force density is N/m³ and is divided by the /// simulation's constant positive density (kg/m³) before the velocity update. #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg_attr( + any(feature = "force-serde", feature = "desktop"), + derive(serde::Serialize, serde::Deserialize) +)] pub enum ForceUnits { Acceleration, ForceDensity, diff --git a/src/lib.rs b/src/lib.rs index 7a600376..6e7e2a74 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,11 @@ pub use simulation::{ AcceptedIntervalPressure, CarrierStepControls, Simulation, SimulationConfig, SimulationError, SmokeSource, StateView, StepReport, StepStage, }; +pub mod force_field; +pub use force_field::{ + FORCE_SCENE_VERSION, ForceFieldError, ForceKind, ForceObject, ForceScene, ForceSnapshot, + ForceSupport, ForceTime, ForceTransform, MAX_FORCE_OBJECTS, SceneObjectId, +}; mod forces; pub use forces::{BodyForce, ForceRegion, ForceReport, ForceUnits, ForcedStepReport}; mod emitter; diff --git a/tests/force_field_contract.rs b/tests/force_field_contract.rs new file mode 100644 index 00000000..d64235e4 --- /dev/null +++ b/tests/force_field_contract.rs @@ -0,0 +1,293 @@ +//! Pure authoring/evaluator tests: no solver, filesystem, subprocess or workloads. +use rheon::*; +fn object(kind: ForceKind) -> ForceObject { + ForceObject { + id: SceneObjectId(1), + name: "Field".into(), + enabled: true, + transform: ForceTransform::default(), + units: ForceUnits::Acceleration, + kind, + support: ForceSupport::Global, + time: ForceTime::default(), + } +} +fn snapshot(objects: Vec) -> ForceSnapshot { + ForceScene { + objects, + ..Default::default() + } + .freeze() + .unwrap() +} +fn sample(o: ForceObject, p: [f64; 3]) -> [f64; 3] { + snapshot(vec![o]).acceleration(p, 0.0, 2.0).unwrap() +} +fn close(a: [f64; 3], b: [f64; 3]) { + for d in 0..3 { + assert!((a[d] - b[d]).abs() < 1e-12, "{a:?} != {b:?}"); + } +} +#[test] +fn directional_units_superposition_and_density() { + let a = object(ForceKind::Directional { + vector: [2.0, -4.0, 0.0], + }); + let mut f = a.clone(); + f.id = SceneObjectId(2); + f.units = ForceUnits::ForceDensity; + let s = snapshot(vec![a, f]); + assert_eq!( + s.acceleration([0.0; 3], 0.0, 2.0).unwrap(), + [3.0, -6.0, 0.0] + ); + assert_eq!( + s.acceleration([0.0; 3], 0.0, 4.0).unwrap(), + [2.5, -5.0, 0.0] + ); +} +#[test] +fn translation_rotation_and_local_box_support() { + let mut o = object(ForceKind::Directional { + vector: [2.0, 0.0, 0.0], + }); + o.transform.translation = [3.0, 4.0, 0.0]; + let h = 0.5_f64.sqrt(); + o.transform.rotation = [0.0, 0.0, h, h]; + o.support = ForceSupport::Box { + half_extent: [2.0, 0.5, 1.0], + edge_fraction: 0.0, + }; + close(sample(o.clone(), [3.0, 5.0, 0.0]), [0.0, 2.0, 0.0]); + assert_eq!(sample(o, [4.0, 4.0, 0.0]), [0.0; 3]); +} +#[test] +fn radial_sign_is_outward_and_centre_is_zero() { + let mut o = object(ForceKind::Radial { strength: 10.0 }); + assert_eq!(sample(o.clone(), [3.0, 4.0, 0.0]), [6.0, 8.0, 0.0]); + assert_eq!(sample(o.clone(), [0.0; 3]), [0.0; 3]); + o.kind = ForceKind::Radial { strength: -10.0 }; + assert_eq!(sample(o, [3.0, 4.0, 0.0]), [-6.0, -8.0, 0.0]); +} +#[test] +fn vortex_handedness_axis_and_rotated_axis() { + let mut o = object(ForceKind::Vortex { strength: 2.0 }); + assert_eq!(sample(o.clone(), [1.0, 0.0, 0.0]), [0.0, 0.0, -2.0]); + assert_eq!(sample(o.clone(), [0.0, 10.0, 0.0]), [0.0; 3]); + let h = 0.5_f64.sqrt(); + o.transform.rotation = [h, 0.0, 0.0, h]; + close(sample(o, [1.0, 0.0, 0.0]), [0.0, 2.0, 0.0]); +} +#[test] +fn support_boundaries_and_smoothstep_weights() { + let mut o = object(ForceKind::Directional { + vector: [8.0, 0.0, 0.0], + }); + o.support = ForceSupport::Sphere { + radius: 2.0, + edge_fraction: 0.5, + }; + assert_eq!(sample(o.clone(), [0.0; 3]), [8.0, 0.0, 0.0]); + assert_eq!(sample(o.clone(), [1.5, 0.0, 0.0]), [4.0, 0.0, 0.0]); + assert_eq!(sample(o.clone(), [2.0, 0.0, 0.0]), [0.0; 3]); + o.support = ForceSupport::Box { + half_extent: [2.0; 3], + edge_fraction: 0.0, + }; + assert_eq!(sample(o.clone(), [-2.0, 0.0, 0.0]), [8.0, 0.0, 0.0]); + assert_eq!(sample(o.clone(), [2.0, 0.0, 0.0]), [0.0; 3]); + o.support = ForceSupport::Box { + half_extent: [2.0; 3], + edge_fraction: 0.5, + }; + assert_eq!(sample(o, [1.5, 0.0, 0.0]), [4.0, 0.0, 0.0]); +} +#[test] +fn enabled_and_half_open_time_use_static_samples() { + let mut o = object(ForceKind::Radial { strength: 2.0 }); + o.time = ForceTime { + start_seconds: 1.0, + end_seconds: Some(2.0), + }; + let s = snapshot(vec![o.clone()]); + for t in [0.0, 0.999, 2.0, 3.0] { + assert_eq!(s.acceleration([1.0, 0.0, 0.0], t, 1.0).unwrap(), [0.0; 3]); + } + assert_eq!( + s.acceleration([1.0, 0.0, 0.0], 1.0, 1.0).unwrap(), + [2.0, 0.0, 0.0] + ); + o.enabled = false; + assert_eq!( + snapshot(vec![o]) + .acceleration([1.0, 0.0, 0.0], 1.5, 1.0) + .unwrap(), + [0.0; 3] + ); +} +#[test] +fn frozen_run_owns_original_identity_and_values() { + let mut scene = ForceScene { + objects: vec![object(ForceKind::Radial { strength: 2.0 })], + ..Default::default() + }; + let frozen = scene.freeze().unwrap(); + scene.objects[0].name = "Edited".into(); + scene.objects[0].enabled = false; + assert_eq!(frozen.scene().objects[0].id, SceneObjectId(1)); + assert_eq!(frozen.scene().objects[0].name, "Field"); + assert_eq!( + frozen.acceleration([1.0, 0.0, 0.0], 0.0, 1.0).unwrap(), + [2.0, 0.0, 0.0] + ); +} +#[test] +fn invalid_schema_ids_and_count_are_rejected() { + let o = object(ForceKind::Radial { strength: 1.0 }); + let mut s = ForceScene { + objects: vec![o.clone()], + ..Default::default() + }; + s.version += 1; + assert_eq!(s.freeze().unwrap_err(), ForceFieldError::UnsupportedVersion); + s.version = FORCE_SCENE_VERSION; + s.objects[0].id = SceneObjectId(0); + assert_eq!(s.freeze().unwrap_err(), ForceFieldError::InvalidIdentity); + s.objects = vec![o.clone(), o.clone()]; + assert_eq!(s.freeze().unwrap_err(), ForceFieldError::InvalidIdentity); + s.objects = vec![o; MAX_FORCE_OBJECTS + 1]; + assert_eq!(s.freeze().unwrap_err(), ForceFieldError::TooManyObjects); +} +#[test] +fn invalid_disabled_objects_are_also_rejected() { + let base = object(ForceKind::Radial { strength: 1.0 }); + let mut cases = vec![]; + let mut o = base.clone(); + o.transform.rotation = [0.0; 4]; + cases.push(o); + let mut o = base.clone(); + o.transform.translation[0] = f64::NAN; + cases.push(o); + let mut o = base.clone(); + o.kind = ForceKind::Radial { + strength: f64::INFINITY, + }; + cases.push(o); + let mut o = base.clone(); + o.time.end_seconds = Some(0.0); + cases.push(o); + let mut o = base.clone(); + o.time.start_seconds = -1.0; + cases.push(o); + let mut o = base.clone(); + o.support = ForceSupport::Sphere { + radius: 0.0, + edge_fraction: 0.0, + }; + cases.push(o); + let mut o = base.clone(); + o.support = ForceSupport::Box { + half_extent: [1.0; 3], + edge_fraction: 1.1, + }; + cases.push(o); + let mut o = base; + o.name = "x".repeat(257); + cases.push(o); + for mut o in cases { + o.enabled = false; + assert_eq!( + ForceScene { + objects: vec![o], + ..Default::default() + } + .freeze() + .unwrap_err(), + ForceFieldError::InvalidObject + ); + } +} +#[test] +fn nonfinite_inputs_overflow_and_tiny_distance_are_safe() { + let s = snapshot(vec![object(ForceKind::Radial { strength: 2.0 })]); + for rho in [0.0, -1.0, f64::INFINITY, f64::NAN] { + assert_eq!( + s.acceleration([0.0; 3], 0.0, rho).unwrap_err(), + ForceFieldError::InvalidSample + ); + } + assert_eq!( + s.acceleration([f64::NAN; 3], 0.0, 1.0).unwrap_err(), + ForceFieldError::InvalidSample + ); + assert_eq!( + s.acceleration([0.0; 3], -1.0, 1.0).unwrap_err(), + ForceFieldError::InvalidSample + ); + assert_eq!( + s.acceleration([1e-300, 0.0, 0.0], 0.0, 1.0).unwrap(), + [2.0, 0.0, 0.0] + ); + let mut o = object(ForceKind::Directional { + vector: [f64::MAX, 0.0, 0.0], + }); + o.units = ForceUnits::ForceDensity; + assert_eq!( + snapshot(vec![o]) + .acceleration([0.0; 3], 0.0, 0.5) + .unwrap_err(), + ForceFieldError::ArithmeticFailure + ); +} +#[cfg(feature = "force-serde")] +#[test] +fn serialization_roundtrip_preserves_ids_and_requires_validation() { + let scene = snapshot(vec![object(ForceKind::Vortex { strength: -3.0 })]) + .scene() + .clone(); + let json = serde_json::to_string(&scene).unwrap(); + let decoded: ForceScene = serde_json::from_str(&json).unwrap(); + assert_eq!(decoded, scene); + decoded.freeze().unwrap(); + let unsupported = json.replace("\"version\":1", "\"version\":99"); + assert_eq!( + serde_json::from_str::(&unsupported) + .unwrap() + .freeze() + .unwrap_err(), + ForceFieldError::UnsupportedVersion + ); + assert!( + serde_json::from_str::( + &json.replace("\"version\":1", "\"scale\":2,\"version\":1") + ) + .is_err() + ); +} + +#[test] +fn radial_and_vortex_normalization_handles_subnormal_and_huge_vectors() { + let tiny = f64::from_bits(1); + let diagonal = 2.0_f64.sqrt(); + close( + sample( + object(ForceKind::Radial { strength: 2.0 }), + [tiny, tiny, 0.0], + ), + [diagonal, diagonal, 0.0], + ); + close( + sample( + object(ForceKind::Radial { strength: 2.0 }), + [f64::MAX, f64::MAX, 0.0], + ), + [diagonal, diagonal, 0.0], + ); + close( + sample( + object(ForceKind::Vortex { strength: 2.0 }), + [tiny, 0.0, tiny], + ), + [diagonal, 0.0, -diagonal], + ); +} From 19e0145ce652fac565cd04b4e07ace3bbaa46954 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:10:04 +0000 Subject: [PATCH 2/9] feat(forces): adapt authored fields to MAC and particle force stages --- docs/plans/force-authoring/contract.md | 182 ++++++++++++++++++++ docs/plans/force-authoring/validation.md | 123 +++++++++++++ src/forces.rs | 90 +++++++++- src/particle_fluid.rs | 42 ++++- src/simulation.rs | 86 +++++++++- tests/force_field_solver_contract.rs | 209 +++++++++++++++++++++++ 6 files changed, 722 insertions(+), 10 deletions(-) create mode 100644 docs/plans/force-authoring/contract.md create mode 100644 docs/plans/force-authoring/validation.md create mode 100644 tests/force_field_solver_contract.rs diff --git a/docs/plans/force-authoring/contract.md b/docs/plans/force-authoring/contract.md new file mode 100644 index 00000000..9dc715a4 --- /dev/null +++ b/docs/plans/force-authoring/contract.md @@ -0,0 +1,182 @@ +# Shared external force contract, version 1 + +Backend owner scope: the library domain, pure evaluator and the closed-box MAC +and particle adapters. Desktop owns controls, inspector, scene handles, outlines, +timeline and run/archive wiring. No desktop source is changed here. + +## Repository and implementation plan + +On 2026-10-20, remote main was verified as +`fd7c0f0aee63bc8710df99f9bb01154e7619b974`; remote +`implementation/ui-hierarchy-20261020` was +`426fd25d22975e1259f5543c77dc771a6ac646ff`. The pre-existing local `work` branch +at `6ba41c7` and its worktree were preserved. Implementation uses separate branch +`implementation/shared-force-domain-20261020` based on the verified main. +No AGENTS.md or .agents/skills files were present in the repository/workspace +instruction directories. Existing Rust 2024/1.92, unsafe-forbid, checked error +and transactional candidate/commit conventions apply. + +The compact contract and disjoint write set were announced before implementation: + +1. Add validated authoring objects, one pure semantic evaluator, owned immutable + run snapshots, optional portable serialization and exact pure tests. +2. Add opt-in MAC/particle entry points; preserve BodyForce/gravity paths and + stage boundaries. Share the MAC lattice/work kernel. +3. Obtain independent equation/source review. Prepare bounded integration tests, + compile/list them, and leave them unexecuted. +4. Record validation and UI requirements, commit on the isolated branch. No push, + merge, deployment, branch removal, credentials or network configuration changes. + +Write set: Cargo.toml; src/{lib,force_field,forces,simulation,particle_fluid}.rs; +tests/force_field{,_solver}_contract.rs; this plan/contract and validation record. +Cargo.lock is unchanged. No scientific L1/MMS, model workload or native simulation +execution is part of this change. + +## Authoring objects and identity + +`ForceScene { version: FORCE_SCENE_VERSION, objects: Vec }` is the +editable document. `SceneObjectId(u64)` is a caller-assigned nonzero stable ID. +The host allocates IDs in one scene namespace for models and forces, retaining +IDs across rename/reorder/save/load and assigning fresh IDs to duplicates. +Freeze rejects zero IDs and duplicates within the force list. Model IDs and +cross-kind uniqueness remain the scene host's responsibility. Particle IDs are +a separate run-local namespace and are not scene-object IDs. + +`ForceObject` carries ID, name, enabled, transform, units, kind, support and time. +Names have a 256 UTF-8 byte limit; a scene has at most 256 force objects. Disabled +objects are still validated. Snapshot metadata/allocation is caller-owned and +outside the solvers' retained-array budgets. Hosts should bound serialized input +bytes before deserialization; freeze's object count limit is not a parser limit. + +`ForceTransform` is translation in metres and quaternion `[x,y,z,w]` mapping +local to world. Squared quaternion norm must be within 1e-12 of one; the backend +does not silently normalize malformed authoring input. The UI must produce a +unit quaternion. There is no scale, parenting, animated pose or mesh attachment. +Support dimensions are edited explicitly in metres. + +## Equations and units + +Let `R` be the quaternion rotation and `q = R^T (world - translation)`. +The raw local vector `v(q)` is: + +| Kind | Raw local vector | +| --- | --- | +| Directional | Authored local `vector` | +| Radial | `strength * q/length(q)`; zero at the centre | +| Vortex | `strength * (q.z,0,-q.x)/hypot(q.x,q.z)`; zero on local Y axis | + +Positive radial strength repels; negative attracts. Positive vortex strength +follows the right-hand rule about local +Y (at local +X it points toward -Z). +Strength is constant magnitude before support weighting, not an inverse-square +law or a calibrated pressure/torque. Direction normalization scales by the +largest absolute component first, avoiding norm underflow/overflow. Coincident +centres and axes are defined as zero; there is no hidden softening radius. + +Support is global, sphere, or local axis-aligned box (rotated with the object). +Sphere uses `r=length(q/radius)` and excludes `r>=1`. Box uses +`r=max(abs(q[d])/half_extent[d])` and hard support +`-half_extent <= q < +half_extent` componentwise. Thus a hard box includes its +lower plane and excludes its upper plane. Box/sphere dimensions are positive. +Support can extend beyond the simulation domain; adapters sample only their +normal domain and preserve their boundary policy. Legacy BodyForce retains its +stricter requirement that its world region fit inside the grid. + +With edge fraction `e` in [0,1], bounded support has weight 1 when `e=0`. +For `e>0`, set `s=clamp((1-r)/e,0,1)`, then weight is `s*s*(3-2*s)`. +It equals one in the inner region and smoothly reaches zero at all outer planes. +Box falloff uses max-norm shells, not Euclidean distance to the nearest surface. +The sphere centre remains fully weighted even with `e=1`. + +Each active object's world value is `R*v(q)*weight`. `ForceUnits::Acceleration` +means m/s²; `ForceUnits::ForceDensity` means N/m³. The latter divides the world +value by positive local mass density in kg/m³. Ordered vector addition produces +one acceleration. Total force in N, velocity targets/drag, inverse-square +singularities, potential-energy claims, material stress, turbulence closure and +source emission are not implemented as force types. + +All authored numeric values must be finite. Sampling requires finite position, +nonnegative finite time and positive finite density. Arithmetic overflow returns +an error; it never clips strength or returns nonfinite accelerations. Subnormal +direction normalization and magnitude bounds have pure regression coverage. + +## Time, snapshot and serialization + +`ForceTime { start_seconds, end_seconds }` activates on `[start,end)`; `None` +means no end. Start is nonnegative, and a supplied end is finite and after start. +Both adapters sample force activation at the accepted interval's start. A step +is not split at activation endpoints, so a force starting inside a step begins +on the next step. Every particle retry samples the same accepted start time. +The first schema supports static strength/pose and these activation intervals. + +`ForceScene::freeze()` validates and independently owns a cloned `ForceScene` +inside `ForceSnapshot`. Only an immutable view is exposed; no Deserialize or +mutable accessor can bypass validation. Draft edits after freeze cannot change +a run. The host must retain that snapshot with run inputs and use it on every +step; per-call borrowing alone does not enforce host-level run consistency. + +Serde is available under `force-serde` without desktop dependencies, and under +`desktop`. The version 1 document uses derived enum variant names and explicit +version. Structs/enum payloads reject unknown fields, unknown enum variants fail, +and freeze rejects unsupported versions. There is no silent migration. Save the +entire versioned scene, including disabled objects, IDs and names; do not save +only enabled vectors. Deserializing gives authoring data requiring freeze again. + +## Solver adapters and legacy compatibility + +- `Simulation::step_with_force_fields(dt, smoke_source, snapshot, cancel)` + samples each component at its own MAC face position before projection. +- `Simulation::step_with_emitter_and_force_fields(dt, emitter, snapshot, cancel)` + combines the existing nozzle source kernel with the same authored force stage. +- `ParticleFluidSimulation::step_with_force_fields(dt, emitter, snapshot, cancel)` + samples post-emission/pre-integration particle positions and the accepted + interval start time. It adds fields to existing gravity, then pair forces. + +MAC uses configured constant density. Its force-density division happens once +inside the common semantic evaluator. The existing closed normal wall faces +remain fixed; pressure may balance/project out applied acceleration. The shared +MAC update computes stored-f32 finite-step work with the existing face mass +`rho*cell_volume`; this measures only the force stage, not full-step energy. +A conservative scalar magnitude bound is applied to every world component for +the existing displacement budget. It sums object magnitudes, with a rounding +allowance, ignores support/cancellation, and can shorten dt more than legacy +componentwise BodyForce budgeting. It is a policy, not a stability theorem. + +Particle force density divides by the evaluated local thermodynamic/SPH density +at the force stage, **not particle mass or material reference density**. This is +the model's reference-volume density with its existing 0.05 reference-density +floor. The adapter introduces no extra density clamp. Existing combined-force +retry limits, domain walls and static-mesh response still apply. Diagnostics +include authored acceleration at their existing pre-integration stage. + +Legacy `BodyForce`, regions, smoke, gravity and old entry points remain supported. +Empty snapshots route through legacy paths. Existing public calls pass no +snapshot. Failure/cancellation retains accepted fields/particles/time/generation; +scratch is overwritten on retry. Authored sampling errors become each adapter's +existing ArithmeticFailure error. Sources and material forces remain separate. + +## UI integration requirements and explicit limits + +Use one stable scene selection identity for model and force rows. Properties map +to ForceObject fields; transform handles update translation/quaternion, while +bounds handles update support sizes. Global force extents need a UI glyph rather +than a physical finite boundary. Vortex arrows follow the signed +Y convention. +The UI may disable edits during a run, but it must still retain the frozen run +snapshot rather than re-read mutable drafts. Keep run fields in history/archive +metadata so historical viewport glyphs cannot be relabeled by draft edits. + +At Simulate: freeze the draft, surface validation failures, put the immutable +snapshot in the run request, and dispatch the relevant new adapter. Capture the +same versioned document with saved run specifications. Old records can map an +absent force document to an empty scene only through an explicit host migration. +Gravity remains additive: a gravity authoring object must not accidentally apply +Earth gravity twice. No desktop or recording schema is changed by this branch. + +Authored fields are initially unsupported in LiquidTransportSimulation, +MaterialSession/SimulationSession, provider/configured-comparison adapters, +free-surface/column, box-flux and viscosity combinations. Do not silently display +fields as applied in those paths; retain their existing APIs or reject unsupported +requests. New field calls also do not publish the legacy accepted-interval +pressure inspection controls, which cannot describe authored snapshots yet. +Combining an authored snapshot with a separate BodyForce list is not exposed. +No solver retains authoring IDs or scene geometry internally. Archive/session +custody and desktop visuals require the coordinated UI owner/parent follow-up. diff --git a/docs/plans/force-authoring/validation.md b/docs/plans/force-authoring/validation.md new file mode 100644 index 00000000..64e4bfd3 --- /dev/null +++ b/docs/plans/force-authoring/validation.md @@ -0,0 +1,123 @@ +# Force authoring validation and review record + +2026-10-20; Rust 1.92.0; isolated branch based on main fd7c0f0. +This record reports code-level checks, not scientific validation or performance. + +## Executed pure checks + +The helper paths in tests/force_field_contract.rs are local constructors, +freeze/evaluate calls and scalar assertions. No Simulation or particle solver +constructor is called there. Internal unit paths were inspected: the bound test +only evaluates fields; the three force-work tests update small local face arrays +through the lattice kernel and do not call advection/pressure/timestepping. + +Tests were enumerated with: + +```sh +cargo test --locked --offline --no-default-features --features force-serde --test force_field_contract -- --list +cargo test --locked --offline --no-default-features --features force-serde --lib -- --list +``` + +Each of these twelve tests ran separately with the exact command below, +substituting one listed name for NAME (all passed): + +```sh +cargo test --locked --offline --no-default-features --features force-serde --test force_field_contract NAME -- --exact +``` + +- directional_units_superposition_and_density +- enabled_and_half_open_time_use_static_samples +- frozen_run_owns_original_identity_and_values +- invalid_disabled_objects_are_also_rejected +- invalid_schema_ids_and_count_are_rejected +- nonfinite_inputs_overflow_and_tiny_distance_are_safe +- radial_and_vortex_normalization_handles_subnormal_and_huge_vectors +- radial_sign_is_outward_and_centre_is_zero +- serialization_roundtrip_preserves_ids_and_requires_validation +- support_boundaries_and_smoothstep_weights +- translation_rotation_and_local_box_support +- vortex_handedness_axis_and_rotated_axis + +Each of these four inspected unit tests ran separately with this exact command +(all passed): + +```sh +cargo test --locked --offline --no-default-features --features force-serde --lib NAME -- --exact +``` + +- force_field::tests::rotated_subnormal_fields_respect_acceleration_bound +- forces::tests::authored_field_face_sampling_density_walls_and_work +- forces::tests::finite_step_work_can_be_negative_and_uses_both_velocity_endpoints +- forces::tests::work_accounts_for_actual_storage_rounding_and_closed_wall_faces + +Thus 16 exact pure/unit tests passed. The last two are existing compatibility +checks. No broad execution filter or full suite was used. Local execution script +and full output: /tmp/rheon-force-pure-tests.sh and /tmp/rheon-force-pure-tests.log. +The initial offline enumeration lacked registry packages; one locked online build +downloaded dependencies using existing settings, then checks proceeded offline. +No credentials or network configuration were changed. Cargo.lock is unchanged. + +## Independent source/equation review + +A separate review agent, force_review, read the domain, both solver adapters, +existing force kernel and the new tests without editing or executing any tests. +It reviewed quaternion transforms, handedness, support/falloff, unit conversion, +snapshot ownership, MAC face boundaries/stored-velocity work, particle density, +force staging, retry/commit and legacy routing. + +The reviewer identified direct hypot normalization of multi-component subnormal +offsets as capable of exceeding the declared magnitude bound. The implementation +now scales directions before normalization and includes relative/absolute +rounding allowances in the conservative bound. New pure regressions cover +subnormal offsets/strength and very large finite offsets. The reviewer rechecked +the correction and reported no remaining blocking correctness findings. + +The reviewer also noted that hosts should cap JSON byte input before parsing; +freeze bounds validated data but does not bound deserializer allocations. + +## Prepared integration tests: not executed + +Every test in tests/force_field_solver_contract.rs is individually ignored with +a review/execution-authorization reason. They were compiled/listed only: + +```sh +cargo test --locked --offline --no-default-features --features force-serde --test force_field_solver_contract -- --list +``` + +The independent reviewer inspected all helper paths and bounds. MAC helpers use +2×2×1 cells, constant rho=2, 1 MiB retained-array budget, pressure iteration cap +100, requested dt=0.001. Particle helpers initialize exactly 64 particles, +1 MiB, no emitter/mesh, requested dt=1e-6, and the existing at-most-16 retry loop. +Each simulation requests at most one accepted step; cancel/retry checks use +separate bounded instances. No files, subprocesses, native UI or model processes +are requested by these tests. + +| Exact test | Planned assertion | +| --- | --- | +| authored_mac_matches_legacy_bounded_directional_force | Exact stored state/work agreement for equivalent local box and legacy region | +| authored_mac_cancellation_retains_accepted_state_and_retry | Accepted state retained at three cancel stages; retry agrees | +| authored_particle_density_conversion_adds_to_existing_acceleration | Pre-integration diagnostic difference equals evaluator with local density | +| authored_particle_cancel_retains_state_and_retry | Cancellation preserves accepted particles/time; retry agrees | +| empty_snapshot_preserves_both_legacy_paths | Empty snapshot agrees with legacy grid and particle paths | + +These are ready for parent review, not an assertion that solver integration has +passed. Execution, if later authorized, must use one exact test name and +`-- --exact --ignored`, never a broad suite. No L1/MMS, native simulation, model +workload, scientific result or hardware/performance claim was produced. + +## Static checks + +Passed without simulation execution: + +```sh +cargo clippy --locked --offline --no-default-features --features force-serde --lib --test force_field_contract --test force_field_solver_contract -- -D warnings +cargo check --locked --offline --no-default-features --lib +cargo fmt --all -- --check +git diff --check +``` + +The integration test's initial compile-only attempt exposed an invalid +SimulationConfig Default assumption in its helper. The helper now supplies the +existing configuration fields explicitly; subsequent compile/list and Clippy +passed. This did not execute a solver. Desktop/parallel builds and full suites +were not run as part of this bounded backend task. diff --git a/src/forces.rs b/src/forces.rs index 6438dbe6..3e6069fb 100644 --- a/src/forces.rs +++ b/src/forces.rs @@ -110,6 +110,45 @@ pub(crate) fn apply_on_domain( velocity: [&mut [f32]; 3], surface: Option>, cancel: &mut impl FnMut(StepStage) -> bool, +) -> Result { + apply_evaluated(grid, density, dt, velocity, surface, cancel, |position| { + let mut sum = [0.0; 3]; + for force in forces { + if force.contains(position) { + let a = force.acceleration(density); + for d in 0..3 { + sum[d] += a[d]; + } + } + } + Ok(sum) + }) +} + +pub(crate) fn apply_fields( + grid: &GridGeometry, + density: f64, + fields: &crate::ForceSnapshot, + time: f64, + dt: f64, + velocity: [&mut [f32]; 3], + cancel: &mut impl FnMut(StepStage) -> bool, +) -> Result { + apply_evaluated(grid, density, dt, velocity, None, cancel, |position| { + fields + .acceleration(position, time, density) + .map_err(|_| SimulationError::ArithmeticFailure) + }) +} + +fn apply_evaluated( + grid: &GridGeometry, + density: f64, + dt: f64, + velocity: [&mut [f32]; 3], + surface: Option>, + cancel: &mut impl FnMut(StepStage) -> bool, + mut acceleration: impl FnMut([f64; 3]) -> Result<[f64; 3], SimulationError>, ) -> Result { let mass = density * grid.cell_volume(); let energy = |fields: &[&mut [f32]; 3]| { @@ -141,12 +180,7 @@ pub(crate) fn apply_on_domain( continue; } let position = grid.face_position(axis, p).expect("bounded face"); - let mut a = 0.0; - for force in forces { - if force.contains(position) { - a += force.acceleration(density)[d]; - } - } + let a = acceleration(position)?[d]; let index = grid.face_unchecked(axis, p); let old = f64::from(velocity[d][index]); let updated = old + dt * a; @@ -227,4 +261,48 @@ mod tests { assert_eq!(y, [0.0; 4]); assert_eq!(z, [0.0; 4]); } + #[test] + fn authored_field_face_sampling_density_walls_and_work() { + let grid = GridGeometry::new([2, 1, 1], [1.0; 3], [0.0; 3]).unwrap(); + let fields = crate::ForceScene { + objects: vec![crate::ForceObject { + id: crate::SceneObjectId(1), + name: "bounded".into(), + enabled: true, + transform: crate::ForceTransform { + translation: [1.0, 0.5, 0.5], + ..Default::default() + }, + units: ForceUnits::ForceDensity, + kind: crate::ForceKind::Directional { + vector: [8.0, 12.0, -16.0], + }, + support: crate::ForceSupport::Sphere { + radius: 0.1, + edge_fraction: 0.0, + }, + time: crate::ForceTime::default(), + }], + ..Default::default() + } + .freeze() + .unwrap(); + let (mut x, mut y, mut z) = ([0.0, 2.0, 0.0], [0.0; 4], [0.0; 4]); + let report = apply_fields( + &grid, + 2.0, + &fields, + 0.0, + 0.25, + [&mut x, &mut y, &mut z], + &mut |_| false, + ) + .unwrap(); + assert_eq!(x, [0.0, 3.0, 0.0]); + assert_eq!(y, [0.0; 4]); + assert_eq!(z, [0.0; 4]); + assert_eq!(report.kinetic_energy_before, 4.0); + assert_eq!(report.kinetic_energy_after, 9.0); + assert_eq!(report.applied_work, 5.0); + } } diff --git a/src/particle_fluid.rs b/src/particle_fluid.rs index 90eca618..d6727280 100644 --- a/src/particle_fluid.rs +++ b/src/particle_fluid.rs @@ -68,7 +68,7 @@ pub struct ParticleStepReport { pub struct ParticleDiagnostic { pub density: f64, pub pressure: f64, - /// Includes gravity and pair forces, excludes subsequent wall response. + /// Includes gravity, authored fields and pair forces; excludes wall response. pub acceleration: [f64; 3], } @@ -379,11 +379,40 @@ impl ParticleFluidSimulation { &mut self, requested_dt: f64, emitter: Option, + cancel: impl FnMut() -> bool, + ) -> Result { + self.step_fields_impl(requested_dt, emitter, None, cancel) + } + + /// Add authored fields to existing gravity/pair forces at pre-integration + /// particle positions (including newly emitted particles). Force density is + /// divided by the same evaluated local density used by the pressure stage. + /// Static field time is the accepted interval start, including every retry. + pub fn step_with_force_fields( + &mut self, + requested_dt: f64, + emitter: Option, + fields: &crate::ForceSnapshot, + cancel: impl FnMut() -> bool, + ) -> Result { + self.step_fields_impl( + requested_dt, + emitter, + (!fields.scene().objects.is_empty()).then_some(fields), + cancel, + ) + } + + fn step_fields_impl( + &mut self, + requested_dt: f64, + emitter: Option, + fields: Option<&crate::ForceSnapshot>, mut cancel: impl FnMut() -> bool, ) -> Result { let mut interval = requested_dt; for _ in 0..16 { - match self.prepare_step(interval, emitter, &mut cancel) { + match self.prepare_step(interval, emitter, fields, &mut cancel) { Err(ParticleFluidError::StabilityLimit { recommended_dt }) => { interval = recommended_dt } @@ -398,6 +427,7 @@ impl ParticleFluidSimulation { &mut self, requested_dt: f64, emitter: Option, + fields: Option<&crate::ForceSnapshot>, cancel: &mut impl FnMut() -> bool, ) -> Result { if !requested_dt.is_finite() || requested_dt <= 0.0 { @@ -639,6 +669,14 @@ impl ParticleFluidSimulation { acceleration: self.config.gravity, damping_rate: 0.0, }; + if let Some(fields) = fields { + let external = fields + .acceleration(p.position, self.time, t.density) + .map_err(|_| ParticleFluidError::ArithmeticFailure)?; + for (a, external) in f.acceleration.iter_mut().zip(external) { + *a += external; + } + } neighbors(p.position, grid, h, bins, heads, next, |j| { if i == j { return; diff --git a/src/simulation.rs b/src/simulation.rs index 2db572d5..bc4ef07e 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -141,6 +141,7 @@ struct BoundaryStepInput<'a> { boundary: crate::BoxFluxStepBoundary, } struct StepDomains<'a, 'p> { + fields: Option<&'a crate::ForceSnapshot>, emitter: Option, barrier: Option<&'a crate::TriangleSurface>, boundary: Option>, @@ -385,6 +386,57 @@ impl Simulation { self.step_impl(requested_dt, source, forces, None, None, cancel) .map(|report| report.legacy.step) } + /// Static authored fields sampled at the accepted interval start. Only the + /// closed-box carrier is supported; pressure projection remains unchanged. + /// The caller retains the immutable snapshot with the run request/metadata. + pub fn step_with_force_fields( + &mut self, + requested_dt: f64, + source: Option, + fields: &crate::ForceSnapshot, + cancel: impl FnMut(StepStage) -> bool, + ) -> Result { + self.step_fields_impl(requested_dt, source, None, fields, cancel) + } + + /// The nozzle remains a separate source, evaluated by its existing kernel. + pub fn step_with_emitter_and_force_fields( + &mut self, + requested_dt: f64, + emitter: crate::RotatingCylinderEmitter, + fields: &crate::ForceSnapshot, + cancel: impl FnMut(StepStage) -> bool, + ) -> Result { + self.step_fields_impl(requested_dt, None, Some(emitter), fields, cancel) + } + + fn step_fields_impl( + &mut self, + requested_dt: f64, + source: Option, + emitter: Option, + fields: &crate::ForceSnapshot, + cancel: impl FnMut(StepStage) -> bool, + ) -> Result { + let outcome = self.prepare_impl( + requested_dt, + source, + &[], + StepDomains { + fields: (!fields.scene().objects.is_empty()).then_some(fields), + emitter, + barrier: None, + boundary: None, + surface: None, + viscosity: None, + provider: None, + }, + cancel, + )?; + self.commit_prepared(&outcome); + Ok(outcome.legacy.step) + } + /// Prescribed rotating, pressure-head-driven two-ended nozzle. Pressure /// projection and appearance transport retain the fixed-box model; the /// visible cylinder is not a moving collision boundary. Failure preserves @@ -401,6 +453,7 @@ impl Simulation { None, &[], StepDomains { + fields: None, emitter: Some(emitter), barrier: None, boundary: None, @@ -482,6 +535,7 @@ impl Simulation { source, forces, StepDomains { + fields: None, emitter: None, barrier: None, boundary: None, @@ -518,6 +572,7 @@ impl Simulation { source, forces, StepDomains { + fields: None, emitter: None, barrier, boundary, @@ -547,6 +602,7 @@ impl Simulation { source, forces, StepDomains { + fields: None, emitter: None, barrier: None, surface, @@ -662,6 +718,7 @@ impl Simulation { ) -> Result { self.inspection_pressure = None; let StepDomains { + fields, emitter, barrier, mut boundary, @@ -687,7 +744,21 @@ impl Simulation { if let Some(s) = source { validate_source(&self.grid, s)?; } - let external_rate = crate::forces::validate(&self.grid, self.config.density, forces)?; + let field_rate = if let Some(fields) = fields { + fields + .acceleration_bound(self.time, self.config.density) + .map_err(|_| SimulationError::ArithmeticFailure)? + * self + .grid + .spacing() + .into_iter() + .map(|h| 1.0 / h) + .sum::() + } else { + 0.0 + }; + let external_rate = + crate::forces::validate(&self.grid, self.config.density, forces)? + field_rate; let emitter_rate = if let Some(emitter) = emitter { emitter.axis_at(self.time)?; emitter.validate(&self.grid, self.config.density)? @@ -837,7 +908,18 @@ impl Simulation { )?; work.smoke_force_work = work.kinetic_after_smoke_force - work.kinetic_after_advection; } - let forces = if forces.is_empty() { + let forces = if let Some(fields) = fields { + checkpoint(&mut cancel, StepStage::BeforeForces)?; + Some(crate::forces::apply_fields( + &self.grid, + self.config.density, + fields, + self.time, + dt, + self.candidate.velocity_mut(), + &mut cancel, + )?) + } else if forces.is_empty() { None } else { checkpoint(&mut cancel, StepStage::BeforeForces)?; diff --git a/tests/force_field_solver_contract.rs b/tests/force_field_solver_contract.rs new file mode 100644 index 00000000..7f9066e7 --- /dev/null +++ b/tests/force_field_solver_contract.rs @@ -0,0 +1,209 @@ +//! Prepared bounded adapter integration checks. Every test is ignored so normal +//! suites cannot execute these simulations before review/explicit authorization. +//! No emitters, model processes, native UI, files, threads, or workloads here. +use rheon::*; +fn fields(kind: ForceKind, units: ForceUnits) -> ForceSnapshot { + ForceScene { + objects: vec![ForceObject { + id: SceneObjectId(1), + name: "adapter".into(), + enabled: true, + transform: ForceTransform::default(), + units, + kind, + support: ForceSupport::Global, + time: ForceTime::default(), + }], + ..Default::default() + } + .freeze() + .unwrap() +} +fn grid_sim() -> Simulation { + Simulation::new( + GridGeometry::new([2, 2, 1], [1.0; 3], [0.0; 3]).unwrap(), + SimulationConfig { + density: 2.0, + memory_limit: 1024 * 1024, + pressure: PressureSettings { + relative_residual: 1e-11, + absolute_residual: 1e-12, + divergence_limit: 1e-9, + max_iterations: 100, + }, + actual_divergence_limit: 1e-6, + max_courant: 1.0, + }, + ) + .unwrap() +} +fn grid_state(s: &Simulation) -> (Vec, u64, u64) { + let state = s.state(); + ( + state + .x + .iter() + .chain(state.y) + .chain(state.z) + .chain(state.tracer) + .map(|v| v.to_bits()) + .collect(), + state.time.to_bits(), + state.generation, + ) +} +#[test] +#[ignore = "bounded solver integration: review and explicit execution authorization required"] +fn authored_mac_matches_legacy_bounded_directional_force() { + let mut authored = grid_sim(); + let mut legacy = grid_sim(); + let mut scene = fields( + ForceKind::Directional { + vector: [8.0, 0.0, 0.0], + }, + ForceUnits::ForceDensity, + ) + .scene() + .clone(); + scene.objects[0].transform.translation = [1.0, 0.5, 0.5]; + scene.objects[0].support = ForceSupport::Box { + half_extent: [1.0, 0.5, 0.5], + edge_fraction: 0.0, + }; + let scene = scene.freeze().unwrap(); + let a = authored + .step_with_force_fields(0.001, None, &scene, |_| false) + .unwrap(); + let b = legacy + .step_with_forces( + 0.001, + None, + &[BodyForce { + value: [8.0, 0.0, 0.0], + units: ForceUnits::ForceDensity, + region: Some(ForceRegion { + lower: [0.0; 3], + upper: [2.0, 1.0, 1.0], + }), + }], + |_| false, + ) + .unwrap(); + assert_eq!(grid_state(&authored), grid_state(&legacy)); + assert_eq!(a.forces, b.forces); +} +#[test] +#[ignore = "bounded solver integration: review and explicit execution authorization required"] +fn authored_mac_cancellation_retains_accepted_state_and_retry() { + let f = fields( + ForceKind::Vortex { strength: 0.25 }, + ForceUnits::Acceleration, + ); + for stage in [ + StepStage::ForceSlice, + StepStage::BeforePressure, + StepStage::BeforeCommit, + ] { + let mut sim = grid_sim(); + let before = grid_state(&sim); + assert!(matches!( + sim.step_with_force_fields(0.001, None, &f, |s| s == stage), + Err(SimulationError::Cancelled { .. }) + )); + assert_eq!(grid_state(&sim), before); + sim.step_with_force_fields(0.001, None, &f, |_| false) + .unwrap(); + let mut reference = grid_sim(); + reference + .step_with_force_fields(0.001, None, &f, |_| false) + .unwrap(); + assert_eq!(grid_state(&sim), grid_state(&reference)); + } +} +fn particle_sim() -> ParticleFluidSimulation { + ParticleFluidSimulation::new( + GridGeometry::new([2; 3], [0.5; 3], [0.0; 3]).unwrap(), + ParticleFluidConfig { + fill: DomainFill::EmittedFluid, + samples_per_axis: 4, + max_particles: 64, + memory_limit: 1024 * 1024, + gravity: [0.0, -1.0, 0.0], + ..Default::default() + }, + ) + .unwrap() +} +#[test] +#[ignore = "bounded solver integration: review and explicit execution authorization required"] +fn authored_particle_density_conversion_adds_to_existing_acceleration() { + for units in [ForceUnits::Acceleration, ForceUnits::ForceDensity] { + let mut reference = particle_sim(); + let mut authored = particle_sim(); + let before: Vec<_> = authored.particles().iter().map(|p| p.position).collect(); + let f = fields(ForceKind::Radial { strength: -0.5 }, units); + let r = reference.step(1e-6, None, || false).unwrap(); + let a = authored + .step_with_force_fields(1e-6, None, &f, || false) + .unwrap(); + assert_eq!(r.dt, a.dt); + assert_eq!(a.particles, 64); + let (_, baseline) = reference.particle_diagnostics().unwrap(); + let (_, actual) = authored.particle_diagnostics().unwrap(); + for ((p, b), a) in before.into_iter().zip(baseline).zip(actual) { + assert_eq!(a.density, b.density); + let expected = f.acceleration(p, 0.0, a.density).unwrap(); + for (d, value) in expected.into_iter().enumerate() { + assert!((a.acceleration[d] - b.acceleration[d] - value).abs() < 1e-10); + } + } + } +} +#[test] +#[ignore = "bounded solver integration: review and explicit execution authorization required"] +fn authored_particle_cancel_retains_state_and_retry() { + let f = fields( + ForceKind::Vortex { strength: 0.1 }, + ForceUnits::Acceleration, + ); + let mut sim = particle_sim(); + let before = sim.particles().to_vec(); + let mut calls = 0; + assert!(matches!( + sim.step_with_force_fields(1e-6, None, &f, || { + calls += 1; + calls == 4 + }), + Err(ParticleFluidError::Cancelled) + )); + assert_eq!(sim.particles(), before); + assert_eq!(sim.time(), 0.0); + assert_eq!(sim.generation(), 0); + sim.step_with_force_fields(1e-6, None, &f, || false) + .unwrap(); + let mut reference = particle_sim(); + reference + .step_with_force_fields(1e-6, None, &f, || false) + .unwrap(); + assert_eq!(sim.particles(), reference.particles()); + assert_eq!(sim.time(), reference.time()); + assert_eq!(sim.generation(), reference.generation()); +} +#[test] +#[ignore = "bounded solver integration: review and explicit execution authorization required"] +fn empty_snapshot_preserves_both_legacy_paths() { + let empty = ForceScene::default().freeze().unwrap(); + let mut a = grid_sim(); + let mut b = grid_sim(); + a.step_with_force_fields(0.001, None, &empty, |_| false) + .unwrap(); + b.step(0.001, None, |_| false).unwrap(); + assert_eq!(grid_state(&a), grid_state(&b)); + let mut a = particle_sim(); + let mut b = particle_sim(); + a.step_with_force_fields(1e-6, None, &empty, || false) + .unwrap(); + b.step(1e-6, None, || false).unwrap(); + assert_eq!(a.particles(), b.particles()); + assert_eq!(a.time(), b.time()); +} From d7bffdd5d6e80e022f15c59d3574739e3b39b352 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:12:32 +0000 Subject: [PATCH 3/9] docs(forces): clarify runtime and recording handoff dependencies --- docs/plans/force-authoring/contract.md | 53 +++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/docs/plans/force-authoring/contract.md b/docs/plans/force-authoring/contract.md index 9dc715a4..a5903472 100644 --- a/docs/plans/force-authoring/contract.md +++ b/docs/plans/force-authoring/contract.md @@ -149,8 +149,9 @@ retry limits, domain walls and static-mesh response still apply. Diagnostics include authored acceleration at their existing pre-integration stage. Legacy `BodyForce`, regions, smoke, gravity and old entry points remain supported. -Empty snapshots route through legacy paths. Existing public calls pass no -snapshot. Failure/cancellation retains accepted fields/particles/time/generation; +Empty snapshots route through legacy numerical kernels. This does not provide +the old MAC inspection/recording stamp on the new entry points. Existing public +calls pass no snapshot. Failure/cancellation retains accepted fields/particles/time/generation; scratch is overwritten on retry. Authored sampling errors become each adapter's existing ArithmeticFailure error. Sources and material forces remain separate. @@ -180,3 +181,51 @@ pressure inspection controls, which cannot describe authored snapshots yet. Combining an authored snapshot with a separate BodyForce list is not exposed. No solver retains authoring IDs or scene geometry internally. Archive/session custody and desktop visuals require the coordinated UI owner/parent follow-up. + + +## Runtime handoff clarification + +This branch is an intermediate backend milestone, not completion of the user's +force-authoring workflow. Main and the supplied UI branch still call the legacy +methods: `CarrierExecution::advance` dispatches `Simulation::step` or +`step_with_emitter`, and `run_material` calls `ParticleFluidSimulation::step`. +No desktop user flow currently passes a ForceSnapshot. The library can consume +one through the three opt-in APIs, but that does not establish tested solver +parity or a finished product flow. + +The default material appearance can render particles as voxels. That view is not +a separate MAC solver. The actual runtime distinction is native carrier versus +particle material; configured carrier/transport are separate session owners. + +There is a blocking dependency for the native carrier recorder: the new force +methods commit without setting `accepted_interval_pressure`, while +`recording::carrier::carrier_publication` rejects nonconstructor publications +without that stamp. Merely changing `CarrierExecution::advance` will cause +recording to fail after an otherwise accepted force step. This includes an empty +snapshot passed to the new MAC method. Existing legacy calls are unchanged. +Before runtime wiring, add force-aware accepted interval control/pressure +custody and recorder support; do not fake the old control stamp or bypass it. + +Minimal coordinated follow-up: + +1. UI owner adds the shared scene objects/IDs and draft controls. Freeze once + before worker/output creation and retain the same snapshot in Request, live + history and run metadata. Historical glyphs must use this frozen scene. +2. Backend/recording owner extends the accepted carrier interval/control record + to describe the snapshot; expose success-qualified pressure after the new + force APIs and preserve the old inspection API's contract for legacy calls. + Add immutable material run force metadata too; MaterialStepControls currently + describes only dt/emitter and cannot disclose these forces. +3. UI owner dispatches native no-emitter/emitter calls to the corresponding new + MAC method and extracts `.step`; material dispatches the particle force call. + Empty-field workflows can keep using the existing public methods, including + their existing recorder stamps. Configured/transport sessions require their + own propagation rather than routing around their owner. +4. Extend versioned frozen native run specs, intent digests, constructor + validation, archive custody/load/rerun and material run metadata with the exact + ForceScene. Keep existing records valid through explicit version migration; + never attach current drafts to old results or reinterpret old hashes. +5. Review/run the prepared bounded tests only when authorized; then add pure + custody/serialization tests and separately reviewed small end-to-end runtime + checks. Existing pure results do not prove adapter equivalence or archive + integration. The current five prepared tests do not cover recording custody. From 8d2b4f8a6868b357df9e9be178c8fa290b7eda77 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:25:15 +0000 Subject: [PATCH 4/9] feat(recording): bind accepted authored forces to carrier and material archives --- docs/plans/force-authoring/contract.md | 47 +- docs/plans/force-authoring/custody.md | 154 +++++ .../evidence/custody-source.sha256 | 13 + ...tion_retains_accepted_state_and_retry.json | 24 + ...ation_retains_accepted_state_and_retry.log | 6 + ...ches_legacy_bounded_directional_force.json | 24 + ...tches_legacy_bounded_directional_force.log | 6 + ...rticle_cancel_retains_state_and_retry.json | 24 + ...article_cancel_retains_state_and_retry.log | 6 + ...version_adds_to_existing_acceleration.json | 24 + ...nversion_adds_to_existing_acceleration.log | 6 + .../pre-custody-adapters/build-features.json | 1 + ..._snapshot_preserves_both_legacy_paths.json | 24 + ...y_snapshot_preserves_both_legacy_paths.log | 6 + .../pre-custody-adapters/enumeration.json | 22 + .../pre-custody-adapters/enumeration.log | 5 + .../pre-custody-adapters/run_bounded.py | 62 ++ .../evidence/pre-custody-adapters/seal.sha256 | 8 + .../pre-custody-adapters/source.sha256 | 203 +++++++ .../evidence/rheon-custody-pure.log | 42 ++ .../evidence/rheon-custody-pure.sh | 13 + docs/plans/force-authoring/validation.md | 115 +++- src/force_field.rs | 6 +- src/lib.rs | 8 +- src/particle_fluid.rs | 38 +- src/recording.rs | 16 +- src/recording/carrier.rs | 44 +- src/recording/force_fields.rs | 557 ++++++++++++++++++ src/recording/material.rs | 48 +- src/simulation.rs | 46 ++ 30 files changed, 1548 insertions(+), 50 deletions(-) create mode 100644 docs/plans/force-authoring/custody.md create mode 100644 docs/plans/force-authoring/evidence/custody-source.sha256 create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.log create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.log create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/build-features.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.log create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.json create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.log create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/run_bounded.py create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/seal.sha256 create mode 100644 docs/plans/force-authoring/evidence/pre-custody-adapters/source.sha256 create mode 100644 docs/plans/force-authoring/evidence/rheon-custody-pure.log create mode 100644 docs/plans/force-authoring/evidence/rheon-custody-pure.sh create mode 100644 src/recording/force_fields.rs diff --git a/docs/plans/force-authoring/contract.md b/docs/plans/force-authoring/contract.md index a5903472..f0388b97 100644 --- a/docs/plans/force-authoring/contract.md +++ b/docs/plans/force-authoring/contract.md @@ -44,8 +44,8 @@ a separate run-local namespace and are not scene-object IDs. `ForceObject` carries ID, name, enabled, transform, units, kind, support and time. Names have a 256 UTF-8 byte limit; a scene has at most 256 force objects. Disabled -objects are still validated. Snapshot metadata/allocation is caller-owned and -outside the solvers' retained-array budgets. Hosts should bound serialized input +objects are still validated. Snapshot metadata/allocation is outside the solvers' retained-array budgets; +accepted custody now retains a shared immutable reference to the run document. Hosts should bound serialized input bytes before deserialization; freeze's object count limit is not a parser limit. `ForceTransform` is translation in metres and quaternion `[x,y,z,w]` mapping @@ -109,7 +109,8 @@ on the next step. Every particle retry samples the same accepted start time. The first schema supports static strength/pose and these activation intervals. `ForceScene::freeze()` validates and independently owns a cloned `ForceScene` -inside `ForceSnapshot`. Only an immutable view is exposed; no Deserialize or +inside `ForceSnapshot` (shared immutably by Arc for cheap accepted custody). +Only an immutable view is exposed; no Deserialize or mutable accessor can bypass validation. Draft edits after freeze cannot change a run. The host must retain that snapshot with run inputs and use it on every step; per-call borrowing alone does not enforce host-level run consistency. @@ -170,16 +171,17 @@ snapshot in the run request, and dispatch the relevant new adapter. Capture the same versioned document with saved run specifications. Old records can map an absent force document to an empty scene only through an explicit host migration. Gravity remains additive: a gravity authoring object must not accidentally apply -Earth gravity twice. No desktop or recording schema is changed by this branch. +Earth gravity twice. No desktop source is changed. Recording now supports the optional versioned +force metadata described in custody.md; the SQL envelope remains version 1. Authored fields are initially unsupported in LiquidTransportSimulation, MaterialSession/SimulationSession, provider/configured-comparison adapters, free-surface/column, box-flux and viscosity combinations. Do not silently display fields as applied in those paths; retain their existing APIs or reject unsupported -requests. New field calls also do not publish the legacy accepted-interval -pressure inspection controls, which cannot describe authored snapshots yet. +requests. New field calls publish a separate force-aware accepted interval; the legacy +pressure accessor still does not mislabel them with incomplete controls. Combining an authored snapshot with a separate BodyForce list is not exposed. -No solver retains authoring IDs or scene geometry internally. Archive/session +Accepted force custody retains the full immutable authored force document. Archive/session custody and desktop visuals require the coordinated UI owner/parent follow-up. @@ -197,25 +199,22 @@ The default material appearance can render particles as voxels. That view is not a separate MAC solver. The actual runtime distinction is native carrier versus particle material; configured carrier/transport are separate session owners. -There is a blocking dependency for the native carrier recorder: the new force -methods commit without setting `accepted_interval_pressure`, while -`recording::carrier::carrier_publication` rejects nonconstructor publications -without that stamp. Merely changing `CarrierExecution::advance` will cause -recording to fail after an otherwise accepted force step. This includes an empty -snapshot passed to the new MAC method. Existing legacy calls are unchanged. -Before runtime wiring, add force-aware accepted interval control/pressure -custody and recorder support; do not fake the old control stamp or bypass it. +At milestone d7bffdd, native carrier recording was blocked because the new force +methods had no qualified pressure stamp. That core dependency is now addressed: +`accepted_force_interval_pressure` and force-aware publication support capture +actual inputs, work and pressure. The legacy accessor remains separate. The +host must still opt into run force binding and constructor metadata before +switching its runtime dispatcher; see custody.md for the exact sequence. Minimal coordinated follow-up: 1. UI owner adds the shared scene objects/IDs and draft controls. Freeze once before worker/output creation and retain the same snapshot in Request, live history and run metadata. Historical glyphs must use this frozen scene. -2. Backend/recording owner extends the accepted carrier interval/control record - to describe the snapshot; expose success-qualified pressure after the new - force APIs and preserve the old inspection API's contract for legacy calls. - Add immutable material run force metadata too; MaterialStepControls currently - describes only dt/emitter and cannot disclose these forces. +2. Core accepted carrier/particle force custody and optional archive metadata are + implemented (custody.md). UI owner must use the run binding and constructor + publication APIs before executing force steps. MaterialStepControls remains + the source/dt declaration; actual force custody is recorded separately. 3. UI owner dispatches native no-emitter/emitter calls to the corresponding new MAC method and extracts `.step`; material dispatches the particle force call. Empty-field workflows can keep using the existing public methods, including @@ -225,7 +224,7 @@ Minimal coordinated follow-up: validation, archive custody/load/rerun and material run metadata with the exact ForceScene. Keep existing records valid through explicit version migration; never attach current drafts to old results or reinterpret old hashes. -5. Review/run the prepared bounded tests only when authorized; then add pure - custody/serialization tests and separately reviewed small end-to-end runtime - checks. Existing pure results do not prove adapter equivalence or archive - integration. The current five prepared tests do not cover recording custody. +5. The five prepared bounded tests passed once on the sealed pre-custody binary + under the later explicit authorization. Seven pure custody/codec tests pass. + No post-custody solver or end-to-end desktop/SQLite workload was executed; + these results do not establish full archive integration or desktop parity. diff --git a/docs/plans/force-authoring/custody.md b/docs/plans/force-authoring/custody.md new file mode 100644 index 00000000..f1e953a8 --- /dev/null +++ b/docs/plans/force-authoring/custody.md @@ -0,0 +1,154 @@ +# Force-aware accepted custody and archive metadata + +This extends the core milestone without editing desktop/request definitions or +session/provider owners. Source review is independent; runtime tests below apply +only to the sealed pre-custody adapter binary. Current custody code has pure codec +coverage and static checks, not executed solver/SQLite/desktop integration. + +## Accepted numerical publications + +`Simulation::accepted_force_interval_pressure()` returns a separate +`AcceptedForceIntervalPressure<'_>` containing actual `values: &[f64]`, +`interval_start`, `report: StepReport`, `controls: CarrierStepControls`, +`fields: &ForceSnapshot`, and `work: Option`. +The force step stamps it only after accepted commit, including for empty authored +documents. Any candidate attempt/reset/alternate publication invalidates both +MAC pressure stamps. `accepted_interval_pressure()` remains exclusively the +legacy view; its layout and legacy control formatting are unchanged. + +`ParticleFluidSimulation::accepted_force_controls()` returns +`Option<&AcceptedParticleForceControls>` containing actual `interval_start`, +original `requested_dt`, `emitter`, accepted `report`, and `fields: ForceSnapshot`. +This stamp is assigned only after a successful authored step. Rejection retains +the previous accepted stamp and diagnostic arrays; a successful legacy call +clears it. Empty authored documents are retained for custody but skipped during +numerical evaluation, avoiding addition of +0 to signed-zero gravity. + +Freeze still deep-copies draft data. ForceSnapshot now shares that immutable +owned document through Arc, so accepted custody clones do not duplicate names or +objects. Snapshot metadata remains outside numerical array budgets. + +## Public recording API additions + +All recording APIs remain native-only, as before. New functions are re-exported +from `rheon::recording`, and codec helpers also live in its `force_fields` module. + +```rust +pub const FORCE_METADATA_VERSION: u32 = 1; +pub const MAX_FORCE_METADATA_BYTES: usize = 256 * 1024; + +pub fn encode_force_metadata(fields: &ForceSnapshot) + -> Result, RecordingError>; +pub fn decode_force_metadata(bytes: &[u8]) + -> Result; +pub fn recorded_force_fields(step: &RecordedStep) + -> Result, RecordingError>; +pub fn validate_run_force_fields(run: &RecordingRun, step: &RecordedStep) + -> Result<(), RecordingError>; + +impl RecordingRun { + pub fn with_force_fields(self, fields: &ForceSnapshot) + -> Result; + pub fn force_fields_digest(&self) + -> Result, RecordingError>; +} + +pub fn carrier_publication_with_force_fields( + sim: &Simulation, fields: &ForceSnapshot, ordinal: usize, limit: usize, +) -> Result; + +pub fn material_publication_with_force_fields( + sim: &ParticleFluidSimulation, + report: Option, + controls: Option, + fields: &ForceSnapshot, ordinal: usize, limit: usize, +) -> Result; +``` + +`RecordedCarrier` and `RecordedMaterial` additionally expose +`pub force_fields: Option`. Absence denotes legacy metadata, not +proof that an arbitrary external producer applied no forces. RecordedMaterial's +standalone decode accepts a step without a run header; use RecordingReader or +explicit `validate_run_force_fields` for run association. RecordedCarrier::decode +has the run argument and checks it directly. + +## Versioned representation and historical hashes + +The SQLite envelope remains user_version=1. No existing run/step digest algorithm, +legacy solver identity, legacy numerical descriptor or legacy controls branch is +rewritten. Golden in-memory run/step fixtures verify the existing byte-level +hash contract. This does not claim that every historical archive has been tested. + +Opt-in run configuration appends exactly a newline followed by: + +``` +external-force-metadata-v1-sha256=<64 lowercase hexadecimal digits> +``` + +The SHA-256 binds the complete versioned force document encoding. It is a +producer declaration, not authentication. Rebinding an already marked run is +rejected. The existing run digest includes configuration, binding this marker +without reinterpreting hashes of old records. Desktop FrozenSpec/intent schema +and digest migration are separate remaining owner work. + +Every constructor/accepted publication for that bound run must carry the same +complete metadata field. Append checks association before starting a SQLite +transaction; selection checks it after existing publication digest verification. +Missing metadata, changed scenes or force fields attached to an unmarked run +are rejected. A worker cannot silently change a run's force intent between steps. + +The field descriptor is: + +``` +name: external_force_scene_v1 +units: 1 +association: immutable_run_input; world_m; activation_at_interval_start_s +stage: external_force_document_v1 +evaluation_time: None +scalar_type: u8 +components: 1 +dimensions: [byte_length,1,1] +``` + +Data has an 8-byte `RHEONFF\0` magic, u32 codec version, u32 ForceScene version, +and u32 object count, all little-endian. Each ordered object carries u64 ID, +u32 UTF-8 name byte count/name, enabled byte, 3 translation f64s, 4 quaternion +f64s, units tag, kind tag/payload, support tag/payload, start f64, end-present +byte and optional end f64. Floats preserve exact IEEE bits including signed zero. +Tags: units acceleration=0/density=1; kinds directional=0/radial=1/vortex=2; +support global=0/sphere=1/box=2. Kind payloads are vector3 or strength1; support +payloads are none, radius+edge, or half_extent3+edge. Boolean bytes are 0/1 only. + +The codec rejects input above 256 KiB before parsing; checks 256 objects and +256-byte names before allocation; rejects malformed UTF-8, tags, booleans, +truncation/trailing bytes and invalid authoring documents; then freezes decoded +data. It is independent of optional Serde. The existing overall per-record +limits still apply to all fields, descriptors and controls together. + +## Required host sequence (UI owner) + +Keep the existing numerical and recorder owners: + +1. Freeze the draft once and retain that snapshot with Request/history/run intent. +2. Build the existing carrier/material RecordingRun and call + `.with_force_fields(&fields)` before `StepRecorder::create`. +3. Capture the constructor with the appropriate new + `*_publication_with_force_fields` wrapper. This publishes the declared scene + before a numerical step exists. +4. Step through the corresponding authored-force API. Use the same frozen scene. +5. Existing `carrier_publication` now recognizes the separate force pressure + stamp. Existing `material_publication` recognizes actual force controls and + checks supplied dt/emitter/report against those inputs before recording the + actual snapshot. Alternatively use the new wrappers to also verify the host's + expected scene. Append through the existing StepRecorder. +6. Load through the existing RecordingReader. Use typed decoded force_fields for + historical glyphs. Rerun only from a properly versioned frozen run intent, + not from this inspection archive or current drafts. + +Old calls with old run metadata keep their old formatting/hash inputs. Empty +force workflows may keep using legacy calls. Do not bind a force run and then +omit its metadata, even for an empty document. + +No src/bin files, MaterialSession, SimulationSession, configured-provider context, +free-surface/column/boundary owners, desktop FrozenSpec schema or request +structures are changed here. Those paths must not silently claim applied fields. diff --git a/docs/plans/force-authoring/evidence/custody-source.sha256 b/docs/plans/force-authoring/evidence/custody-source.sha256 new file mode 100644 index 00000000..fda02f81 --- /dev/null +++ b/docs/plans/force-authoring/evidence/custody-source.sha256 @@ -0,0 +1,13 @@ +f925dae7a0d74e6024e6e0d64962a275ae4f28fedec5b016341671021b8239ae Cargo.toml +3b5836a7be9f1290c698cadaff010e11b93e33b4af839c8549391c23e7d8ec0c Cargo.lock +adecbc5f92b89179a2e38885be2acbcddd42df40061f00ffa5d758224ed045a5 src/force_field.rs +7b3b48c0813c995a937a44ca1b624411c59dde6419b62041b67456cde2d692d9 src/forces.rs +30f5b78a98f6d0c010955a25fc00cc2de4aadf159d260e95afd3f3143dc6f9c3 src/lib.rs +538c498122bbc1045ae59d7363ff87e1dd9a40003fb48eeae835228ad4c65893 src/particle_fluid.rs +c08ede6330c4bf6ed7e858e556806ec80628522ceb42286cf52714bf7430c9a1 src/simulation.rs +47b2b24bbe9af777ceafdefa6f0b82200a60cea590d0b64f326de1d50c6d7d2d src/recording.rs +5a9b10b929f650423ad9e58bd7047bfe80cc7bf18138d74bd628c2f3f17af711 src/recording/carrier.rs +e72e256aa3af46fef9002822de5281caf1de66ba070eb2109f77b083e20ae6ce src/recording/material.rs +2cf3ec9ec191c35c814d48ace301feaccd6cc73311a66970c9d112dad432c4a9 src/recording/force_fields.rs +1f97de31f8db1db38ff581e0ce702ffad7c16284fb29c39e8d61fc51ac9c645c tests/force_field_contract.rs +9efdf712baaa8e662e77339a81074751ed10ef01b9675f99351fac2a662b41fc tests/force_field_solver_contract.rs diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json new file mode 100644 index 00000000..9dbfff6e --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-pre-custody/force_adapter_tests", + "authored_mac_cancellation_retains_accepted_state_and_retry", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd", + "returncode": 0, + "wall_seconds": 0.010855268000000251, + "signals": [], + "log_bytes": 183, + "log_sha256": "afacaf787c62bce2c0b3d899cc8f6a8cc90603b6061a8d09ae43f3f6f62967ba", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log new file mode 100644 index 00000000..68ab1316 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_mac_cancellation_retains_accepted_state_and_retry ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.json new file mode 100644 index 00000000..63ff8dbc --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-pre-custody/force_adapter_tests", + "authored_mac_matches_legacy_bounded_directional_force", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd", + "returncode": 0, + "wall_seconds": 0.013335342000004857, + "signals": [], + "log_bytes": 178, + "log_sha256": "88f61a43755daae4b113baff6e001c8a7ed5c0fcc35238104adae17d5775ae05", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.log b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.log new file mode 100644 index 00000000..1924d5bb --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_mac_matches_legacy_bounded_directional_force.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_mac_matches_legacy_bounded_directional_force ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.json new file mode 100644 index 00000000..6e62ed47 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-pre-custody/force_adapter_tests", + "authored_particle_cancel_retains_state_and_retry", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd", + "returncode": 0, + "wall_seconds": 0.021448432000056528, + "signals": [], + "log_bytes": 173, + "log_sha256": "b4e51b96b649205def3c0fd7311c3c5bc89aaf6252812eb1b44f86ab9f873c7b", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.log b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.log new file mode 100644 index 00000000..61e2700f --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_cancel_retains_state_and_retry.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_particle_cancel_retains_state_and_retry ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s + diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json new file mode 100644 index 00000000..c0b969d4 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-pre-custody/force_adapter_tests", + "authored_particle_density_conversion_adds_to_existing_acceleration", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd", + "returncode": 0, + "wall_seconds": 0.01906560099996568, + "signals": [], + "log_bytes": 191, + "log_sha256": "233b50b8d9be352930ae7bb3cb8c3a38b27951468dbf5d2114d5f087ebd7d26e", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log new file mode 100644 index 00000000..1b58b19b --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_particle_density_conversion_adds_to_existing_acceleration ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s + diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/build-features.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/build-features.json new file mode 100644 index 00000000..9e8eff24 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/build-features.json @@ -0,0 +1 @@ +{"rustc":4758242423518056681,"features":"[\"force-serde\"]","declared_features":"[\"default\", \"desktop\", \"force-serde\", \"parallel\", \"png-export\", \"work-observation\"]","target":17260259951606174431,"profile":5612075431994834192,"path":1817014210143136823,"deps":[[390430846265316719,"gltf",false,11586693289549932636],[3646101781514403606,"rustix",false,6553390319389300857],[4878511589877685828,"rusqlite",false,7357609022033758554],[6557439603276904804,"serde",false,18150741541386485220],[8160210889872729633,"serde_json",false,8983802469690437438],[9015757307373990982,"cap_std",false,3442701369777828215],[9857275760291862238,"sha2",false,8098286442164053832],[13077212702700853852,"base64",false,13812028478587927365],[13536970318350685827,"rheon",false,9119528104381679303]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/rheon-9d566ed71bad85b4/dep-test-integration-test-force_field_solver_contract","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0} \ No newline at end of file diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.json new file mode 100644 index 00000000..d7a74527 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-pre-custody/force_adapter_tests", + "empty_snapshot_preserves_both_legacy_paths", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd", + "returncode": 0, + "wall_seconds": 0.010476964999952543, + "signals": [], + "log_bytes": 167, + "log_sha256": "31b9cdd2dd7435206b3c379717e23d5ad99b3c269bc53ea182b19ea0181be917", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.log b/docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.log new file mode 100644 index 00000000..68520794 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/empty_snapshot_preserves_both_legacy_paths.log @@ -0,0 +1,6 @@ + +running 1 test +test empty_snapshot_preserves_both_legacy_paths ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.json b/docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.json new file mode 100644 index 00000000..d6ea871f --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.json @@ -0,0 +1,22 @@ +{ + "command": [ + "/workspace/force-validation-pre-custody/force_adapter_tests", + "--list", + "--format", + "terse" + ], + "binary_sha256": "0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd", + "returncode": 0, + "wall_seconds": 0.007437605000063741, + "signals": [], + "log_bytes": 302, + "log_sha256": "95c1ecca9477fc2f362c688db8122e321d78f516dcbb7bf222f6af6a1da26964", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.log b/docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.log new file mode 100644 index 00000000..63c6c2f0 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/enumeration.log @@ -0,0 +1,5 @@ +authored_mac_cancellation_retains_accepted_state_and_retry: test +authored_mac_matches_legacy_bounded_directional_force: test +authored_particle_cancel_retains_state_and_retry: test +authored_particle_density_conversion_adds_to_existing_acceleration: test +empty_snapshot_preserves_both_legacy_paths: test diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/run_bounded.py b/docs/plans/force-authoring/evidence/pre-custody-adapters/run_bounded.py new file mode 100644 index 00000000..b2b7dd94 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/run_bounded.py @@ -0,0 +1,62 @@ +import hashlib, json, os, pathlib, resource, signal, subprocess, sys, time +root = pathlib.Path('/workspace/force-validation-pre-custody') +binary = root / 'force_adapter_tests' +expected_hash = '0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd' +names = [ + 'authored_mac_matches_legacy_bounded_directional_force', + 'authored_mac_cancellation_retains_accepted_state_and_retry', + 'authored_particle_density_conversion_adds_to_existing_acceleration', + 'authored_particle_cancel_retains_state_and_retry', + 'empty_snapshot_preserves_both_legacy_paths', +] +def bounded(): + resource.setrlimit(resource.RLIMIT_CPU, (20,20)) + resource.setrlimit(resource.RLIMIT_AS, (512*1024*1024,512*1024*1024)) + resource.setrlimit(resource.RLIMIT_FSIZE, (1024*1024,1024*1024)) + resource.setrlimit(resource.RLIMIT_CORE, (0,0)) + # Check effective child limits before exec; failure prevents execution. + for limit, expected in [(resource.RLIMIT_CPU,20),(resource.RLIMIT_AS,512*1024*1024),(resource.RLIMIT_FSIZE,1024*1024)]: + assert resource.getrlimit(limit) == (expected,expected) + +def run(label, args): + assert hashlib.sha256(binary.read_bytes()).hexdigest() == expected_hash + path = root / (label+'.log') + command = [str(binary), *args] + began = time.monotonic() + signals = [] + with path.open('xb') as log: + child = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, + cwd='/workspace/Rheon-force', env={**os.environ,'RUST_BACKTRACE':'0'}, + preexec_fn=bounded, start_new_session=True) + try: + code = child.wait(timeout=30) + except subprocess.TimeoutExpired: + child.send_signal(signal.SIGTERM); signals.append('TERM after 30 s') + try: code = child.wait(timeout=1) + except subprocess.TimeoutExpired: + child.kill(); signals.append('KILL owned child after 1 s grace'); code=child.wait() + data = path.read_bytes() + record = {'command':command,'binary_sha256':expected_hash,'returncode':code, + 'wall_seconds':time.monotonic()-began,'signals':signals,'log_bytes':len(data), + 'log_sha256':hashlib.sha256(data).hexdigest(), + 'limits':{'wall_seconds':30,'term_to_owned_child_kill_seconds':1,'cpu_seconds':20, + 'address_space_bytes':512*1024*1024,'file_and_log_bytes':1024*1024,'test_threads':1}} + (root/(label+'.json')).write_text(json.dumps(record,indent=2)+'\n') + print(json.dumps(record),flush=True) + if code != 0 or signals or len(data) >= 1024*1024: + raise SystemExit('STOP: failure, timeout or log bound; no retries') + return data.decode('utf-8') + +if sys.argv[1:] == ['enumerate']: + out = run('enumeration',['--list','--format','terse']) + actual = [line[:-6] for line in out.splitlines() if line.endswith(': test')] + assert len(actual)==5 and set(actual)==set(names), 'STOP: unexpected enumeration' + print(out) +elif sys.argv[1:] == ['execute']: + listed = (root/'enumeration.log').read_text() + assert {line[:-6] for line in listed.splitlines() if line.endswith(': test')} == set(names) + for name in names: + out = run(name,[name,'--exact','--ignored','--test-threads=1','--nocapture']) + if out.count('running 1 test') != 1 or f'test {name} ... ok' not in out or 'test result: ok. 1 passed; 0 failed; 0 ignored;' not in out: + raise SystemExit('STOP: unexpected test selection or outcome; no retries') +else: raise SystemExit('enumerate or execute required') diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/seal.sha256 b/docs/plans/force-authoring/evidence/pre-custody-adapters/seal.sha256 new file mode 100644 index 00000000..21eaecec --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/seal.sha256 @@ -0,0 +1,8 @@ +0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd /workspace/force-validation-pre-custody/force_adapter_tests +d932a6edb656d2bc06837ec6c07b76aef287a219e22c0b5110a833a49ac337e3 src/force_field.rs +7b3b48c0813c995a937a44ca1b624411c59dde6419b62041b67456cde2d692d9 src/forces.rs +4b3b1f85c354902edd83ec2bce61ad57a1667ca8291d0d7a1b286bb6199417bd src/simulation.rs +bbc9530dd7d367e9bdce0689c70f043d164aa503d5a4ebf647e15b7959c18571 src/particle_fluid.rs +9efdf712baaa8e662e77339a81074751ed10ef01b9675f99351fac2a662b41fc tests/force_field_solver_contract.rs +f925dae7a0d74e6024e6e0d64962a275ae4f28fedec5b016341671021b8239ae Cargo.toml +3b5836a7be9f1290c698cadaff010e11b93e33b4af839c8549391c23e7d8ec0c Cargo.lock diff --git a/docs/plans/force-authoring/evidence/pre-custody-adapters/source.sha256 b/docs/plans/force-authoring/evidence/pre-custody-adapters/source.sha256 new file mode 100644 index 00000000..5fdda523 --- /dev/null +++ b/docs/plans/force-authoring/evidence/pre-custody-adapters/source.sha256 @@ -0,0 +1,203 @@ +3b5836a7be9f1290c698cadaff010e11b93e33b4af839c8549391c23e7d8ec0c Cargo.lock +f925dae7a0d74e6024e6e0d64962a275ae4f28fedec5b016341671021b8239ae Cargo.toml +09ff66fa19e0cb8bbad00b6032e28f1cf77b0a37a73013648177b35297f98800 rust-toolchain.toml +0163a872c314b2b3bb7f732348aafdbfa7b162dceea74807b7e7e3eb60ab0849 src/advection.rs +1541bc444d1b098c5a59a221b7068538b013362db46ed66e37eb4931ad425e4f src/aligned_strain.rs +46c12612d5527989d1472711967bf798a88113a7cc662c0185b9749ac572f83d src/bin/desktop/README.md +7a42a42bbe8d044f703c2c52d488bf8a4e8e3b3fb1448851efcfb04599d5bf31 src/bin/desktop/accepted_session.rs +922c98958bec45dbd4dee199e0bbdbdd6e3fe4ac52d7a70472ce8132461416fc src/bin/desktop/ai_comparison/accepted_provider.rs +643636776b009337fb80dfdf3d5dbee975e06ed7f7a7cf3f534c07301b61c729 src/bin/desktop/ai_comparison/accepted_provider_contract_tests.rs +11ae34ba76dc93a87f15c8ce9f745403b2d22d0194987d945cf2edc53c624237 src/bin/desktop/ai_comparison/accepted_provider_tests.rs +6cfddefe84a64c89f126598b817d22afe6d1b5731e1d533ea92cecbd1870c942 src/bin/desktop/ai_comparison/accepted_recording_contract_tests.rs +7038fb09ef2bd33a17495414c4c0986a6fb63ba949fe440e9d19927baca712b2 src/bin/desktop/ai_comparison/adapter.rs +3959018b52697ef072884864c01de4682d1b4cd351c273426d914d02f0641fa1 src/bin/desktop/ai_comparison/configured_track.rs +a0dcc1c09d188ad14fc42e78535cd51bcb9bacae0522f4f95262340e4bd86b7c src/bin/desktop/ai_comparison/configured_track_tests.rs +6232225760a7646d2b6759f3df7bd2fee34bed00c6a3a1058e68a96fced4311e src/bin/desktop/ai_comparison/cpu_contract_tests.rs +80559d9d69d8164c24b8dd45f71c51b972fc8af9349c9e56b3e36b14b827b41c src/bin/desktop/ai_comparison/diagnostic_runs.rs +1e6b8011f945ba05ce04c8e4c7f88de23bd53fc99c776311fd30d3021a1c7fe2 src/bin/desktop/ai_comparison/diagnostic_view.rs +a75e78beff3c85dec528c54c15a29c6ce6db7c918279bda2af7b5af91104991b src/bin/desktop/ai_comparison/diagnostic_worker.rs +fd39822957fed70fac97bf5025cdb57028817c414175320b901119dac4fa50a8 src/bin/desktop/ai_comparison/diagnostic_worker_tests.rs +610ba29f66eb91083f9bbd26b823b14e4bde4887eb057e929c089d6cd4592e55 src/bin/desktop/ai_comparison/engine.rs +584b5fd3dc9307f6fbf758474cd3f02e6ffcefb00e90ca3f068b838c15c786ad src/bin/desktop/ai_comparison/engine_typed_tests.rs +83f8864382f0c0c8c454b54dc0e9c389654a325849c6802b7205a2b031040076 src/bin/desktop/ai_comparison/mod.rs +bbbe43744cad1791038e07895967f744b86bb77fa9642d0fc80e4b00bee2204d src/bin/desktop/ai_comparison/model_process.rs +088668d3b29f3650c2145f09199d7ff8cbf38e1de78b1c4d73c882e8fc1610b3 src/bin/desktop/ai_comparison/prediction_contract_tests.rs +2464e347414d3c75e7568dda6a5fd102896c5f194cf3ba8b3bb34961566fce0d src/bin/desktop/ai_comparison/record.rs +c8817ed933ff31c4e9cdd7c0eb2e4c35f50a6868568496e2c67444e11ad80321 src/bin/desktop/ai_comparison/scene.rs +568f7b852a21714405e37d55ba00a56b3f4b07e58037ed26f8a67cfd4fbd268d src/bin/desktop/ai_comparison/view.rs +6fb6c5b49cf57a473f9911d89965e8812c5d9915ecc1d3b654ca2f7ecfa8cc6b src/bin/desktop/ai_comparison/worker.rs +9f96f1d6b11b46e7fd523825e19a8fc0e0ec862669acd129f5e39e91f83c89ff src/bin/desktop/archive_view.rs +a94fea1f039a49f8d0eb79b019b3fbd0f2c9da1762ba854d10cba1e5fd87a25b src/bin/desktop/build_identity.rs +b6bcc84a45bd5979de357c965c84426c25e81161ee1f06022e57a93a46643f7d src/bin/desktop/camera.rs +0d96fb2acdfd63374e5f0004d9ead8b43d9fe2f6959c0d01f9c828458ae1799e src/bin/desktop/carrier_execution.rs +78d7be8e49798e2420ebac5c3fb756e1b195e43ef113ae03ce1b2d3362009204 src/bin/desktop/collision_ui.rs +ef52e29f4d9c59007c11ff973ed6a43c33ac9e1d583819ede11ebd7aea9ec0a5 src/bin/desktop/comparison.rs +813cc86187ef7313deacbe9d9cd15340f37ecdf62f5234a45f033909415459b9 src/bin/desktop/comparison_archive.rs +ec8ea0799522d5a7fadac47b06b42f26717f935bbb9a1677fc5052f096c1cc82 src/bin/desktop/comparison_field_view.rs +e6716e8c707f30cc20304b16e3201f8fb1fd838650bd08a43a95ab4d68cbbf46 src/bin/desktop/configured_comparison_tests.rs +05d317554c4db033dd35c5e153dceaea34367eed33125ec8c7f59466f83ede36 src/bin/desktop/configured_controls.rs +7560ff5395acbd50ce111b1e50ae5e8aca1b1bab5356ec195929eec0994f24bb src/bin/desktop/configured_producer_exit_tests.rs +81e24d69cd5838de3becbc31453ce0a10947e7fe956287862957fcc06eae6e38 src/bin/desktop/configured_worker_tests.rs +d5eaf6358b037d1f11177abbbd83bfa8aa18c263b195c3911b8f183b3334fba9 src/bin/desktop/emitter_controls.rs +9fc4eaafd1118abda50b70d1cc71c843fc1ca71d420d2cd47f16a20fcc9e8e9d src/bin/desktop/engineering.rs +1702f437b17dd42d2bf1311ca14d0301a53712392abc6591ab8962b48cba511d src/bin/desktop/export_ui.rs +e4be154bbbf40a36f370b68ec9e15fea663fb589d476a43eb298c0e8c0f0d821 src/bin/desktop/history.rs +df451a986476b9df252a9a4dbc1e33218bf5ab3cb268b83fa78b47dfffdae184 src/bin/desktop/identity.rs +bb6bb83529896323fbf5414d6e7f41f8462b21e80a6d1f620edb6eecd2fc6649 src/bin/desktop/inspector.rs +75208acd210d4558a34127b98cf77f2c898a560f991f6bed3bee3a4eb160b9a2 src/bin/desktop/particle_view.rs +ab2c841612f39b16f5bd1a363593eae5388bf4a81ec495dfb89e43bd7436580b src/bin/desktop/run_set_view.rs +e194a48350954edf13cdcd01371d320cc210069b4256ba955f2ddd1132301cc1 src/bin/desktop/run_sets.rs +45fa8cf6976da7f8ab3eff27fd69ba5511c98c4be541739eefcdf762826abac9 src/bin/desktop/run_sets/configured.rs +eb1d8e77019eb57afd9e5fd5f57af55f8ff320527ec8efc935945bd7ff477806 src/bin/desktop/run_sets/configured/archive.rs +c948c24fbe3d0bbb4d281c427244f7908a16506236c0f24b194490c64033cd11 src/bin/desktop/run_sets/source.rs +d66735427fc8b6be57d93812d30fb4b8c357419acd70b7b0564aa48216bb2660 src/bin/desktop/settings.rs +e8c3d5cc588d4efac9918544fc5012696a05bdfa7adcba7ce18b07c5a260f6b8 src/bin/desktop/transport_view.rs +599046403df150499064337c2ce3b6996f1ece69f4e876f882200f079b7cd530 src/bin/desktop/viewport.rs +00000ec93161c04d9ff0515e5ae2ab6855a5467f56f23a4d6fc6c1059ef8ada9 src/bin/rheon-ai-compare.rs +76a92d5c928919f832904395319644f14eb36bc2e1c33aeab1146d4b1480ce48 src/bin/rheon-desktop.rs +5116a121256c8bb57d086caedbda8151f3593cb99d3e84b1d04fea25b12dfb51 src/bin/rheon.rs +554b3cdc916e7527bbfdbd6a54b8cf7e27549c3c661015b259c8b2d18784bbb0 src/box_flux.rs +e2ef3afcd8adffe0f79c0030eb56e5bb55943775e474a1dab2a7fe78ac38441b src/box_flux_step.rs +101c91ced5efc250f3a12bc47d5bbf04b812096caf8580c0229362e959839115 src/collision.rs +40b86c5bab2d458c8b31b1f8def21c5f715acecb37813fb57417f7fe363aca84 src/column_mac.rs +373f63ac001fe8e9f8017a912033b3cc431169c6e4c86c2e6b514ee945f1f9b0 src/column_momentum.rs +bd2a20a11f927c2a886b524a4a45791100f9c3e8119b7648153b7775cb6a3299 src/column_shear.rs +cd58d4d7d1c3d94092374ebd65fd191e3cf58f7ae68caeaf30ca15d36df666e0 src/column_surface.rs +dc0f2a3a8ebcb108c6a094ec59a467f1390b846dd27c505b16248a64dc1031b3 src/compressible.rs +b296021b8dd2f763b6eeab1e49e78bf06ec2ec801ac9595441e9e3920235d8e7 src/coupled_discrete.rs +90e9249fd82cc829a84cea9162d680971e660bfc6c32619355b353d17b25a431 src/dense3d_sequence.rs +57ea2badaeb329fe91647df98be60bff8dbe693e41076979eda4b79ad8f48f84 src/emitter.rs +47d0b180dafc7e57ad1b205f2a12c5a472a78c1d60a7c3f7521ec470211a89c4 src/emitter_controls.rs +afe2b79d11f1b8c0ccd16dcaae51a22c8d35ba2e7a23c362729fe907b208f0d3 src/export.rs +0181db7ffbad932b7e3492ce1a8e260736389e4ded3472c81fe2697861f6777a src/fitted_height.rs +3cc724d376d115fccd8b6c658d7f26b6436f4d13d105a5ae8416fd9c04b17a47 src/fixed_bottom_ale.rs +d932a6edb656d2bc06837ec6c07b76aef287a219e22c0b5110a833a49ac337e3 src/force_field.rs +7b3b48c0813c995a937a44ca1b624411c59dde6419b62041b67456cde2d692d9 src/forces.rs +f43fd9a71da6aac985dfcfb6cc471318227c1727d6e0dad8e2a5feb5b3728a8e src/free_surface.rs +712d04f27d55d7e77f17effefbf2660ca3b8a000de5b7aa51b42b89fe5d3b634 src/geometry.rs +be66f9c1feef8c689c9d0a04037a4c0aa1c76a681050f2107c8facda3869d17a src/lib.rs +f99dce6a99f6af8a9aef2436af7f33de58dac3be5998a48e9187fbdf2acc3f93 src/liquid_step.rs +24623038f62261ce6a17b034e9b6d82c5f0aba02dd0df7aad174c885ed2a8c96 src/liquid_volume.rs +49874ff972f16a972d706236b34e62f83e994c57e39b642c6d94a1a6f4b7fb66 src/material.rs +4e18e446ad82f10523f72bae1b7cc6007f2ecf5ad05407bb666be3e2ae9be960 src/material_session.rs +71503d2c5939dbb59946bc157eedafecdedd0831f1e81d29e794fba7290dfd24 src/mesh_collision.rs +3e8f2d79e59b3f27c7ffeeacc83d9b7d08faafa2fffa7de81dfd8665b36a6890 src/mesh_import.rs +8ab9f26199f36d76cb2f5e4cb58d40905205f0d6fc90988549f7e389c489a3ea src/motion.rs +2c6d4cd75cdb0f2d560d0f79df4e53914e10f340de0740b939311d8cc35d5dee src/observation.rs +b45262632483ce08773924b407eeffbe99bd62663094ce6fd71b24d132026b07 src/obstacle_pressure.rs +745da07d5e949276a2e0cb7ac4bdde4aedf557bf6189d5a51581d8ce79b1dc1f src/obstacle_shear.rs +cd98684d5204908e5344b2628a8f93ea2a55a90c6678e03a71f2d75ab1f2be38 src/operator.rs +ee5420673a59ae4fb247b3542878e74902f7a87056744d4303fb400be074677e src/parallel.rs +bbc9530dd7d367e9bdce0689c70f043d164aa503d5a4ebf647e15b7959c18571 src/particle_fluid.rs +39ddb79dd3de196e83545679e556c0cd8ce9afae9db9282161f54b8f22f713a5 src/pressure.rs +ae687a8822dd6cfeadf5dc6e13e7983d8d1afcdf9e5db1857d4dce64cd0e3fb8 src/pressure_session.rs +79167b70a8d164a2367f43816feab443eefb41f23bc4bd940716dd5fd32d495b src/provider_step.rs +d022f8f46c288538298896ae444c9780265a6bf77fa9a4e1ba4b9732cf026eb2 src/recording.rs +b9c83015df214b426315f1840cbd2029d3f4b85525aef2fc8592e97468d54509 src/recording/carrier.rs +5a82201f4583cf9eb4cc38d43c7ee88bc54d10a84df46272c2dd84091aa4a307 src/recording/comparison.rs +279aff886f1354af203429a2e43cce73cdb485ee5ddd90201d1498e5e7872b7f src/recording/diagnostic.rs +e01a730b04fd34d7fdc797ede735cb45fa743101dd51e8fd406b02bacdd43d00 src/recording/material.rs +980ea5af7094016b13e7d0f39110aa2ff673b0173416e0fe583e6266b7c27f57 src/recording/provider.rs +90c1230c2cbfb2415c74e5d3c9b66e74f68c7b2096bf4063c5513ac2c6feaa75 src/recording/provider/comparison.rs +d7b2a1f7cfa866b33887f7c8f804999058d6e70a1c9d71421372d151152ebccb src/recording/provider/runtime.rs +6bf158f71f795078b5a88a5662ec2169693508e555f0f98d539657873aaabbcf src/sampling.rs +21fa031c06eeff8c2f722beb5553af9ba86d0afab479f4036916480338dc1411 src/scheduling.rs +367c370e2257e35de09943e8dc3e1257b994c5e01d1409af1a989682f41c583e src/session.rs +d99ae990e9c7ddc956f95f414a25832411a4dc84ab434df95ff9f8f68cf52a83 src/session/configured.rs +0b4140af63144df8ff1839782b3c70295c18b96decc67aba348a37a33e9f996d src/sgs.rs +4b3b1f85c354902edd83ec2bce61ad57a1667ca8291d0d7a1b286bb6199417bd src/simulation.rs +0ab7db08a9016e73d664c730e4ff513d42599a84c680ea061e6604859432a5a5 src/solver_provider.rs +124ca65dd1dec3a8a89043198fb085453a42ca8aff0dc87c67b04b6a986109be src/solver_provider/configured.rs +763debfefc6f9375849c8f503995e20660fc25efef9e827d5942d980e14fe693 src/solver_provider/context.rs +c597450ad73a4dede64a1fedcc65a0b4d5551f594f29b2d0cf7c85b0e2e14796 src/solver_provider/modal.rs +df50f3761fd7258ca42026ba852d2c326a1a5efa42daa4021967b799493b1080 src/static_obstacle.rs +1007f35400b5e1b3206653b9f9ccaafbc169617ad896aec54f94fc792eba2fae src/tracer_barrier.rs +6fa240053738411bb93b3e365def39fdc8a93597cd0c0523dcd1af39cbc68163 src/translated_viscous.rs +3cb7643d321fa94957fafdc101abf0ee9ef273f99c9a512938124e72f32178aa src/viscosity.rs +56c3f03d957df0a7fd0c00a1af229f47f1c21cdf53cbf3770656a9bb0b66735e src/vtk_export.rs +2e6c78e51d457bfe0cda4972dbfec5677e9e3a5e4f42a8647aec8aa1f4b73f2d tests/advection_contract.rs +7b43a757245edc097874bfd23ec0690d1d4a8df9eb6186ab86e843720dc7a687 tests/aligned_stokes_contract.rs +d2270f86bd04c36501b59f01772656bda264d2c70b8613429701a268a305da16 tests/aligned_strain_contract.rs +d3a23e9ae3973044183d22c5dbc9b2a00c2d0d731cae1777426337310949133d tests/box_flux_contract.rs +4adc5ddf0e878cffc3e70b47bcb0f98cad07aef4489a41f9fa7d1695304bd94f tests/box_flux_step_contract.rs +83d3c9a839dd2c722b3c509e051350a6ca1c754cbe0ae5dbb521e7d3346c6aef tests/cli_contract.rs +8625b6a0d01271b90f17fd22b0ea4f5d0869827fad6537720fb631ce565367ac tests/collision_contract.rs +3fc23b6ff935696bb96082e824cc1a5c2cfe32821559074f6ec00b3b8000afdf tests/column_mac_contract.rs +4ea859692dc437e98bdce6fc3fdfb55a8821ffc52699c8a7596ddc7d75e8aec7 tests/column_momentum_contract.rs +466dab4d7dd6cfa464de3428da8b0b9aadfb3a2699810ca302e9bec90db62f12 tests/column_shear_contract.rs +515f39b3bf06fe23c1115201cefa1d4f1d69660ce4298daf3d4b7e16f5844759 tests/column_surface_contract.rs +ddb88a1faba015e1f636d66d6dbc8a770a1875ad52ada6d6a0a773b5c0e4f0e2 tests/comparison_archive_contract.rs +2bfcf188fbde8ba9908c90365eb10a3e708853d0a928267541a5cfb5358b22f2 tests/comparison_field_view_contract.rs +9f1582b436e268a7bef6d42dccc3ff0754b963942301eb26442d7f33790d5b7d tests/compressible_contract.rs +821ddd7d173c84453650d2cde938bf024d907e2f062a894ea2b182e44ac56932 tests/configured_archive_binding_contract.rs +ba4a8f26fbf0324d705a80a4b108b1e9d09a293ac2eb29a8278fe01f65dc5a00 tests/configured_provider_session_contract.rs +8205a1b5ddb1248d545edc27d4b23f27fb89717f311ec336d01bb3202591f2fa tests/configured_recording_comparison_contract.rs +29b29888112126677262bdd05792cad70170338eaac8c4767fc2ab0292a9e927 tests/configured_run_sets_contract.rs +3a730e8dbeaf8fd81a19eee5717b4719f62af83e42c395691535a1ed6f1a0116 tests/coupled_discrete_contract.rs +678058ce656cde9d0b8c5f3ac9de4da5a81ca3f76d5003c1954ff3929d964625 tests/dense3d_controls_contract.rs +016ba99a4a234ced02a1613d5cc4af6072f3e070f185bf8ad25ca73202b128a7 tests/dense3d_sequence_contract.rs +b492194a384779fd15ba967bac93a5068d1635d5ff5b443d13a31252716134d7 tests/diagnostic_pressure_oracle_contract.rs +3cd9e0a01916ed416cffd93f96aa463447beed404f202e3e2f5783c76122b758 tests/diagnostic_recording_contract.rs +9288b53744ff31fb4e338be13d8b4e9e68ea58aec2e78a15d5aa8850da0a9d4d tests/emitter_contract.rs +7b3ad890d44d70652c04e119156dd6ecc32cb79587c87e49b1fd2fc72f6caa66 tests/emitter_controls_contract.rs +299ace3dc49a9990b52cd2ee55c974769c3acda5fc9ce43331fdb0cbfd34c070 tests/export_contract.rs +19968c75c9dd95c1c19cc2a4f6a605d4a047bc20290562fe693fc7e8b628fbe2 tests/extruded_third_contract.rs +391e6d3c992442a5a3a2e38f4d1304a20602047808831bd873567b1d8224a61f tests/fitted_height_contract.rs +006968122943cd48290f302cdc33115d7c40058d992f8f38df1d5fbf28f57289 tests/fixed_bottom_ale_contract.rs +958ba74ebde596aac8375eb436898a1e2342ba1b7868c08bfee99c09c02dc27c tests/fixtures/accepted_pressure_adapter.py +1a27b9a7754d7e04ef3a05d1df57f9cff9d708f24b6ca2a913f761ed55e2c317 tests/fixtures/collision/README.md +19ee1217f457696346ea61ca36ab09c26256d732cf9b02c962a81878cabfca4e tests/fixtures/collision/cube.bin +d7aaad641dffa913e68fddeb404e7cc06714f7ff546a902cfb72ad0fa404d4da tests/fixtures/collision/cube.glb +12a32152f8323a8bdea42b7c25ada7ff49737d2328aafda671497029f3aa0521 tests/fixtures/collision/cube.obj +a0717a2b27629734e7946773671e34a1f8b7d572949a7c69a21d1e239c56a38e tests/fixtures/collision/domain-box.obj +4e050abe9a4a6ed810aa29c2d73051ac3cef413aa036ce33f711f87a6e6ce278 tests/fixtures/collision/embedded.gltf +a37f48c741dc15ea24ad75796984661531955400bac7417ef43a0f4b1db0c116 tests/fixtures/collision/hierarchy.gltf +01ad2acb063b21c29cc379191c1048f5a365d707de428693cec7ae30c744e98f tests/fixtures/configured_box_pressure_adapter.py +9ec95cd43f44f8f8f2f908057b135e16d0a92d790c60dca934946d9ab8cba889 tests/fixtures/pressure_adapter_correction.py +e3dceb0db85270f79a835149463892a8eee19442815dbc93abf3434132410047 tests/fixtures/pressure_adapter_full.py +1f97de31f8db1db38ff581e0ce702ffad7c16284fb29c39e8d61fc51ac9c645c tests/force_field_contract.rs +9efdf712baaa8e662e77339a81074751ed10ef01b9675f99351fac2a662b41fc tests/force_field_solver_contract.rs +b8a5da48feab2eef82cc1600945cc30ff15082c0b447d8b5eb0c635821cf27eb tests/forces_contract.rs +8f9584c26f7a816bb53f15bb70ddec168c0949adf9ad7847ad2edcfb0af40e9d tests/free_surface_contract.rs +87b34154f609767f1521a57531f6f45df4d965685113dcb43806069299865f4a tests/geometry_contract.rs +e8b3b88536bd5d38dfda7d1454e916277e5314bbb9410bda81630faffe76c621 tests/implementation_contract.rs +dede02f6a5fc9eff67d87814b1845bfc6295c7a22098121217fee70822a4c3fb tests/liquid_scale_division_contract.rs +e1aa67f673de131f97a1be651221db3f74a8beeed105076532ceeac2b92b5b12 tests/liquid_step_contract.rs +9eac2c3feedf24efd3e34f1651ead5ea14f036dbfb238af829650d5b77006ec2 tests/liquid_volume_contract.rs +c5a1e89205b6cb65df69a1c10fa905f85d5efa6bc11c591211d509cb40161257 tests/material_contract.rs +9db6a8cc0558b4b04b4039ab8b7c74bd57c2f69a15d05b2b553713713a8f387b tests/material_reference_contract.rs +9fadbca17897a93e9dc95be24a42c5181bfeaaf35aad650ea4bf6f477b9e580f tests/material_session_contract.rs +e8977a66763745cfb807e9bcae392be3aa39ebca113aa3db4c4d4eb39447c8ca tests/mesh_collision_contract.rs +a568e510cb457f8d90bffd2c99eb1c05d64ce5ac4c34949a58479831ffad3da9 tests/mesh_import_contract.rs +290def29e35b9597d1c56757f5fb70d9ac100d95ca67b616c54533d31f8e0f82 tests/native_comparison_oracle_range_contract.rs +54a89d2df244839dec1931cde92117cf1bb832ffb0b6eb2ca32ca9ec28a8f7ed tests/observation_contract.rs +7e5318ca45b6932818accfc748cc70ae6cf1719f6047c2d2f9343eb8cedcd020 tests/obstacle_flow_contract.rs +b0ea207c02df69aed1f5f3eb6abda8fdeb245b271c325b383c3748d945c5095e tests/operator_contract.rs +b91c7c87698c1a7b1d3b92c7b252781d96616df37c8a6ef391d4229a251668f2 tests/parallel_contract.rs +9e36d134d2391786457ff23bb20df63a5aadf29a8ae508cffec2de824528bc93 tests/particle_inspection_contract.rs +3c413b5c903194ba8f1a83970275d066ba93a5568f0d43a27f93788f3c4af5fd tests/plate_coupling_contract.rs +244afca41c35a929588f5dd0168ae682b1ae0457f0edc3f821b95ed93d7f2f36 tests/pressure_context_contract.rs +f343f7f23965d963df8d734d624c63b12acaf165936ba2635e1b71816ee16c91 tests/pressure_contract.rs +a2471a0faa91bac0acd4c824ee4b1175fe6f827357246218684ae47104aea0ce tests/pressure_session_contract.rs +116c03830ed617e585a6eabe1dd9d8e826ed833039e4397de565d218ec9852e5 tests/provider_step_contract.rs +8a3a5cd307b9b5e2b646165502029018eeb9e36268986b679094a9a1e43dfba0 tests/recording_carrier_contract.rs +4c27fc27b241c033eeae0903dd143fe1800cc8ea5ab9829c8b13e22105ee133b tests/recording_comparison_contract.rs +3db4e56207de52576aae5838d30e02894590ba843d1af8d10e5e7fd5a772ee8a tests/recording_contract.rs +0766d5d9ac55fd8102c23b9492e11d41e4caa931249531a8b36824b2a97039c0 tests/recording_provider_contract.rs +7f02cf7510be8bd9bca9b3af380e80401ead89ee9b5c2e2561de65e31c5f6cd1 tests/recording_provider_runtime_contract.rs +3e99ff52088aa37d4043453a57c82bbe6de0da84e990796df76a3453f1ae004d tests/sampling_contract.rs +f3bf401a00ccb9ba67de70ff72c6c3a0b7216525073a6f6c8239ec022e0bec1f tests/saved_comparison_ui_contract.rs +6e001075a4307da0e51df04c4521bc8599194ce06fbf010c08e34bab8f693ef9 tests/saved_run_set_contract.rs +39cb5152583c1662f8dfb71217409025084c4e05be1a18e1b7b3912fa4d21ca5 tests/saved_source_contract.rs +4f53c8d35eeb44819791341b8ce1a80421d9562ab8413f528a3234da233632f7 tests/session_configuration_contract.rs +50f7a8981d1bc22ad63f990b01027b0ae26c4dc01192dc5dd7d9723f7e44da68 tests/session_contract.rs +4cdc0a85d21d77784ef142ba04f1ef20fb0333555d7bbb15b43465f475934769 tests/session_interval_start_contract.rs +f81fb0696307aa07abba0d8b44316430aeab19317c57d38db3222cdc9e33ddc6 tests/simulation_contract.rs +0c063df217150bf407af3f6af9d39101bc37aa3192e00e0833b43042ebc06143 tests/solver_provider_contract.rs +fcc7546886b865ada1641dd7ee3549534f2273d908c19047d886c49a048157de tests/static_obstacle_contract.rs +20cb58baf441b56e7b5f72b9424de265c7bf656bb2c3532d4718220b8c291ce2 tests/submarine_stokes_contract.rs +433edf41b549171f3a3c412e6d9233345f4ca8f45e6f5ebe5468638d3db9aca8 tests/tracer_barrier_contract.rs +d6fda116af1ae38a3e9216b97136562c144bfe1a5269734d002c3117c9b65e79 tests/translated_viscous_contract.rs +e1fd504cb399b313bbb1df752f79e72cb51556c5af6d85dbb91cb5c690fb5c8d tests/translation_contract.rs +b716ef20283784d9a61bc95702c97c30c8e7d00c80f95530c46d27e667586c9d tests/viscosity_contract.rs +3df0cd4bfe900adb855fa65d9ec267e43544961fd5608d4f1ab24258c6a44532 tests/vtk_export_contract.rs diff --git a/docs/plans/force-authoring/evidence/rheon-custody-pure.log b/docs/plans/force-authoring/evidence/rheon-custody-pure.log new file mode 100644 index 00000000..35c7daf4 --- /dev/null +++ b/docs/plans/force-authoring/evidence/rheon-custody-pure.log @@ -0,0 +1,42 @@ + +running 1 test +test recording::force_fields::tests::binary_rejects_truncation_trailing_unknown_versions_and_invalid_objects ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + + +running 1 test +test recording::force_fields::tests::binary_roundtrip_preserves_all_variants_ids_disabled_and_float_bits ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + + +running 1 test +test recording::force_fields::tests::field_schema_limits_and_accepted_custody_fail_closed ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + + +running 1 test +test recording::force_fields::tests::legacy_run_hash_and_absent_metadata_are_unchanged ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + + +running 1 test +test recording::force_fields::tests::malformed_run_digest_never_panics_and_is_not_rebound ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + + +running 1 test +test recording::force_fields::tests::maximum_valid_scene_fits_bounded_codec ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + + +running 1 test +test recording::force_fields::tests::run_binding_rejects_absent_mismatched_and_unbound_metadata ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 30 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/rheon-custody-pure.sh b/docs/plans/force-authoring/evidence/rheon-custody-pure.sh new file mode 100644 index 00000000..5f978717 --- /dev/null +++ b/docs/plans/force-authoring/evidence/rheon-custody-pure.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail +for custody_test in \ + recording::force_fields::tests::binary_rejects_truncation_trailing_unknown_versions_and_invalid_objects \ + recording::force_fields::tests::binary_roundtrip_preserves_all_variants_ids_disabled_and_float_bits \ + recording::force_fields::tests::field_schema_limits_and_accepted_custody_fail_closed \ + recording::force_fields::tests::legacy_run_hash_and_absent_metadata_are_unchanged \ + recording::force_fields::tests::malformed_run_digest_never_panics_and_is_not_rebound \ + recording::force_fields::tests::maximum_valid_scene_fits_bounded_codec \ + recording::force_fields::tests::run_binding_rejects_absent_mismatched_and_unbound_metadata +do + /workspace/.rheon-tools/target/debug/deps/rheon-84cc08b87dcd124f "$custody_test" --exact --test-threads=1 +done diff --git a/docs/plans/force-authoring/validation.md b/docs/plans/force-authoring/validation.md index 64e4bfd3..b5abfc75 100644 --- a/docs/plans/force-authoring/validation.md +++ b/docs/plans/force-authoring/validation.md @@ -1,6 +1,8 @@ # Force authoring validation and review record 2026-10-20; Rust 1.92.0; isolated branch based on main fd7c0f0. +The initial milestone record below is retained; the later authorized execution +and custody results at the end supersede its originally unexecuted status. This record reports code-level checks, not scientific validation or performance. ## Executed pure checks @@ -75,7 +77,7 @@ the correction and reported no remaining blocking correctness findings. The reviewer also noted that hosts should cap JSON byte input before parsing; freeze bounds validated data but does not bound deserializer allocations. -## Prepared integration tests: not executed +## Prepared integration tests: initially not executed Every test in tests/force_field_solver_contract.rs is individually ignored with a review/execution-authorization reason. They were compiled/listed only: @@ -121,3 +123,114 @@ SimulationConfig Default assumption in its helper. The helper now supplies the existing configuration fields explicitly; subsequent compile/list and Clippy passed. This did not execute a solver. Desktop/parallel builds and full suites were not run as part of this bounded backend task. + + +## Later authorized bounded adapter execution + +Parent explicitly authorized one execution of each of the five enumerated tests, +serially, from the already compiled binary under supported process limits. Source +and helper inspection reconfirmed the fixture/retry/iteration bounds before any +execution. The pre-custody source was d7bffdd5d6e80e022f15c59d3574739e3b39b352 +(code identical to 19e0145; d7bffdd adds documentation only). + +The existing binary was copied without rebuilding, hashed and enumerated. Cargo's +saved fingerprint reports only feature force-serde, with no parallel feature. +Binary SHA-256: +`0042e68eb0de3f63e3d4ef98506a88f417a4f965f63c41b64890f4ee9e9003cd`. +Full tracked source/Cargo/toolchain manifest SHA-256: +`29179208bf0def784bf22404d0cef6f8333e2481f5727a424cd372ba24d530d8`. +Test source SHA-256: +`9efdf712baaa8e662e77339a81074751ed10ef01b9675f99351fac2a662b41fc`. + +Actual harness commands: + +```sh +python3 /workspace/force-validation-pre-custody/run_bounded.py enumerate +python3 /workspace/force-validation-pre-custody/run_bounded.py execute +``` + +Enumeration executed `force_adapter_tests --list --format terse` and required +exactly the five expected names. Each actual execution command was the sealed +binary path `/workspace/force-validation-pre-custody/force_adapter_tests`, one +exact test name listed above, followed by: + +``` +--exact --ignored --test-threads=1 --nocapture +``` + +Each child checked/applied hard+soft CPU=20 s, address space=536870912 bytes, +file size=1048576 bytes and disabled core files before exec. Parent waited at most +30 s, with TERM then owned-child KILL after a 1 s grace on timeout. Stdout/stderr +went directly to a unique regular log subject to the child's file-size limit. +The harness rechecked the binary hash before each process, required exactly one +selected passing test and stopped on failure/timeout/signals/selection mismatch +or a full log. No tuning/retry occurred. No cgroups, mounts, security configuration, +models or adapter subprocesses were provisioned. + +All five returned code 0, selected exactly one test and passed once. No timeout, +resource-limit signal, unexpected selection or bound failure was observed. Log +sizes in the planned execution order were 178, 183, 191, 173 and 167 bytes. These +are observed harness outcomes, not a performance or scientific validation claim. + +Full source manifests, fixture seals, exact command/result JSON, raw logs, +feature fingerprint and the executed harness source are preserved under +`evidence/pre-custody-adapters/`. The large compiled binary itself is retained +locally under /workspace/force-validation-pre-custody, not committed. + +These results apply to the pre-custody code only. No solver test was rerun after +accepted custody/recording changes, honoring the one-execution authorization. +No post-custody SQLite/native/desktop workload or full parity test was executed. + +## Custody codec and source checks + +Seven new tests in `recording::force_fields::tests` were source/helper inspected, +compiled and enumerated with: + +```sh +cargo test --locked --offline --no-default-features --lib -- --list +``` + +Each then ran separately from the compiled library test binary +`/workspace/.rheon-tools/target/debug/deps/rheon-84cc08b87dcd124f`, with exactly +one name below and `--exact --test-threads=1`. All seven passed: + +- recording::force_fields::tests::binary_rejects_truncation_trailing_unknown_versions_and_invalid_objects +- recording::force_fields::tests::binary_roundtrip_preserves_all_variants_ids_disabled_and_float_bits +- recording::force_fields::tests::field_schema_limits_and_accepted_custody_fail_closed +- recording::force_fields::tests::legacy_run_hash_and_absent_metadata_are_unchanged +- recording::force_fields::tests::malformed_run_digest_never_panics_and_is_not_rebound +- recording::force_fields::tests::maximum_valid_scene_fits_bounded_codec +- recording::force_fields::tests::run_binding_rejects_absent_mismatched_and_unbound_metadata + +All helpers use in-memory documents/fields/hash fixtures only, with no solver, +SQLite, filesystem fixture, subprocess or native UI. A separate review agent +inspected these paths without executing them. It confirmed the writer/reader +association checks, success-only accepted controls, MAC stamp invalidation and +unchanged legacy hash/serialization branches. It identified an avoidable +0 +addition for empty particle fields; that evaluation is now elided while accepted +metadata retains the empty document. No remaining blocking findings were reported. + +Pure fixture golden hashes independently calculated from the historical byte +contract are run `781b179b9818a048d50183f33244e73968dc7ec02e35a258f5bba3463886986b` +and step `579f8cc614789a49c128f54c90ed449e6bd09a35fda8b7c6958b449ceef39b76`. +This is fixture evidence, not exhaustive historical archive validation. + +Post-custody static checks passed: + +```sh +cargo check --locked --offline --no-default-features --lib +cargo check --locked --offline --no-default-features --features force-serde --lib +cargo clippy --locked --offline --no-default-features --lib -- -D warnings +cargo fmt --all -- --check +git diff --check +``` + +The exact local pure invocation script/output are /tmp/rheon-custody-pure.sh and +/tmp/rheon-custody-pure.log. Remaining runtime/intent/archive UI work is specified +in custody.md. Desktop, request definitions and session/provider owners are +untouched; no publish/merge/deployment occurred. + +Post-custody source/Cargo/test seal is evidence/custody-source.sha256; its SHA-256 +is `d873dd98d5d59d1f1aac2221cb5f1f30ae574bd6dff91c95d95d82a0c927a9ca`. +It deliberately differs from the pre-custody runtime test seal; the runtime +results must not be attributed to this newer source. diff --git a/src/force_field.rs b/src/force_field.rs index e6804a44..7821e478 100644 --- a/src/force_field.rs +++ b/src/force_field.rs @@ -1,7 +1,7 @@ //! Solver-independent, static external fields in SI coordinates. //! Authoring data is mutable; only a validated, owned snapshot can be evaluated. use crate::ForceUnits; -use std::fmt; +use std::{fmt, sync::Arc}; pub const FORCE_SCENE_VERSION: u32 = 1; pub const MAX_FORCE_OBJECTS: usize = 256; @@ -169,7 +169,7 @@ impl std::error::Error for ForceFieldError {} /// The host retains this with run metadata, separate from later draft edits. #[derive(Debug, Clone)] pub struct ForceSnapshot { - scene: ForceScene, + scene: Arc, } impl ForceScene { pub fn freeze(&self) -> Result { @@ -186,7 +186,7 @@ impl ForceScene { object.validate()?; } Ok(ForceSnapshot { - scene: self.clone(), + scene: Arc::new(self.clone()), }) } } diff --git a/src/lib.rs b/src/lib.rs index 6e7e2a74..7aac4f09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,8 +56,8 @@ pub use tracer_barrier::{ }; mod simulation; pub use simulation::{ - AcceptedIntervalPressure, CarrierStepControls, Simulation, SimulationConfig, SimulationError, - SmokeSource, StateView, StepReport, StepStage, + AcceptedForceIntervalPressure, AcceptedIntervalPressure, CarrierStepControls, Simulation, + SimulationConfig, SimulationError, SmokeSource, StateView, StepReport, StepStage, }; pub mod force_field; pub use force_field::{ @@ -72,8 +72,8 @@ mod material; pub use material::{DomainFill, FluidMaterial, FluidPhase, FluidPreset, ParticleFluidConfig}; mod particle_fluid; pub use particle_fluid::{ - FluidParticle, ParticleDiagnostic, ParticleFluidError, ParticleFluidSimulation, - ParticleStepReport, + AcceptedParticleForceControls, FluidParticle, ParticleDiagnostic, ParticleFluidError, + ParticleFluidSimulation, ParticleStepReport, }; #[cfg(feature = "png-export")] mod export; diff --git a/src/particle_fluid.rs b/src/particle_fluid.rs index d6727280..9ff43111 100644 --- a/src/particle_fluid.rs +++ b/src/particle_fluid.rs @@ -83,6 +83,17 @@ struct Force { damping_rate: f64, } +/// Inputs/report of the last accepted authored-field update. Rejected attempts +/// preserve this publication because its particle diagnostics are owned separately. +#[derive(Debug, Clone)] +pub struct AcceptedParticleForceControls { + pub interval_start: f64, + pub requested_dt: f64, + pub emitter: Option, + pub report: ParticleStepReport, + pub fields: crate::ForceSnapshot, +} + pub struct ParticleFluidSimulation { grid: GridGeometry, config: ParticleFluidConfig, @@ -104,6 +115,7 @@ pub struct ParticleFluidSimulation { emission_sequence: usize, allocated_bytes: usize, collision_mesh: Option>, + accepted_force_controls: Option, } fn norm(v: [f64; 3]) -> f64 { v.into_iter().map(|x| x * x).sum::().sqrt() @@ -297,6 +309,7 @@ impl ParticleFluidSimulation { emission_sequence: 0, allocated_bytes: config.memory_limit - remaining, collision_mesh: None, + accepted_force_controls: None, }) } /// Attach a static boundary transactionally. Bounds must fit entirely in @@ -373,6 +386,9 @@ impl ParticleFluidSimulation { pub fn particle_spacing(&self) -> f64 { self.spacing } + pub fn accepted_force_controls(&self) -> Option<&AcceptedParticleForceControls> { + self.accepted_force_controls.as_ref() + } /// Coordinates and velocities are physical metres and metres/second. /// A failed step retains accepted particles, source credits and clock. pub fn step( @@ -395,12 +411,7 @@ impl ParticleFluidSimulation { fields: &crate::ForceSnapshot, cancel: impl FnMut() -> bool, ) -> Result { - self.step_fields_impl( - requested_dt, - emitter, - (!fields.scene().objects.is_empty()).then_some(fields), - cancel, - ) + self.step_fields_impl(requested_dt, emitter, Some(fields), cancel) } fn step_fields_impl( @@ -416,7 +427,18 @@ impl ParticleFluidSimulation { Err(ParticleFluidError::StabilityLimit { recommended_dt }) => { interval = recommended_dt } - result => return result, + Ok(report) => { + self.accepted_force_controls = + fields.map(|fields| AcceptedParticleForceControls { + interval_start: self.evaluation_time, + requested_dt, + emitter, + report, + fields: fields.clone(), + }); + return Ok(report); + } + Err(error) => return Err(error), } } Err(ParticleFluidError::StabilityLimit { @@ -669,7 +691,7 @@ impl ParticleFluidSimulation { acceleration: self.config.gravity, damping_rate: 0.0, }; - if let Some(fields) = fields { + if let Some(fields) = fields.filter(|f| !f.scene().objects.is_empty()) { let external = fields .acceleration(p.position, self.time, t.density) .map_err(|_| ParticleFluidError::ArithmeticFailure)?; diff --git a/src/recording.rs b/src/recording.rs index ad00278e..70f44a78 100644 --- a/src/recording.rs +++ b/src/recording.rs @@ -2,15 +2,22 @@ //! keep-all/stop-at-limit storage. This preserves inspection fields, not complete //! integrator state. See `docs/api/recording.md` for the SQLite consumer contract. mod carrier; +pub mod force_fields; +pub use force_fields::{ + FORCE_METADATA_VERSION, MAX_FORCE_METADATA_BYTES, decode_force_metadata, encode_force_metadata, + recorded_force_fields, validate_run_force_fields, +}; pub mod comparison; pub mod diagnostic; mod material; pub mod provider; use crate::{GridGeometry, SessionIdentity, SnapshotStage}; -pub use carrier::{RecordedCarrier, carrier_publication, carrier_run}; +pub use carrier::{ + RecordedCarrier, carrier_publication, carrier_publication_with_force_fields, carrier_run, +}; pub use material::{ - RecordedMaterial, material_publication, material_run, material_snapshot_publication, - material_snapshot_run, + RecordedMaterial, material_publication, material_publication_with_force_fields, material_run, + material_snapshot_publication, material_snapshot_run, }; use rusqlite::{Connection, OpenFlags, OptionalExtension, limits::Limit, params}; use sha2::{Digest, Sha256}; @@ -97,6 +104,7 @@ impl RecordingRun { for value in [&self.solver, &self.configuration, &self.provenance] { valid_text(value)?; } + self.force_fields_digest()?; GridGeometry::new(self.counts, self.spacing, self.origin) .map_err(|_| RecordingError::Invalid("recorded geometry"))?; Ok(()) @@ -428,6 +436,7 @@ impl StepRecorder { return Err(RecordingError::Limit("record count")); } let bytes = step.payload_bytes()?; + validate_run_force_fields(&self.run, step)?; if bytes > self.limits.max_record_bytes { return Err(RecordingError::Limit("record payload")); } @@ -763,6 +772,7 @@ impl RecordingReader { { return Err(RecordingError::Integrity("publication digest")); } + validate_run_force_fields(&self.run, &step)?; Ok(step) } /// Structural integrity plus every publication hash and adjacent generation. diff --git a/src/recording/carrier.rs b/src/recording/carrier.rs index 2c591ce6..f0a3d555 100644 --- a/src/recording/carrier.rs +++ b/src/recording/carrier.rs @@ -49,7 +49,15 @@ pub fn carrier_publication( limit: usize, ) -> Result { let state = sim.state(); - let pressure = sim.accepted_interval_pressure(); + let authored = sim.accepted_force_interval_pressure(); + let pressure = sim.accepted_interval_pressure().or_else(|| { + authored.as_ref().map(|p| crate::AcceptedIntervalPressure { + values: p.values, + interval_start: p.interval_start, + report: p.report, + controls: p.controls, + }) + }); let constructor = state.generation == 0 && state.time == 0.0; if !constructor && pressure.is_none() { return Err(RecordingError::Unsupported( @@ -62,7 +70,14 @@ pub fn carrier_publication( } ( Some(p.report.dt), - format!("{:?}; report={:?}", p.controls, p.report), + if let Some(authored) = &authored { + format!( + "external-force-metadata-v1; source_controls={:?}; report={:?}; external_stage_work={:?}", + p.controls, p.report, authored.work + ) + } else { + format!("{:?}; report={:?}", p.controls, p.report) + }, Some(p.interval_start), ) } else { @@ -155,7 +170,7 @@ pub fn carrier_publication( data, }); } - let step = RecordedStep { + let mut step = RecordedStep { ordinal, generation: state.generation, time: state.time, @@ -168,11 +183,26 @@ pub fn carrier_publication( controls, fields, }; + if let Some(authored) = authored { + force_fields::append_fields(&mut step, authored.fields, limit)?; + } if step.payload_bytes()? > limit { return Err(RecordingError::Limit("carrier record payload")); } Ok(step) } +/// Constructor metadata or checked actual accepted metadata. The same fields +/// must bind the RecordingRun through with_force_fields before recorder creation. +pub fn carrier_publication_with_force_fields( + sim: &Simulation, + fields: &crate::ForceSnapshot, + ordinal: usize, + limit: usize, +) -> Result { + let mut step = carrier_publication(sim, ordinal, limit)?; + force_fields::require_fields(&mut step, fields, limit)?; + Ok(step) +} /// Selected owned carrier values; one bounded step, never a live simulation. pub struct RecordedCarrier { pub grid: GridGeometry, @@ -186,6 +216,7 @@ pub struct RecordedCarrier { pub tracer: Vec, /// Actual interval start and Pa values; cell0 gauge, not endpoint pressure. pub interval_pressure: Option<(f64, Vec)>, + pub force_fields: Option, } impl RecordedCarrier { pub fn decode(run: &RecordingRun, step: &RecordedStep) -> Result { @@ -202,12 +233,14 @@ impl RecordedCarrier { .ok_or(RecordingError::Unsupported("carrier method"))?; let grid = GridGeometry::new(run.counts, run.spacing, run.origin) .map_err(|_| RecordingError::Invalid("carrier geometry"))?; + validate_run_force_fields(run, step)?; + let force_fields = recorded_force_fields(step)?; let initial = step.stage == SnapshotStage::Constructor; - if step.fields.len() != if initial { 4 } else { 5 } + if step.fields.len() != (if initial { 4 } else { 5 }) + usize::from(force_fields.is_some()) || step .fields .iter() - .any(|f| !NAMES.contains(&f.name.as_str())) + .any(|f| !NAMES.contains(&f.name.as_str()) && f.name != force_fields::FIELD) { return Err(RecordingError::Invalid("carrier field inventory")); } @@ -290,6 +323,7 @@ impl RecordedCarrier { z: faces.next().unwrap(), tracer, interval_pressure, + force_fields, }) } pub fn state(&self) -> StateView<'_> { diff --git a/src/recording/force_fields.rs b/src/recording/force_fields.rs new file mode 100644 index 00000000..e0281383 --- /dev/null +++ b/src/recording/force_fields.rs @@ -0,0 +1,557 @@ +//! Versioned, bit-preserving force metadata inside the existing archive envelope. +//! Legacy SQL schema, legacy field inventories and hash algorithms are unchanged. +use super::*; +use crate::{ + ForceKind, ForceObject, ForceScene, ForceSnapshot, ForceSupport, ForceTime, ForceTransform, + ForceUnits, SceneObjectId, +}; + +pub const FORCE_METADATA_VERSION: u32 = 1; +pub const MAX_FORCE_METADATA_BYTES: usize = 256 * 1024; +const MAGIC: &[u8; 8] = b"RHEONFF\0"; +pub(super) const FIELD: &str = "external_force_scene_v1"; +const MARKER: &str = "\nexternal-force-metadata-v1-sha256="; +const ASSOCIATION: &str = "immutable_run_input; world_m; activation_at_interval_start_s"; +const STAGE: &str = "external_force_document_v1"; + +/// Little-endian binary encoding with explicit codec and authoring versions. +/// Floats retain exact bits, including signed zero; input is already validated. +pub fn encode_force_metadata(fields: &ForceSnapshot) -> Result, RecordingError> { + let scene = fields.scene(); + let mut out = Vec::new(); + out.extend_from_slice(MAGIC); + out.extend_from_slice(&FORCE_METADATA_VERSION.to_le_bytes()); + out.extend_from_slice(&scene.version.to_le_bytes()); + out.extend_from_slice(&(scene.objects.len() as u32).to_le_bytes()); + for o in &scene.objects { + out.extend_from_slice(&o.id.0.to_le_bytes()); + out.extend_from_slice(&(o.name.len() as u32).to_le_bytes()); + out.extend_from_slice(o.name.as_bytes()); + out.push(u8::from(o.enabled)); + floats(&mut out, o.transform.translation); + floats(&mut out, o.transform.rotation); + out.push(match o.units { + ForceUnits::Acceleration => 0, + ForceUnits::ForceDensity => 1, + }); + match o.kind { + ForceKind::Directional { vector } => { + out.push(0); + floats(&mut out, vector); + } + ForceKind::Radial { strength } => { + out.push(1); + floats(&mut out, [strength]); + } + ForceKind::Vortex { strength } => { + out.push(2); + floats(&mut out, [strength]); + } + } + match o.support { + ForceSupport::Global => out.push(0), + ForceSupport::Sphere { + radius, + edge_fraction, + } => { + out.push(1); + floats(&mut out, [radius, edge_fraction]); + } + ForceSupport::Box { + half_extent, + edge_fraction, + } => { + out.push(2); + floats(&mut out, half_extent); + floats(&mut out, [edge_fraction]); + } + } + floats(&mut out, [o.time.start_seconds]); + out.push(u8::from(o.time.end_seconds.is_some())); + if let Some(end) = o.time.end_seconds { + floats(&mut out, [end]); + } + } + if out.len() > MAX_FORCE_METADATA_BYTES { + return Err(RecordingError::Limit("force metadata bytes")); + } + Ok(out) +} +fn floats(out: &mut Vec, values: [f64; N]) { + for value in values { + out.extend_from_slice(&value.to_le_bytes()); + } +} +struct Input<'a> { + bytes: &'a [u8], +} +impl Input<'_> { + fn take(&mut self, n: usize) -> Result<&[u8], RecordingError> { + let (head, tail) = self + .bytes + .split_at_checked(n) + .ok_or(RecordingError::Invalid("truncated force metadata"))?; + self.bytes = tail; + Ok(head) + } + fn byte(&mut self) -> Result { + Ok(self.take(1)?[0]) + } + fn boolean(&mut self) -> Result { + match self.byte()? { + 0 => Ok(false), + 1 => Ok(true), + _ => Err(RecordingError::Invalid("force boolean")), + } + } + fn u32(&mut self) -> Result { + Ok(u32::from_le_bytes(self.take(4)?.try_into().unwrap())) + } + fn f64(&mut self) -> Result { + Ok(f64::from_le_bytes(self.take(8)?.try_into().unwrap())) + } + fn vector(&mut self) -> Result<[f64; N], RecordingError> { + let mut out = [0.0; N]; + for v in &mut out { + *v = self.f64()?; + } + Ok(out) + } +} +/// Bounds input before parsing, validates all objects and rejects trailing bytes. +pub fn decode_force_metadata(bytes: &[u8]) -> Result { + if bytes.len() > MAX_FORCE_METADATA_BYTES { + return Err(RecordingError::Limit("force metadata bytes")); + } + let mut input = Input { bytes }; + if input.take(8)? != MAGIC || input.u32()? != FORCE_METADATA_VERSION { + return Err(RecordingError::Unsupported("force metadata codec version")); + } + let version = input.u32()?; + let count = input.u32()? as usize; + if count > crate::MAX_FORCE_OBJECTS { + return Err(RecordingError::Limit("force object count")); + } + let mut objects = Vec::new(); + for _ in 0..count { + let id = SceneObjectId(u64::from_le_bytes(input.take(8)?.try_into().unwrap())); + let len = input.u32()? as usize; + if len > 256 { + return Err(RecordingError::Limit("force name bytes")); + } + let name = std::str::from_utf8(input.take(len)?) + .map_err(|_| RecordingError::Invalid("force name UTF-8"))? + .to_owned(); + let enabled = input.boolean()?; + let transform = ForceTransform { + translation: input.vector()?, + rotation: input.vector()?, + }; + let units = match input.byte()? { + 0 => ForceUnits::Acceleration, + 1 => ForceUnits::ForceDensity, + _ => return Err(RecordingError::Unsupported("force units")), + }; + let kind = match input.byte()? { + 0 => ForceKind::Directional { + vector: input.vector()?, + }, + 1 => ForceKind::Radial { + strength: input.f64()?, + }, + 2 => ForceKind::Vortex { + strength: input.f64()?, + }, + _ => return Err(RecordingError::Unsupported("force kind")), + }; + let support = match input.byte()? { + 0 => ForceSupport::Global, + 1 => ForceSupport::Sphere { + radius: input.f64()?, + edge_fraction: input.f64()?, + }, + 2 => ForceSupport::Box { + half_extent: input.vector()?, + edge_fraction: input.f64()?, + }, + _ => return Err(RecordingError::Unsupported("force support")), + }; + let start_seconds = input.f64()?; + let end_seconds = if input.boolean()? { + Some(input.f64()?) + } else { + None + }; + objects.push(ForceObject { + id, + name, + enabled, + transform, + units, + kind, + support, + time: ForceTime { + start_seconds, + end_seconds, + }, + }); + } + if !input.bytes.is_empty() { + return Err(RecordingError::Invalid("trailing force metadata")); + } + ForceScene { version, objects } + .freeze() + .map_err(|_| RecordingError::Invalid("force document")) +} + +impl RecordingRun { + /// Explicit opt-in. Bind the complete scene's versioned binary digest to the + /// existing run header. Does not alter any historical encoding/hash algorithm. + pub fn with_force_fields(mut self, fields: &ForceSnapshot) -> Result { + self.validate()?; + if self.configuration.contains(MARKER) { + return Err(RecordingError::Invalid("force run already bound")); + } + let digest = Sha256::digest(encode_force_metadata(fields)?); + self.configuration.push_str(MARKER); + for byte in digest { + self.configuration.push_str(&format!("{byte:02x}")); + } + self.validate()?; + Ok(self) + } + pub fn force_fields_digest(&self) -> Result, RecordingError> { + let Some((prefix, hex)) = self.configuration.rsplit_once(MARKER) else { + return Ok(None); + }; + if prefix.contains(MARKER) + || hex.len() != 64 + || !hex + .bytes() + .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b)) + { + return Err(RecordingError::Invalid("force run digest")); + } + let mut digest = [0; 32]; + for (i, out) in digest.iter_mut().enumerate() { + *out = u8::from_str_radix(&hex[2 * i..2 * i + 2], 16) + .map_err(|_| RecordingError::Invalid("force run digest"))?; + } + Ok(Some(digest)) + } +} + +/// Decode a selected publication's actual full force document. Absence means +/// legacy metadata, not proof that an arbitrary producer applied zero force. +pub fn recorded_force_fields(step: &RecordedStep) -> Result, RecordingError> { + let Some(field) = step.field(FIELD) else { + return Ok(None); + }; + if field.units != "1" + || field.association != ASSOCIATION + || field.stage != STAGE + || field.scalar_type != "u8" + || field.components != 1 + || field.evaluation_time.is_some() + || field.dimensions != [field.data.len() as u64, 1, 1] + || step.fields.iter().filter(|f| f.name == FIELD).count() != 1 + { + return Err(RecordingError::Invalid("force metadata field schema")); + } + Ok(Some(decode_force_metadata(&field.data)?)) +} +/// Pure association check shared by append/read and available to codec callers. +pub fn validate_run_force_fields( + run: &RecordingRun, + step: &RecordedStep, +) -> Result<(), RecordingError> { + let expected = run.force_fields_digest()?; + let fields = recorded_force_fields(step)?; + match (expected, fields) { + (None, None) => Ok(()), + (Some(expected), Some(fields)) + if expected.as_slice() + == Sha256::digest(encode_force_metadata(&fields)?).as_slice() => + { + Ok(()) + } + _ => Err(RecordingError::Integrity( + "run/step force document mismatch", + )), + } +} +/// Attach run input metadata to a constructor publication, or require that the +/// accepted publication already contains exactly the supplied actual document. +pub(super) fn require_fields( + step: &mut RecordedStep, + fields: &ForceSnapshot, + limit: usize, +) -> Result<(), RecordingError> { + if let Some(actual) = recorded_force_fields(step)? { + if encode_force_metadata(&actual)? != encode_force_metadata(fields)? { + return Err(RecordingError::Integrity( + "accepted force document mismatch", + )); + } + } else if step.stage == SnapshotStage::Constructor { + append_fields(step, fields, limit)?; + } else { + return Err(RecordingError::Invalid("accepted force custody absent")); + } + Ok(()) +} +pub(super) fn append_fields( + step: &mut RecordedStep, + fields: &ForceSnapshot, + limit: usize, +) -> Result<(), RecordingError> { + if step.field(FIELD).is_some() { + return Err(RecordingError::Invalid("duplicate force metadata")); + } + let data = encode_force_metadata(fields)?; + step.fields.push(RecordedField { + name: FIELD.into(), + units: "1".into(), + association: ASSOCIATION.into(), + stage: STAGE.into(), + evaluation_time: None, + dimensions: [data.len() as u64, 1, 1], + components: 1, + scalar_type: "u8".into(), + data, + }); + if step.payload_bytes()? > limit { + return Err(RecordingError::Limit("force record payload")); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + fn fields() -> ForceSnapshot { + ForceScene { + version: crate::FORCE_SCENE_VERSION, + objects: vec![ + ForceObject { + id: SceneObjectId(1), + name: "directional".into(), + enabled: true, + transform: ForceTransform { + translation: [-0.0, 2.0, 3.0], + rotation: [-0.0, 0.0, 0.0, 1.0], + }, + units: ForceUnits::Acceleration, + kind: ForceKind::Directional { + vector: [-0.0, -2.0, 3.0], + }, + support: ForceSupport::Global, + time: ForceTime::default(), + }, + ForceObject { + id: SceneObjectId(u64::MAX), + name: "attraction λ".into(), + enabled: false, + transform: ForceTransform::default(), + units: ForceUnits::ForceDensity, + kind: ForceKind::Radial { strength: -4.0 }, + support: ForceSupport::Sphere { + radius: 2.0, + edge_fraction: 0.25, + }, + time: ForceTime { + start_seconds: 1.0, + end_seconds: Some(2.0), + }, + }, + ForceObject { + id: SceneObjectId(3), + name: "vortex".into(), + enabled: true, + transform: ForceTransform::default(), + units: ForceUnits::Acceleration, + kind: ForceKind::Vortex { strength: 5.0 }, + support: ForceSupport::Box { + half_extent: [1.0, 2.0, 3.0], + edge_fraction: 1.0, + }, + time: ForceTime::default(), + }, + ], + } + .freeze() + .unwrap() + } + fn run() -> RecordingRun { + RecordingRun { + identity: crate::SessionIdentity { + run_id: 7, + source_revision: "legacy-source".into(), + }, + solver: "legacy-solver".into(), + configuration: "legacy-config".into(), + counts: [1; 3], + spacing: [1.0; 3], + origin: [0.0; 3], + provenance: "legacy-provenance".into(), + } + } + fn step() -> RecordedStep { + RecordedStep { + ordinal: 0, + generation: 0, + time: 0.0, + dt: None, + stage: SnapshotStage::Constructor, + controls: "legacy-controls".into(), + fields: vec![RecordedField { + name: "sample".into(), + units: "1".into(), + association: "cell".into(), + stage: "constructor".into(), + evaluation_time: None, + dimensions: [1; 3], + components: 1, + scalar_type: "u8".into(), + data: vec![9], + }], + } + } + #[test] + fn binary_roundtrip_preserves_all_variants_ids_disabled_and_float_bits() { + let original = fields(); + let bytes = encode_force_metadata(&original).unwrap(); + assert_eq!(&bytes[..20], b"RHEONFF\0\x01\0\0\0\x01\0\0\0\x03\0\0\0"); + let decoded = decode_force_metadata(&bytes).unwrap(); + assert_eq!(encode_force_metadata(&decoded).unwrap(), bytes); + assert_eq!(decoded.scene(), original.scene()); + assert_eq!( + decoded.scene().objects[0].transform.translation[0].to_bits(), + (-0.0_f64).to_bits() + ); + } + #[test] + fn binary_rejects_truncation_trailing_unknown_versions_and_invalid_objects() { + let bytes = encode_force_metadata(&fields()).unwrap(); + for n in 0..bytes.len() { + assert!(decode_force_metadata(&bytes[..n]).is_err(), "prefix {n}"); + } + let mut bad = bytes.clone(); + bad.push(0); + assert!(decode_force_metadata(&bad).is_err()); + for offset in [8, 12] { + let mut bad = bytes.clone(); + bad[offset] = 99; + assert!(decode_force_metadata(&bad).is_err()); + } + let mut bad = bytes.clone(); + bad[20..28].fill(0); + assert!(decode_force_metadata(&bad).is_err()); + let mut bad = bytes.clone(); + bad[16..20].copy_from_slice(&257_u32.to_le_bytes()); + assert!(decode_force_metadata(&bad).is_err()); + assert!(decode_force_metadata(&vec![0; MAX_FORCE_METADATA_BYTES + 1]).is_err()); + } + #[test] + fn maximum_valid_scene_fits_bounded_codec() { + let mut scene = fields().scene().clone(); + let mut object = scene.objects[2].clone(); + object.name = "x".repeat(256); + scene.objects = (1..=crate::MAX_FORCE_OBJECTS) + .map(|id| { + let mut o = object.clone(); + o.id = SceneObjectId(id as u64); + o + }) + .collect(); + let fields = scene.freeze().unwrap(); + let bytes = encode_force_metadata(&fields).unwrap(); + assert!(bytes.len() <= MAX_FORCE_METADATA_BYTES); + assert_eq!( + encode_force_metadata(&decode_force_metadata(&bytes).unwrap()).unwrap(), + bytes + ); + } + #[test] + fn run_binding_rejects_absent_mismatched_and_unbound_metadata() { + let original = fields(); + let bound = run().with_force_fields(&original).unwrap(); + let mut s = step(); + assert!(validate_run_force_fields(&bound, &s).is_err()); + append_fields(&mut s, &original, 1024 * 1024).unwrap(); + validate_run_force_fields(&bound, &s).unwrap(); + assert!(validate_run_force_fields(&run(), &s).is_err()); + let empty = ForceScene::default().freeze().unwrap(); + let wrong = run().with_force_fields(&empty).unwrap(); + assert!(validate_run_force_fields(&wrong, &s).is_err()); + assert!(bound.with_force_fields(&original).is_err()); + } + #[test] + fn field_schema_limits_and_accepted_custody_fail_closed() { + let original = fields(); + let mut s = step(); + require_fields(&mut s, &original, 1024 * 1024).unwrap(); + assert!( + require_fields( + &mut s, + &ForceScene::default().freeze().unwrap(), + 1024 * 1024 + ) + .is_err() + ); + s.fields.last_mut().unwrap().association = "particle_index_within_run".into(); + assert!(recorded_force_fields(&s).is_err()); + let mut s = step(); + assert!(append_fields(&mut s, &original, 1).is_err()); + let mut s = step(); + s.generation = 1; + s.time = 0.1; + s.dt = Some(0.1); + s.stage = SnapshotStage::Accepted; + assert!(require_fields(&mut s, &original, 1024 * 1024).is_err()); + } + #[test] + fn malformed_run_digest_never_panics_and_is_not_rebound() { + for text in ["λ".repeat(32), "x".repeat(64), "0".repeat(63)] { + let mut r = run(); + r.configuration.push_str(MARKER); + r.configuration.push_str(&text); + assert!(r.force_fields_digest().is_err()); + assert!(r.validate().is_err()); + } + } + #[test] + fn legacy_run_hash_and_absent_metadata_are_unchanged() { + let r = run(); + let s = step(); + let limits = RecordingLimits { + max_database_bytes: 1048576, + max_record_bytes: 4096, + max_records: 3, + }; + let hash = r + .digest(limits) + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + // Independent byte-level SHA-256 calculation of the pre-change contract. + assert_eq!( + hash, + "781b179b9818a048d50183f33244e73968dc7ec02e35a258f5bba3463886986b" + ); + let step_hash = s + .digest(&r.digest(limits)) + .iter() + .map(|b| format!("{b:02x}")) + .collect::(); + assert_eq!( + step_hash, + "579f8cc614789a49c128f54c90ed449e6bd09a35fda8b7c6958b449ceef39b76" + ); + assert!(r.force_fields_digest().unwrap().is_none()); + assert!(recorded_force_fields(&s).unwrap().is_none()); + validate_run_force_fields(&r, &s).unwrap(); + assert_eq!(s.controls, "legacy-controls"); + assert_eq!(s.fields.len(), 1); + } +} diff --git a/src/recording/material.rs b/src/recording/material.rs index 115398fc..7f1cd86c 100644 --- a/src/recording/material.rs +++ b/src/recording/material.rs @@ -88,7 +88,31 @@ pub fn material_publication( ordinal: usize, limit: usize, ) -> Result { - publication( + if let Some(actual) = sim.accepted_force_controls() { + let supplied = controls.ok_or(RecordingError::Invalid("force material controls absent"))?; + let report = report.ok_or(RecordingError::Invalid("force material report absent"))?; + let report_bits = |r: ParticleStepReport| { + [ + r.dt, + r.emitted_mass, + r.total_mass, + r.max_speed, + r.max_pressure, + r.mean_temperature_kelvin, + ] + .map(f64::to_bits) + }; + if supplied.requested_dt.to_bits() != actual.requested_dt.to_bits() + || supplied.emitter.map(|c| c.emitter) != actual.emitter + || report.particles != actual.report.particles + || report_bits(report) != report_bits(actual.report) + { + return Err(RecordingError::Invalid( + "force material controls/report mismatch", + )); + } + } + let mut step = publication( sim.generation(), sim.time(), sim.particles(), @@ -97,7 +121,25 @@ pub fn material_publication( controls, ordinal, limit, - ) + )?; + if let Some(actual) = sim.accepted_force_controls() { + step.controls = format!("external-force-metadata-v1; {}", step.controls); + force_fields::append_fields(&mut step, &actual.fields, limit)?; + } + Ok(step) +} +/// Construct force input metadata or check it against the actual accepted stamp. +pub fn material_publication_with_force_fields( + sim: &ParticleFluidSimulation, + report: Option, + controls: Option, + fields: &crate::ForceSnapshot, + ordinal: usize, + limit: usize, +) -> Result { + let mut step = material_publication(sim, report, controls, ordinal, limit)?; + force_fields::require_fields(&mut step, fields, limit)?; + Ok(step) } /// Public API consumers use their canonical immutable snapshot directly. pub fn material_snapshot_publication( @@ -300,6 +342,7 @@ pub struct RecordedMaterial { pub dt: Option, pub particles: Vec, pub diagnostics: Option<(f64, Vec)>, + pub force_fields: Option, } impl RecordedMaterial { pub fn decode(step: &RecordedStep) -> Result { @@ -398,6 +441,7 @@ impl RecordedMaterial { dt: step.dt, particles, diagnostics, + force_fields: recorded_force_fields(step)?, }) } /// Use the existing VTK particle contract with selected values and times. diff --git a/src/simulation.rs b/src/simulation.rs index bc4ef07e..b9062237 100644 --- a/src/simulation.rs +++ b/src/simulation.rs @@ -232,6 +232,16 @@ pub struct AcceptedIntervalPressure<'a> { pub report: StepReport, pub controls: CarrierStepControls, } +/// Force-aware accepted interval. The legacy accessor never labels these steps +/// with incomplete controls. Invalidated before the next candidate attempt. +pub struct AcceptedForceIntervalPressure<'a> { + pub values: &'a [f64], + pub interval_start: f64, + pub report: StepReport, + pub controls: CarrierStepControls, + pub fields: &'a crate::ForceSnapshot, + pub work: Option, +} /// Actual inputs of the success-stamped fixed-box carrier publication. #[derive(Debug, Clone, Copy)] pub struct CarrierStepControls { @@ -252,6 +262,12 @@ pub struct Simulation { generation: u64, paused: bool, inspection_pressure: Option<(f64, StepReport, CarrierStepControls)>, + inspection_force_pressure: Option<( + f64, + ForcedStepReport, + CarrierStepControls, + crate::ForceSnapshot, + )>, } impl Simulation { pub fn new(grid: GridGeometry, config: SimulationConfig) -> Result { @@ -298,6 +314,7 @@ impl Simulation { generation: 0, paused: false, inspection_pressure: None, + inspection_force_pressure: None, }) } pub fn implementation(&self) -> PressureImplementation { @@ -321,6 +338,19 @@ impl Simulation { controls, }) } + /// Force-aware counterpart of accepted_interval_pressure. Retains the actual + /// immutable scene, source controls and external-stage work on success only. + pub fn accepted_force_interval_pressure(&self) -> Option> { + let (interval_start, report, controls, fields) = self.inspection_force_pressure.as_ref()?; + Some(AcceptedForceIntervalPressure { + values: self.workspace.pressure(), + interval_start: *interval_start, + report: report.step, + controls: *controls, + fields, + work: report.forces, + }) + } pub fn allocated_bytes(&self) -> usize { self.allocated_bytes } @@ -339,6 +369,7 @@ impl Simulation { } pub fn reset(&mut self) -> Result<(), SimulationError> { self.inspection_pressure = None; + self.inspection_force_pressure = None; let next = self .generation .checked_add(1) @@ -418,6 +449,7 @@ impl Simulation { fields: &crate::ForceSnapshot, cancel: impl FnMut(StepStage) -> bool, ) -> Result { + let interval_start = self.time; let outcome = self.prepare_impl( requested_dt, source, @@ -434,6 +466,16 @@ impl Simulation { cancel, )?; self.commit_prepared(&outcome); + self.inspection_force_pressure = Some(( + interval_start, + outcome.legacy.step, + CarrierStepControls { + requested_dt, + source, + emitter, + }, + fields.clone(), + )); Ok(outcome.legacy.step) } @@ -641,6 +683,7 @@ impl Simulation { crate::ColumnMacError, > { self.inspection_pressure = None; + self.inspection_force_pressure = None; if self.paused { return Err(crate::ColumnMacError::Paused); } @@ -697,6 +740,7 @@ impl Simulation { } pub(crate) fn commit_column_mac(&mut self, generation: u64) { self.inspection_pressure = None; + self.inspection_force_pressure = None; std::mem::swap(&mut self.accepted.x, &mut self.candidate.x); std::mem::swap(&mut self.accepted.y, &mut self.candidate.y); std::mem::swap(&mut self.accepted.z, &mut self.candidate.z); @@ -704,6 +748,7 @@ impl Simulation { } pub(crate) fn commit_prepared(&mut self, outcome: &StepOutcome) { self.inspection_pressure = None; + self.inspection_force_pressure = None; std::mem::swap(&mut self.accepted, &mut self.candidate); self.time = outcome.legacy.step.step.time; self.generation = outcome.legacy.step.step.generation; @@ -717,6 +762,7 @@ impl Simulation { mut cancel: impl FnMut(StepStage) -> bool, ) -> Result { self.inspection_pressure = None; + self.inspection_force_pressure = None; let StepDomains { fields, emitter, From 3e839cbf7c6aca56e8297ef740f7e48543de77b1 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:25:35 +0000 Subject: [PATCH 5/9] docs(forces): preserve sealed test logs as binary evidence --- docs/plans/force-authoring/evidence/.gitattributes | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 docs/plans/force-authoring/evidence/.gitattributes diff --git a/docs/plans/force-authoring/evidence/.gitattributes b/docs/plans/force-authoring/evidence/.gitattributes new file mode 100644 index 00000000..1749851a --- /dev/null +++ b/docs/plans/force-authoring/evidence/.gitattributes @@ -0,0 +1,2 @@ +# Preserve captured test output bytes, including libtest's trailing blank lines. +*.log -diff -text From 1d44c8429701f205f678b602040d4f62ea6457b3 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:34:12 +0000 Subject: [PATCH 6/9] test(forces): prepare bounded post-custody recording cases --- .../recording-integration-plan.md | 106 +++++ tests/force_recording_integration_contract.rs | 439 ++++++++++++++++++ 2 files changed, 545 insertions(+) create mode 100644 docs/plans/force-authoring/recording-integration-plan.md create mode 100644 tests/force_recording_integration_contract.rs diff --git a/docs/plans/force-authoring/recording-integration-plan.md b/docs/plans/force-authoring/recording-integration-plan.md new file mode 100644 index 00000000..be78d0f8 --- /dev/null +++ b/docs/plans/force-authoring/recording-integration-plan.md @@ -0,0 +1,106 @@ +# Prepared post-custody integration validation + +Draft PR #145 publishes the core at 3e839cbf7c6aca56e8297ef740f7e48543de77b1. +This follow-up adds only ignored test source and this plan. It changes no core +or desktop source and executes no numerical/SQLite tests. The compiled cases +are proposals for separate review/authorization, not passed integration evidence. + +## Independent change-impact review + +A separate reviewer compared d7bffdd..3e839cb. Equations, acceleration bounds and +force-stage kernels did not change; success/error wrappers, Arc snapshot ownership, +accepted pressure/control custody and empty particle routing did. It recommends +one exact rerun of all five existing adapter tests against a newly sealed current +binary, under the existing supervisor, before claiming current adapter coverage: + +- authored_mac_matches_legacy_bounded_directional_force +- authored_mac_cancellation_retains_accepted_state_and_retry +- authored_particle_density_conversion_adds_to_existing_acceleration +- authored_particle_cancel_retains_state_and_retry +- empty_snapshot_preserves_both_legacy_paths + +No rerun has occurred. The published five passes remain pre-custody results. +No broad numerical/MMS suite is warranted by this change-impact review. + +## Two smallest prepared recording cases + +Both are individually ignored in tests/force_recording_integration_contract.rs. +Every helper was inspected independently without execution. Both have one frozen +force object: id=1, identity transform, global directional [2,0,0] N/m³, enabled, +start=0/no end. The alternate document changes only the name, verifying that +complete intent (including names) stays bound, not just the applied vector. + +`carrier_force_archive_round_trip_and_failed_attempt_invalidation` + +- One 2×2×1 grid, spacing=[1,1,1], origin=0, rho=2, initially zero fields. +- Solver retained-array cap=1 MiB, default Jacobi PCG, max pressure iterations=100, + relative residual=1e-11, absolute residual=1e-12, predicted divergence=1e-9, + stored divergence=1e-6, max Courant=1; no source/emitter. +- Exactly two public numerical calls, dt request=.001: one accepted authored step + then one cancelled at BeforeCommit. No retries; <=200 pressure iterations total. +- Assert separate actual pressure/controls/work/scene custody, legacy accessor + absence, wrong-scene wrapper rejection, and accepted state preservation with + both pressure stamps invalidated by the cancelled attempt. +- Append constructor and accepted publications; deliberately change a third + publication's metadata and require the exact run/document mismatch error before + transaction. Finish failed with accepted generation1; read back durable prefix. +- Assert original run header, exactly two records, constructor pressure absent, + full scene bytes, native numerical/pressure bits, dt/start and failed lifecycle. + +`particle_force_archive_round_trip_and_rejected_control_custody` + +- One 1 m cube (geometry2³, spacing=.5), exactly64 water parcels (4³), zero initial + velocities; standard ParticleFluidConfig defaults except fill=EmittedFluid, + samples_per_axis=4, max_particles=64, memory=1 MiB and gravity=[0,-1,0]. +- No emitter/mesh. Exactly three calls: accepted dt=1e-6, changed-scene dt=1e-6 + cancelled on callback4, and invalid dt=0. At most16 preparations in the accepted + call, at most2 entries for callback4 cancellation, and one invalid validation + entry: <=19 prepare entries, <=17 density/force evaluations, one accepted step. +- Assert original requested dt/report/scene and evaluation-time custody, accepted + particles/diagnostics/time/generation retained after failed calls, wrong scene, + wrong caller dt and wrong report rejection. +- The same constructor/accepted append and third metadata-mismatch rejection + pattern checks the durable two-record prefix, exact particle/diagnostic bits, + evaluation time/accepted dt, original scene and failed lifecycle. + +## Archive, process bounds and teardown + +Each case owns one fresh temporary directory and one SQLite database; create_dir +fails rather than overwriting an existing path. RecordingLimits are database1MiB, +record64KiB, max_records=3 (two commits plus one attempted mismatch). One verify +scans the two recorded rows, followed by explicit selects0/1. No raw SQL mutation, +external fixture, model, subprocess adapter or parallel feature pool is used. + +Normal teardown drops recorder/reader/simulation, audits at most3 regular files, +per-file<=1MiB and total<=3MiB, and asserts deletion of only the owned directory. +Drop provides best-effort cleanup during unwinding. These audits are post-run +checks, not peak resource enforcement. The owned path is logged for reconciliation +if process termination prevents cleanup. + +Future execution must retain the existing process supervisor unchanged: 30s wall, +TERM then owned-child KILL after1s, CPU20s, address space512MiB, file/log1MiB, +test-threads1, no parallel feature; seal/enumerate the actual binary and select one +exact ignored test per child, serially, stopping on first failure/bound/selection +problem without tuning/retry. No execution is authorized by this plan. + +## Compilation/listing only + +Performed without executing test bodies: + +```sh +cargo test --locked --offline --no-default-features --features force-serde --test force_recording_integration_contract -- --list +``` + +It enumerated exactly the two named cases above. Source review reported no +blocking defects. These cases deliberately do not cover successful legacy stamp +clearing after an authored call, historical archives, emitters, provider/session/ +free-surface/UI paths, raw corruption, large workloads or scientific validation. +Existing source review and pure golden-hash evidence cover only their stated scope. + +Compile-only test source SHA-256: +`5943923df13e26acf03e9b1b0ac3d703158bd641d382471fd8e9217b7a17aea1`. +Compile-only binary SHA-256: +`28f5c0d3c398667a0049d276c132729ba4356f8371be9292aade9e4dc3c3354d`. +These identify preparation artifacts only; any future execution must recheck them. +Targeted Clippy (lib + this test target, warnings denied), formatting and diff +checks also passed without executing numerical/SQLite test bodies. diff --git a/tests/force_recording_integration_contract.rs b/tests/force_recording_integration_contract.rs new file mode 100644 index 00000000..9709e036 --- /dev/null +++ b/tests/force_recording_integration_contract.rs @@ -0,0 +1,439 @@ +//! Prepared post-custody numerical/SQLite cases. Compilation/listing is safe; +//! execution requires separate authorization and the existing bounded supervisor. +#![cfg(not(target_arch = "wasm32"))] +use rheon::{recording::*, *}; +use std::{ + path::PathBuf, + sync::atomic::{AtomicUsize, Ordering}, +}; + +// Own exactly one fresh directory; never remove an existing directory on create +// failure. Normal cleanup is asserted, with a drop fallback during unwinding. +struct Archive { + directory: Option, +} +impl Archive { + fn new() -> Self { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let path = std::env::temp_dir().join(format!( + "rheon-force-custody-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + std::fs::create_dir(&path).unwrap(); + eprintln!("owned recording directory: {}", path.display()); + Self { + directory: Some(path), + } + } + fn path(&self) -> PathBuf { + self.directory.as_ref().unwrap().join("run.sqlite") + } + fn cleanup(mut self) { + let path = self.directory.as_ref().unwrap(); + let mut total = 0; + let mut files = 0; + for entry in std::fs::read_dir(path).unwrap() { + let metadata = entry.unwrap().metadata().unwrap(); + assert!(metadata.is_file()); + assert!(metadata.len() <= 1024 * 1024); + total += metadata.len(); + files += 1; + } + assert!(files <= 3 && total <= 3 * 1024 * 1024); + std::fs::remove_dir_all(path).unwrap(); + self.directory = None; + } +} +impl Drop for Archive { + fn drop(&mut self) { + if let Some(path) = &self.directory { + let _ = std::fs::remove_dir_all(path); + } + } +} +fn limits() -> RecordingLimits { + RecordingLimits { + max_database_bytes: 1024 * 1024, + max_record_bytes: 64 * 1024, + max_records: 3, + } +} +fn identity(run_id: u64) -> SessionIdentity { + SessionIdentity { + run_id, + source_revision: "prepared-force-custody-contract".into(), + } +} +fn fields(name: &str) -> ForceSnapshot { + ForceScene { + objects: vec![ForceObject { + id: SceneObjectId(1), + name: name.into(), + enabled: true, + transform: ForceTransform::default(), + units: ForceUnits::ForceDensity, + kind: ForceKind::Directional { + vector: [2.0, 0.0, 0.0], + }, + support: ForceSupport::Global, + time: ForceTime::default(), + }], + ..Default::default() + } + .freeze() + .unwrap() +} +fn bits(state: StateView<'_>) -> (Vec, u64, u64) { + ( + state + .x + .iter() + .chain(state.y) + .chain(state.z) + .chain(state.tracer) + .map(|v| v.to_bits()) + .collect(), + state.time.to_bits(), + state.generation, + ) +} +fn particle_bits(particles: &[FluidParticle]) -> Vec { + particles + .iter() + .flat_map(|p| { + p.position + .into_iter() + .chain(p.velocity) + .chain([p.mass, p.temperature_kelvin]) + .map(f64::to_bits) + .chain([u64::from(p.material)]) + }) + .collect() +} +fn diagnostic_bits(diagnostics: &[ParticleDiagnostic]) -> Vec { + diagnostics + .iter() + .flat_map(|d| { + [d.density, d.pressure] + .into_iter() + .chain(d.acceleration) + .map(f64::to_bits) + }) + .collect() +} +fn same_scene(actual: &ForceSnapshot, expected: &ForceSnapshot) { + assert_eq!( + encode_force_metadata(actual).unwrap(), + encode_force_metadata(expected).unwrap() + ); +} +fn inject_changed_scene(step: &mut RecordedStep, changed: &ForceSnapshot) { + let field = step + .fields + .iter_mut() + .find(|f| f.name == "external_force_scene_v1") + .unwrap(); + field.data = encode_force_metadata(changed).unwrap(); + field.dimensions = [field.data.len() as u64, 1, 1]; +} + +#[test] +#[ignore = "post-custody numerical/SQLite integration: explicit bounded execution authorization required"] +fn carrier_force_archive_round_trip_and_failed_attempt_invalidation() { + let archive = Archive::new(); + let bounds = limits(); + let force = fields("accepted"); + let changed = fields("changed draft"); + let mut sim = Simulation::new( + GridGeometry::new([2, 2, 1], [1.0; 3], [0.0; 3]).unwrap(), + SimulationConfig { + density: 2.0, + memory_limit: 1024 * 1024, + pressure: PressureSettings { + relative_residual: 1e-11, + absolute_residual: 1e-12, + divergence_limit: 1e-9, + max_iterations: 100, + }, + actual_divergence_limit: 1e-6, + max_courant: 1.0, + }, + ) + .unwrap(); + assert!(sim.accepted_interval_pressure().is_none()); + assert!(sim.accepted_force_interval_pressure().is_none()); + let run = carrier_run(identity(1), &sim, "prepared bounded case") + .unwrap() + .with_force_fields(&force) + .unwrap(); + let mut writer = StepRecorder::create(&archive.path(), run.clone(), bounds).unwrap(); + writer + .append( + &carrier_publication_with_force_fields(&sim, &force, 0, bounds.max_record_bytes) + .unwrap(), + ) + .unwrap(); + let report = sim + .step_with_force_fields(0.001, None, &force, |_| false) + .unwrap(); + assert!(sim.accepted_interval_pressure().is_none()); + let pressure = sim.accepted_force_interval_pressure().unwrap(); + assert_eq!(pressure.interval_start.to_bits(), 0.0_f64.to_bits()); + assert_eq!( + pressure.controls.requested_dt.to_bits(), + 0.001_f64.to_bits() + ); + assert!(pressure.controls.source.is_none() && pressure.controls.emitter.is_none()); + assert_eq!(pressure.report.dt.to_bits(), report.step.dt.to_bits()); + assert_eq!(pressure.report.generation, report.step.generation); + assert_eq!(pressure.work, report.forces); + same_scene(pressure.fields, &force); + let expected_pressure: Vec<_> = pressure.values.iter().map(|p| p.to_bits()).collect(); + let expected_state = bits(sim.state()); + assert!( + carrier_publication_with_force_fields(&sim, &changed, 1, bounds.max_record_bytes).is_err() + ); + writer + .append( + &carrier_publication_with_force_fields(&sim, &force, 1, bounds.max_record_bytes) + .unwrap(), + ) + .unwrap(); + let mut rejected = + carrier_publication_with_force_fields(&sim, &force, 2, bounds.max_record_bytes).unwrap(); + assert!(matches!( + sim.step_with_force_fields(0.001, None, &changed, |s| s == StepStage::BeforeCommit), + Err(SimulationError::Cancelled { .. }) + )); + assert_eq!(bits(sim.state()), expected_state); + assert!(sim.accepted_interval_pressure().is_none()); + assert!(sim.accepted_force_interval_pressure().is_none()); + assert!(carrier_publication(&sim, 2, bounds.max_record_bytes).is_err()); + inject_changed_scene(&mut rejected, &changed); + assert!(matches!( + writer.append(&rejected), + Err(RecordingError::Integrity( + "run/step force document mismatch" + )) + )); + assert_eq!(writer.recorded_count(), 2); + writer + .finish( + "failed", + "deliberately rejected changed force intent", + Some(1), + ) + .unwrap(); + drop(writer); + let reader = RecordingReader::open(&archive.path(), bounds).unwrap(); + reader.verify().unwrap(); + assert_eq!(reader.run(), &run); + assert_eq!(reader.count().unwrap(), 2); + let initial = RecordedCarrier::decode(reader.run(), &reader.select(0).unwrap()).unwrap(); + assert!(initial.interval_pressure.is_none()); + same_scene(initial.force_fields.as_ref().unwrap(), &force); + let accepted = RecordedCarrier::decode(reader.run(), &reader.select(1).unwrap()).unwrap(); + assert_eq!(bits(accepted.state()), expected_state); + assert_eq!(accepted.dt.unwrap().to_bits(), report.step.dt.to_bits()); + assert_eq!( + accepted.interval_pressure.as_ref().unwrap().0.to_bits(), + 0.0_f64.to_bits() + ); + same_scene(accepted.force_fields.as_ref().unwrap(), &force); + assert_eq!( + accepted + .interval_pressure + .as_ref() + .unwrap() + .1 + .iter() + .map(|p| p.to_bits()) + .collect::>(), + expected_pressure + ); + assert_eq!(reader.status().unwrap().outcome, "failed"); + drop(reader); + drop(sim); + archive.cleanup(); +} + +#[test] +#[ignore = "post-custody numerical/SQLite integration: explicit bounded execution authorization required"] +fn particle_force_archive_round_trip_and_rejected_control_custody() { + let archive = Archive::new(); + let bounds = limits(); + let force = fields("accepted"); + let changed = fields("changed draft"); + let mut sim = ParticleFluidSimulation::new( + GridGeometry::new([2; 3], [0.5; 3], [0.0; 3]).unwrap(), + ParticleFluidConfig { + fill: DomainFill::EmittedFluid, + samples_per_axis: 4, + max_particles: 64, + memory_limit: 1024 * 1024, + gravity: [0.0, -1.0, 0.0], + ..Default::default() + }, + ) + .unwrap(); + let run = material_run(identity(2), &sim, "prepared bounded case") + .unwrap() + .with_force_fields(&force) + .unwrap(); + let mut writer = StepRecorder::create(&archive.path(), run.clone(), bounds).unwrap(); + writer + .append( + &material_publication_with_force_fields( + &sim, + None, + None, + &force, + 0, + bounds.max_record_bytes, + ) + .unwrap(), + ) + .unwrap(); + let report = sim + .step_with_force_fields(1e-6, None, &force, || false) + .unwrap(); + let controls = MaterialStepControls { + requested_dt: 1e-6, + emitter: None, + }; + let stamp = sim.accepted_force_controls().unwrap(); + same_scene(&stamp.fields, &force); + assert_eq!( + stamp.requested_dt.to_bits(), + controls.requested_dt.to_bits() + ); + assert_eq!(stamp.report.dt.to_bits(), report.dt.to_bits()); + assert_eq!(stamp.report.particles, 64); + let expected_particles = particle_bits(sim.particles()); + let (evaluation, diagnostics) = sim.particle_diagnostics().unwrap(); + let expected_diagnostics = diagnostic_bits(diagnostics); + assert_eq!(stamp.interval_start.to_bits(), evaluation.to_bits()); + let expected_time = sim.time().to_bits(); + let expected_generation = sim.generation(); + writer + .append( + &material_publication_with_force_fields( + &sim, + Some(report), + Some(controls), + &force, + 1, + bounds.max_record_bytes, + ) + .unwrap(), + ) + .unwrap(); + let mut cancellation_calls = 0; + assert!(matches!( + sim.step_with_force_fields(1e-6, None, &changed, || { + cancellation_calls += 1; + cancellation_calls == 4 + }), + Err(ParticleFluidError::Cancelled) + )); + assert!(matches!( + sim.step_with_force_fields(0.0, None, &changed, || false), + Err(ParticleFluidError::InvalidTimeStep) + )); + same_scene(&sim.accepted_force_controls().unwrap().fields, &force); + assert_eq!(particle_bits(sim.particles()), expected_particles); + assert_eq!( + diagnostic_bits(sim.particle_diagnostics().unwrap().1), + expected_diagnostics + ); + assert_eq!( + sim.particle_diagnostics().unwrap().0.to_bits(), + evaluation.to_bits() + ); + assert_eq!(sim.time().to_bits(), expected_time); + assert_eq!(sim.generation(), expected_generation); + assert!( + material_publication_with_force_fields( + &sim, + Some(report), + Some(controls), + &changed, + 2, + bounds.max_record_bytes + ) + .is_err() + ); + assert!( + material_publication_with_force_fields( + &sim, + Some(report), + Some(MaterialStepControls { + requested_dt: 2e-6, + emitter: None + }), + &force, + 2, + bounds.max_record_bytes + ) + .is_err() + ); + let mut wrong_report = report; + wrong_report.dt *= 2.0; + assert!( + material_publication_with_force_fields( + &sim, + Some(wrong_report), + Some(controls), + &force, + 2, + bounds.max_record_bytes + ) + .is_err() + ); + let mut rejected = material_publication_with_force_fields( + &sim, + Some(report), + Some(controls), + &force, + 2, + bounds.max_record_bytes, + ) + .unwrap(); + inject_changed_scene(&mut rejected, &changed); + assert!(matches!( + writer.append(&rejected), + Err(RecordingError::Integrity( + "run/step force document mismatch" + )) + )); + assert_eq!(writer.recorded_count(), 2); + writer + .finish( + "failed", + "deliberately rejected changed force intent", + Some(1), + ) + .unwrap(); + drop(writer); + let reader = RecordingReader::open(&archive.path(), bounds).unwrap(); + reader.verify().unwrap(); + assert_eq!(reader.run(), &run); + assert_eq!(reader.count().unwrap(), 2); + let initial = RecordedMaterial::decode(&reader.select(0).unwrap()).unwrap(); + assert!(initial.diagnostics.is_none()); + same_scene(initial.force_fields.as_ref().unwrap(), &force); + let accepted = RecordedMaterial::decode(&reader.select(1).unwrap()).unwrap(); + same_scene(accepted.force_fields.as_ref().unwrap(), &force); + assert_eq!(particle_bits(&accepted.particles), expected_particles); + let (actual_evaluation, actual_diagnostics) = accepted.diagnostics.as_ref().unwrap(); + assert_eq!(actual_evaluation.to_bits(), evaluation.to_bits()); + assert_eq!(diagnostic_bits(actual_diagnostics), expected_diagnostics); + assert_eq!(accepted.dt.unwrap().to_bits(), report.dt.to_bits()); + assert_eq!(accepted.time.to_bits(), expected_time); + assert_eq!(accepted.generation, expected_generation); + assert_eq!(reader.status().unwrap().outcome, "failed"); + drop(reader); + drop(sim); + archive.cleanup(); +} From 7970a5db8136c3771ea416db86c58753aea95566 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:39:03 +0000 Subject: [PATCH 7/9] test(forces): retain failed recording evidence before teardown --- .../recording-integration-plan.md | 24 +++++++++- tests/force_recording_integration_contract.rs | 45 ++++++++++++++++--- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/docs/plans/force-authoring/recording-integration-plan.md b/docs/plans/force-authoring/recording-integration-plan.md index be78d0f8..636b5141 100644 --- a/docs/plans/force-authoring/recording-integration-plan.md +++ b/docs/plans/force-authoring/recording-integration-plan.md @@ -73,7 +73,12 @@ external fixture, model, subprocess adapter or parallel feature pool is used. Normal teardown drops recorder/reader/simulation, audits at most3 regular files, per-file<=1MiB and total<=3MiB, and asserts deletion of only the owned directory. -Drop provides best-effort cleanup during unwinding. These audits are post-run +Drop retains/logs the owned directory on failure instead of deleting it. +KeepOnPanic retains outer writer/reader resources during assertion unwinding, +preventing test-owned SQLite destructors from changing the failure-boundary +recording before the child exits. Successful explicit drops/cleanup are unchanged. +A synchronous SQL operation may already have rolled back before returning an +error; this does not promise a pre-error or in-flight database image. These audits are post-run checks, not peak resource enforcement. The owned path is logged for reconciliation if process termination prevents cleanup. @@ -97,6 +102,9 @@ clearing after an authored call, historical archives, emitters, provider/session free-surface/UI paths, raw corruption, large workloads or scientific validation. Existing source review and pure golden-hash evidence cover only their stated scope. +Original 1d44 preparation artifacts (superseded for authorized execution by the +later corrected-head seals): + Compile-only test source SHA-256: `5943923df13e26acf03e9b1b0ac3d703158bd641d382471fd8e9217b7a17aea1`. Compile-only binary SHA-256: @@ -104,3 +112,17 @@ Compile-only binary SHA-256: These identify preparation artifacts only; any future execution must recheck them. Targeted Clippy (lib + this test target, warnings denied), formatting and diff checks also passed without executing numerical/SQLite test bodies. + + +## Authorized execution preparation: failure preservation correction + +The parent later authorized all five current adapter tests and both recording +cases once each under the existing process limits. Before execution, inspection +found that the original Archive Drop removed failed recordings. A reviewed +test-only correction now retains the logged owned directory on failure and +suppresses outer SQLite resource destruction only during panic unwinding. +There are no new solver calls, fixtures, process capabilities or changed limits. +The exact corrected commit/binary seals and outcomes will be recorded separately. +No cleanup of unrelated paths is permitted, and failed originals are not repaired +or rerun. This supersedes the original preparation-only execution hold for these +seven exact tests, not for other numerical/SQLite workloads. diff --git a/tests/force_recording_integration_contract.rs b/tests/force_recording_integration_contract.rs index 9709e036..5ed216c5 100644 --- a/tests/force_recording_integration_contract.rs +++ b/tests/force_recording_integration_contract.rs @@ -8,7 +8,7 @@ use std::{ }; // Own exactly one fresh directory; never remove an existing directory on create -// failure. Normal cleanup is asserted, with a drop fallback during unwinding. +// failure. Only explicit successful cleanup removes it; failures retain evidence. struct Archive { directory: Option, } @@ -48,7 +48,38 @@ impl Archive { impl Drop for Archive { fn drop(&mut self) { if let Some(path) = &self.directory { - let _ = std::fs::remove_dir_all(path); + eprintln!("preserved recording evidence: {}", path.display()); + } + } +} + +// Each test runs alone in an owned child. On panic retain open SQLite resources +// until process exit so their destructors cannot modify the original failure +// recording/journal before the supervisor preserves the logged owned directory. +// Successful explicit drops retain their normal behavior; no unsafe code. +struct KeepOnPanic(Option); +impl KeepOnPanic { + fn new(value: T) -> Self { + Self(Some(value)) + } +} +impl std::ops::Deref for KeepOnPanic { + type Target = T; + fn deref(&self) -> &T { + self.0.as_ref().unwrap() + } +} +impl std::ops::DerefMut for KeepOnPanic { + fn deref_mut(&mut self) -> &mut T { + self.0.as_mut().unwrap() + } +} +impl Drop for KeepOnPanic { + fn drop(&mut self) { + if std::thread::panicking() + && let Some(value) = self.0.take() + { + std::mem::forget(value); } } } @@ -167,7 +198,8 @@ fn carrier_force_archive_round_trip_and_failed_attempt_invalidation() { .unwrap() .with_force_fields(&force) .unwrap(); - let mut writer = StepRecorder::create(&archive.path(), run.clone(), bounds).unwrap(); + let mut writer = + KeepOnPanic::new(StepRecorder::create(&archive.path(), run.clone(), bounds).unwrap()); writer .append( &carrier_publication_with_force_fields(&sim, &force, 0, bounds.max_record_bytes) @@ -226,7 +258,7 @@ fn carrier_force_archive_round_trip_and_failed_attempt_invalidation() { ) .unwrap(); drop(writer); - let reader = RecordingReader::open(&archive.path(), bounds).unwrap(); + let reader = KeepOnPanic::new(RecordingReader::open(&archive.path(), bounds).unwrap()); reader.verify().unwrap(); assert_eq!(reader.run(), &run); assert_eq!(reader.count().unwrap(), 2); @@ -281,7 +313,8 @@ fn particle_force_archive_round_trip_and_rejected_control_custody() { .unwrap() .with_force_fields(&force) .unwrap(); - let mut writer = StepRecorder::create(&archive.path(), run.clone(), bounds).unwrap(); + let mut writer = + KeepOnPanic::new(StepRecorder::create(&archive.path(), run.clone(), bounds).unwrap()); writer .append( &material_publication_with_force_fields( @@ -416,7 +449,7 @@ fn particle_force_archive_round_trip_and_rejected_control_custody() { ) .unwrap(); drop(writer); - let reader = RecordingReader::open(&archive.path(), bounds).unwrap(); + let reader = KeepOnPanic::new(RecordingReader::open(&archive.path(), bounds).unwrap()); reader.verify().unwrap(); assert_eq!(reader.run(), &run); assert_eq!(reader.count().unwrap(), 2); From 43ec505218c43594641ecbb80873bec8c88d7d2c Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 22:42:34 +0000 Subject: [PATCH 8/9] test(forces): record bounded post-custody validation results --- docs/plans/force-authoring/contract.md | 6 +- docs/plans/force-authoring/custody.md | 6 +- .../adapter_tests-build-features.json | 1 + ...tion_retains_accepted_state_and_retry.json | 24 ++ ...ation_retains_accepted_state_and_retry.log | 6 + ...ches_legacy_bounded_directional_force.json | 24 ++ ...tches_legacy_bounded_directional_force.log | 6 + ...rticle_cancel_retains_state_and_retry.json | 24 ++ ...article_cancel_retains_state_and_retry.log | 6 + ...version_adds_to_existing_acceleration.json | 24 ++ ...nversion_adds_to_existing_acceleration.log | 6 + ..._snapshot_preserves_both_legacy_paths.json | 24 ++ ...y_snapshot_preserves_both_legacy_paths.log | 6 + .../adapters/enumeration.json | 22 ++ .../adapters/enumeration.log | 5 + .../adapters/run_bounded.py | 62 ++++++ .../post-custody-7970a5d/binary-seals.sha256 | 2 + .../recording_tests-build-features.json | 1 + ..._trip_and_failed_attempt_invalidation.json | 24 ++ ...d_trip_and_failed_attempt_invalidation.log | 15 ++ .../recordings/enumeration.json | 22 ++ .../recordings/enumeration.log | 2 + ...und_trip_and_rejected_control_custody.json | 24 ++ ...ound_trip_and_rejected_control_custody.log | 15 ++ .../recordings/run_bounded.py | 57 +++++ .../post-custody-7970a5d/source-head.txt | 1 + .../post-custody-7970a5d/source.sha256 | 205 ++++++++++++++++++ .../recording-integration-plan.md | 4 +- docs/plans/force-authoring/validation.md | 101 +++++++++ 29 files changed, 719 insertions(+), 6 deletions(-) create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapter_tests-build-features.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/run_bounded.py create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/binary-seals.sha256 create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recording_tests-build-features.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.json create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.log create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/run_bounded.py create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/source-head.txt create mode 100644 docs/plans/force-authoring/evidence/post-custody-7970a5d/source.sha256 diff --git a/docs/plans/force-authoring/contract.md b/docs/plans/force-authoring/contract.md index f0388b97..c7850124 100644 --- a/docs/plans/force-authoring/contract.md +++ b/docs/plans/force-authoring/contract.md @@ -226,5 +226,7 @@ Minimal coordinated follow-up: never attach current drafts to old results or reinterpret old hashes. 5. The five prepared bounded tests passed once on the sealed pre-custody binary under the later explicit authorization. Seven pure custody/codec tests pass. - No post-custody solver or end-to-end desktop/SQLite workload was executed; - these results do not establish full archive integration or desktop parity. + The later authorized current-code run passed five adapter and two tiny + recording cases on 7970a5d under the existing supervisor (validation.md). + Desktop, emitter/provider/session and historical archive workflows remain + outside this evidence; these results do not establish desktop parity. diff --git a/docs/plans/force-authoring/custody.md b/docs/plans/force-authoring/custody.md index f1e953a8..e0550028 100644 --- a/docs/plans/force-authoring/custody.md +++ b/docs/plans/force-authoring/custody.md @@ -1,9 +1,9 @@ # Force-aware accepted custody and archive metadata This extends the core milestone without editing desktop/request definitions or -session/provider owners. Source review is independent; runtime tests below apply -only to the sealed pre-custody adapter binary. Current custody code has pure codec -coverage and static checks, not executed solver/SQLite/desktop integration. +session/provider owners. Source review is independent. The later authorized validation on corrected head +7970a5d passed five adapter and two bounded recording cases once each; exact seals +and limits are in validation.md. This does not establish desktop integration. ## Accepted numerical publications diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapter_tests-build-features.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapter_tests-build-features.json new file mode 100644 index 00000000..9e8eff24 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapter_tests-build-features.json @@ -0,0 +1 @@ +{"rustc":4758242423518056681,"features":"[\"force-serde\"]","declared_features":"[\"default\", \"desktop\", \"force-serde\", \"parallel\", \"png-export\", \"work-observation\"]","target":17260259951606174431,"profile":5612075431994834192,"path":1817014210143136823,"deps":[[390430846265316719,"gltf",false,11586693289549932636],[3646101781514403606,"rustix",false,6553390319389300857],[4878511589877685828,"rusqlite",false,7357609022033758554],[6557439603276904804,"serde",false,18150741541386485220],[8160210889872729633,"serde_json",false,8983802469690437438],[9015757307373990982,"cap_std",false,3442701369777828215],[9857275760291862238,"sha2",false,8098286442164053832],[13077212702700853852,"base64",false,13812028478587927365],[13536970318350685827,"rheon",false,9119528104381679303]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/rheon-9d566ed71bad85b4/dep-test-integration-test-force_field_solver_contract","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0} \ No newline at end of file diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json new file mode 100644 index 00000000..065f5ecd --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/adapter_tests", + "authored_mac_cancellation_retains_accepted_state_and_retry", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773", + "returncode": 0, + "wall_seconds": 0.004781140999966738, + "signals": [], + "log_bytes": 183, + "log_sha256": "afacaf787c62bce2c0b3d899cc8f6a8cc90603b6061a8d09ae43f3f6f62967ba", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log new file mode 100644 index 00000000..68ab1316 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_cancellation_retains_accepted_state_and_retry.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_mac_cancellation_retains_accepted_state_and_retry ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.json new file mode 100644 index 00000000..cbcf0f73 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/adapter_tests", + "authored_mac_matches_legacy_bounded_directional_force", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773", + "returncode": 0, + "wall_seconds": 0.0053261050002220145, + "signals": [], + "log_bytes": 178, + "log_sha256": "88f61a43755daae4b113baff6e001c8a7ed5c0fcc35238104adae17d5775ae05", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.log new file mode 100644 index 00000000..1924d5bb --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_mac_matches_legacy_bounded_directional_force.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_mac_matches_legacy_bounded_directional_force ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.json new file mode 100644 index 00000000..9a0290a0 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/adapter_tests", + "authored_particle_cancel_retains_state_and_retry", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773", + "returncode": 0, + "wall_seconds": 0.009160894000160624, + "signals": [], + "log_bytes": 173, + "log_sha256": "ce53f41065888d0e29d6c41412f7a969b7f3f0f6ed8ec8c1e1007ada3c53b8c8", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.log new file mode 100644 index 00000000..c442331b --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_cancel_retains_state_and_retry.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_particle_cancel_retains_state_and_retry ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json new file mode 100644 index 00000000..41c2f0de --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/adapter_tests", + "authored_particle_density_conversion_adds_to_existing_acceleration", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773", + "returncode": 0, + "wall_seconds": 0.017757716000232904, + "signals": [], + "log_bytes": 191, + "log_sha256": "233b50b8d9be352930ae7bb3cb8c3a38b27951468dbf5d2114d5f087ebd7d26e", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log new file mode 100644 index 00000000..1b58b19b --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/authored_particle_density_conversion_adds_to_existing_acceleration.log @@ -0,0 +1,6 @@ + +running 1 test +test authored_particle_density_conversion_adds_to_existing_acceleration ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.01s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.json new file mode 100644 index 00000000..329de3db --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/adapter_tests", + "empty_snapshot_preserves_both_legacy_paths", + "--exact", + "--ignored", + "--test-threads=1", + "--nocapture" + ], + "binary_sha256": "ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773", + "returncode": 0, + "wall_seconds": 0.009219619999839779, + "signals": [], + "log_bytes": 167, + "log_sha256": "31b9cdd2dd7435206b3c379717e23d5ad99b3c269bc53ea182b19ea0181be917", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.log new file mode 100644 index 00000000..68520794 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/empty_snapshot_preserves_both_legacy_paths.log @@ -0,0 +1,6 @@ + +running 1 test +test empty_snapshot_preserves_both_legacy_paths ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.json new file mode 100644 index 00000000..9c0da8bc --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.json @@ -0,0 +1,22 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/adapter_tests", + "--list", + "--format", + "terse" + ], + "binary_sha256": "ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773", + "returncode": 0, + "wall_seconds": 0.004854455999975471, + "signals": [], + "log_bytes": 302, + "log_sha256": "95c1ecca9477fc2f362c688db8122e321d78f516dcbb7bf222f6af6a1da26964", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.log new file mode 100644 index 00000000..63c6c2f0 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/enumeration.log @@ -0,0 +1,5 @@ +authored_mac_cancellation_retains_accepted_state_and_retry: test +authored_mac_matches_legacy_bounded_directional_force: test +authored_particle_cancel_retains_state_and_retry: test +authored_particle_density_conversion_adds_to_existing_acceleration: test +empty_snapshot_preserves_both_legacy_paths: test diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/run_bounded.py b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/run_bounded.py new file mode 100644 index 00000000..13b74a69 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/adapters/run_bounded.py @@ -0,0 +1,62 @@ +import hashlib, json, os, pathlib, resource, signal, subprocess, sys, time +root = pathlib.Path('/workspace/force-validation-post-custody-7970a5d/adapters') +binary = root.parent / 'adapter_tests' +expected_hash = 'ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773' +names = [ + 'authored_mac_matches_legacy_bounded_directional_force', + 'authored_mac_cancellation_retains_accepted_state_and_retry', + 'authored_particle_density_conversion_adds_to_existing_acceleration', + 'authored_particle_cancel_retains_state_and_retry', + 'empty_snapshot_preserves_both_legacy_paths', +] +def bounded(): + resource.setrlimit(resource.RLIMIT_CPU, (20,20)) + resource.setrlimit(resource.RLIMIT_AS, (512*1024*1024,512*1024*1024)) + resource.setrlimit(resource.RLIMIT_FSIZE, (1024*1024,1024*1024)) + resource.setrlimit(resource.RLIMIT_CORE, (0,0)) + # Check effective child limits before exec; failure prevents execution. + for limit, expected in [(resource.RLIMIT_CPU,20),(resource.RLIMIT_AS,512*1024*1024),(resource.RLIMIT_FSIZE,1024*1024)]: + assert resource.getrlimit(limit) == (expected,expected) + +def run(label, args): + assert hashlib.sha256(binary.read_bytes()).hexdigest() == expected_hash + path = root / (label+'.log') + command = [str(binary), *args] + began = time.monotonic() + signals = [] + with path.open('xb') as log: + child = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, + cwd='/workspace/Rheon-force', env={**os.environ,'RUST_BACKTRACE':'0'}, + preexec_fn=bounded, start_new_session=True) + try: + code = child.wait(timeout=30) + except subprocess.TimeoutExpired: + child.send_signal(signal.SIGTERM); signals.append('TERM after 30 s') + try: code = child.wait(timeout=1) + except subprocess.TimeoutExpired: + child.kill(); signals.append('KILL owned child after 1 s grace'); code=child.wait() + data = path.read_bytes() + record = {'command':command,'binary_sha256':expected_hash,'returncode':code, + 'wall_seconds':time.monotonic()-began,'signals':signals,'log_bytes':len(data), + 'log_sha256':hashlib.sha256(data).hexdigest(), + 'limits':{'wall_seconds':30,'term_to_owned_child_kill_seconds':1,'cpu_seconds':20, + 'address_space_bytes':512*1024*1024,'file_and_log_bytes':1024*1024,'test_threads':1}} + (root/(label+'.json')).write_text(json.dumps(record,indent=2)+'\n') + print(json.dumps(record),flush=True) + if code != 0 or signals or len(data) >= 1024*1024: + raise SystemExit('STOP: failure, timeout or log bound; no retries') + return data.decode('utf-8') + +if sys.argv[1:] == ['enumerate']: + out = run('enumeration',['--list','--format','terse']) + actual = [line[:-6] for line in out.splitlines() if line.endswith(': test')] + assert len(actual)==5 and set(actual)==set(names), 'STOP: unexpected enumeration' + print(out) +elif sys.argv[1:] == ['execute']: + listed = (root/'enumeration.log').read_text() + assert {line[:-6] for line in listed.splitlines() if line.endswith(': test')} == set(names) + for name in names: + out = run(name,[name,'--exact','--ignored','--test-threads=1','--nocapture']) + if out.count('running 1 test') != 1 or f'test {name} ... ok' not in out or 'test result: ok. 1 passed; 0 failed; 0 ignored;' not in out: + raise SystemExit('STOP: unexpected test selection or outcome; no retries') +else: raise SystemExit('enumerate or execute required') diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/binary-seals.sha256 b/docs/plans/force-authoring/evidence/post-custody-7970a5d/binary-seals.sha256 new file mode 100644 index 00000000..94fa00e4 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/binary-seals.sha256 @@ -0,0 +1,2 @@ +ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773 adapter_tests +4ee715678545204a996b70121d26dcb930578e7e5f94cafc27d111372af69e5b recording_tests diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recording_tests-build-features.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recording_tests-build-features.json new file mode 100644 index 00000000..39cab08b --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recording_tests-build-features.json @@ -0,0 +1 @@ +{"rustc":4758242423518056681,"features":"[\"force-serde\"]","declared_features":"[\"default\", \"desktop\", \"force-serde\", \"parallel\", \"png-export\", \"work-observation\"]","target":13037667827364744955,"profile":5612075431994834192,"path":9318281768106816026,"deps":[[390430846265316719,"gltf",false,11586693289549932636],[3646101781514403606,"rustix",false,6553390319389300857],[4878511589877685828,"rusqlite",false,7357609022033758554],[6557439603276904804,"serde",false,18150741541386485220],[8160210889872729633,"serde_json",false,8983802469690437438],[9015757307373990982,"cap_std",false,3442701369777828215],[9857275760291862238,"sha2",false,8098286442164053832],[13077212702700853852,"base64",false,13812028478587927365],[13536970318350685827,"rheon",false,9119528104381679303]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/rheon-e00c1f219195a422/dep-test-integration-test-force_recording_integration_contract","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0} \ No newline at end of file diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.json new file mode 100644 index 00000000..9a60ce50 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/recording_tests", + "carrier_force_archive_round_trip_and_failed_attempt_invalidation", + "--exact", + "--ignored", + "--test-threads=1", + "--show-output" + ], + "binary_sha256": "4ee715678545204a996b70121d26dcb930578e7e5f94cafc27d111372af69e5b", + "returncode": 0, + "wall_seconds": 0.03396315300005881, + "signals": [], + "log_bytes": 477, + "log_sha256": "6ca285401870fd486829d2944e68c0b1006ebcb0abd49224f8b731acb0692bde", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.log new file mode 100644 index 00000000..28e2e6a0 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/carrier_force_archive_round_trip_and_failed_attempt_invalidation.log @@ -0,0 +1,15 @@ + +running 1 test +test carrier_force_archive_round_trip_and_failed_attempt_invalidation ... ok + +successes: + +---- carrier_force_archive_round_trip_and_failed_attempt_invalidation stdout ---- +owned recording directory: /workspace/force-validation-post-custody-7970a5d/scratch/rheon-force-custody-7014-0 + + +successes: + carrier_force_archive_round_trip_and_failed_attempt_invalidation + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.01s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.json new file mode 100644 index 00000000..1bddc080 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.json @@ -0,0 +1,22 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/recording_tests", + "--list", + "--format", + "terse" + ], + "binary_sha256": "4ee715678545204a996b70121d26dcb930578e7e5f94cafc27d111372af69e5b", + "returncode": 0, + "wall_seconds": 0.005066334000275674, + "signals": [], + "log_bytes": 140, + "log_sha256": "6f93a64aa09ed72267da9a802da363485d0cf4072182133304003b2c811f89c2", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.log new file mode 100644 index 00000000..a1643a36 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/enumeration.log @@ -0,0 +1,2 @@ +carrier_force_archive_round_trip_and_failed_attempt_invalidation: test +particle_force_archive_round_trip_and_rejected_control_custody: test diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.json b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.json new file mode 100644 index 00000000..91af2293 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.json @@ -0,0 +1,24 @@ +{ + "command": [ + "/workspace/force-validation-post-custody-7970a5d/recording_tests", + "particle_force_archive_round_trip_and_rejected_control_custody", + "--exact", + "--ignored", + "--test-threads=1", + "--show-output" + ], + "binary_sha256": "4ee715678545204a996b70121d26dcb930578e7e5f94cafc27d111372af69e5b", + "returncode": 0, + "wall_seconds": 0.03394221500002459, + "signals": [], + "log_bytes": 471, + "log_sha256": "f32aa9002de05a1d169d6c8e4a7975df43303f2c70710e7566c7159e0a0212d3", + "limits": { + "wall_seconds": 30, + "term_to_owned_child_kill_seconds": 1, + "cpu_seconds": 20, + "address_space_bytes": 536870912, + "file_and_log_bytes": 1048576, + "test_threads": 1 + } +} diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.log b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.log new file mode 100644 index 00000000..e3e09142 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/particle_force_archive_round_trip_and_rejected_control_custody.log @@ -0,0 +1,15 @@ + +running 1 test +test particle_force_archive_round_trip_and_rejected_control_custody ... ok + +successes: + +---- particle_force_archive_round_trip_and_rejected_control_custody stdout ---- +owned recording directory: /workspace/force-validation-post-custody-7970a5d/scratch/rheon-force-custody-7016-0 + + +successes: + particle_force_archive_round_trip_and_rejected_control_custody + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.02s + diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/run_bounded.py b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/run_bounded.py new file mode 100644 index 00000000..ba3d6ff6 --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/recordings/run_bounded.py @@ -0,0 +1,57 @@ +import hashlib, json, os, pathlib, resource, signal, subprocess, sys, time +root = pathlib.Path('/workspace/force-validation-post-custody-7970a5d/recordings') +binary = root.parent / 'recording_tests' +expected_hash = '4ee715678545204a996b70121d26dcb930578e7e5f94cafc27d111372af69e5b' +names = ['carrier_force_archive_round_trip_and_failed_attempt_invalidation', 'particle_force_archive_round_trip_and_rejected_control_custody'] + +def bounded(): + resource.setrlimit(resource.RLIMIT_CPU, (20,20)) + resource.setrlimit(resource.RLIMIT_AS, (512*1024*1024,512*1024*1024)) + resource.setrlimit(resource.RLIMIT_FSIZE, (1024*1024,1024*1024)) + resource.setrlimit(resource.RLIMIT_CORE, (0,0)) + # Check effective child limits before exec; failure prevents execution. + for limit, expected in [(resource.RLIMIT_CPU,20),(resource.RLIMIT_AS,512*1024*1024),(resource.RLIMIT_FSIZE,1024*1024)]: + assert resource.getrlimit(limit) == (expected,expected) + +def run(label, args): + assert hashlib.sha256(binary.read_bytes()).hexdigest() == expected_hash + path = root / (label+'.log') + command = [str(binary), *args] + began = time.monotonic() + signals = [] + with path.open('xb') as log: + child = subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, + cwd='/workspace/Rheon-force', env={**os.environ,'RUST_BACKTRACE':'0'}, + preexec_fn=bounded, start_new_session=True) + try: + code = child.wait(timeout=30) + except subprocess.TimeoutExpired: + child.send_signal(signal.SIGTERM); signals.append('TERM after 30 s') + try: code = child.wait(timeout=1) + except subprocess.TimeoutExpired: + child.kill(); signals.append('KILL owned child after 1 s grace'); code=child.wait() + data = path.read_bytes() + record = {'command':command,'binary_sha256':expected_hash,'returncode':code, + 'wall_seconds':time.monotonic()-began,'signals':signals,'log_bytes':len(data), + 'log_sha256':hashlib.sha256(data).hexdigest(), + 'limits':{'wall_seconds':30,'term_to_owned_child_kill_seconds':1,'cpu_seconds':20, + 'address_space_bytes':512*1024*1024,'file_and_log_bytes':1024*1024,'test_threads':1}} + (root/(label+'.json')).write_text(json.dumps(record,indent=2)+'\n') + print(json.dumps(record),flush=True) + if code != 0 or signals or len(data) >= 1024*1024: + raise SystemExit('STOP: failure, timeout or log bound; no retries') + return data.decode('utf-8') + +if sys.argv[1:] == ['enumerate']: + out = run('enumeration',['--list','--format','terse']) + actual = [line[:-6] for line in out.splitlines() if line.endswith(': test')] + assert len(actual)==2 and set(actual)==set(names), 'STOP: unexpected enumeration' + print(out) +elif sys.argv[1:] == ['execute']: + listed = (root/'enumeration.log').read_text() + assert {line[:-6] for line in listed.splitlines() if line.endswith(': test')} == set(names) + for name in names: + out = run(name,[name,'--exact','--ignored','--test-threads=1','--show-output']) + if out.count('running 1 test') != 1 or f'test {name} ... ok' not in out or 'test result: ok. 1 passed; 0 failed; 0 ignored;' not in out: + raise SystemExit('STOP: unexpected test selection or outcome; no retries') +else: raise SystemExit('enumerate or execute required') diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/source-head.txt b/docs/plans/force-authoring/evidence/post-custody-7970a5d/source-head.txt new file mode 100644 index 00000000..7841dceb --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/source-head.txt @@ -0,0 +1 @@ +7970a5db8136c3771ea416db86c58753aea95566 diff --git a/docs/plans/force-authoring/evidence/post-custody-7970a5d/source.sha256 b/docs/plans/force-authoring/evidence/post-custody-7970a5d/source.sha256 new file mode 100644 index 00000000..238a7ffa --- /dev/null +++ b/docs/plans/force-authoring/evidence/post-custody-7970a5d/source.sha256 @@ -0,0 +1,205 @@ +3b5836a7be9f1290c698cadaff010e11b93e33b4af839c8549391c23e7d8ec0c Cargo.lock +f925dae7a0d74e6024e6e0d64962a275ae4f28fedec5b016341671021b8239ae Cargo.toml +09ff66fa19e0cb8bbad00b6032e28f1cf77b0a37a73013648177b35297f98800 rust-toolchain.toml +0163a872c314b2b3bb7f732348aafdbfa7b162dceea74807b7e7e3eb60ab0849 src/advection.rs +1541bc444d1b098c5a59a221b7068538b013362db46ed66e37eb4931ad425e4f src/aligned_strain.rs +46c12612d5527989d1472711967bf798a88113a7cc662c0185b9749ac572f83d src/bin/desktop/README.md +7a42a42bbe8d044f703c2c52d488bf8a4e8e3b3fb1448851efcfb04599d5bf31 src/bin/desktop/accepted_session.rs +922c98958bec45dbd4dee199e0bbdbdd6e3fe4ac52d7a70472ce8132461416fc src/bin/desktop/ai_comparison/accepted_provider.rs +643636776b009337fb80dfdf3d5dbee975e06ed7f7a7cf3f534c07301b61c729 src/bin/desktop/ai_comparison/accepted_provider_contract_tests.rs +11ae34ba76dc93a87f15c8ce9f745403b2d22d0194987d945cf2edc53c624237 src/bin/desktop/ai_comparison/accepted_provider_tests.rs +6cfddefe84a64c89f126598b817d22afe6d1b5731e1d533ea92cecbd1870c942 src/bin/desktop/ai_comparison/accepted_recording_contract_tests.rs +7038fb09ef2bd33a17495414c4c0986a6fb63ba949fe440e9d19927baca712b2 src/bin/desktop/ai_comparison/adapter.rs +3959018b52697ef072884864c01de4682d1b4cd351c273426d914d02f0641fa1 src/bin/desktop/ai_comparison/configured_track.rs +a0dcc1c09d188ad14fc42e78535cd51bcb9bacae0522f4f95262340e4bd86b7c src/bin/desktop/ai_comparison/configured_track_tests.rs +6232225760a7646d2b6759f3df7bd2fee34bed00c6a3a1058e68a96fced4311e src/bin/desktop/ai_comparison/cpu_contract_tests.rs +80559d9d69d8164c24b8dd45f71c51b972fc8af9349c9e56b3e36b14b827b41c src/bin/desktop/ai_comparison/diagnostic_runs.rs +1e6b8011f945ba05ce04c8e4c7f88de23bd53fc99c776311fd30d3021a1c7fe2 src/bin/desktop/ai_comparison/diagnostic_view.rs +a75e78beff3c85dec528c54c15a29c6ce6db7c918279bda2af7b5af91104991b src/bin/desktop/ai_comparison/diagnostic_worker.rs +fd39822957fed70fac97bf5025cdb57028817c414175320b901119dac4fa50a8 src/bin/desktop/ai_comparison/diagnostic_worker_tests.rs +610ba29f66eb91083f9bbd26b823b14e4bde4887eb057e929c089d6cd4592e55 src/bin/desktop/ai_comparison/engine.rs +584b5fd3dc9307f6fbf758474cd3f02e6ffcefb00e90ca3f068b838c15c786ad src/bin/desktop/ai_comparison/engine_typed_tests.rs +83f8864382f0c0c8c454b54dc0e9c389654a325849c6802b7205a2b031040076 src/bin/desktop/ai_comparison/mod.rs +bbbe43744cad1791038e07895967f744b86bb77fa9642d0fc80e4b00bee2204d src/bin/desktop/ai_comparison/model_process.rs +088668d3b29f3650c2145f09199d7ff8cbf38e1de78b1c4d73c882e8fc1610b3 src/bin/desktop/ai_comparison/prediction_contract_tests.rs +2464e347414d3c75e7568dda6a5fd102896c5f194cf3ba8b3bb34961566fce0d src/bin/desktop/ai_comparison/record.rs +c8817ed933ff31c4e9cdd7c0eb2e4c35f50a6868568496e2c67444e11ad80321 src/bin/desktop/ai_comparison/scene.rs +568f7b852a21714405e37d55ba00a56b3f4b07e58037ed26f8a67cfd4fbd268d src/bin/desktop/ai_comparison/view.rs +6fb6c5b49cf57a473f9911d89965e8812c5d9915ecc1d3b654ca2f7ecfa8cc6b src/bin/desktop/ai_comparison/worker.rs +9f96f1d6b11b46e7fd523825e19a8fc0e0ec862669acd129f5e39e91f83c89ff src/bin/desktop/archive_view.rs +a94fea1f039a49f8d0eb79b019b3fbd0f2c9da1762ba854d10cba1e5fd87a25b src/bin/desktop/build_identity.rs +b6bcc84a45bd5979de357c965c84426c25e81161ee1f06022e57a93a46643f7d src/bin/desktop/camera.rs +0d96fb2acdfd63374e5f0004d9ead8b43d9fe2f6959c0d01f9c828458ae1799e src/bin/desktop/carrier_execution.rs +78d7be8e49798e2420ebac5c3fb756e1b195e43ef113ae03ce1b2d3362009204 src/bin/desktop/collision_ui.rs +ef52e29f4d9c59007c11ff973ed6a43c33ac9e1d583819ede11ebd7aea9ec0a5 src/bin/desktop/comparison.rs +813cc86187ef7313deacbe9d9cd15340f37ecdf62f5234a45f033909415459b9 src/bin/desktop/comparison_archive.rs +ec8ea0799522d5a7fadac47b06b42f26717f935bbb9a1677fc5052f096c1cc82 src/bin/desktop/comparison_field_view.rs +e6716e8c707f30cc20304b16e3201f8fb1fd838650bd08a43a95ab4d68cbbf46 src/bin/desktop/configured_comparison_tests.rs +05d317554c4db033dd35c5e153dceaea34367eed33125ec8c7f59466f83ede36 src/bin/desktop/configured_controls.rs +7560ff5395acbd50ce111b1e50ae5e8aca1b1bab5356ec195929eec0994f24bb src/bin/desktop/configured_producer_exit_tests.rs +81e24d69cd5838de3becbc31453ce0a10947e7fe956287862957fcc06eae6e38 src/bin/desktop/configured_worker_tests.rs +d5eaf6358b037d1f11177abbbd83bfa8aa18c263b195c3911b8f183b3334fba9 src/bin/desktop/emitter_controls.rs +9fc4eaafd1118abda50b70d1cc71c843fc1ca71d420d2cd47f16a20fcc9e8e9d src/bin/desktop/engineering.rs +1702f437b17dd42d2bf1311ca14d0301a53712392abc6591ab8962b48cba511d src/bin/desktop/export_ui.rs +e4be154bbbf40a36f370b68ec9e15fea663fb589d476a43eb298c0e8c0f0d821 src/bin/desktop/history.rs +df451a986476b9df252a9a4dbc1e33218bf5ab3cb268b83fa78b47dfffdae184 src/bin/desktop/identity.rs +bb6bb83529896323fbf5414d6e7f41f8462b21e80a6d1f620edb6eecd2fc6649 src/bin/desktop/inspector.rs +75208acd210d4558a34127b98cf77f2c898a560f991f6bed3bee3a4eb160b9a2 src/bin/desktop/particle_view.rs +ab2c841612f39b16f5bd1a363593eae5388bf4a81ec495dfb89e43bd7436580b src/bin/desktop/run_set_view.rs +e194a48350954edf13cdcd01371d320cc210069b4256ba955f2ddd1132301cc1 src/bin/desktop/run_sets.rs +45fa8cf6976da7f8ab3eff27fd69ba5511c98c4be541739eefcdf762826abac9 src/bin/desktop/run_sets/configured.rs +eb1d8e77019eb57afd9e5fd5f57af55f8ff320527ec8efc935945bd7ff477806 src/bin/desktop/run_sets/configured/archive.rs +c948c24fbe3d0bbb4d281c427244f7908a16506236c0f24b194490c64033cd11 src/bin/desktop/run_sets/source.rs +d66735427fc8b6be57d93812d30fb4b8c357419acd70b7b0564aa48216bb2660 src/bin/desktop/settings.rs +e8c3d5cc588d4efac9918544fc5012696a05bdfa7adcba7ce18b07c5a260f6b8 src/bin/desktop/transport_view.rs +599046403df150499064337c2ce3b6996f1ece69f4e876f882200f079b7cd530 src/bin/desktop/viewport.rs +00000ec93161c04d9ff0515e5ae2ab6855a5467f56f23a4d6fc6c1059ef8ada9 src/bin/rheon-ai-compare.rs +76a92d5c928919f832904395319644f14eb36bc2e1c33aeab1146d4b1480ce48 src/bin/rheon-desktop.rs +5116a121256c8bb57d086caedbda8151f3593cb99d3e84b1d04fea25b12dfb51 src/bin/rheon.rs +554b3cdc916e7527bbfdbd6a54b8cf7e27549c3c661015b259c8b2d18784bbb0 src/box_flux.rs +e2ef3afcd8adffe0f79c0030eb56e5bb55943775e474a1dab2a7fe78ac38441b src/box_flux_step.rs +101c91ced5efc250f3a12bc47d5bbf04b812096caf8580c0229362e959839115 src/collision.rs +40b86c5bab2d458c8b31b1f8def21c5f715acecb37813fb57417f7fe363aca84 src/column_mac.rs +373f63ac001fe8e9f8017a912033b3cc431169c6e4c86c2e6b514ee945f1f9b0 src/column_momentum.rs +bd2a20a11f927c2a886b524a4a45791100f9c3e8119b7648153b7775cb6a3299 src/column_shear.rs +cd58d4d7d1c3d94092374ebd65fd191e3cf58f7ae68caeaf30ca15d36df666e0 src/column_surface.rs +dc0f2a3a8ebcb108c6a094ec59a467f1390b846dd27c505b16248a64dc1031b3 src/compressible.rs +b296021b8dd2f763b6eeab1e49e78bf06ec2ec801ac9595441e9e3920235d8e7 src/coupled_discrete.rs +90e9249fd82cc829a84cea9162d680971e660bfc6c32619355b353d17b25a431 src/dense3d_sequence.rs +57ea2badaeb329fe91647df98be60bff8dbe693e41076979eda4b79ad8f48f84 src/emitter.rs +47d0b180dafc7e57ad1b205f2a12c5a472a78c1d60a7c3f7521ec470211a89c4 src/emitter_controls.rs +afe2b79d11f1b8c0ccd16dcaae51a22c8d35ba2e7a23c362729fe907b208f0d3 src/export.rs +0181db7ffbad932b7e3492ce1a8e260736389e4ded3472c81fe2697861f6777a src/fitted_height.rs +3cc724d376d115fccd8b6c658d7f26b6436f4d13d105a5ae8416fd9c04b17a47 src/fixed_bottom_ale.rs +adecbc5f92b89179a2e38885be2acbcddd42df40061f00ffa5d758224ed045a5 src/force_field.rs +7b3b48c0813c995a937a44ca1b624411c59dde6419b62041b67456cde2d692d9 src/forces.rs +f43fd9a71da6aac985dfcfb6cc471318227c1727d6e0dad8e2a5feb5b3728a8e src/free_surface.rs +712d04f27d55d7e77f17effefbf2660ca3b8a000de5b7aa51b42b89fe5d3b634 src/geometry.rs +30f5b78a98f6d0c010955a25fc00cc2de4aadf159d260e95afd3f3143dc6f9c3 src/lib.rs +f99dce6a99f6af8a9aef2436af7f33de58dac3be5998a48e9187fbdf2acc3f93 src/liquid_step.rs +24623038f62261ce6a17b034e9b6d82c5f0aba02dd0df7aad174c885ed2a8c96 src/liquid_volume.rs +49874ff972f16a972d706236b34e62f83e994c57e39b642c6d94a1a6f4b7fb66 src/material.rs +4e18e446ad82f10523f72bae1b7cc6007f2ecf5ad05407bb666be3e2ae9be960 src/material_session.rs +71503d2c5939dbb59946bc157eedafecdedd0831f1e81d29e794fba7290dfd24 src/mesh_collision.rs +3e8f2d79e59b3f27c7ffeeacc83d9b7d08faafa2fffa7de81dfd8665b36a6890 src/mesh_import.rs +8ab9f26199f36d76cb2f5e4cb58d40905205f0d6fc90988549f7e389c489a3ea src/motion.rs +2c6d4cd75cdb0f2d560d0f79df4e53914e10f340de0740b939311d8cc35d5dee src/observation.rs +b45262632483ce08773924b407eeffbe99bd62663094ce6fd71b24d132026b07 src/obstacle_pressure.rs +745da07d5e949276a2e0cb7ac4bdde4aedf557bf6189d5a51581d8ce79b1dc1f src/obstacle_shear.rs +cd98684d5204908e5344b2628a8f93ea2a55a90c6678e03a71f2d75ab1f2be38 src/operator.rs +ee5420673a59ae4fb247b3542878e74902f7a87056744d4303fb400be074677e src/parallel.rs +538c498122bbc1045ae59d7363ff87e1dd9a40003fb48eeae835228ad4c65893 src/particle_fluid.rs +39ddb79dd3de196e83545679e556c0cd8ce9afae9db9282161f54b8f22f713a5 src/pressure.rs +ae687a8822dd6cfeadf5dc6e13e7983d8d1afcdf9e5db1857d4dce64cd0e3fb8 src/pressure_session.rs +79167b70a8d164a2367f43816feab443eefb41f23bc4bd940716dd5fd32d495b src/provider_step.rs +47b2b24bbe9af777ceafdefa6f0b82200a60cea590d0b64f326de1d50c6d7d2d src/recording.rs +5a9b10b929f650423ad9e58bd7047bfe80cc7bf18138d74bd628c2f3f17af711 src/recording/carrier.rs +5a82201f4583cf9eb4cc38d43c7ee88bc54d10a84df46272c2dd84091aa4a307 src/recording/comparison.rs +279aff886f1354af203429a2e43cce73cdb485ee5ddd90201d1498e5e7872b7f src/recording/diagnostic.rs +2cf3ec9ec191c35c814d48ace301feaccd6cc73311a66970c9d112dad432c4a9 src/recording/force_fields.rs +e72e256aa3af46fef9002822de5281caf1de66ba070eb2109f77b083e20ae6ce src/recording/material.rs +980ea5af7094016b13e7d0f39110aa2ff673b0173416e0fe583e6266b7c27f57 src/recording/provider.rs +90c1230c2cbfb2415c74e5d3c9b66e74f68c7b2096bf4063c5513ac2c6feaa75 src/recording/provider/comparison.rs +d7b2a1f7cfa866b33887f7c8f804999058d6e70a1c9d71421372d151152ebccb src/recording/provider/runtime.rs +6bf158f71f795078b5a88a5662ec2169693508e555f0f98d539657873aaabbcf src/sampling.rs +21fa031c06eeff8c2f722beb5553af9ba86d0afab479f4036916480338dc1411 src/scheduling.rs +367c370e2257e35de09943e8dc3e1257b994c5e01d1409af1a989682f41c583e src/session.rs +d99ae990e9c7ddc956f95f414a25832411a4dc84ab434df95ff9f8f68cf52a83 src/session/configured.rs +0b4140af63144df8ff1839782b3c70295c18b96decc67aba348a37a33e9f996d src/sgs.rs +c08ede6330c4bf6ed7e858e556806ec80628522ceb42286cf52714bf7430c9a1 src/simulation.rs +0ab7db08a9016e73d664c730e4ff513d42599a84c680ea061e6604859432a5a5 src/solver_provider.rs +124ca65dd1dec3a8a89043198fb085453a42ca8aff0dc87c67b04b6a986109be src/solver_provider/configured.rs +763debfefc6f9375849c8f503995e20660fc25efef9e827d5942d980e14fe693 src/solver_provider/context.rs +c597450ad73a4dede64a1fedcc65a0b4d5551f594f29b2d0cf7c85b0e2e14796 src/solver_provider/modal.rs +df50f3761fd7258ca42026ba852d2c326a1a5efa42daa4021967b799493b1080 src/static_obstacle.rs +1007f35400b5e1b3206653b9f9ccaafbc169617ad896aec54f94fc792eba2fae src/tracer_barrier.rs +6fa240053738411bb93b3e365def39fdc8a93597cd0c0523dcd1af39cbc68163 src/translated_viscous.rs +3cb7643d321fa94957fafdc101abf0ee9ef273f99c9a512938124e72f32178aa src/viscosity.rs +56c3f03d957df0a7fd0c00a1af229f47f1c21cdf53cbf3770656a9bb0b66735e src/vtk_export.rs +2e6c78e51d457bfe0cda4972dbfec5677e9e3a5e4f42a8647aec8aa1f4b73f2d tests/advection_contract.rs +7b43a757245edc097874bfd23ec0690d1d4a8df9eb6186ab86e843720dc7a687 tests/aligned_stokes_contract.rs +d2270f86bd04c36501b59f01772656bda264d2c70b8613429701a268a305da16 tests/aligned_strain_contract.rs +d3a23e9ae3973044183d22c5dbc9b2a00c2d0d731cae1777426337310949133d tests/box_flux_contract.rs +4adc5ddf0e878cffc3e70b47bcb0f98cad07aef4489a41f9fa7d1695304bd94f tests/box_flux_step_contract.rs +83d3c9a839dd2c722b3c509e051350a6ca1c754cbe0ae5dbb521e7d3346c6aef tests/cli_contract.rs +8625b6a0d01271b90f17fd22b0ea4f5d0869827fad6537720fb631ce565367ac tests/collision_contract.rs +3fc23b6ff935696bb96082e824cc1a5c2cfe32821559074f6ec00b3b8000afdf tests/column_mac_contract.rs +4ea859692dc437e98bdce6fc3fdfb55a8821ffc52699c8a7596ddc7d75e8aec7 tests/column_momentum_contract.rs +466dab4d7dd6cfa464de3428da8b0b9aadfb3a2699810ca302e9bec90db62f12 tests/column_shear_contract.rs +515f39b3bf06fe23c1115201cefa1d4f1d69660ce4298daf3d4b7e16f5844759 tests/column_surface_contract.rs +ddb88a1faba015e1f636d66d6dbc8a770a1875ad52ada6d6a0a773b5c0e4f0e2 tests/comparison_archive_contract.rs +2bfcf188fbde8ba9908c90365eb10a3e708853d0a928267541a5cfb5358b22f2 tests/comparison_field_view_contract.rs +9f1582b436e268a7bef6d42dccc3ff0754b963942301eb26442d7f33790d5b7d tests/compressible_contract.rs +821ddd7d173c84453650d2cde938bf024d907e2f062a894ea2b182e44ac56932 tests/configured_archive_binding_contract.rs +ba4a8f26fbf0324d705a80a4b108b1e9d09a293ac2eb29a8278fe01f65dc5a00 tests/configured_provider_session_contract.rs +8205a1b5ddb1248d545edc27d4b23f27fb89717f311ec336d01bb3202591f2fa tests/configured_recording_comparison_contract.rs +29b29888112126677262bdd05792cad70170338eaac8c4767fc2ab0292a9e927 tests/configured_run_sets_contract.rs +3a730e8dbeaf8fd81a19eee5717b4719f62af83e42c395691535a1ed6f1a0116 tests/coupled_discrete_contract.rs +678058ce656cde9d0b8c5f3ac9de4da5a81ca3f76d5003c1954ff3929d964625 tests/dense3d_controls_contract.rs +016ba99a4a234ced02a1613d5cc4af6072f3e070f185bf8ad25ca73202b128a7 tests/dense3d_sequence_contract.rs +b492194a384779fd15ba967bac93a5068d1635d5ff5b443d13a31252716134d7 tests/diagnostic_pressure_oracle_contract.rs +3cd9e0a01916ed416cffd93f96aa463447beed404f202e3e2f5783c76122b758 tests/diagnostic_recording_contract.rs +9288b53744ff31fb4e338be13d8b4e9e68ea58aec2e78a15d5aa8850da0a9d4d tests/emitter_contract.rs +7b3ad890d44d70652c04e119156dd6ecc32cb79587c87e49b1fd2fc72f6caa66 tests/emitter_controls_contract.rs +299ace3dc49a9990b52cd2ee55c974769c3acda5fc9ce43331fdb0cbfd34c070 tests/export_contract.rs +19968c75c9dd95c1c19cc2a4f6a605d4a047bc20290562fe693fc7e8b628fbe2 tests/extruded_third_contract.rs +391e6d3c992442a5a3a2e38f4d1304a20602047808831bd873567b1d8224a61f tests/fitted_height_contract.rs +006968122943cd48290f302cdc33115d7c40058d992f8f38df1d5fbf28f57289 tests/fixed_bottom_ale_contract.rs +958ba74ebde596aac8375eb436898a1e2342ba1b7868c08bfee99c09c02dc27c tests/fixtures/accepted_pressure_adapter.py +1a27b9a7754d7e04ef3a05d1df57f9cff9d708f24b6ca2a913f761ed55e2c317 tests/fixtures/collision/README.md +19ee1217f457696346ea61ca36ab09c26256d732cf9b02c962a81878cabfca4e tests/fixtures/collision/cube.bin +d7aaad641dffa913e68fddeb404e7cc06714f7ff546a902cfb72ad0fa404d4da tests/fixtures/collision/cube.glb +12a32152f8323a8bdea42b7c25ada7ff49737d2328aafda671497029f3aa0521 tests/fixtures/collision/cube.obj +a0717a2b27629734e7946773671e34a1f8b7d572949a7c69a21d1e239c56a38e tests/fixtures/collision/domain-box.obj +4e050abe9a4a6ed810aa29c2d73051ac3cef413aa036ce33f711f87a6e6ce278 tests/fixtures/collision/embedded.gltf +a37f48c741dc15ea24ad75796984661531955400bac7417ef43a0f4b1db0c116 tests/fixtures/collision/hierarchy.gltf +01ad2acb063b21c29cc379191c1048f5a365d707de428693cec7ae30c744e98f tests/fixtures/configured_box_pressure_adapter.py +9ec95cd43f44f8f8f2f908057b135e16d0a92d790c60dca934946d9ab8cba889 tests/fixtures/pressure_adapter_correction.py +e3dceb0db85270f79a835149463892a8eee19442815dbc93abf3434132410047 tests/fixtures/pressure_adapter_full.py +1f97de31f8db1db38ff581e0ce702ffad7c16284fb29c39e8d61fc51ac9c645c tests/force_field_contract.rs +9efdf712baaa8e662e77339a81074751ed10ef01b9675f99351fac2a662b41fc tests/force_field_solver_contract.rs +e9ca49fdeb0f474cdcddb40930c3773dc71762a709ac7dec6200fd5db31859a7 tests/force_recording_integration_contract.rs +b8a5da48feab2eef82cc1600945cc30ff15082c0b447d8b5eb0c635821cf27eb tests/forces_contract.rs +8f9584c26f7a816bb53f15bb70ddec168c0949adf9ad7847ad2edcfb0af40e9d tests/free_surface_contract.rs +87b34154f609767f1521a57531f6f45df4d965685113dcb43806069299865f4a tests/geometry_contract.rs +e8b3b88536bd5d38dfda7d1454e916277e5314bbb9410bda81630faffe76c621 tests/implementation_contract.rs +dede02f6a5fc9eff67d87814b1845bfc6295c7a22098121217fee70822a4c3fb tests/liquid_scale_division_contract.rs +e1aa67f673de131f97a1be651221db3f74a8beeed105076532ceeac2b92b5b12 tests/liquid_step_contract.rs +9eac2c3feedf24efd3e34f1651ead5ea14f036dbfb238af829650d5b77006ec2 tests/liquid_volume_contract.rs +c5a1e89205b6cb65df69a1c10fa905f85d5efa6bc11c591211d509cb40161257 tests/material_contract.rs +9db6a8cc0558b4b04b4039ab8b7c74bd57c2f69a15d05b2b553713713a8f387b tests/material_reference_contract.rs +9fadbca17897a93e9dc95be24a42c5181bfeaaf35aad650ea4bf6f477b9e580f tests/material_session_contract.rs +e8977a66763745cfb807e9bcae392be3aa39ebca113aa3db4c4d4eb39447c8ca tests/mesh_collision_contract.rs +a568e510cb457f8d90bffd2c99eb1c05d64ce5ac4c34949a58479831ffad3da9 tests/mesh_import_contract.rs +290def29e35b9597d1c56757f5fb70d9ac100d95ca67b616c54533d31f8e0f82 tests/native_comparison_oracle_range_contract.rs +54a89d2df244839dec1931cde92117cf1bb832ffb0b6eb2ca32ca9ec28a8f7ed tests/observation_contract.rs +7e5318ca45b6932818accfc748cc70ae6cf1719f6047c2d2f9343eb8cedcd020 tests/obstacle_flow_contract.rs +b0ea207c02df69aed1f5f3eb6abda8fdeb245b271c325b383c3748d945c5095e tests/operator_contract.rs +b91c7c87698c1a7b1d3b92c7b252781d96616df37c8a6ef391d4229a251668f2 tests/parallel_contract.rs +9e36d134d2391786457ff23bb20df63a5aadf29a8ae508cffec2de824528bc93 tests/particle_inspection_contract.rs +3c413b5c903194ba8f1a83970275d066ba93a5568f0d43a27f93788f3c4af5fd tests/plate_coupling_contract.rs +244afca41c35a929588f5dd0168ae682b1ae0457f0edc3f821b95ed93d7f2f36 tests/pressure_context_contract.rs +f343f7f23965d963df8d734d624c63b12acaf165936ba2635e1b71816ee16c91 tests/pressure_contract.rs +a2471a0faa91bac0acd4c824ee4b1175fe6f827357246218684ae47104aea0ce tests/pressure_session_contract.rs +116c03830ed617e585a6eabe1dd9d8e826ed833039e4397de565d218ec9852e5 tests/provider_step_contract.rs +8a3a5cd307b9b5e2b646165502029018eeb9e36268986b679094a9a1e43dfba0 tests/recording_carrier_contract.rs +4c27fc27b241c033eeae0903dd143fe1800cc8ea5ab9829c8b13e22105ee133b tests/recording_comparison_contract.rs +3db4e56207de52576aae5838d30e02894590ba843d1af8d10e5e7fd5a772ee8a tests/recording_contract.rs +0766d5d9ac55fd8102c23b9492e11d41e4caa931249531a8b36824b2a97039c0 tests/recording_provider_contract.rs +7f02cf7510be8bd9bca9b3af380e80401ead89ee9b5c2e2561de65e31c5f6cd1 tests/recording_provider_runtime_contract.rs +3e99ff52088aa37d4043453a57c82bbe6de0da84e990796df76a3453f1ae004d tests/sampling_contract.rs +f3bf401a00ccb9ba67de70ff72c6c3a0b7216525073a6f6c8239ec022e0bec1f tests/saved_comparison_ui_contract.rs +6e001075a4307da0e51df04c4521bc8599194ce06fbf010c08e34bab8f693ef9 tests/saved_run_set_contract.rs +39cb5152583c1662f8dfb71217409025084c4e05be1a18e1b7b3912fa4d21ca5 tests/saved_source_contract.rs +4f53c8d35eeb44819791341b8ce1a80421d9562ab8413f528a3234da233632f7 tests/session_configuration_contract.rs +50f7a8981d1bc22ad63f990b01027b0ae26c4dc01192dc5dd7d9723f7e44da68 tests/session_contract.rs +4cdc0a85d21d77784ef142ba04f1ef20fb0333555d7bbb15b43465f475934769 tests/session_interval_start_contract.rs +f81fb0696307aa07abba0d8b44316430aeab19317c57d38db3222cdc9e33ddc6 tests/simulation_contract.rs +0c063df217150bf407af3f6af9d39101bc37aa3192e00e0833b43042ebc06143 tests/solver_provider_contract.rs +fcc7546886b865ada1641dd7ee3549534f2273d908c19047d886c49a048157de tests/static_obstacle_contract.rs +20cb58baf441b56e7b5f72b9424de265c7bf656bb2c3532d4718220b8c291ce2 tests/submarine_stokes_contract.rs +433edf41b549171f3a3c412e6d9233345f4ca8f45e6f5ebe5468638d3db9aca8 tests/tracer_barrier_contract.rs +d6fda116af1ae38a3e9216b97136562c144bfe1a5269734d002c3117c9b65e79 tests/translated_viscous_contract.rs +e1fd504cb399b313bbb1df752f79e72cb51556c5af6d85dbb91cb5c690fb5c8d tests/translation_contract.rs +b716ef20283784d9a61bc95702c97c30c8e7d00c80f95530c46d27e667586c9d tests/viscosity_contract.rs +3df0cd4bfe900adb855fa65d9ec267e43544961fd5608d4f1ab24258c6a44532 tests/vtk_export_contract.rs diff --git a/docs/plans/force-authoring/recording-integration-plan.md b/docs/plans/force-authoring/recording-integration-plan.md index 636b5141..64c81a52 100644 --- a/docs/plans/force-authoring/recording-integration-plan.md +++ b/docs/plans/force-authoring/recording-integration-plan.md @@ -1,7 +1,9 @@ # Prepared post-custody integration validation Draft PR #145 publishes the core at 3e839cbf7c6aca56e8297ef740f7e48543de77b1. -This follow-up adds only ignored test source and this plan. It changes no core +This original preparation record is retained; the later authorized execution +on corrected head 7970a5d is documented in validation.md. +This follow-up initially added only ignored test source and this plan. It changes no core or desktop source and executes no numerical/SQLite tests. The compiled cases are proposals for separate review/authorization, not passed integration evidence. diff --git a/docs/plans/force-authoring/validation.md b/docs/plans/force-authoring/validation.md index b5abfc75..b86be15c 100644 --- a/docs/plans/force-authoring/validation.md +++ b/docs/plans/force-authoring/validation.md @@ -234,3 +234,104 @@ Post-custody source/Cargo/test seal is evidence/custody-source.sha256; its SHA-2 is `d873dd98d5d59d1f1aac2221cb5f1f30ae574bd6dff91c95d95d82a0c927a9ca`. It deliberately differs from the pre-custody runtime test seal; the runtime results must not be attributed to this newer source. + +## Authorized post-custody validation on corrected head 7970a5d + +The parent authorized the five existing adapter tests plus the two independently +reviewed recording cases once each, serially, with the existing process bounds. +Before running, source inspection found cleanup-prone failure handling in the +recording tests. Test-only commit +`7970a5db8136c3771ea416db86c58753aea95566`, based on authorized +`1d44c8429701f205f678b602040d4f62ea6457b3`, corrects failure preservation. +No src/** or adapter-test source changed from 1d44. Independent review confirmed +unchanged helpers/call counts/limits and no blocking finding before execution. + +Archive Drop now retains/logs owned directories on failure. KeepOnPanic suppresses +outer writer/reader destruction during assertion unwinding, while successful +explicit closes/cleanup remain unchanged. This preserves test-boundary failure +files, not a pre-error/in-flight image: API-internal rollback/constructor cleanup +may already have happened before returning an error. Signals also leave the +owned directory. No unexpected test failure occurred in this run, so there are +no failed originals to reconcile. No unrelated cleanup was performed. + +Build only (no Cargo test execution): + +```sh +cargo test --locked --offline --no-default-features --features force-serde --test force_field_solver_contract --test force_recording_integration_contract --no-run +``` + +Both binaries were copied and sealed; saved Cargo fingerprints report only +force-serde, with no parallel feature. Exact seals: + +``` +adapter_tests: +ceea9adb9d9023107e7211ca4792d87d6fb7f3d4cc0b6e06f60f313ad1dca773 +recording_tests: +4ee715678545204a996b70121d26dcb930578e7e5f94cafc27d111372af69e5b +full source manifest: +26cce65fa43bab619cd0951dbc523945c37fd0bc955c06b3451448ca82cc5277 +adapter test source: +9efdf712baaa8e662e77339a81074751ed10ef01b9675f99351fac2a662b41fc +corrected recording test source: +e9ca49fdeb0f474cdcddb40930c3773dc71762a709ac7dec6200fd5db31859a7 +``` + +Artifacts are local under /workspace/force-validation-post-custody-7970a5d. +Source seals, feature fingerprints, exact executed harnesses, command/result +JSON and byte-preserved logs are committed under evidence/post-custody-7970a5d. +The large binary copies are local only. Binary hashes were rechecked before each +child. The supervisor's bounded() and run() function text is byte-identical to +the earlier supported harness; only paths, binary seals, exact names/count and +libtest output flag were configured. No supervisor redesign occurred. + +Exact harness invocations (all inherit the explicitly owned scratch root): + +```sh +TMPDIR=/workspace/force-validation-post-custody-7970a5d/scratch python3 /workspace/force-validation-post-custody-7970a5d/adapters/run_bounded.py enumerate +TMPDIR=/workspace/force-validation-post-custody-7970a5d/scratch python3 /workspace/force-validation-post-custody-7970a5d/recordings/run_bounded.py enumerate +TMPDIR=/workspace/force-validation-post-custody-7970a5d/scratch python3 /workspace/force-validation-post-custody-7970a5d/adapters/run_bounded.py execute +TMPDIR=/workspace/force-validation-post-custody-7970a5d/scratch python3 /workspace/force-validation-post-custody-7970a5d/recordings/run_bounded.py execute +``` + +Enumeration used each binary's `--list --format terse` and required exactly its +five/two expected names. Each adapter invocation was the sealed adapter_tests +path, one exact name and `--exact --ignored --test-threads=1 --nocapture`. +Each recording invocation used recording_tests, one exact name and +`--exact --ignored --test-threads=1 --show-output`. The latter keeps successful +name/result lines verifiable while retaining captured owned-path output. + +| Exact test | Result | Log bytes | +| --- | --- | ---: | +| authored_mac_matches_legacy_bounded_directional_force | 1 passed; exit0 | 178 | +| authored_mac_cancellation_retains_accepted_state_and_retry | 1 passed; exit0 | 183 | +| authored_particle_density_conversion_adds_to_existing_acceleration | 1 passed; exit0 | 191 | +| authored_particle_cancel_retains_state_and_retry | 1 passed; exit0 | 173 | +| empty_snapshot_preserves_both_legacy_paths | 1 passed; exit0 | 167 | +| carrier_force_archive_round_trip_and_failed_attempt_invalidation | 1 passed; exit0 | 477 | +| particle_force_archive_round_trip_and_rejected_control_custody | 1 passed; exit0 | 471 | + +Each child applied/checked hard+soft CPU20s, address space536870912 bytes, +file/log1048576 bytes, disabled core files, test-threads1 and no parallel pool. +Parent wall timeout was30s with TERM then owned-child KILL after1s. No timeout, +signal, unexpected selection or observed bound failure occurred; there was no +retry/tuning. All seven result/log SHA-256 and byte counts were verified after +execution without rerunning tests. No cgroup/mount/security setting was changed. + +Recording cases used one fresh explicitly owned directory/database each, with +RecordingLimits database1MiB/record64KiB/max_records3 and two committed rows. +Each verified its expected mismatched third append rejection before transaction, +failed-prefix lifecycle, actual custody and exact readback. Carrier used four +cells, one accepted + one cancelled call, <=200 pressure iterations. Particle +used64 parcels, one accepted + one callback4-cancelled + one invalid call, +<=19 preparation entries. After all assertions passed, only those owned +recordings were successfully torn down. The task's scratch root was verified +empty afterward; no unrelated path was removed. Existing post-run file audits +passed; they do not replace process peak-resource enforcement. + +A preflight Clippy collapsible-if finding in the new test guard was fixed before +sealing/execution; no runtime failure was tuned or retried. Targeted Clippy with +warnings denied, formatting and diff checks passed. This evidence establishes +bounded current adapter/recording behavior at 7970a5d, not scientific L1/MMS, +calibrated physics, desktop parity, emitter/provider/free-surface/session paths, +successful legacy stamp clearing after authored steps or exhaustive historical +archive compatibility. No additional numerical/SQLite tests were executed. From a4326a78a747bc868874fd16e434d780fd5a6d35 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Tue, 20 Oct 2026 23:47:40 +0000 Subject: [PATCH 9/9] docs(forces): admit reviewed backend source for fresh qualification --- .../NATIVE_ADMISSION_INTEGRATION_REVIEW.md | 82 +++++++++++++++++++ ...n-native-sources-integration-20261014.json | 22 ++--- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/docs/education/NATIVE_ADMISSION_INTEGRATION_REVIEW.md b/docs/education/NATIVE_ADMISSION_INTEGRATION_REVIEW.md index 64884aa6..06d86c1e 100644 --- a/docs/education/NATIVE_ADMISSION_INTEGRATION_REVIEW.md +++ b/docs/education/NATIVE_ADMISSION_INTEGRATION_REVIEW.md @@ -1079,3 +1079,85 @@ No numerical kernel, tolerance, recorder/model contract, selector, qualification gate, historical inventory or A1 status changes. Initial 94062 execution receipts retain that historical subject; fresh clean committed qualification, affected checks and current-head hosted gates are required before integration. + + +## Shared authored force fields and accepted recording custody, 2026-10-20 + +This explicit source-admission extension covers the backend force-domain branch +at `43ec505218c43594641ecbb80873bec8c88d7d2c`, based on main +`fd7c0f0aee63bc8710df99f9bb01154e7619b974`. The admission commit changes only +this appended record and its version-three inventory binding; native source +bytes remain those of the reviewed subject. The complete conservative closure +grows from 124 to 126 paths: two added, eight changed, 116 unchanged, none +removed. Every `src/` path, build/toolchain input and existing independent +qualification input remains selected, including unchanged desktop sources. + +The bounded source scope is: + +- `src/force_field.rs` adds version-one authoring values, stable object IDs, + rigid transforms, enabled state, static half-open activation intervals, + directional/radial/vortex fields and global/sphere/box support with optional + smooth edge attenuation. Validated immutable snapshots share one evaluator. + Acceleration is in m/s²; force density is in N/m³ and requires positive + density in kg/m³. Finite-input, zero-distance and bounded-object checks + refuse unsupported or nonfinite evaluation. +- `src/forces.rs` shares the existing MAC face-update loop with authored + acceleration evaluation. Closed-box walls retain their existing treatment. + `src/simulation.rs` adds explicit force-aware fixed-box stepping and + success-stamped pressure/actual-force custody. The legacy pressure accessor + cannot expose incomplete force controls; candidate attempts invalidate the + scratch pressure stamp before use. Pressure projection itself is unchanged. +- `src/particle_fluid.rs` adds the same evaluator to gravity and pair forces + at pre-integration particle positions, using the evaluated local particle + density for force-density conversion. Accepted force controls are stamped + only on success; rejected attempts preserve the existing accepted particle + publication. Source emitters, constitutive stress and turbulence closures + remain distinct owners. +- `src/recording/force_fields.rs` adds a bounded version-one binary metadata + codec with exact float bits and scene validation. Force-aware run identity + binds the scene digest; append and read check run/publication agreement. + `src/recording.rs`, `recording/carrier.rs` and `recording/material.rs` + retain actual accepted force inputs and decode them for selected archives. + Historical unforced hash/control paths remain separate and unchanged. +- `Cargo.toml` adds an opt-in `force-serde` feature using existing dependency + identities; `Cargo.lock` is unchanged. `src/lib.rs` adds public exports. + No desktop/request/worker source is changed by this backend branch. + +Exact added/changed source SHA256 pins: + +- `Cargo.toml`: `f925dae7a0d74e6024e6e0d64962a275ae4f28fedec5b016341671021b8239ae` +- `src/force_field.rs`: `adecbc5f92b89179a2e38885be2acbcddd42df40061f00ffa5d758224ed045a5` +- `src/forces.rs`: `7b3b48c0813c995a937a44ca1b624411c59dde6419b62041b67456cde2d692d9` +- `src/lib.rs`: `30f5b78a98f6d0c010955a25fc00cc2de4aadf159d260e95afd3f3143dc6f9c3` +- `src/particle_fluid.rs`: `538c498122bbc1045ae59d7363ff87e1dd9a40003fb48eeae835228ad4c65893` +- `src/recording.rs`: `47b2b24bbe9af777ceafdefa6f0b82200a60cea590d0b64f326de1d50c6d7d2d` +- `src/recording/carrier.rs`: `5a9b10b929f650423ad9e58bd7047bfe80cc7bf18138d74bd628c2f3f17af711` +- `src/recording/force_fields.rs`: `2cf3ec9ec191c35c814d48ace301feaccd6cc73311a66970c9d112dad432c4a9` +- `src/recording/material.rs`: `e72e256aa3af46fef9002822de5281caf1de66ba070eb2109f77b083e20ae6ce` +- `src/simulation.rs`: `c08ede6330c4bf6ed7e858e556806ec80628522ceb42286cf52714bf7430c9a1` + +This admits an attempt at fresh clean committed-source native/rational +qualification only. It is not a numerical receipt, scientific validation of the +new force laws, native-window evidence or completed desktop parity. Pure +evaluator/codec checks and the seven bounded adapter/recording tests have their +own pinned subjects under `../plans/force-authoring/`; they do not substitute +for the fresh independent qualification required here. Later desktop/runtime +composition must re-establish complete admission and fresh evidence for its +actual committed source. + +The historical v1/v2 inventories and all preceding review text/receipts retain +their original bytes and subjects. The v3 ancestry/schema and predecessor +digests remain authoritative. No source selector, numerical threshold, +qualification runner, mathematical oracle, workflow or immutable primary +identity is changed. A stale or malformed present inventory must still refuse +admission rather than fall back. Fresh receipt binding must include the actual +committed manifest and this complete review record; earlier standalone CI +success or historical receipts cannot qualify this admission commit. + +Independent review of this exact two-file candidate is required before atomic +commit and publication. This record contains no claim that an unseen patch was +previously approved. Native/rational CI uses its existing 20-minute job bound. +The separate research-book job performs admission preflight and real packet +rejection tests normally and under optimized Python within its existing +90-minute job bound; readiness requires those actual current-head outcomes, +not an assumption that the standalone qualifier includes packet tests. diff --git a/docs/education/aligned-strain-native-sources-integration-20261014.json b/docs/education/aligned-strain-native-sources-integration-20261014.json index 2169dc08..1445dd19 100644 --- a/docs/education/aligned-strain-native-sources-integration-20261014.json +++ b/docs/education/aligned-strain-native-sources-integration-20261014.json @@ -1,16 +1,16 @@ { "schema": "rheon-reviewed-aligned-strain-native-sources-v3", "reviewed_base": "21d4122c3d0edb2ca5c3ce089175c70035d737a3", - "scope": "Independently reviewed 1000 cells/m and 10000-axis native/independent-preview controls, including honest diagnostic preflight work/reference-memory accounting; complete 124-path closure with 15 approved changes and 109 unchanged inputs. Fresh committed-source native/rational and hosted acceptance required; measured A1 remains blocked.", + "scope": "Explicit shared authored force-domain, fixed-box MAC/particle adapters and accepted recording custody at 43ec505; complete 126-path closure with two added, eight changed and 116 unchanged inputs. Source admission only; fresh committed-source native/rational and packet/publication checks required; desktop/runtime composition remains separately qualified.", "preserved_inventory_sha256": { "docs/education/aligned-strain-native-sources.json": "84c036618b32de66623d8ded24ae8cf95194457e36b9a130087a9c2bf3bd3f6a", "docs/education/aligned-strain-native-sources-runtime-20261013.json": "6ada2133501aab449ed7ee73f802d8a4593db50c857f0a8f8ce2af4507721d6d" }, "review_record": "docs/education/NATIVE_ADMISSION_INTEGRATION_REVIEW.md", - "review_sha256": "5836c3fa71bf20b72dbde79a2f5d27dd281ffc3930e0af7ad81f9039fd3edd42", + "review_sha256": "0003bab3b6e4332592319783b140dab64c651b3b0ca1a54d11020e13209e20de", "source_sha256": { "Cargo.lock": "3b5836a7be9f1290c698cadaff010e11b93e33b4af839c8549391c23e7d8ec0c", - "Cargo.toml": "10a7b438ec5e7bdb2c8eb95137cf5a42d1cd35687ed47bf63b05a3e42dc1674e", + "Cargo.toml": "f925dae7a0d74e6024e6e0d64962a275ae4f28fedec5b016341671021b8239ae", "examples/aligned_strain.rs": "11910f4536bbb0057bd7462864f4d2af07d37dcd02103f41d1b93674dcca4547", "rust-toolchain.toml": "09ff66fa19e0cb8bbad00b6032e28f1cf77b0a37a73013648177b35297f98800", "src/advection.rs": "0163a872c314b2b3bb7f732348aafdbfa7b162dceea74807b7e7e3eb60ab0849", @@ -83,10 +83,11 @@ "src/export.rs": "afe2b79d11f1b8c0ccd16dcaae51a22c8d35ba2e7a23c362729fe907b208f0d3", "src/fitted_height.rs": "0181db7ffbad932b7e3492ce1a8e260736389e4ded3472c81fe2697861f6777a", "src/fixed_bottom_ale.rs": "3cc724d376d115fccd8b6c658d7f26b6436f4d13d105a5ae8416fd9c04b17a47", - "src/forces.rs": "c5585940330096890016ff68f25645aa12cdaec2b796a8b234c138e88dd892bb", + "src/force_field.rs": "adecbc5f92b89179a2e38885be2acbcddd42df40061f00ffa5d758224ed045a5", + "src/forces.rs": "7b3b48c0813c995a937a44ca1b624411c59dde6419b62041b67456cde2d692d9", "src/free_surface.rs": "f43fd9a71da6aac985dfcfb6cc471318227c1727d6e0dad8e2a5feb5b3728a8e", "src/geometry.rs": "712d04f27d55d7e77f17effefbf2660ca3b8a000de5b7aa51b42b89fe5d3b634", - "src/lib.rs": "c6dfcf187c1cb2bb64835bd7c72a7094b6127730c55bb6ee47d2e676354317ff", + "src/lib.rs": "30f5b78a98f6d0c010955a25fc00cc2de4aadf159d260e95afd3f3143dc6f9c3", "src/liquid_step.rs": "f99dce6a99f6af8a9aef2436af7f33de58dac3be5998a48e9187fbdf2acc3f93", "src/liquid_volume.rs": "24623038f62261ce6a17b034e9b6d82c5f0aba02dd0df7aad174c885ed2a8c96", "src/material.rs": "49874ff972f16a972d706236b34e62f83e994c57e39b642c6d94a1a6f4b7fb66", @@ -99,15 +100,16 @@ "src/obstacle_shear.rs": "745da07d5e949276a2e0cb7ac4bdde4aedf557bf6189d5a51581d8ce79b1dc1f", "src/operator.rs": "cd98684d5204908e5344b2628a8f93ea2a55a90c6678e03a71f2d75ab1f2be38", "src/parallel.rs": "ee5420673a59ae4fb247b3542878e74902f7a87056744d4303fb400be074677e", - "src/particle_fluid.rs": "ed4ab916e602a55b2245beba5bdbff268fcb704b67fa41248d47ad2598f2468f", + "src/particle_fluid.rs": "538c498122bbc1045ae59d7363ff87e1dd9a40003fb48eeae835228ad4c65893", "src/pressure.rs": "39ddb79dd3de196e83545679e556c0cd8ce9afae9db9282161f54b8f22f713a5", "src/pressure_session.rs": "ae687a8822dd6cfeadf5dc6e13e7983d8d1afcdf9e5db1857d4dce64cd0e3fb8", "src/provider_step.rs": "79167b70a8d164a2367f43816feab443eefb41f23bc4bd940716dd5fd32d495b", - "src/recording.rs": "d022f8f46c288538298896ae444c9780265a6bf77fa9a4e1ba4b9732cf026eb2", - "src/recording/carrier.rs": "b9c83015df214b426315f1840cbd2029d3f4b85525aef2fc8592e97468d54509", + "src/recording.rs": "47b2b24bbe9af777ceafdefa6f0b82200a60cea590d0b64f326de1d50c6d7d2d", + "src/recording/carrier.rs": "5a9b10b929f650423ad9e58bd7047bfe80cc7bf18138d74bd628c2f3f17af711", "src/recording/comparison.rs": "5a82201f4583cf9eb4cc38d43c7ee88bc54d10a84df46272c2dd84091aa4a307", "src/recording/diagnostic.rs": "279aff886f1354af203429a2e43cce73cdb485ee5ddd90201d1498e5e7872b7f", - "src/recording/material.rs": "e01a730b04fd34d7fdc797ede735cb45fa743101dd51e8fd406b02bacdd43d00", + "src/recording/force_fields.rs": "2cf3ec9ec191c35c814d48ace301feaccd6cc73311a66970c9d112dad432c4a9", + "src/recording/material.rs": "e72e256aa3af46fef9002822de5281caf1de66ba070eb2109f77b083e20ae6ce", "src/recording/provider.rs": "980ea5af7094016b13e7d0f39110aa2ff673b0173416e0fe583e6266b7c27f57", "src/recording/provider/comparison.rs": "90c1230c2cbfb2415c74e5d3c9b66e74f68c7b2096bf4063c5513ac2c6feaa75", "src/recording/provider/runtime.rs": "d7b2a1f7cfa866b33887f7c8f804999058d6e70a1c9d71421372d151152ebccb", @@ -116,7 +118,7 @@ "src/session.rs": "367c370e2257e35de09943e8dc3e1257b994c5e01d1409af1a989682f41c583e", "src/session/configured.rs": "d99ae990e9c7ddc956f95f414a25832411a4dc84ab434df95ff9f8f68cf52a83", "src/sgs.rs": "0b4140af63144df8ff1839782b3c70295c18b96decc67aba348a37a33e9f996d", - "src/simulation.rs": "8dea790daf2a2d0801db3003094f9fffe034c9b87946d520a1eacc266c95f078", + "src/simulation.rs": "c08ede6330c4bf6ed7e858e556806ec80628522ceb42286cf52714bf7430c9a1", "src/solver_provider.rs": "0ab7db08a9016e73d664c730e4ff513d42599a84c680ea061e6604859432a5a5", "src/solver_provider/configured.rs": "124ca65dd1dec3a8a89043198fb085453a42ca8aff0dc87c67b04b6a986109be", "src/solver_provider/context.rs": "763debfefc6f9375849c8f503995e20660fc25efef9e827d5942d980e14fe693",