From f0931b47f7c8e7d677ebfcc7d58144990ad58b35 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Thu, 30 Oct 2025 02:33:23 -0700
Subject: [PATCH 1/3] tests(core): add golden motion fixtures (JSON) + harness

---
 .../tests/fixtures/motion-fixtures.json       |  8 +++
 .../rmg-core/tests/motion_golden_fixtures.rs  | 72 +++++++++++++++++++
 2 files changed, 80 insertions(+)
 create mode 100644 crates/rmg-core/tests/fixtures/motion-fixtures.json
 create mode 100644 crates/rmg-core/tests/motion_golden_fixtures.rs

diff --git a/crates/rmg-core/tests/fixtures/motion-fixtures.json b/crates/rmg-core/tests/fixtures/motion-fixtures.json
new file mode 100644
index 00000000..7de05d1c
--- /dev/null
+++ b/crates/rmg-core/tests/fixtures/motion-fixtures.json
@@ -0,0 +1,8 @@
+{
+  "cases": [
+    { "label": "motion-fixture-1", "pos": [1.0, 2.0, 3.0], "vel": [0.5, -1.0, 0.25], "expected_pos": [1.5, 1.0, 3.25] },
+    { "label": "motion-fixture-2", "pos": [10.0, -2.0, 3.5], "vel": [0.125, 2.0, -1.5], "expected_pos": [10.125, 0.0, 2.0] },
+    { "label": "motion-fixture-3", "pos": [0.0, 0.0, 0.0], "vel": [0.0, 0.0, 0.0], "expected_pos": [0.0, 0.0, 0.0] }
+  ]
+}
+
diff --git a/crates/rmg-core/tests/motion_golden_fixtures.rs b/crates/rmg-core/tests/motion_golden_fixtures.rs
new file mode 100644
index 00000000..26a512cb
--- /dev/null
+++ b/crates/rmg-core/tests/motion_golden_fixtures.rs
@@ -0,0 +1,72 @@
+#![allow(missing_docs)]
+use once_cell::sync::Lazy;
+use serde::Deserialize;
+
+use rmg_core::{
+    decode_motion_payload, encode_motion_payload, make_node_id, make_type_id, ApplyResult, Engine,
+    GraphStore, NodeRecord, MOTION_RULE_NAME,
+};
+
+static RAW: &str = include_str!("fixtures/motion-fixtures.json");
+
+#[derive(Debug, Deserialize)]
+struct MotionCase {
+    label: String,
+    pos: [f32; 3],
+    vel: [f32; 3],
+    expected_pos: [f32; 3],
+}
+
+#[derive(Debug, Deserialize)]
+struct MotionFixtures {
+    cases: Vec<MotionCase>,
+}
+
+static FIXTURES: Lazy<MotionFixtures> =
+    Lazy::new(|| serde_json::from_str(RAW).expect("parse motion fixtures"));
+
+#[test]
+fn motion_golden_fixtures_apply_as_expected() {
+    let entity_ty = make_type_id("entity");
+    for case in &FIXTURES.cases {
+        let ent = make_node_id(&case.label);
+        // Create a fresh engine and insert entity with payload
+        let mut store = GraphStore::default();
+        let payload = encode_motion_payload(case.pos, case.vel);
+        store.insert_node(
+            ent,
+            NodeRecord {
+                ty: entity_ty,
+                payload: Some(payload),
+            },
+        );
+        let mut engine = Engine::new(store, ent);
+        engine
+            .register_rule(rmg_core::motion_rule())
+            .expect("register motion rule");
+
+        let tx = engine.begin();
+        let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply");
+        assert!(matches!(res, ApplyResult::Applied));
+        engine.commit(tx).expect("commit");
+
+        // Verify payload bytes decode to expected values
+        let node = engine.node(&ent).expect("node exists");
+        let (pos, vel) =
+            decode_motion_payload(node.payload.as_ref().expect("payload")).expect("decode");
+        for i in 0..3 {
+            assert_eq!(
+                vel[i].to_bits(),
+                case.vel[i].to_bits(),
+                "vel component {}",
+                i
+            );
+            assert_eq!(
+                pos[i].to_bits(),
+                case.expected_pos[i].to_bits(),
+                "pos component {}",
+                i
+            );
+        }
+    }
+}

