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/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", diff --git a/docs/plans/force-authoring/contract.md b/docs/plans/force-authoring/contract.md new file mode 100644 index 00000000..c7850124 --- /dev/null +++ b/docs/plans/force-authoring/contract.md @@ -0,0 +1,232 @@ +# 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 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 +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` (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. + +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 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. + +## 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 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 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. +Accepted force custody retains the full immutable authored force document. 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. + +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. 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 + 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. The five prepared bounded tests passed once on the sealed pre-custody binary + under the later explicit authorization. Seven pure custody/codec tests pass. + 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 new file mode 100644 index 00000000..e0550028 --- /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. 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 + +`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/.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 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/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/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/recording-integration-plan.md b/docs/plans/force-authoring/recording-integration-plan.md new file mode 100644 index 00000000..64c81a52 --- /dev/null +++ b/docs/plans/force-authoring/recording-integration-plan.md @@ -0,0 +1,130 @@ +# Prepared post-custody integration validation + +Draft PR #145 publishes the core at 3e839cbf7c6aca56e8297ef740f7e48543de77b1. +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. + +## 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 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. + +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. + +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: +`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. + + +## 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/docs/plans/force-authoring/validation.md b/docs/plans/force-authoring/validation.md new file mode 100644 index 00000000..b86be15c --- /dev/null +++ b/docs/plans/force-authoring/validation.md @@ -0,0 +1,337 @@ +# 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 + +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: 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: + +```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. + + +## 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. + +## 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. diff --git a/src/force_field.rs b/src/force_field.rs new file mode 100644 index 00000000..7821e478 --- /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, sync::Arc}; + +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: Arc, +} +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: Arc::new(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..3e6069fb 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, @@ -106,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]| { @@ -137,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; @@ -223,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/lib.rs b/src/lib.rs index 7a600376..7aac4f09 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -56,8 +56,13 @@ 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::{ + 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}; @@ -67,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 90eca618..9ff43111 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], } @@ -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,21 +386,59 @@ 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( &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, 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 } - 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 { @@ -398,6 +449,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 +691,14 @@ impl ParticleFluidSimulation { acceleration: self.config.gravity, damping_rate: 0.0, }; + 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)?; + 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/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 2db572d5..b9062237 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>, @@ -231,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 { @@ -251,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 { @@ -297,6 +314,7 @@ impl Simulation { generation: 0, paused: false, inspection_pressure: None, + inspection_force_pressure: None, }) } pub fn implementation(&self) -> PressureImplementation { @@ -320,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 } @@ -338,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) @@ -385,6 +417,68 @@ 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 interval_start = self.time; + 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); + self.inspection_force_pressure = Some(( + interval_start, + outcome.legacy.step, + CarrierStepControls { + requested_dt, + source, + emitter, + }, + fields.clone(), + )); + 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 +495,7 @@ impl Simulation { None, &[], StepDomains { + fields: None, emitter: Some(emitter), barrier: None, boundary: None, @@ -482,6 +577,7 @@ impl Simulation { source, forces, StepDomains { + fields: None, emitter: None, barrier: None, boundary: None, @@ -518,6 +614,7 @@ impl Simulation { source, forces, StepDomains { + fields: None, emitter: None, barrier, boundary, @@ -547,6 +644,7 @@ impl Simulation { source, forces, StepDomains { + fields: None, emitter: None, barrier: None, surface, @@ -585,6 +683,7 @@ impl Simulation { crate::ColumnMacError, > { self.inspection_pressure = None; + self.inspection_force_pressure = None; if self.paused { return Err(crate::ColumnMacError::Paused); } @@ -641,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); @@ -648,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; @@ -661,7 +762,9 @@ impl Simulation { mut cancel: impl FnMut(StepStage) -> bool, ) -> Result { self.inspection_pressure = None; + self.inspection_force_pressure = None; let StepDomains { + fields, emitter, barrier, mut boundary, @@ -687,7 +790,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 +954,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_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], + ); +} 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()); +} diff --git a/tests/force_recording_integration_contract.rs b/tests/force_recording_integration_contract.rs new file mode 100644 index 00000000..5ed216c5 --- /dev/null +++ b/tests/force_recording_integration_contract.rs @@ -0,0 +1,472 @@ +//! 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. Only explicit successful cleanup removes it; failures retain evidence. +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 { + 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); + } + } +} +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 = + KeepOnPanic::new(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 = KeepOnPanic::new(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 = + KeepOnPanic::new(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 = KeepOnPanic::new(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(); +}