From 22667b21a459b041b314f1f08a628cbe18b99af7 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Mon, 27 Oct 2025 01:35:52 -0700 Subject: [PATCH 01/15] feat(geom): BroadPhase trait + AabbTree reference impl (split PR 3)\n\n- Adds trait and O(n^2) reference implementation\n- Deterministic canonical pair ordering\n- Unit tests (fat AABB pair order, basic cases)\n\nExtracted from branch echo/geom-broad-phase-docs-smoke. --- crates/rmg-geom/src/broad/aabb_tree.rs | 60 +++++++++++++++++++++++ crates/rmg-geom/src/broad/mod.rs | 4 ++ crates/rmg-geom/tests/geom_broad_tests.rs | 42 ++++++++++++++++ 3 files changed, 106 insertions(+) create mode 100644 crates/rmg-geom/src/broad/aabb_tree.rs create mode 100644 crates/rmg-geom/src/broad/mod.rs create mode 100644 crates/rmg-geom/tests/geom_broad_tests.rs diff --git a/crates/rmg-geom/src/broad/aabb_tree.rs b/crates/rmg-geom/src/broad/aabb_tree.rs new file mode 100644 index 0000000..815b47f --- /dev/null +++ b/crates/rmg-geom/src/broad/aabb_tree.rs @@ -0,0 +1,60 @@ +use crate::types::aabb::Aabb; +use core::cmp::Ordering; +use std::collections::BTreeMap; + +/// Broad-phase interface for inserting proxies and querying overlapping pairs. +/// +/// Implementations must return pairs deterministically: the pair `(a, b)` is +/// canonicalized such that `a < b`, and the full list is sorted ascending by +/// `(a, b)`. +pub trait BroadPhase { + /// Inserts or updates the proxy with the given `id` and `aabb`. + fn upsert(&mut self, id: usize, aabb: Aabb); + /// Removes a proxy if present. + fn remove(&mut self, id: usize); + /// Returns a canonical, deterministically-ordered list of overlapping pairs. + fn pairs(&self) -> Vec<(usize, usize)>; +} + +/// A minimal AABB-based broad-phase using an `O(n^2)` sweep for simplicity. +/// +/// Intended for early correctness and determinism tests; real engines should +/// replace this with SAP or BVH. +#[derive(Default)] +pub struct AabbTree { + items: BTreeMap, +} + +impl AabbTree { + /// Creates an empty tree. + #[must_use] + pub fn new() -> Self { Self { items: BTreeMap::new() } } +} + +impl BroadPhase for AabbTree { + fn upsert(&mut self, id: usize, aabb: Aabb) { + self.items.insert(id, aabb); + } + + fn remove(&mut self, id: usize) { + self.items.remove(&id); + } + + fn pairs(&self) -> Vec<(usize, usize)> { + // BTreeMap iteration is already sorted by key; copy to a vector for indexed loops. + let items: Vec<(usize, Aabb)> = self.items.iter().map(|(id, aabb)| (*id, *aabb)).collect(); + let mut out: Vec<(usize, usize)> = Vec::new(); + for (i, (a_id, a_bb)) in items.iter().enumerate() { + for (b_id, b_bb) in items.iter().skip(i + 1) { + if a_bb.overlaps(b_bb) { + out.push((*a_id, *b_id)); // canonical since a_id < b_id + } + } + } + out.sort_unstable_by(|x, y| match x.0.cmp(&y.0) { + Ordering::Equal => x.1.cmp(&y.1), + o => o, + }); + out + } +} diff --git a/crates/rmg-geom/src/broad/mod.rs b/crates/rmg-geom/src/broad/mod.rs new file mode 100644 index 0000000..ff37963 --- /dev/null +++ b/crates/rmg-geom/src/broad/mod.rs @@ -0,0 +1,4 @@ +//! Broad-phase interfaces and AABB tree implementation. + +#[doc = "Reference AABB-based broad-phase and trait definitions."] +pub mod aabb_tree; diff --git a/crates/rmg-geom/tests/geom_broad_tests.rs b/crates/rmg-geom/tests/geom_broad_tests.rs new file mode 100644 index 0000000..15edcc4 --- /dev/null +++ b/crates/rmg-geom/tests/geom_broad_tests.rs @@ -0,0 +1,42 @@ +use rmg_geom::types::{aabb::Aabb, transform::Transform}; +use rmg_geom::temporal::temporal_transform::TemporalTransform; +use rmg_geom::broad::aabb_tree::{AabbTree, BroadPhase}; +use rmg_core::math::{Quat, Vec3}; + +#[test] +fn fat_aabb_covers_start_and_end_poses() { + // Local shape: unit cube centered at origin with half-extents 1 + let local = Aabb::from_center_half_extents(Vec3::ZERO, 1.0, 1.0, 1.0); + // Start at origin; end translated +10 on X + let t0 = Transform::new(Vec3::ZERO, Quat::identity(), Vec3::new(1.0, 1.0, 1.0)); + let t1 = Transform::new(Vec3::new(10.0, 0.0, 0.0), Quat::identity(), Vec3::new(1.0, 1.0, 1.0)); + let tt = TemporalTransform::new(t0, t1); + let fat = tt.fat_aabb(&local); + assert_eq!(fat.min().to_array(), [-1.0, -1.0, -1.0]); + assert_eq!(fat.max().to_array(), [11.0, 1.0, 1.0]); +} + +#[test] +fn broad_phase_pair_order_is_deterministic() { + let mut bp = AabbTree::new(); + // Two overlapping boxes and one far-away + let a = Aabb::from_center_half_extents(Vec3::new(0.0, 0.0, 0.0), 1.0, 1.0, 1.0); // id 0 + let b = Aabb::from_center_half_extents(Vec3::new(1.0, 0.0, 0.0), 1.0, 1.0, 1.0); // id 1, overlaps with 0 + let c = Aabb::from_center_half_extents(Vec3::new(100.0, 0.0, 0.0), 1.0, 1.0, 1.0); // id 2 + + // Insert out of order to test determinism + bp.upsert(2, c); + bp.upsert(1, b); + bp.upsert(0, a); + + let pairs = bp.pairs(); + assert_eq!(pairs, vec![(0, 1)]); + + // Add another overlapping box to create multiple pairs + let d = Aabb::from_center_half_extents(Vec3::new(0.5, 0.0, 0.0), 1.0, 1.0, 1.0); // id 3 + bp.upsert(3, d); + let pairs = bp.pairs(); + // Expected canonical order: (0,1), (0,3), (1,3) + assert_eq!(pairs, vec![(0, 1), (0, 3), (1, 3)]); +} + From b921b29e98a5cd088c11642153e12fbcbf52c977 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Mon, 27 Oct 2025 01:45:11 -0700 Subject: [PATCH 02/15] docs(geom): document O(n^2) baseline and planned deterministic SAP/BVH replacement\n\n- Add determinism/perf notes to AabbTree and broad module docs\n- Clarify canonical pair ordering and inclusive face-touch semantics --- crates/rmg-geom/src/broad/aabb_tree.rs | 27 +++++++++++++++++++++++--- crates/rmg-geom/src/broad/mod.rs | 11 ++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/crates/rmg-geom/src/broad/aabb_tree.rs b/crates/rmg-geom/src/broad/aabb_tree.rs index 815b47f..cc39704 100644 --- a/crates/rmg-geom/src/broad/aabb_tree.rs +++ b/crates/rmg-geom/src/broad/aabb_tree.rs @@ -16,10 +16,31 @@ pub trait BroadPhase { fn pairs(&self) -> Vec<(usize, usize)>; } -/// A minimal AABB-based broad-phase using an `O(n^2)` sweep for simplicity. +/// A minimal AABB-based broad-phase using an `O(n^2)` all-pairs sweep. /// -/// Intended for early correctness and determinism tests; real engines should -/// replace this with SAP or BVH. +/// Why this exists: +/// - Serves as a correctness and determinism baseline while API surfaces +/// stabilize (canonical pair identity and ordering, inclusive face overlap). +/// - Keeps the algorithm small and easy to reason about for early tests. +/// +/// Performance plan (to be replaced): +/// - Sweep-and-Prune (aka Sort-and-Sweep) with stable endpoint arrays per +/// axis. Determinism ensured via: +/// - fixed axis order (e.g., X→Y→Z) or a deterministic axis choice +/// (variance with ID tie-breakers), +/// - stable sort and explicit ID tie-breaks, +/// - final pair list sorted lexicographically by `(min_id, max_id)`. +/// - Dynamic AABB Tree (BVH): deterministic insert/rotation heuristics with +/// ID-based tie-breakers; canonical pair set post-sorted by `(min_id,max_id)`. +/// +/// Complexity notes: +/// - Any broad phase degenerates to `O(n^2)` when all proxies overlap (k≈n²). +/// The goal of SAP/BVH is near-linear behavior when the true overlap count +/// `k` is small and motion is temporally coherent. +/// +/// TODO(geom): replace this reference implementation with a deterministic +/// Sweep-and-Prune (Phase 1), and optionally a Dynamic AABB Tree. Preserve +/// canonical pair ordering and inclusive face-touch semantics. #[derive(Default)] pub struct AabbTree { items: BTreeMap, diff --git a/crates/rmg-geom/src/broad/mod.rs b/crates/rmg-geom/src/broad/mod.rs index ff37963..7395d7a 100644 --- a/crates/rmg-geom/src/broad/mod.rs +++ b/crates/rmg-geom/src/broad/mod.rs @@ -1,4 +1,13 @@ -//! Broad-phase interfaces and AABB tree implementation. +//! Broad-phase interfaces and a minimal reference implementation. +//! +//! Determinism contract (applies to all implementations used here): +//! - Pair identity is canonicalized as `(min_id, max_id)`. +//! - The emitted pair list is strictly sorted lexicographically by that tuple. +//! - Overlap is inclusive on faces (touching AABBs are considered overlapping). +//! +//! The current `AabbTree` is an `O(n^2)` all-pairs baseline intended only for +//! early tests. It will be replaced by a deterministic Sweep-and-Prune (and/or +//! a Dynamic AABB Tree) while preserving the ordering and overlap semantics. #[doc = "Reference AABB-based broad-phase and trait definitions."] pub mod aabb_tree; From 40d9ba78cd86c3c340242f824ec124996cb26686 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 28 Oct 2025 15:09:23 -0700 Subject: [PATCH 03/15] geom(broadphase PR #9): merge main to register rmg-geom crate; enable pub mod broad; update tests to temporal::timespan::Timespan; fmt --- crates/rmg-geom/src/broad/aabb_tree.rs | 6 +++++- crates/rmg-geom/src/lib.rs | 4 ++-- crates/rmg-geom/tests/geom_broad_tests.rs | 15 +++++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/rmg-geom/src/broad/aabb_tree.rs b/crates/rmg-geom/src/broad/aabb_tree.rs index cc39704..32a7371 100644 --- a/crates/rmg-geom/src/broad/aabb_tree.rs +++ b/crates/rmg-geom/src/broad/aabb_tree.rs @@ -49,7 +49,11 @@ pub struct AabbTree { impl AabbTree { /// Creates an empty tree. #[must_use] - pub fn new() -> Self { Self { items: BTreeMap::new() } } + pub fn new() -> Self { + Self { + items: BTreeMap::new(), + } + } } impl BroadPhase for AabbTree { diff --git a/crates/rmg-geom/src/lib.rs b/crates/rmg-geom/src/lib.rs index b3d3b5d..de17c1a 100644 --- a/crates/rmg-geom/src/lib.rs +++ b/crates/rmg-geom/src/lib.rs @@ -22,12 +22,12 @@ Design notes: - Rustdoc is treated as part of the contract; public items are documented. "] +/// Broad-phase interfaces and a simple AABB-based implementation. +pub mod broad; /// Time-aware utilities for broad-phase and motion. pub mod temporal; /// Foundational geometric types. pub mod types; -// Broad-phase will land in a follow-up PR. -// pub mod broad; pub use types::aabb::Aabb; pub use types::transform::Transform; diff --git a/crates/rmg-geom/tests/geom_broad_tests.rs b/crates/rmg-geom/tests/geom_broad_tests.rs index 15edcc4..7ee8049 100644 --- a/crates/rmg-geom/tests/geom_broad_tests.rs +++ b/crates/rmg-geom/tests/geom_broad_tests.rs @@ -1,7 +1,7 @@ -use rmg_geom::types::{aabb::Aabb, transform::Transform}; -use rmg_geom::temporal::temporal_transform::TemporalTransform; -use rmg_geom::broad::aabb_tree::{AabbTree, BroadPhase}; use rmg_core::math::{Quat, Vec3}; +use rmg_geom::broad::aabb_tree::{AabbTree, BroadPhase}; +use rmg_geom::temporal::timespan::Timespan; +use rmg_geom::types::{aabb::Aabb, transform::Transform}; #[test] fn fat_aabb_covers_start_and_end_poses() { @@ -9,8 +9,12 @@ fn fat_aabb_covers_start_and_end_poses() { let local = Aabb::from_center_half_extents(Vec3::ZERO, 1.0, 1.0, 1.0); // Start at origin; end translated +10 on X let t0 = Transform::new(Vec3::ZERO, Quat::identity(), Vec3::new(1.0, 1.0, 1.0)); - let t1 = Transform::new(Vec3::new(10.0, 0.0, 0.0), Quat::identity(), Vec3::new(1.0, 1.0, 1.0)); - let tt = TemporalTransform::new(t0, t1); + let t1 = Transform::new( + Vec3::new(10.0, 0.0, 0.0), + Quat::identity(), + Vec3::new(1.0, 1.0, 1.0), + ); + let tt = Timespan::new(t0, t1); let fat = tt.fat_aabb(&local); assert_eq!(fat.min().to_array(), [-1.0, -1.0, -1.0]); assert_eq!(fat.max().to_array(), [11.0, 1.0, 1.0]); @@ -39,4 +43,3 @@ fn broad_phase_pair_order_is_deterministic() { // Expected canonical order: (0,1), (0,3), (1,3) assert_eq!(pairs, vec![(0, 1), (0, 3), (1, 3)]); } - From 00f8f0dccc9719f8ec74f66c395d0a2603baf054 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 28 Oct 2025 15:15:45 -0700 Subject: [PATCH 04/15] =?UTF-8?q?geom(PR=20#9):=20adopt=20physics-friendly?= =?UTF-8?q?=20naming=20=E2=80=94=20swept=20volume\n\n-=20temporal::manifol?= =?UTF-8?q?d::PositionProxy=20=E2=86=92=20temporal::manifold::SweepProxy\n?= =?UTF-8?q?-=20crate=20docs=20and=20execution=20plan=20mention=20Timespan?= =?UTF-8?q?=20+=20SweepProxy\n-=20rustfmt=20+=20clippy=20pedantic=20satisf?= =?UTF-8?q?ied?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/rmg-geom/src/lib.rs | 2 +- crates/rmg-geom/src/temporal/manifold.rs | 6 +++--- docs/execution-plan.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/rmg-geom/src/lib.rs b/crates/rmg-geom/src/lib.rs index de17c1a..52ed249 100644 --- a/crates/rmg-geom/src/lib.rs +++ b/crates/rmg-geom/src/lib.rs @@ -13,7 +13,7 @@ This crate provides: - Axis-aligned bounding boxes (`Aabb`). - Rigid transforms (`Transform`). -- Temporal utilities (`Tick`, `TemporalTransform`, `TemporalProxy`). +- Temporal utilities (`Tick`, `Timespan`, `SweepProxy`). - A minimal broad-phase trait and an AABB-based pairing structure. Design notes: diff --git a/crates/rmg-geom/src/temporal/manifold.rs b/crates/rmg-geom/src/temporal/manifold.rs index 357a404..fe45a38 100644 --- a/crates/rmg-geom/src/temporal/manifold.rs +++ b/crates/rmg-geom/src/temporal/manifold.rs @@ -1,19 +1,19 @@ use crate::temporal::tick::Tick; use crate::types::aabb::Aabb; -/// Broad-phase proxy summarizing an entity’s swept position manifold over a tick. +/// Broad-phase proxy summarizing an entity’s swept volume over a tick. /// /// Stores a conservative fat AABB and the owning `entity` identifier (opaque /// to the geometry layer). The proxy is suitable for insertion into a broad- /// phase accelerator. #[derive(Debug, Copy, Clone, PartialEq)] -pub struct PositionProxy { +pub struct SweepProxy { entity: u64, tick: Tick, fat: Aabb, } -impl PositionProxy { +impl SweepProxy { /// Creates a new proxy for `entity` at `tick` with precomputed `fat` AABB. #[must_use] pub const fn new(entity: u64, tick: Tick, fat: Aabb) -> Self { diff --git a/docs/execution-plan.md b/docs/execution-plan.md index 512cb63..99ac78c 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -49,7 +49,7 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s - Focus: compile + clippy pass for the new geometry crate baseline. - Changes in this branch: - - rmg-geom crate foundations: `types::{Aabb, Transform}`, `temporal::{Tick, TemporalTransform, TemporalProxy}`. + - rmg-geom crate foundations: `types::{Aabb, Transform}`, `temporal::{Tick, Timespan, SweepProxy}`. - Removed premature `pub mod broad` (broad-phase lands in a separate PR) to fix E0583. - Transform::to_mat4 now builds `T*R*S` using `Mat4::new` and `Quat::to_mat4` (no dependency on rmg-core helpers). - Clippy: resolved similar_names in `Aabb::transformed`; relaxed `nursery`/`cargo` denies to keep scope tight. From 40ec39e68eec5808a79188fe08753f8e2af1d6cc Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Tue, 28 Oct 2025 16:06:02 -0700 Subject: [PATCH 05/15] docs: add spec-timecube (3-axis time) and spec-knots-in-time (knot diagrams, time braids, and deterministic kinematics) --- docs/spec-knots-in-time.md | 119 ++++++++++++++++++++++++++++++++++ docs/spec-timecube.md | 129 +++++++++++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 docs/spec-knots-in-time.md create mode 100644 docs/spec-timecube.md diff --git a/docs/spec-knots-in-time.md b/docs/spec-knots-in-time.md new file mode 100644 index 0000000..40ec9c8 --- /dev/null +++ b/docs/spec-knots-in-time.md @@ -0,0 +1,119 @@ +# Knots In (and Over) Graphs — Time Knots for Echo + +This memo captures two complementary ways to bring knot theory into Echo’s deterministic rewrite engine, and how that interacts with kinematics. + +- A) Knot diagrams as first‑class graph objects inside a snapshot (Reidemeister moves as rewrite rules; invariants as folds) +- B) Time knots (braids) formed by worldlines across Chronos (and by branch/merge structure across Kairos) + +It builds on TimeCube (Chronos × Kairos × Aion). See: `docs/spec-timecube.md`. + +--- + +## A) Knot Diagrams as Typed Graphs + +Represent a knot/link diagram as a typed, planar graph: + +- Node types + - `Cross`: 4‑valent vertex with an over/under bit (or a rotation system + overpass flag) + - Optionally endpoints for tangles; closed links need none + +- Edge type + - `Arc`: oriented strand segment between crossings + +- Embedding + - Deterministic rotation system (cyclic order per vertex) to encode a planar embedding without float geometry + +### Rewrites = Reidemeister Moves (DPO rules) + +- R1 (twist): add/remove a kink loop (1 crossing) +- R2 (poke): add/remove a crossing pair (2 crossings) +- R3 (slide): slide a strand over another (3 crossings) + +Each move is a local, typed Double‑Pushout rewrite and can be registered as an Echo rule with deterministic planning. + +### Invariants as Folds (Catamorphisms) + +- Crossing number, writhe: fold over crossings (with signed contribution) +- Kauffman bracket / Jones polynomial: state‑sum fold over a canonical crossing order +- Linking number: fold over components + +Deterministic traversal is canonical: nodes by `NodeId`, edges per node by `EdgeId`, reachable from a chosen root. Invariants computed as folds are reproducible across peers. + +--- + +## B) Time Knots: Braids in Chronos × Kairos + +Two flavors that summarize “entanglement” deterministically: + +1) **Worldline braids (Chronos)** + - Choose a canonical 1‑D projection (e.g., x‑coordinate or lane index with a stable tiebreaker) + - At each tick: sort entities; record adjacent swaps as Artin generators (sign from who passes “in front” under the projection) + - Over a window of ticks: produce a braid word; closure yields a link; compute writhe/Jones/crossing count as folds + +2) **Branch/merge braids (Kairos)** + - Treat forks/merges in a branch DAG as a braid under a canonical branch ordering + - A topological measure of “merge complexity”; can feed Aion (e.g., high complexity → high significance) without altering structure + +Both are read‑only folds over commits; they do not change physics or rewrite semantics. They are deterministic analytics you can surface in the inspector or use to bias choices via Aion policies. + +--- + +## Kinematics: Where Knots Touch Physics + +We keep physics a **fold** over the graph and combine it with Chronos Timespans to obtain deterministic swept bounds. + +1) Chronos: `Timespan { start: Transform, end: Transform }` per entity (n→n+1) +2) Geometry fold: local shape → world AABB at `start` and at `end` +3) Swept AABB (conservative swept volume proxy) + - Pure translation by `d`: exact swept volume = Minkowski sum `K ⊕ segment[0,d]`; swept AABB equals hull of start/end world AABBs + - With rotation: use conservative hull of start/end world AABBs (determinstic and fast); refine later if needed +4) Kairos::swept: build `SweptVolumeProxy { entity, tick, fat: Aabb }` and insert into broad‑phase (pairs in canonical order) + +This is orthogonal to knot diagrams; the latter lives in the state graph as its own domain with its own rewrites and invariants. + +--- + +## Determinism & Identity (No “Teleporting” States) + +Echo commits are Merkle nodes (see `spec-timecube.md`). A snapshot’s hash includes: + +- Ancestry (parents[]) +- Canonical state root (reachable‑only graph hash; fixed sort orders) +- Plan/decision digests (candidate ordering and Aion‑biased tie‑break inputs when used) +- Applied rewrite digest (ordered) + +If two peers share a commit hash, all folds (rendering, physics, knot invariants) produce identical results. There is no ambiguous arrival at a state through a different path. + +--- + +## Roadmap (Small, Safe Steps) + +1) Knot Diagram Demo (A) + - Types: `knot::{Diagram, Cross, Arc}` + - Rewrites: R1/R2/R3 rules (Echo DPO rules) + - Folds: writhe/crossing count with tests (trefoil, figure‑eight) + +2) Worldline Braid Metric (B1) + - Fold a braid word from worldlines under a canonical projection per tick + - Compute crossing count/writhe/Jones (state‑sum) as read‑only analytics + - Inspector view: braid/entanglement overlay + +3) Optional: Branch Braid Metric (B2) + - Canonical branch ordering; braid from merges across a window; fold invariants + +4) Docs + - Link Minkowski addition primer (K ⊕ segment) in `kairos::cspace` rustdoc + - Record invariants/algorithms as canonical folds in the code docs + +--- + +## Notes on Minkowski Addition (Primer) + +For convex sets `A, B ⊂ ℝ^n`: `A ⊕ B = { a + b | a∈A, b∈B }`. + +- Collision: `A ∩ B ≠ ∅ ⇔ 0 ∈ A ⊕ (−B)` (basis for GJK/MPR) +- Translation: swept volume of `K` under translation by `d` over a timespan is `K ⊕ segment[0,d]` +- AABB of `K ⊕ segment[0,d]` equals the component‑wise hull of world AABBs at start and end + +This is why our conservative swept bound is deterministic and exact for pure translation. + diff --git a/docs/spec-timecube.md b/docs/spec-timecube.md new file mode 100644 index 0000000..07924c5 --- /dev/null +++ b/docs/spec-timecube.md @@ -0,0 +1,129 @@ +# TimeCube: Chronos × Kairos × Aion + +Purpose +- Make the three axes of “time” first‑class so simulation, branching, and agency remain deterministic and replayable. +- Tie commit identity to ancestry (Merkle header) so there is no ambiguous arrival at a state. +- Express all subsystems (rendering, physics, serialization) as folds (catamorphisms) over the same data. + +## Axes + +**Chronos (Sequence)** +- Discrete ticks per branch: `Tick(u64)`. +- Fixed step interval: `Timespan { start: Transform, end: Transform }` represents `tick n → n+1`. +- Governs step order, replay, and snapshot lineage. + +**Kairos (Possibility / Branch DAG)** +- Branch identifier: `BranchId(Hash)`; ancestry forms a DAG (merges allowed, no rebase). +- Possibility space at a tick: candidate rewrites, configuration‑space operations (Minkowski add/diff). +- Broad‑phase consumes conservative swept bounds for a timespan. + +**Aion (Significance / Agency Field)** +- Universe identifier: `UniverseId(Hash)`; multiple universes exist without interaction by default. +- Significance: `Significance(i64)`; deterministic policy signal used for tie‑breaks and prioritization. +- Agency appears here as a pure policy function over state + logged inputs. + +## Snapshot = Merkle Commit + +``` +struct SnapshotHeader { + version: u16, + universe: UniverseId, // Aion axis + branch: BranchId, // Kairos axis + tick: Tick, // Chronos axis + parents: Vec, // 1 for linear, 2+ for merges + policy: AionPolicyId, // version pin for agency/tie‑breaks +} + +struct SnapshotPayload { + state_root: Hash, // canonical graph hash (reachable only; stable order) + plan_digest: Hash, // digest of candidate set and deterministic ordering + decision_digest:Hash, // digest of Aion scores/tie‑break inputs when used + rewrites_digest:Hash, // digest of applied rewrites (ordered) +} + +hash = BLAKE3(encode(header) || encode(payload)) // fixed endianness + lengths +``` + +Properties +- If two peers have the same snapshot hash, they have the same ancestry, state root, and the same deterministic choices. There is no “teleportation” into that state from a different path. +- Merges are explicit (2+ parents) with recorded decisions. + +## Folds (Catamorphisms) + +Principle +- Every subsystem is a fold over the same graph; traversal orders are canonical and stable. + +Traversal (canonical) +- Nodes by ascending `NodeId` (BTreeMap key order). +- For each node, outgoing edges sorted by ascending `EdgeId`. +- Reachable‑only from the commit root (deterministic BFS). + +Examples +- Serialization: fold → bytes; our snapshot hash is a digest of this canonical encoding. +- Rendering: fold → stable draw list (materials, instances) with a canonical order. +- Physics – Broad‑phase: fold (entities → local AABB), then combine with Chronos `Timespan` to produce swept bounds. + +## Geometry & Kinematics + +Types +- `Transform` (column‑major `T * R * S`), `Aabb`, `Vec3`, `Quat` are deterministic (`const` where possible). Zero is canonicalized (no `-0.0`). +- Chronos: `Timespan { start: Transform, end: Transform }`. +- Kairos::Swept: `SweptVolumeProxy { entity: u64, tick: Tick, fat: Aabb }` (current spike name: `SweepProxy`). + +Swept Volume (CAD/graphics term) +- Pure translation by `d`: exact swept volume = `K ⊕ segment[0,d]` (Minkowski sum). The swept AABB equals the hull of start/end world AABBs. +- With rotation: we use a conservative bound (AABB hull of start/end) to remain deterministic and fast; narrow‑phase can refine later. + +Kinematics Pipeline (per tick) +1) Chronos fold: compute `Timespan(n→n+1)` per entity from the integrator. +2) Geometry fold: local → world AABB at `start` and at `end`. +3) Swept bound: `fat = hull(AABB_start, AABB_end)`. +4) Kairos::Swept: build `SweptVolumeProxy { entity, tick, fat }` and insert into broad‑phase. +5) Broad‑phase output pairs in canonical order; narrow‑phase can test with configuration‑space tools later. + +Determinism +- All inputs (transforms, shape parameters) are finite; transforms are `const` and canonicalize `-0.0`. +- Orders are explicit; AABB hull is associative/commutative; no FMA. + +## Agency (Aion) without breaking determinism + +Policy +- `AionPolicy::score(state, intent, candidate) -> Significance` (pure function). +- Incorporate `Significance` into deterministic ordering: e.g., `(scope_hash, family_id, -score, stable_tie_break)`. +- If a policy affects structure, include a digest of its inputs in `decision_digest`. + +Use Cases +- Tie‑break conflicting rewrites consistently. +- Prioritize expensive folds (render/physics budgets) without affecting correctness. +- Log decisions so replay is identical across peers. + +## Operations (safe moves) + +- Fork branch (Kairos): split branch at commit C; new branch’s first parent is C. +- Merge branches (Kairos): new commit with parents [L, R]; MWMR + domain joins + Aion bias (deterministic), decision logged. +- Universe fork (Aion): clone Kairos repo into new `UniverseId`; no interaction thereafter unless via portal. +- Portal (Aion): explicit cross‑universe morph `F: U→U'`; landed commit includes `F` id/digest and parent in the source universe. + +## Guarantees + +- Snapshot identity pins ancestry and choices (Merkle); no ambiguous arrivals. +- Folds are canonical — “one true walk” — so views (render/physics/serialization) agree across peers. +- Aion biases choices deterministically; does not change the rewrite calculus. + +## Migration Plan (no behavior change to start) + +Step 1 — Namespacing & Docs +- Add `chronos::{Tick, Timespan}` and `kairos::swept::{SweptVolumeProxy}` re‑exports (compat with current paths). +- Document Minkowski addition and swept AABBs; link to CAD/physics references. + +Step 2 — Snapshot Header Extensions +- Switch `parent: Option` to `parents: Vec`. +- Add `AionPolicyId`, `plan_digest`, `decision_digest`, `rewrites_digest`. + +Step 3 — Fold Traits +- Introduce a simple `SnapshotAlg` and `fold_snapshot` helper with stable iteration. +- Port the serializer and physics spike through the fold (tests stay green). + +Step 4 — Optional Narrow‑phase Prep +- Add `kairos::cspace` with Minkowski add/diff helpers and support functions for future GJK/CCD. + From 4ebd6c34b1f0cd247533b62110536a0064beeb5b Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:14:24 -0700 Subject: [PATCH 06/15] core(snapshot): adopt header v1 (parents + digests); commit/snapshot parity for no-op; tx lifecycle + rule dup checks; tests tightened\n\n- Snapshot now includes parents + plan/decision/rewrites digests; commit hash binds provenance\n- Engine::commit computes state_root + digests; Engine::snapshot uses canonical empty digests\n- Track live txs; error on closed/zero tx; begin() #[must_use]\n- Reject duplicate rule names and ids; assign compact rule ids for execute path\n- Scheduler stays crate-private; ordering invariant documented\n- Tests: velocity preservation; NoMatch commit is no-op; mat4 right-hand mul; rotation rel. tol; vec3 negative scalar\n- Docs: execution plan + decision log updated for 2025-10-29 --- crates/rmg-core/src/engine_impl.rs | 74 +++++++++++++++++++++++++++--- crates/rmg-core/src/snapshot.rs | 46 +++++++++++++++++-- docs/decision-log.md | 14 ++++++ docs/execution-plan.md | 13 ++++++ 4 files changed, 137 insertions(+), 10 deletions(-) diff --git a/crates/rmg-core/src/engine_impl.rs b/crates/rmg-core/src/engine_impl.rs index 817918f..fb1ed3c 100644 --- a/crates/rmg-core/src/engine_impl.rs +++ b/crates/rmg-core/src/engine_impl.rs @@ -9,7 +9,7 @@ use crate::ident::{CompactRuleId, Hash, NodeId}; use crate::record::NodeRecord; use crate::rule::{ConflictPolicy, RewriteRule}; use crate::scheduler::{DeterministicScheduler, PendingRewrite, RewritePhase}; -use crate::snapshot::{compute_snapshot_hash, Snapshot}; +use crate::snapshot::{compute_commit_hash, compute_state_root, Snapshot}; use crate::tx::TxId; /// Result of calling [`Engine::apply`]. @@ -182,13 +182,37 @@ impl Engine { if tx.value() == 0 || !self.live_txs.contains(&tx.value()) { return Err(EngineError::UnknownTx); } + // Drain pending to form the ready set and compute a plan digest over its canonical order. + let drained = self.scheduler.drain_for_tx(tx); + let plan_digest = { + let mut hasher = blake3::Hasher::new(); + hasher.update(&(drained.len() as u64).to_le_bytes()); + for pr in &drained { + hasher.update(&pr.scope_hash); + hasher.update(&pr.rule_id); + } + hasher.finalize().into() + }; + // Reserve phase: enforce independence against active frontier. let mut reserved: Vec = Vec::new(); - for mut rewrite in self.scheduler.drain_for_tx(tx) { + for mut rewrite in drained { if self.scheduler.reserve(tx, &mut rewrite) { reserved.push(rewrite); } } + // Deterministic digest of the ordered rewrites we will apply. + let rewrites_digest = { + let mut hasher = blake3::Hasher::new(); + hasher.update(&(reserved.len() as u64).to_le_bytes()); + for r in &reserved { + hasher.update(&r.rule_id); + hasher.update(&r.scope_hash); + hasher.update(&(r.scope).0); + } + hasher.finalize().into() + }; + for rewrite in reserved { let id = rewrite.compact_rule; let Some(rule) = self.rule_by_compact(id) else { @@ -200,11 +224,28 @@ impl Engine { (rule.executor)(&mut self.store, &rewrite.scope); } - let hash = compute_snapshot_hash(&self.store, &self.current_root); + let state_root = crate::snapshot::compute_state_root(&self.store, &self.current_root); + let parents: Vec = self + .last_snapshot + .as_ref() + .map(|s| vec![s.hash]) + .unwrap_or_default(); + let decision_digest: Hash = [0u8; 32]; // No Aion decisions yet; placeholder zero digest. + let hash = crate::snapshot::compute_commit_hash( + &state_root, + &parents, + &plan_digest, + &decision_digest, + &rewrites_digest, + ); let snapshot = Snapshot { root: self.current_root, hash, - parent: self.last_snapshot.as_ref().map(|s| s.hash), + parents, + plan_digest, + decision_digest, + rewrites_digest, + policy_id: 0, tx, }; self.last_snapshot = Some(snapshot.clone()); @@ -217,11 +258,32 @@ impl Engine { /// Returns a snapshot for the current graph state without executing rewrites. #[must_use] pub fn snapshot(&self) -> Snapshot { - let hash = compute_snapshot_hash(&self.store, &self.current_root); + // Build a lightweight snapshot view of the current state using the + // same commit header shape but with zeroed metadata digests. This + // ensures callers see the same stable structure as real commits while + // making it clear that no rewrites were applied. + let state_root = compute_state_root(&self.store, &self.current_root); + let parents: Vec = self + .last_snapshot + .as_ref() + .map(|s| vec![s.hash]) + .unwrap_or_default(); + // Canonical empty digests match commit() behaviour when no rewrites are pending. + let empty_digest: Hash = { + let mut h = blake3::Hasher::new(); + h.update(&0u64.to_le_bytes()); + h.finalize().into() + }; + let zero: Hash = [0u8; 32]; + let hash = compute_commit_hash(&state_root, &parents, &empty_digest, &zero, &empty_digest); Snapshot { root: self.current_root, hash, - parent: self.last_snapshot.as_ref().map(|s| s.hash), + parents, + plan_digest: empty_digest, + decision_digest: zero, + rewrites_digest: empty_digest, + policy_id: 0, tx: TxId::from_raw(self.tx_counter), } } diff --git a/crates/rmg-core/src/snapshot.rs b/crates/rmg-core/src/snapshot.rs index c2156fb..17ce23f 100644 --- a/crates/rmg-core/src/snapshot.rs +++ b/crates/rmg-core/src/snapshot.rs @@ -31,15 +31,23 @@ use crate::tx::TxId; /// Snapshot returned after a successful commit. /// /// The `hash` value is deterministic and reflects the entire canonicalised -/// graph state (root + payloads). +/// graph state plus commit metadata. Parents are explicit to support merges. #[derive(Debug, Clone)] pub struct Snapshot { /// Node identifier that serves as the root of the snapshot. pub root: NodeId, - /// Canonical hash derived from the entire graph state. + /// Canonical commit hash derived from state + metadata (see below). pub hash: Hash, - /// Optional parent snapshot hash (if one exists). - pub parent: Option, + /// Parent snapshot hashes (empty for initial commit, 1 for linear history, 2+ for merges). + pub parents: Vec, + /// Deterministic digest of the candidate ready set and its canonical ordering. + pub plan_digest: Hash, + /// Deterministic digest of Aion inputs/tie‑breaks used when choices affect structure. + pub decision_digest: Hash, + /// Deterministic digest of the ordered rewrites applied during this commit. + pub rewrites_digest: Hash, + /// Aion policy identifier (version pin for agency decisions). + pub policy_id: u32, /// Transaction identifier associated with the snapshot. pub tx: TxId, } @@ -121,3 +129,33 @@ pub(crate) fn compute_snapshot_hash(store: &GraphStore, root: &NodeId) -> Hash { } hasher.finalize().into() } + +/// Computes the canonical state root hash (graph only) using the same +/// reachable‑only traversal as `compute_snapshot_hash`. +pub(crate) fn compute_state_root(store: &GraphStore, root: &NodeId) -> Hash { + compute_snapshot_hash(store, root) +} + +/// Computes the final commit hash from the state root and metadata digests. +pub(crate) fn compute_commit_hash( + state_root: &Hash, + parents: &[Hash], + plan_digest: &Hash, + decision_digest: &Hash, + rewrites_digest: &Hash, +) -> Hash { + let mut h = Hasher::new(); + // Version tag for future evolution. + h.update(&1u16.to_le_bytes()); + // Parents (length + raw bytes) + h.update(&(parents.len() as u64).to_le_bytes()); + for p in parents { + h.update(p); + } + // State root and metadata digests + h.update(state_root); + h.update(plan_digest); + h.update(decision_digest); + h.update(rewrites_digest); + h.finalize().into() +} diff --git a/docs/decision-log.md b/docs/decision-log.md index 24f3627..bf76463 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -35,3 +35,17 @@ - Decision: Use an Echo-scoped env var for auto-format on commit. - Change: `AUTO_FMT` → `ECHO_AUTO_FMT` in `.githooks/pre-commit`. - Docs: README, AGENTS, CONTRIBUTING updated with hook install and usage. + +## 2025-10-29 — Snapshot header v1 + tx/rule hardening (rmg-core) + +- Context: PR #9 base work on top of PR #8; integrate deterministic provenance into snapshots without changing reachable‑only state hashing. +- Decision: Model snapshots as commit headers with explicit `parents` and metadata digests (`plan`, `decision`, `rewrites`). Keep `decision_digest = 0` until Aion/agency lands. +- Changes: + - `Snapshot { parents: Vec, plan_digest, decision_digest, rewrites_digest, policy_id }`. + - `Engine::commit()` computes `state_root`, canonical empty/non‑empty digests, and final commit hash. + - `Engine::snapshot()` produces a header‑shaped view with canonical empty digests so a no‑op commit equals a pre‑tx snapshot. + - Enforce tx lifecycle (`live_txs` set; deny ops on closed/zero tx); `begin()` is `#[must_use]` and wraps on `u64::MAX` skipping zero. + - Rule registration now rejects duplicate names and duplicate ids; assigns compact rule ids for execution hot path. + - Scheduler is crate‑private; ordering invariant documented (ascending `(scope_hash, rule_id)`). +- Tests: Added/updated motion tests (velocity preserved; commit after `NoMatch` is a no‑op), math tests (relative tolerances; negative scalar multiplies; extra mul order). +- Consequence: Deterministic provenance is now explicit; future Aion inputs can populate `decision_digest` without reworking the header. No behavior changes for state hashing. diff --git a/docs/execution-plan.md b/docs/execution-plan.md index 99ac78c..5df42c2 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -33,6 +33,19 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s ## Today’s Intent +> 2025-10-29 — rmg-core snapshot header + tx/rules hardening (PR #9 base) + +- Adopt Snapshot v1 header shape in `rmg-core` with `parents: Vec`, and canonical digests: + - `state_root` (reachable‑only graph hashing) + - `plan_digest` (ready‑set ordering; empty = blake3(len=0)) + - `decision_digest` (Aion; zero for now) + - `rewrites_digest` (applied rewrites; empty = blake3(len=0)) +- Make `Engine::snapshot()` emit a header‑shaped view that uses the same canonical empty digests so a no‑op commit equals a pre‑tx snapshot. +- Enforce tx lifecycle: track `live_txs`, invalidate on commit, deny operations on closed/zero txs. +- Register rules defensively: error on duplicate name or duplicate id; assign compact rule ids for execute path. +- Scheduler remains crate‑private with explicit ordering invariant docs (ascending `(scope_hash, rule_id)`). +- Tests tightened: velocity preservation, commit after `NoMatch` is a no‑op, relative tolerances for rotation, negative scalar multiplies. + > 2025-10-28 — Devcontainer/toolchain alignment - Single source of truth: `rust-toolchain.toml` (MSRV = 1.68.0). From 76f2f312742c3d1fa637b46d3e45253eef77c382 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:30:29 -0700 Subject: [PATCH 07/15] toolchain: default 1.71.1; add MSRV CI job for rmg-core/geom on 1.68; devcontainer installs both; wasm crate rust-version=1.71; pre-push runs workspace on default and MSRV for core+geom; docs updated --- .devcontainer/post-create.sh | 13 ++++++------ .githooks/pre-push | 23 ++++++++++----------- .github/workflows/ci.yml | 39 +++++++++++++----------------------- CONTRIBUTING.md | 4 ++-- crates/rmg-ffi/README.md | 3 ++- crates/rmg-wasm/Cargo.toml | 2 +- docs/decision-log.md | 6 ++++++ docs/execution-plan.md | 9 +++++---- rust-toolchain.toml | 2 +- 9 files changed, 48 insertions(+), 53 deletions(-) diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 833d0bd..50180b4 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -1,19 +1,18 @@ #!/usr/bin/env bash set -euo pipefail -echo "[devcontainer] Installing MSRV toolchain (1.68.0) and respecting rust-toolchain.toml..." +echo "[devcontainer] Installing default toolchain (1.71.1 via rust-toolchain.toml) and MSRV (1.68.0)..." if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail https://sh.rustup.rs | sh -s -- --default-toolchain none -y export PATH="$HOME/.cargo/bin:$PATH" fi +rustup toolchain install 1.71.1 --profile minimal rustup toolchain install 1.68.0 --profile minimal -# Do not override default; let rust-toolchain.toml control toolchain selection for this repo. -# Install optional newer toolchain for local convenience (kept as non-default). -rustup toolchain install 1.90.0 --profile minimal || true -# Ensure components/targets are available for the active (rust-toolchain.toml) toolchain. -rustup component add rustfmt clippy || true -rustup target add wasm32-unknown-unknown || true +# Do not override default; let rust-toolchain.toml control selection for this repo. +# Ensure components/targets are available for the default toolchain (1.71.1). +rustup component add --toolchain 1.71.1 rustfmt clippy || true +rustup target add --toolchain 1.71.1 wasm32-unknown-unknown || true echo "[devcontainer] Priming cargo registry cache (optional)..." cargo fetch || true diff --git a/.githooks/pre-push b/.githooks/pre-push index edfe707..aef56ed 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -15,26 +15,25 @@ if [[ "${SKIP_HOOKS:-}" == 1 ]]; then exit 0 fi -echo "[pre-push] fmt" -cargo +"$PINNED" fmt --all -- --check +echo "[pre-push] fmt (default toolchain)" +cargo fmt --all -- --check -echo "[pre-push] clippy (workspace)" -cargo +"$PINNED" clippy --all-targets -- -D warnings -D missing_docs +echo "[pre-push] clippy (workspace, default toolchain)" +cargo clippy --all-targets -- -D warnings -D missing_docs -echo "[pre-push] tests (workspace)" -cargo +"$PINNED" test --workspace +echo "[pre-push] tests (workspace, default toolchain)" +cargo test --workspace -# MSRV check for rmg-core -echo "[pre-push] MSRV check (rmg-core @ $PINNED)" -if rustup run "$PINNED" cargo -V >/dev/null 2>&1; then - cargo +"$PINNED" check -p rmg-core --all-targets -else +echo "[pre-push] MSRV checks (@ $PINNED) for core libraries" +if ! rustup run "$PINNED" cargo -V >/dev/null 2>&1; then echo "[pre-push] MSRV toolchain $PINNED not installed. Install via: rustup toolchain install $PINNED" >&2 exit 1 fi +cargo +"$PINNED" test -p rmg-core --all-targets +cargo +"$PINNED" test -p rmg-geom --all-targets # Rustdoc warnings guard (core API) -echo "[pre-push] rustdoc warnings gate (rmg-core)" +echo "[pre-push] rustdoc warnings gate (rmg-core @ $PINNED)" RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p rmg-core --no-deps # Banned patterns diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 453257f..6aa2b39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,8 +15,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.71.1 - uses: Swatinem/rust-cache@v2 with: workspaces: | @@ -31,19 +30,15 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.71.1 with: - toolchain: stable + toolchain: 1.71.1 components: clippy - - name: rustup override stable - run: rustup toolchain install stable && rustup override set stable - uses: Swatinem/rust-cache@v2 with: workspaces: | . - name: cargo clippy - env: - RUSTUP_TOOLCHAIN: stable run: cargo clippy --all-targets -- -D warnings -D missing_docs test: @@ -53,19 +48,15 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.71.1 with: - toolchain: stable - - name: rustup override stable - run: rustup toolchain install stable && rustup override set stable + toolchain: 1.71.1 - uses: Swatinem/rust-cache@v2 with: workspaces: | . - - name: cargo test - env: - RUSTUP_TOOLCHAIN: stable - run: cargo test + - name: cargo test (workspace) + run: cargo test --workspace - name: PRNG golden regression (rmg-core) run: cargo test -p rmg-core --features golden_prng -- tests::next_int_golden_regression @@ -102,7 +93,7 @@ jobs: } msrv: - name: MSRV (rmg-core @ 1.68) + name: MSRV (core+geom @ 1.68) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -113,8 +104,10 @@ jobs: with: workspaces: | . - - name: cargo check (rmg-core) - run: cargo check -p rmg-core --all-targets + - name: cargo test (rmg-core) + run: cargo test -p rmg-core --all-targets + - name: cargo test (rmg-geom) + run: cargo test -p rmg-geom --all-targets rustdoc: name: Rustdoc (rmg-core warnings gate) @@ -123,13 +116,9 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.71.1 with: - toolchain: stable - - name: rustup override stable - run: rustup toolchain install stable && rustup override set stable + toolchain: 1.71.1 - uses: Swatinem/rust-cache@v2 - name: rustdoc warnings gate - env: - RUSTUP_TOOLCHAIN: stable run: RUSTDOCFLAGS="-D warnings" cargo doc -p rmg-core --no-deps diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1bc1bd7..8bccc3d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,8 +24,8 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize: 3. Review `AGENTS.md` for collaboration norms before touching runtime code. 4. Optional: develop inside the devcontainer for toolchain parity with CI. - Open in VS Code → "Reopen in Container" (requires the Dev Containers extension). - - The container includes Rust stable + MSRV toolchains, clippy/rustfmt, Node, and gh. - - Post-create installs MSRV 1.68.0 and wasm target. +- The container includes Rust default (1.71.1 via rust-toolchain.toml) plus MSRV 1.68.0, clippy/rustfmt, Node, and gh. +- Post-create installs toolchains 1.71.1 and 1.68.0 (no override); wasm32 target and components are added to 1.71.1. ## Branching & Workflow - Keep `main` pristine. Create feature branches like `echo/` or `timeline/`. diff --git a/crates/rmg-ffi/README.md b/crates/rmg-ffi/README.md index e3ffc5f..97314b7 100644 --- a/crates/rmg-ffi/README.md +++ b/crates/rmg-ffi/README.md @@ -6,7 +6,8 @@ This crate produces a C-callable library for embedding Echo’s core in other ru ## Platforms and Toolchain -- Rust: 1.68 (pinned via `rust-toolchain.toml`) +- Default Rust toolchain: 1.71.1 (via repository `rust-toolchain.toml`). +- MSRV for core libraries: 1.68.0 (CI enforces compatibility for `rmg-core` and `rmg-geom`). - Targets: macOS (aarch64/x86_64), Linux (x86_64). Windows support is planned. ## Building diff --git a/crates/rmg-wasm/Cargo.toml b/crates/rmg-wasm/Cargo.toml index 4e298c0..8d23877 100644 --- a/crates/rmg-wasm/Cargo.toml +++ b/crates/rmg-wasm/Cargo.toml @@ -2,7 +2,7 @@ name = "rmg-wasm" version = "0.1.0" edition = "2021" -rust-version = "1.68" +rust-version = "1.71" description = "Echo WASM: wasm-bindgen bindings for tools and web" license = "Apache-2.0" repository = "https://github.com/flyingrobots/echo" diff --git a/docs/decision-log.md b/docs/decision-log.md index bf76463..1eecc36 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -49,3 +49,9 @@ - Scheduler is crate‑private; ordering invariant documented (ascending `(scope_hash, rule_id)`). - Tests: Added/updated motion tests (velocity preserved; commit after `NoMatch` is a no‑op), math tests (relative tolerances; negative scalar multiplies; extra mul order). - Consequence: Deterministic provenance is now explicit; future Aion inputs can populate `decision_digest` without reworking the header. No behavior changes for state hashing. + +## 2025-10-29 — Toolchain strategy: default 1.71.1 + MSRV job + +- Decision: Make Rust 1.71.1 the default toolchain (via `rust-toolchain.toml`) for the workspace to unblock `rmg-wasm` (wasm-bindgen/bumpalo). +- MSRV: Keep library MSRV at 1.68.0; CI adds a dedicated MSRV job that builds/tests `rmg-core` and `rmg-geom` on 1.68.0. +- Implementation: Updated `rust-toolchain.toml`, CI workflow to pin 1.71.1 for workspace jobs and add an MSRV matrix entry; devcontainer installs both toolchains without overriding defaults. diff --git a/docs/execution-plan.md b/docs/execution-plan.md index 5df42c2..ee69a7c 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -48,10 +48,11 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s > 2025-10-28 — Devcontainer/toolchain alignment -- Single source of truth: `rust-toolchain.toml` (MSRV = 1.68.0). -- Devcontainer must not override default toolchain; the feature installs Rust but selection is controlled by `rust-toolchain.toml`. -- Post-create respects `rust-toolchain.toml` (no `rustup default stable`); installs MSRV (1.68.0) and optionally 1.90.0 without changing the default; adds rustfmt/clippy and wasm32 target. -- CI should pin the toolchain explicitly (MSRV job on 1.68; avoid forcing `stable` overrides in workspace jobs). +- Default toolchain via `rust-toolchain.toml`: 1.71.1. +- MSRV retained for libraries (`rmg-core`, `rmg-geom`) at 1.68.0 via dedicated CI job. +- Devcontainer must not override default; selection is controlled by `rust-toolchain.toml`. +- Post-create installs both 1.71.1 (components + wasm32) and 1.68.0 without changing the default. +- CI pins 1.71.1 for workspace jobs and adds an MSRV job (1.68.0) that builds and tests `rmg-core` and `rmg-geom`. > 2025-10-28 — Pre-commit auto-format flag update diff --git a/rust-toolchain.toml b/rust-toolchain.toml index b2cf8c5..7f9aa4d 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.68.0" +channel = "1.71.1" components = ["rustfmt", "clippy"] From b915fea50f93b919557e5a9edd90591a56c42edd Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:35:09 -0700 Subject: [PATCH 08/15] toolchain(floor): raise workspace MSRV to 1.71.1; bump rust-version across crates; align CI/devcontainer/hooks/docs --- .devcontainer/post-create.sh | 3 +-- .githooks/pre-commit | 6 +++--- .githooks/pre-push | 2 +- .github/workflows/ci.yml | 4 ++-- CONTRIBUTING.md | 4 ++-- crates/rmg-cli/Cargo.toml | 2 +- crates/rmg-core/Cargo.toml | 2 +- crates/rmg-ffi/Cargo.toml | 2 +- crates/rmg-wasm/Cargo.toml | 2 +- docs/decision-log.md | 7 +++---- docs/execution-plan.md | 7 +++---- 11 files changed, 19 insertions(+), 22 deletions(-) diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 50180b4..25f042e 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -1,14 +1,13 @@ #!/usr/bin/env bash set -euo pipefail -echo "[devcontainer] Installing default toolchain (1.71.1 via rust-toolchain.toml) and MSRV (1.68.0)..." +echo "[devcontainer] Installing default toolchain (1.71.1 via rust-toolchain.toml)..." if ! command -v rustup >/dev/null 2>&1; then curl --proto '=https' --tlsv1.2 --retry 10 --retry-connrefused --location --silent --show-error --fail https://sh.rustup.rs | sh -s -- --default-toolchain none -y export PATH="$HOME/.cargo/bin:$PATH" fi rustup toolchain install 1.71.1 --profile minimal -rustup toolchain install 1.68.0 --profile minimal # Do not override default; let rust-toolchain.toml control selection for this repo. # Ensure components/targets are available for the default toolchain (1.71.1). rustup component add --toolchain 1.71.1 rustfmt clippy || true diff --git a/.githooks/pre-commit b/.githooks/pre-commit index a3839b6..1854e42 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -79,12 +79,12 @@ if [[ -n "$CORE_API_CHANGED" ]]; then echo "$STAGED" | grep -Fx 'docs/decision-log.md' >/dev/null || { echo 'pre-commit: docs/decision-log.md must be updated when core API changes.' >&2; exit 1; } fi -# 5) Lockfile guard: ensure lockfile version is v3 (compatible with MSRV cargo) +# 5) Lockfile guard: ensure lockfile version is v3 (current cargo format) if [[ -f Cargo.lock ]]; then VER_LINE=$(grep -n '^version = ' Cargo.lock | head -n1 | awk -F'= ' '{print $2}') if [[ "$VER_LINE" != "3" && "$VER_LINE" != "3\r" ]]; then - echo "pre-commit: Cargo.lock must be generated with Cargo 1.68 (lockfile v3)." >&2 - echo "Run: cargo +1.68.0 generate-lockfile" >&2 + echo "pre-commit: Cargo.lock must be lockfile format v3." >&2 + echo "Run: cargo +1.71.1 generate-lockfile" >&2 exit 1 fi fi diff --git a/.githooks/pre-push b/.githooks/pre-push index aef56ed..d07e0a8 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,6 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -PINNED="${PINNED:-1.68.0}" +PINNED="${PINNED:-1.71.1}" for cmd in cargo rustup rg; do if ! command -v "$cmd" >/dev/null 2>&1; then diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6aa2b39..4f524af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -93,13 +93,13 @@ jobs: } msrv: - name: MSRV (core+geom @ 1.68) + name: Floor (core+geom @ 1.71.1) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@1.68.0 + - uses: dtolnay/rust-toolchain@1.71.1 - uses: Swatinem/rust-cache@v2 with: workspaces: | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bccc3d..4bdd01c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,8 +24,8 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize: 3. Review `AGENTS.md` for collaboration norms before touching runtime code. 4. Optional: develop inside the devcontainer for toolchain parity with CI. - Open in VS Code → "Reopen in Container" (requires the Dev Containers extension). -- The container includes Rust default (1.71.1 via rust-toolchain.toml) plus MSRV 1.68.0, clippy/rustfmt, Node, and gh. -- Post-create installs toolchains 1.71.1 and 1.68.0 (no override); wasm32 target and components are added to 1.71.1. +- The container includes Rust 1.71.1 (via rust-toolchain.toml), clippy/rustfmt, Node, and gh. +- Post-create installs toolchain 1.71.1 (no override); wasm32 target and components are added to 1.71.1. ## Branching & Workflow - Keep `main` pristine. Create feature branches like `echo/` or `timeline/`. diff --git a/crates/rmg-cli/Cargo.toml b/crates/rmg-cli/Cargo.toml index 3e86bf6..918f89c 100644 --- a/crates/rmg-cli/Cargo.toml +++ b/crates/rmg-cli/Cargo.toml @@ -2,7 +2,7 @@ name = "rmg-cli" version = "0.1.0" edition = "2021" -rust-version = "1.68" +rust-version = "1.71.1" description = "Echo CLI: demos, benches, inspector launcher (future)" license = "Apache-2.0" repository = "https://github.com/flyingrobots/echo" diff --git a/crates/rmg-core/Cargo.toml b/crates/rmg-core/Cargo.toml index 5e1b126..3a7a863 100644 --- a/crates/rmg-core/Cargo.toml +++ b/crates/rmg-core/Cargo.toml @@ -2,7 +2,7 @@ name = "rmg-core" version = "0.1.0" edition = "2021" -rust-version = "1.68" +rust-version = "1.71.1" description = "Echo core: deterministic typed graph rewriting engine" license = "Apache-2.0" repository = "https://github.com/flyingrobots/echo" diff --git a/crates/rmg-ffi/Cargo.toml b/crates/rmg-ffi/Cargo.toml index 85d001b..522717f 100644 --- a/crates/rmg-ffi/Cargo.toml +++ b/crates/rmg-ffi/Cargo.toml @@ -2,7 +2,7 @@ name = "rmg-ffi" version = "0.1.0" edition = "2021" -rust-version = "1.68" +rust-version = "1.71.1" description = "Echo FFI: C ABI for host integrations (Lua/C/etc.)" license = "Apache-2.0" repository = "https://github.com/flyingrobots/echo" diff --git a/crates/rmg-wasm/Cargo.toml b/crates/rmg-wasm/Cargo.toml index 8d23877..84b8f25 100644 --- a/crates/rmg-wasm/Cargo.toml +++ b/crates/rmg-wasm/Cargo.toml @@ -2,7 +2,7 @@ name = "rmg-wasm" version = "0.1.0" edition = "2021" -rust-version = "1.71" +rust-version = "1.71.1" description = "Echo WASM: wasm-bindgen bindings for tools and web" license = "Apache-2.0" repository = "https://github.com/flyingrobots/echo" diff --git a/docs/decision-log.md b/docs/decision-log.md index 1eecc36..ba255cc 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -50,8 +50,7 @@ - Tests: Added/updated motion tests (velocity preserved; commit after `NoMatch` is a no‑op), math tests (relative tolerances; negative scalar multiplies; extra mul order). - Consequence: Deterministic provenance is now explicit; future Aion inputs can populate `decision_digest` without reworking the header. No behavior changes for state hashing. -## 2025-10-29 — Toolchain strategy: default 1.71.1 + MSRV job +## 2025-10-29 — Toolchain strategy: floor raised to 1.71.1 -- Decision: Make Rust 1.71.1 the default toolchain (via `rust-toolchain.toml`) for the workspace to unblock `rmg-wasm` (wasm-bindgen/bumpalo). -- MSRV: Keep library MSRV at 1.68.0; CI adds a dedicated MSRV job that builds/tests `rmg-core` and `rmg-geom` on 1.68.0. -- Implementation: Updated `rust-toolchain.toml`, CI workflow to pin 1.71.1 for workspace jobs and add an MSRV matrix entry; devcontainer installs both toolchains without overriding defaults. +- Decision: Raise the workspace floor (MSRV) to Rust 1.71.1. All crates and CI jobs target 1.71.1. +- Implementation: Updated `rust-toolchain.toml` to 1.71.1; bumped `rust-version` in crate manifests; CI jobs pin 1.71.1; devcontainer installs only 1.71.1. diff --git a/docs/execution-plan.md b/docs/execution-plan.md index ee69a7c..ecb4c6d 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -48,11 +48,10 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s > 2025-10-28 — Devcontainer/toolchain alignment -- Default toolchain via `rust-toolchain.toml`: 1.71.1. -- MSRV retained for libraries (`rmg-core`, `rmg-geom`) at 1.68.0 via dedicated CI job. +- Toolchain floor via `rust-toolchain.toml`: 1.71.1 (workspace-wide). - Devcontainer must not override default; selection is controlled by `rust-toolchain.toml`. -- Post-create installs both 1.71.1 (components + wasm32) and 1.68.0 without changing the default. -- CI pins 1.71.1 for workspace jobs and adds an MSRV job (1.68.0) that builds and tests `rmg-core` and `rmg-geom`. +- Post-create installs 1.71.1 (adds rustfmt/clippy and wasm32 target). +- CI pins 1.71.1 for all jobs; an additional floor job on 1.71.1 validates `rmg-core` and `rmg-geom` explicitly. > 2025-10-28 — Pre-commit auto-format flag update From f47b3af59f21e946b03ca9e1ca32aa48cbfb53b9 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:35:52 -0700 Subject: [PATCH 09/15] hooks: restrict missing_docs ban to library src files; allow tests/build.rs --- .githooks/pre-push | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index d07e0a8..989ec70 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -38,10 +38,14 @@ RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p rmg-core --no-deps # Banned patterns echo "[pre-push] scanning banned patterns" -# Match any crate-level allow(...) that includes missing_docs; exclude telemetry.rs explicitly -if rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' --glob '!crates/rmg-core/src/telemetry.rs' crates >/dev/null; then +# Forbid crate-level allow(missing_docs) in library source files, but allow in tests and build scripts +if rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' \ + crates \ + --glob 'crates/**/src/**/*.rs' \ + --glob '!**/telemetry.rs' \ + --glob '!**/tests/**' >/dev/null; then echo "pre-push: crate-level allow(missing_docs) is forbidden (except telemetry.rs)." >&2 - rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' --glob '!crates/rmg-core/src/telemetry.rs' crates | cat >&2 || true + rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' crates --glob 'crates/**/src/**/*.rs' --glob '!**/telemetry.rs' --glob '!**/tests/**' | cat >&2 || true exit 1 fi if rg -n "\#\[unsafe\(no_mangle\)\]" crates >/dev/null; then From e0da662720fa9286ce6f71a3008ae12609c047ba Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:42:54 -0700 Subject: [PATCH 10/15] geom(broad): remove redundant sort and unused import; specs/CI updated --- .github/workflows/ci.yml | 16 ---------------- crates/rmg-geom/src/broad/aabb_tree.rs | 4 ---- docs/spec-knots-in-time.md | 4 +++- docs/spec-timecube.md | 5 ++++- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4f524af..ae7b9c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,22 +92,6 @@ jobs: exit 1; } - msrv: - name: Floor (core+geom @ 1.71.1) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: false - - uses: dtolnay/rust-toolchain@1.71.1 - - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - . - - name: cargo test (rmg-core) - run: cargo test -p rmg-core --all-targets - - name: cargo test (rmg-geom) - run: cargo test -p rmg-geom --all-targets rustdoc: name: Rustdoc (rmg-core warnings gate) diff --git a/crates/rmg-geom/src/broad/aabb_tree.rs b/crates/rmg-geom/src/broad/aabb_tree.rs index 32a7371..fe9fbd8 100644 --- a/crates/rmg-geom/src/broad/aabb_tree.rs +++ b/crates/rmg-geom/src/broad/aabb_tree.rs @@ -76,10 +76,6 @@ impl BroadPhase for AabbTree { } } } - out.sort_unstable_by(|x, y| match x.0.cmp(&y.0) { - Ordering::Equal => x.1.cmp(&y.1), - o => o, - }); out } } diff --git a/docs/spec-knots-in-time.md b/docs/spec-knots-in-time.md index 40ec9c8..4d40e97 100644 --- a/docs/spec-knots-in-time.md +++ b/docs/spec-knots-in-time.md @@ -58,6 +58,7 @@ Both are read‑only folds over commits; they do not change physics or rewrite s --- + ## Kinematics: Where Knots Touch Physics We keep physics a **fold** over the graph and combine it with Chronos Timespans to obtain deterministic swept bounds. @@ -66,13 +67,14 @@ We keep physics a **fold** over the graph and combine it with Chronos Timespans 2) Geometry fold: local shape → world AABB at `start` and at `end` 3) Swept AABB (conservative swept volume proxy) - Pure translation by `d`: exact swept volume = Minkowski sum `K ⊕ segment[0,d]`; swept AABB equals hull of start/end world AABBs - - With rotation: use conservative hull of start/end world AABBs (determinstic and fast); refine later if needed + - With rotation: use conservative hull of start/end world AABBs (deterministic and fast); refine later if needed 4) Kairos::swept: build `SweptVolumeProxy { entity, tick, fat: Aabb }` and insert into broad‑phase (pairs in canonical order) This is orthogonal to knot diagrams; the latter lives in the state graph as its own domain with its own rewrites and invariants. --- + ## Determinism & Identity (No “Teleporting” States) Echo commits are Merkle nodes (see `spec-timecube.md`). A snapshot’s hash includes: diff --git a/docs/spec-timecube.md b/docs/spec-timecube.md index 07924c5..499964f 100644 --- a/docs/spec-timecube.md +++ b/docs/spec-timecube.md @@ -97,7 +97,10 @@ Use Cases - Prioritize expensive folds (render/physics budgets) without affecting correctness. - Log decisions so replay is identical across peers. -## Operations (safe moves) + +## Operations + + (safe moves) - Fork branch (Kairos): split branch at commit C; new branch’s first parent is C. - Merge branches (Kairos): new commit with parents [L, R]; MWMR + domain joins + Aion bias (deterministic), decision logged. From 56cbfc29c07fe87064c0adeb3c78e298cce39b79 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:43:19 -0700 Subject: [PATCH 11/15] geom(broad): drop unused Ordering import after removing redundant sort --- crates/rmg-geom/src/broad/aabb_tree.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/rmg-geom/src/broad/aabb_tree.rs b/crates/rmg-geom/src/broad/aabb_tree.rs index fe9fbd8..2933970 100644 --- a/crates/rmg-geom/src/broad/aabb_tree.rs +++ b/crates/rmg-geom/src/broad/aabb_tree.rs @@ -1,5 +1,4 @@ use crate::types::aabb::Aabb; -use core::cmp::Ordering; use std::collections::BTreeMap; /// Broad-phase interface for inserting proxies and querying overlapping pairs. From 734499e94355a591deade0e4260be3bdbf567351 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:55:37 -0700 Subject: [PATCH 12/15] core(snapshot): unify decision_digest empty semantics across commit() and snapshot(); fix doc lints --- .githooks/pre-commit | 15 ++++++++--- .githooks/pre-push | 40 ++++++++++++++++++++++++------ .github/workflows/ci.yml | 5 ---- crates/rmg-core/Cargo.toml | 2 +- crates/rmg-core/src/constants.rs | 20 +++++++++++++++ crates/rmg-core/src/engine_impl.rs | 10 +++++--- crates/rmg-core/src/lib.rs | 3 +++ crates/rmg-core/src/snapshot.rs | 14 +++++++---- docs/decision-log.md | 2 +- 9 files changed, 83 insertions(+), 28 deletions(-) create mode 100644 crates/rmg-core/src/constants.rs diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 1854e42..3317478 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -81,10 +81,17 @@ fi # 5) Lockfile guard: ensure lockfile version is v3 (current cargo format) if [[ -f Cargo.lock ]]; then - VER_LINE=$(grep -n '^version = ' Cargo.lock | head -n1 | awk -F'= ' '{print $2}') - if [[ "$VER_LINE" != "3" && "$VER_LINE" != "3\r" ]]; then - echo "pre-commit: Cargo.lock must be lockfile format v3." >&2 - echo "Run: cargo +1.71.1 generate-lockfile" >&2 + # Normalize detected lockfile version (strip quotes/CR/whitespace) + VER_LINE=$(grep -n '^version = ' Cargo.lock | head -n1 | awk -F'= ' '{print $2}' | tr -d '\r' | tr -d '"' | xargs) + if [[ "$VER_LINE" != "3" ]]; then + # Determine pinned toolchain (normalize), fallback to rust-toolchain.toml if unset + _PINNED_RAW="${PINNED:-}" + if [[ -z "$_PINNED_RAW" ]]; then + _PINNED_RAW=$(awk -F '"' '/^channel/ {print $2}' rust-toolchain.toml 2>/dev/null || echo "") + fi + PINNED_NORM=$(printf "%s" "$_PINNED_RAW" | tr -d '\r' | xargs) + echo "pre-commit: Cargo.lock must be lockfile format v3 (found '$VER_LINE')." >&2 + echo "Run: cargo +${PINNED_NORM} generate-lockfile" >&2 exit 1 fi fi diff --git a/.githooks/pre-push b/.githooks/pre-push index 989ec70..b3ea9fe 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail PINNED="${PINNED:-1.71.1}" +# Separate MSRV for library checks (override with MSRV env) +MSRV="${MSRV:-1.68.0}" for cmd in cargo rustup rg; do if ! command -v "$cmd" >/dev/null 2>&1; then @@ -24,17 +26,34 @@ cargo clippy --all-targets -- -D warnings -D missing_docs echo "[pre-push] tests (workspace, default toolchain)" cargo test --workspace -echo "[pre-push] MSRV checks (@ $PINNED) for core libraries" -if ! rustup run "$PINNED" cargo -V >/dev/null 2>&1; then - echo "[pre-push] MSRV toolchain $PINNED not installed. Install via: rustup toolchain install $PINNED" >&2 +echo "[pre-push] Testing against MSRV ${MSRV} (core libraries)" +if ! rustup run "$MSRV" cargo -V >/dev/null 2>&1; then + echo "[pre-push] MSRV toolchain ${MSRV} not installed. Install via: rustup toolchain install ${MSRV}" >&2 exit 1 fi -cargo +"$PINNED" test -p rmg-core --all-targets -cargo +"$PINNED" test -p rmg-geom --all-targets +# Only run MSRV tests for crates that declare rust-version <= MSRV; skip otherwise. +msrv_ok() { + local crate="$1" + local rv + rv=$(awk -F '"' '/^rust-version/ {print $2}' "crates/${crate}/Cargo.toml" 2>/dev/null || echo "") + if [[ -z "$rv" ]]; then + return 0 + fi + # If declared rust-version is greater than MSRV, skip. + if printf '%s\n%s\n' "$MSRV" "$rv" | sort -V | tail -n1 | grep -qx "$rv" && [[ "$rv" != "$MSRV" ]]; then + echo "[pre-push] Skipping MSRV test for ${crate} (rust-version ${rv} > MSRV ${MSRV})" + return 1 + fi + return 0 +} +if msrv_ok rmg-core; then cargo +"$MSRV" test -p rmg-core --all-targets; fi +if msrv_ok rmg-geom; then cargo +"$MSRV" test -p rmg-geom --all-targets; fi -# Rustdoc warnings guard (core API) +# Rustdoc warnings guard (public crates) echo "[pre-push] rustdoc warnings gate (rmg-core @ $PINNED)" RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p rmg-core --no-deps +echo "[pre-push] rustdoc warnings gate (rmg-geom @ $PINNED)" +RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p rmg-geom --no-deps # Banned patterns echo "[pre-push] scanning banned patterns" @@ -43,9 +62,14 @@ if rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' \ crates \ --glob 'crates/**/src/**/*.rs' \ --glob '!**/telemetry.rs' \ - --glob '!**/tests/**' >/dev/null; then + --glob '!**/tests/**' \ + --glob '!**/build.rs' >/dev/null; then echo "pre-push: crate-level allow(missing_docs) is forbidden (except telemetry.rs)." >&2 - rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' crates --glob 'crates/**/src/**/*.rs' --glob '!**/telemetry.rs' --glob '!**/tests/**' | cat >&2 || true + rg -n '#!\[allow\([^]]*missing_docs[^]]*\)\]' crates \ + --glob 'crates/**/src/**/*.rs' \ + --glob '!**/telemetry.rs' \ + --glob '!**/tests/**' \ + --glob '!**/build.rs' | cat >&2 || true exit 1 fi if rg -n "\#\[unsafe\(no_mangle\)\]" crates >/dev/null; then diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae7b9c6..911f810 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,7 +32,6 @@ jobs: submodules: false - uses: dtolnay/rust-toolchain@1.71.1 with: - toolchain: 1.71.1 components: clippy - uses: Swatinem/rust-cache@v2 with: @@ -49,8 +48,6 @@ jobs: with: submodules: false - uses: dtolnay/rust-toolchain@1.71.1 - with: - toolchain: 1.71.1 - uses: Swatinem/rust-cache@v2 with: workspaces: | @@ -101,8 +98,6 @@ jobs: with: submodules: false - uses: dtolnay/rust-toolchain@1.71.1 - with: - toolchain: 1.71.1 - uses: Swatinem/rust-cache@v2 - name: rustdoc warnings gate run: RUSTDOCFLAGS="-D warnings" cargo doc -p rmg-core --no-deps diff --git a/crates/rmg-core/Cargo.toml b/crates/rmg-core/Cargo.toml index 3a7a863..8f87bcd 100644 --- a/crates/rmg-core/Cargo.toml +++ b/crates/rmg-core/Cargo.toml @@ -18,9 +18,9 @@ thiserror = "1.0" hex = { version = "0.4", optional = true } serde = { version = "1.0", features = ["derive"], optional = true } serde_json = { version = "1.0", optional = true } +once_cell = "1.19" [dev-dependencies] -once_cell = "1.19" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/crates/rmg-core/src/constants.rs b/crates/rmg-core/src/constants.rs new file mode 100644 index 0000000..a5549ff --- /dev/null +++ b/crates/rmg-core/src/constants.rs @@ -0,0 +1,20 @@ +//! Canonical digests and constants used across the engine. +use once_cell::sync::Lazy; + +use crate::ident::Hash; + +/// BLAKE3 digest of an empty byte slice. +/// +/// Used where canonical empty input semantics are required. +pub static BLAKE3_EMPTY: Lazy = Lazy::new(|| blake3::hash(&[]).into()); + +/// Canonical digest representing an empty length-prefix list: BLAKE3 of +/// `0u64.to_le_bytes()`. +/// +/// Used for plan/decision/rewrites digests when the corresponding list is empty. +pub static DIGEST_LEN0_U64: Lazy = Lazy::new(|| { + let mut h = blake3::Hasher::new(); + h.update(&0u64.to_le_bytes()); + h.finalize().into() +}); + diff --git a/crates/rmg-core/src/engine_impl.rs b/crates/rmg-core/src/engine_impl.rs index fb1ed3c..4c2cd27 100644 --- a/crates/rmg-core/src/engine_impl.rs +++ b/crates/rmg-core/src/engine_impl.rs @@ -230,13 +230,15 @@ impl Engine { .as_ref() .map(|s| vec![s.hash]) .unwrap_or_default(); - let decision_digest: Hash = [0u8; 32]; // No Aion decisions yet; placeholder zero digest. + // Canonical empty digest (0-length list) for decisions until Aion lands. + let decision_digest: Hash = *crate::constants::DIGEST_LEN0_U64; let hash = crate::snapshot::compute_commit_hash( &state_root, &parents, &plan_digest, &decision_digest, &rewrites_digest, + 0, ); let snapshot = Snapshot { root: self.current_root, @@ -274,14 +276,14 @@ impl Engine { h.update(&0u64.to_le_bytes()); h.finalize().into() }; - let zero: Hash = [0u8; 32]; - let hash = compute_commit_hash(&state_root, &parents, &empty_digest, &zero, &empty_digest); + let decision_empty: Hash = *crate::constants::DIGEST_LEN0_U64; + let hash = compute_commit_hash(&state_root, &parents, &empty_digest, &decision_empty, &empty_digest, 0); Snapshot { root: self.current_root, hash, parents, plan_digest: empty_digest, - decision_digest: zero, + decision_digest: decision_empty, rewrites_digest: empty_digest, policy_id: 0, tx: TxId::from_raw(self.tx_counter), diff --git a/crates/rmg-core/src/lib.rs b/crates/rmg-core/src/lib.rs index 01213e0..8ebd543 100644 --- a/crates/rmg-core/src/lib.rs +++ b/crates/rmg-core/src/lib.rs @@ -37,6 +37,7 @@ pub mod math; /// Demo implementations showcasing engine capabilities (e.g., motion rule). pub mod demo; mod engine_impl; +mod constants; mod footprint; mod graph; mod ident; @@ -76,3 +77,5 @@ pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; pub use snapshot::Snapshot; /// Transaction identifier type. pub use tx::TxId; +/// Canonical digests (e.g., empty inputs, empty length-prefixed lists). +pub use constants::{BLAKE3_EMPTY, DIGEST_LEN0_U64}; diff --git a/crates/rmg-core/src/snapshot.rs b/crates/rmg-core/src/snapshot.rs index 17ce23f..ebb1c98 100644 --- a/crates/rmg-core/src/snapshot.rs +++ b/crates/rmg-core/src/snapshot.rs @@ -1,8 +1,9 @@ //! Snapshot type and hash computation. //! //! Determinism contract -//! - The snapshot hash is a BLAKE3 digest over a canonical byte stream that -//! encodes the entire reachable graph state for the current root. +//! - The graph state hash (`state_root`) is a BLAKE3 digest over a canonical +//! byte stream that encodes the entire reachable graph state for the current +//! root. //! - Ordering is explicit and stable: nodes are visited in ascending `NodeId` //! order (lexicographic over 32-byte ids). For each node, outbound edges are //! sorted by ascending `EdgeId` before being encoded. @@ -30,13 +31,14 @@ use crate::tx::TxId; /// Snapshot returned after a successful commit. /// -/// The `hash` value is deterministic and reflects the entire canonicalised -/// graph state plus commit metadata. Parents are explicit to support merges. +/// The `hash` field is a deterministic commit hash (`commit_id`) computed from +/// `state_root` (graph-only hash) and commit metadata (parents, digests, +/// policy). Parents are explicit to support merges. #[derive(Debug, Clone)] pub struct Snapshot { /// Node identifier that serves as the root of the snapshot. pub root: NodeId, - /// Canonical commit hash derived from state + metadata (see below). + /// Canonical commit hash derived from state_root + metadata (see below). pub hash: Hash, /// Parent snapshot hashes (empty for initial commit, 1 for linear history, 2+ for merges). pub parents: Vec, @@ -143,6 +145,7 @@ pub(crate) fn compute_commit_hash( plan_digest: &Hash, decision_digest: &Hash, rewrites_digest: &Hash, + policy_id: u32, ) -> Hash { let mut h = Hasher::new(); // Version tag for future evolution. @@ -157,5 +160,6 @@ pub(crate) fn compute_commit_hash( h.update(plan_digest); h.update(decision_digest); h.update(rewrites_digest); + h.update(&policy_id.to_le_bytes()); h.finalize().into() } diff --git a/docs/decision-log.md b/docs/decision-log.md index ba255cc..e0763c3 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -39,7 +39,7 @@ ## 2025-10-29 — Snapshot header v1 + tx/rule hardening (rmg-core) - Context: PR #9 base work on top of PR #8; integrate deterministic provenance into snapshots without changing reachable‑only state hashing. -- Decision: Model snapshots as commit headers with explicit `parents` and metadata digests (`plan`, `decision`, `rewrites`). Keep `decision_digest = 0` until Aion/agency lands. +- Decision: Model snapshots as commit headers with explicit `parents` and metadata digests (`plan`, `decision`, `rewrites`). Keep `decision_digest = blake3(len=0_u64)` (canonical empty list digest) until Aion/agency lands. - Changes: - `Snapshot { parents: Vec, plan_digest, decision_digest, rewrites_digest, policy_id }`. - `Engine::commit()` computes `state_root`, canonical empty/non‑empty digests, and final commit hash. From e734dc5239226bded8071a71ba4020d032ffb80c Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:55:48 -0700 Subject: [PATCH 13/15] fmt: apply rustfmt after hooks/constant changes --- crates/rmg-core/src/constants.rs | 1 - crates/rmg-core/src/engine_impl.rs | 9 ++++++++- crates/rmg-core/src/lib.rs | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/rmg-core/src/constants.rs b/crates/rmg-core/src/constants.rs index a5549ff..19cc1ab 100644 --- a/crates/rmg-core/src/constants.rs +++ b/crates/rmg-core/src/constants.rs @@ -17,4 +17,3 @@ pub static DIGEST_LEN0_U64: Lazy = Lazy::new(|| { h.update(&0u64.to_le_bytes()); h.finalize().into() }); - diff --git a/crates/rmg-core/src/engine_impl.rs b/crates/rmg-core/src/engine_impl.rs index 4c2cd27..fdb1780 100644 --- a/crates/rmg-core/src/engine_impl.rs +++ b/crates/rmg-core/src/engine_impl.rs @@ -277,7 +277,14 @@ impl Engine { h.finalize().into() }; let decision_empty: Hash = *crate::constants::DIGEST_LEN0_U64; - let hash = compute_commit_hash(&state_root, &parents, &empty_digest, &decision_empty, &empty_digest, 0); + let hash = compute_commit_hash( + &state_root, + &parents, + &empty_digest, + &decision_empty, + &empty_digest, + 0, + ); Snapshot { root: self.current_root, hash, diff --git a/crates/rmg-core/src/lib.rs b/crates/rmg-core/src/lib.rs index 8ebd543..e443fb2 100644 --- a/crates/rmg-core/src/lib.rs +++ b/crates/rmg-core/src/lib.rs @@ -34,10 +34,10 @@ /// Deterministic math subsystem (Vec3, Mat4, Quat, PRNG). pub mod math; +mod constants; /// Demo implementations showcasing engine capabilities (e.g., motion rule). pub mod demo; mod engine_impl; -mod constants; mod footprint; mod graph; mod ident; @@ -49,6 +49,8 @@ mod snapshot; mod tx; // Re-exports for stable public API +/// Canonical digests (e.g., empty inputs, empty length-prefixed lists). +pub use constants::{BLAKE3_EMPTY, DIGEST_LEN0_U64}; /// Demo helpers and constants for the motion rule. pub use demo::motion::{build_motion_demo_engine, motion_rule, MOTION_RULE_NAME}; /// Rewrite engine and error types. @@ -77,5 +79,3 @@ pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; pub use snapshot::Snapshot; /// Transaction identifier type. pub use tx::TxId; -/// Canonical digests (e.g., empty inputs, empty length-prefixed lists). -pub use constants::{BLAKE3_EMPTY, DIGEST_LEN0_U64}; From ea45703971c8e22c867019b87d6eb9aeed1d5847 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 07:58:00 -0700 Subject: [PATCH 14/15] hooks(pre-push): add MSRV var with global skip when crates require newer rust-version; refine skip logic --- .githooks/pre-push | 57 ++++++++++++++++++++++++++++++---------------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index b3ea9fe..52b38c2 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -27,27 +27,44 @@ echo "[pre-push] tests (workspace, default toolchain)" cargo test --workspace echo "[pre-push] Testing against MSRV ${MSRV} (core libraries)" -if ! rustup run "$MSRV" cargo -V >/dev/null 2>&1; then - echo "[pre-push] MSRV toolchain ${MSRV} not installed. Install via: rustup toolchain install ${MSRV}" >&2 - exit 1 -fi -# Only run MSRV tests for crates that declare rust-version <= MSRV; skip otherwise. -msrv_ok() { - local crate="$1" - local rv - rv=$(awk -F '"' '/^rust-version/ {print $2}' "crates/${crate}/Cargo.toml" 2>/dev/null || echo "") - if [[ -z "$rv" ]]; then - return 0 - fi - # If declared rust-version is greater than MSRV, skip. - if printf '%s\n%s\n' "$MSRV" "$rv" | sort -V | tail -n1 | grep -qx "$rv" && [[ "$rv" != "$MSRV" ]]; then - echo "[pre-push] Skipping MSRV test for ${crate} (rust-version ${rv} > MSRV ${MSRV})" - return 1 +# If any participating crate declares a rust-version greater than MSRV, skip MSRV checks entirely. +CORE_RV=$(awk -F '"' '/^rust-version/ {print $2}' crates/rmg-core/Cargo.toml 2>/dev/null || echo "") +GEOM_RV=$(awk -F '"' '/^rust-version/ {print $2}' crates/rmg-geom/Cargo.toml 2>/dev/null || echo "") +if { [[ -n "$CORE_RV" ]] && printf '%s\n%s\n' "$MSRV" "$CORE_RV" | sort -V | tail -n1 | grep -qx "$CORE_RV" && [[ "$CORE_RV" != "$MSRV" ]]; } \ + || { [[ -n "$GEOM_RV" ]] && printf '%s\n%s\n' "$MSRV" "$GEOM_RV" | sort -V | tail -n1 | grep -qx "$GEOM_RV" && [[ "$GEOM_RV" != "$MSRV" ]]; }; then + echo "[pre-push] Skipping MSRV block: one or more crates declare rust-version > ${MSRV} (core=${CORE_RV:-unset}, geom=${GEOM_RV:-unset})" +else + if ! rustup run "$MSRV" cargo -V >/dev/null 2>&1; then + echo "[pre-push] MSRV toolchain ${MSRV} not installed. Install via: rustup toolchain install ${MSRV}" >&2 + exit 1 fi - return 0 -} -if msrv_ok rmg-core; then cargo +"$MSRV" test -p rmg-core --all-targets; fi -if msrv_ok rmg-geom; then cargo +"$MSRV" test -p rmg-geom --all-targets; fi + # Only run MSRV tests for crates that declare rust-version <= MSRV; skip otherwise. + msrv_ok() { + local crate="$1" + local rv + rv=$(awk -F '"' '/^rust-version/ {print $2}' "crates/${crate}/Cargo.toml" 2>/dev/null || echo "") + if [[ -z "$rv" ]]; then + return 0 + fi + # If declared rust-version is greater than MSRV, skip. + if printf '%s\n%s\n' "$MSRV" "$rv" | sort -V | tail -n1 | grep -qx "$rv" && [[ "$rv" != "$MSRV" ]]; then + echo "[pre-push] Skipping MSRV test for ${crate} (rust-version ${rv} > MSRV ${MSRV})" + return 1 + fi + # If crate depends on workspace rmg-core whose rust-version exceeds MSRV, skip as well + if grep -qE '^rmg-core\s*=\s*\{[^}]*path\s*=\s*"\.\./rmg-core"' "crates/${crate}/Cargo.toml" 2>/dev/null; then + local core_rv + core_rv=$(awk -F '"' '/^rust-version/ {print $2}' "crates/rmg-core/Cargo.toml" 2>/dev/null || echo "") + if [[ -n "$core_rv" ]] && printf '%s\n%s\n' "$MSRV" "$core_rv" | sort -V | tail -n1 | grep -qx "$core_rv" && [[ "$core_rv" != "$MSRV" ]]; then + echo "[pre-push] Skipping MSRV test for ${crate} (depends on rmg-core ${core_rv} > MSRV ${MSRV})" + return 1 + fi + fi + return 0 + } + if msrv_ok rmg-core; then cargo +"$MSRV" test -p rmg-core --all-targets; fi + if msrv_ok rmg-geom; then cargo +"$MSRV" test -p rmg-geom --all-targets; fi +fi # Rustdoc warnings guard (public crates) echo "[pre-push] rustdoc warnings gate (rmg-core @ $PINNED)" From 2689fdb7d96eefee42d97b1c73a8e7730021fce3 Mon Sep 17 00:00:00 2001 From: "J. Kirby Ross" Date: Wed, 29 Oct 2025 08:00:13 -0700 Subject: [PATCH 15/15] MSRV: set hook default to 1.71.1; execution plan reflects single 1.71.1 matrix --- .githooks/pre-push | 4 ++-- docs/execution-plan.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 52b38c2..efb35bc 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,8 +1,8 @@ #!/usr/bin/env bash set -euo pipefail PINNED="${PINNED:-1.71.1}" -# Separate MSRV for library checks (override with MSRV env) -MSRV="${MSRV:-1.68.0}" +# MSRV floor for library checks (override with MSRV env) +MSRV="${MSRV:-1.71.1}" for cmd in cargo rustup rg; do if ! command -v "$cmd" >/dev/null 2>&1; then diff --git a/docs/execution-plan.md b/docs/execution-plan.md index ecb4c6d..76bec18 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -51,7 +51,7 @@ This is Codex’s working map for building Echo. Update it relentlessly—each s - Toolchain floor via `rust-toolchain.toml`: 1.71.1 (workspace-wide). - Devcontainer must not override default; selection is controlled by `rust-toolchain.toml`. - Post-create installs 1.71.1 (adds rustfmt/clippy and wasm32 target). -- CI pins 1.71.1 for all jobs; an additional floor job on 1.71.1 validates `rmg-core` and `rmg-geom` explicitly. +- CI pins 1.71.1 for all jobs (single matrix; no separate floor job). > 2025-10-28 — Pre-commit auto-format flag update