From 51af5f3495afd51a1fa0de37603b53a10eb26d42 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Thu, 30 Oct 2025 03:45:58 -0700
Subject: [PATCH 2/3] docs: record PR-01 (golden motion fixtures) in execution
 plan + decision log

---
 docs/decision-log.md   | 1 +
 docs/execution-plan.md | 6 ++++++
 2 files changed, 7 insertions(+)

diff --git a/docs/decision-log.md b/docs/decision-log.md
index c874d9dd..04110f56 100644
--- a/docs/decision-log.md
+++ b/docs/decision-log.md
@@ -20,6 +20,7 @@
 
 The following entries use a heading + bullets format for richer context.
 | 2025-10-30 | rmg-core determinism hardening | Added reachability-only snapshot hashing; closed tx lifecycle; duplicate rule detection; deterministic scheduler drain order; expanded motion payload docs; tests for duplicate rule name/id and no‑op commit. | Locks determinism contract and surfaces API invariants; prepares PR #7 for a safe merge train. | Clippy clean for rmg-core; workspace push withheld pending further feedback. |
+| 2025-10-30 | Tests | Add golden motion fixtures (JSON) + minimal harness validating motion rule bytes/values | Establishes deterministic test baseline for motion; supports future benches and tooling | No runtime impact; PR-01 linked to umbrella and milestone |
 | 2025-10-28 | PR #7 merged | Reachability-only snapshot hashing; ports demo registers rule; guarded ports footprint; scheduler `finalize_tx()` clears `pending`; `PortKey` u30 mask; hooks+CI hardened (toolchain pin, rustdoc fixes). | Determinism + memory hygiene; remove test footguns; pass CI with stable toolchain while keeping rmg-core MSRV=1.68. | Queued follow-ups: #13 (Mat4 canonical zero + MulAssign), #14 (geom train), #15 (devcontainer). |
 | 2025-10-27 | MWMR reserve gate | Engine calls `scheduler.finalize_tx()` at commit; compact rule id used on execute path; per‑tx telemetry summary behind feature. | Enforce independence and clear active frontier deterministically; keep ordering stable with `(scope_hash, family_id)`. | Toolchain pinned to Rust 1.68; add design note for telemetry graph snapshot replay. |
  
diff --git a/docs/execution-plan.md b/docs/execution-plan.md
index 712630d9..fd42734b 100644
--- a/docs/execution-plan.md
+++ b/docs/execution-plan.md
@@ -33,6 +33,12 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s
 
 ## Today’s Intent
 
+> 2025-10-30 — PR-01: Golden motion fixtures (tests-only)
+
+- Add JSON golden fixtures and a minimal harness for the motion rule under `crates/rmg-core/tests/`.
+- Scope: tests-only; no runtime changes.
+- Links: PR-01 and tracking issue are associated for visibility.
+
 > 2025-10-29 — Geom fat AABB midpoint sampling (merge-train)
 
 - Update `rmg-geom::temporal::Timespan::fat_aabb` to union AABBs at start, mid (t=0.5), and end to conservatively bound rotations about off‑centre pivots.

From b1eb5658349ea20eda3710aa2c85ee7ed1124cf6 Mon Sep 17 00:00:00 2001
From: "J. Kirby Ross" <james@flyingrobots.dev>
Date: Thu, 30 Oct 2025 04:42:16 -0700
Subject: [PATCH 3/3] tests(core): fix clippy expect_fun_call in golden
 fixtures test

---
 .../tests/fixtures/motion-fixtures.json       | 171 +++++++++++++++++-
 .../rmg-core/tests/motion_golden_fixtures.rs  |  77 ++++++--
 2 files changed, 224 insertions(+), 24 deletions(-)

