diff --git a/changelog/entries/2026-07-11-physics-spec-persistence.json b/changelog/entries/2026-07-11-physics-spec-persistence.json new file mode 100644 index 000000000..0269d236e --- /dev/null +++ b/changelog/entries/2026-07-11-physics-spec-persistence.json @@ -0,0 +1,10 @@ +{ + "id": "2026-07-11-physics-spec-persistence", + "version": "0.9.4", + "date": "2026-07-11", + "category": "feat", + "title": "Persisted physics assertions re-verify via receipts", + "summary": "predict_physics gains a label arg: the spec persists on the document, build_receipt re-solves it as physics.static claims, and verify_receipt classifies Holds/Stale/Violated.", + "features": ["physics", "receipt", "verification"], + "mcpTools": ["predict_physics", "build_receipt", "verify_receipt"] +} diff --git a/crates/vcad-ir/src/lib.rs b/crates/vcad-ir/src/lib.rs index 603666d59..282f12a49 100644 --- a/crates/vcad-ir/src/lib.rs +++ b/crates/vcad-ir/src/lib.rs @@ -1807,6 +1807,10 @@ pub struct Document { /// `check_clearance` and receipt verification whenever geometry changes. #[serde(default, skip_serializing_if = "Vec::is_empty")] pub clearance_specs: Vec, + /// Named static-physics assertions (loads, supports, limits), re-solved by + /// `predict_physics` and receipt verification whenever geometry changes. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub physics_specs: Vec, } /// A named minimum-clearance assertion between two groups of parts. @@ -1829,6 +1833,104 @@ pub struct ClearanceSpec { pub min_mm: f64, } +/// Axis-aligned box region (mm, world frame, Z-up) selecting FEA grid nodes. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))] +pub struct PhysicsRegion { + /// Minimum corner `[x, y, z]` in mm. + pub min: [f64; 3], + /// Maximum corner `[x, y, z]` in mm. + pub max: [f64; 3], +} + +/// A load for static analysis: total force spread over a box region. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))] +pub struct PhysicsLoad { + /// Region the force is distributed over. + pub region: PhysicsRegion, + /// Total force vector `[fx, fy, fz]` in N. + pub force: [f64; 3], +} + +/// A fixed (anchored) region for static analysis. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))] +pub struct PhysicsSupport { + /// Region whose grid nodes are anchored. + pub region: PhysicsRegion, + /// Which translations are fixed `[x, y, z]`; absent means all fixed. + #[serde(default, skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "ts-rs", ts(optional))] + pub fix: Option<[bool; 3]>, +} + +/// Solve tier for a persisted physics spec: which resolution the assertion is +/// re-verified at, and which claim basis the resulting claims carry +/// (`predict` → predicted/provisional, `verify` → verified). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))] +#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))] +pub enum PhysicsFidelity { + /// Coarse fast solve; claims stamped `basis: predicted`. + Predict, + /// Fine solve, same oracle; claims stamped `basis: verified`. + Verify, +} + +/// A named static-structural assertion (voxel FEA) persisted on the document. +/// +/// Stores everything needed to re-run the solve — loads, supports, material +/// properties, limits, and fidelity — so `build_receipt` re-emits it as +/// `physics.static.