diff --git a/crates/rmg-core/tests/fixtures/motion-fixtures.json b/crates/rmg-core/tests/fixtures/motion-fixtures.json
index 7de05d1c..72564022 100644
--- a/crates/rmg-core/tests/fixtures/motion-fixtures.json
+++ b/crates/rmg-core/tests/fixtures/motion-fixtures.json
@@ -1,8 +1,167 @@
 {
   "cases": [
-    { "label": "motion-fixture-1", "pos": [1.0, 2.0, 3.0], "vel": [0.5, -1.0, 0.25], "expected_pos": [1.5, 1.0, 3.25] },
-    { "label": "motion-fixture-2", "pos": [10.0, -2.0, 3.5], "vel": [0.125, 2.0, -1.5], "expected_pos": [10.125, 0.0, 2.0] },
-    { "label": "motion-fixture-3", "pos": [0.0, 0.0, 0.0], "vel": [0.0, 0.0, 0.0], "expected_pos": [0.0, 0.0, 0.0] }
-  ]
-}
-
+    {
+      "label": "motion-fixture-1",
+      "pos": [
+        1.0,
+        2.0,
+        3.0
+      ],
+      "vel": [
+        0.5,
+        -1.0,
+        0.25
+      ],
+      "expected_pos": [
+        1.5,
+        1.0,
+        3.25
+      ]
+    },
+    {
+      "label": "motion-fixture-2",
+      "pos": [
+        10.0,
+        -2.0,
+        3.5
+      ],
+      "vel": [
+        0.125,
+        2.0,
+        -1.5
+      ],
+      "expected_pos": [
+        10.125,
+        0.0,
+        2.0
+      ]
+    },
+    {
+      "label": "motion-fixture-3",
+      "pos": [
+        0.0,
+        0.0,
+        0.0
+      ],
+      "vel": [
+        0.0,
+        0.0,
+        0.0
+      ],
+      "expected_pos": [
+        0.0,
+        0.0,
+        0.0
+      ]
+    },
+    {
+      "label": "motion-neg-pos",
+      "pos": [
+        -1.0,
+        -2.0,
+        -3.0
+      ],
+      "vel": [
+        0.5,
+        -1.0,
+        0.25
+      ],
+      "expected_pos": [
+        -0.5,
+        -3.0,
+        -2.75
+      ]
+    },
+    {
+      "label": "motion-large",
+      "pos": [
+        1000000.0,
+        -1000000.0,
+        1000000.0
+      ],
+      "vel": [
+        1000000.0,
+        1000000.0,
+        -1000000.0
+      ],
+      "expected_pos": [
+        2000000.0,
+        0.0,
+        0.0
+      ]
+    },
+    {
+      "label": "motion-small",
+      "pos": [
+        1e-06,
+        -1e-06,
+        1e-06
+      ],
+      "vel": [
+        1e-06,
+        1e-06,
+        -1e-06
+      ],
+      "expected_pos": [
+        2e-06,
+        0.0,
+        0.0
+      ]
+    },
+    {
+      "label": "motion-large+tiny",
+      "pos": [
+        1000000.0,
+        1000000.0,
+        1000000.0
+      ],
+      "vel": [
+        1e-06,
+        -1e-06,
+        1e-06
+      ],
+      "expected_pos": [
+        1000000.0,
+        1000000.0,
+        1000000.0
+      ]
+    },
+    {
+      "label": "motion-subnormal",
+      "pos": [
+        1.4012985e-45,
+        0.0,
+        -1.4012985e-45
+      ],
+      "vel": [
+        1.4012985e-45,
+        1.4012985e-45,
+        1.4012985e-45
+      ],
+      "expected_pos": [
+        2.802597e-45,
+        1.4012985e-45,
+        0.0
+      ]
+    },
+    {
+      "label": "motion-near-zero-vel",
+      "pos": [
+        3.0,
+        -2.0,
+        5.0
+      ],
+      "vel": [
+        1e-12,
+        -1e-12,
+        1e-12
+      ],
+      "expected_pos": [
+        3.0,
+        -2.0,
+        5.0
+      ]
+    }
+  ],
+  "dt": 1.0
+}
\ No newline at end of file
diff --git a/crates/rmg-core/tests/motion_golden_fixtures.rs b/crates/rmg-core/tests/motion_golden_fixtures.rs
index 26a512cb..34d5bf67 100644
--- a/crates/rmg-core/tests/motion_golden_fixtures.rs
+++ b/crates/rmg-core/tests/motion_golden_fixtures.rs
@@ -1,10 +1,11 @@
 #![allow(missing_docs)]
+use bytes::Bytes;
 use once_cell::sync::Lazy;
 use serde::Deserialize;
 
 use rmg_core::{
-    decode_motion_payload, encode_motion_payload, make_node_id, make_type_id, ApplyResult, Engine,
-    GraphStore, NodeRecord, MOTION_RULE_NAME,
+    build_motion_demo_engine, decode_motion_payload, encode_motion_payload, make_node_id,
+    make_type_id, ApplyResult, Engine, NodeRecord, MOTION_RULE_NAME,
 };
 
 static RAW: &str = include_str!("fixtures/motion-fixtures.json");
@@ -28,45 +29,85 @@ static FIXTURES: Lazy<MotionFixtures> =
 #[test]
 fn motion_golden_fixtures_apply_as_expected() {
     let entity_ty = make_type_id("entity");
+    let mut engine: Engine = build_motion_demo_engine();
+
     for case in &FIXTURES.cases {
         let ent = make_node_id(&case.label);
-        // Create a fresh engine and insert entity with payload
-        let mut store = GraphStore::default();
         let payload = encode_motion_payload(case.pos, case.vel);
-        store.insert_node(
+        engine.insert_node(
             ent,
             NodeRecord {
                 ty: entity_ty,
                 payload: Some(payload),
             },
         );
-        let mut engine = Engine::new(store, ent);
-        engine
-            .register_rule(rmg_core::motion_rule())
-            .expect("register motion rule");
 
         let tx = engine.begin();
-        let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply");
+        let res = engine
+            .apply(tx, MOTION_RULE_NAME, &ent)
+            .unwrap_or_else(|_| panic!("apply motion rule failed for case: {}", case.label));
         assert!(matches!(res, ApplyResult::Applied));
         engine.commit(tx).expect("commit");
 
-        // Verify payload bytes decode to expected values
         let node = engine.node(&ent).expect("node exists");
         let (pos, vel) =
             decode_motion_payload(node.payload.as_ref().expect("payload")).expect("decode");
-        for i in 0..3 {
+        for (i, v) in vel.iter().enumerate() {
             assert_eq!(
-                vel[i].to_bits(),
+                v.to_bits(),
                 case.vel[i].to_bits(),
-                "vel component {}",
-                i
+                "[{}] velocity[{}] mismatch: got {:?}, expected {:?}",
+                case.label,
+                i,
+                v,
+                case.vel[i]
             );
+        }
+        for (i, p) in pos.iter().enumerate() {
             assert_eq!(
-                pos[i].to_bits(),
+                p.to_bits(),
                 case.expected_pos[i].to_bits(),
-                "pos component {}",
-                i
+                "[{}] position[{}] mismatch: got {:?}, expected {:?}",
+                case.label,
+                i,
+                p,
+                case.expected_pos[i]
             );
         }
     }
 }
+
+#[test]
+fn motion_apply_no_payload_returns_nomatch() {
+    let entity_ty = make_type_id("entity");
+    let ent = make_node_id("no-payload");
+    let mut engine = build_motion_demo_engine();
+    engine.insert_node(
+        ent,
+        NodeRecord {
+            ty: entity_ty,
+            payload: None,
+        },
+    );
+    let tx = engine.begin();
+    let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply");
+    assert!(matches!(res, ApplyResult::NoMatch));
+}
+
+#[test]
+fn motion_apply_invalid_payload_size_returns_nomatch() {
+    let entity_ty = make_type_id("entity");
+    let ent = make_node_id("bad-payload");
+    let mut engine = build_motion_demo_engine();
+    let bad = Bytes::from(vec![0u8; 10]);
+    engine.insert_node(
+        ent,
+        NodeRecord {
+            ty: entity_ty,
+            payload: Some(bad),
+        },
+    );
+    let tx = engine.begin();
+    let res = engine.apply(tx, MOTION_RULE_NAME, &ent).expect("apply");
+    assert!(matches!(res, ApplyResult::NoMatch));
+}
