diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 833d0bd..25f042e 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -1,19 +1,17 @@ #!/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)..." 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.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 +rustup toolchain install 1.71.1 --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 +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-commit b/.githooks/pre-commit index a3839b6..3317478 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -79,12 +79,19 @@ 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 + # 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 edfe707..efb35bc 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,6 +1,8 @@ #!/usr/bin/env bash set -euo pipefail -PINNED="${PINNED:-1.68.0}" +PINNED="${PINNED:-1.71.1}" +# 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 @@ -15,34 +17,76 @@ 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 +echo "[pre-push] Testing against MSRV ${MSRV} (core libraries)" +# 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 - echo "[pre-push] MSRV toolchain $PINNED not installed. Install via: rustup toolchain install $PINNED" >&2 - exit 1 + 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 + 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 (core API) -echo "[pre-push] rustdoc warnings gate (rmg-core)" +# 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" -# 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/**' \ + --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[^]]*\)\]' --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/**' \ + --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 453257f..911f810 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,14 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@1.71.1 with: - toolchain: stable 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 +47,13 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - name: rustup override stable - run: rustup toolchain install stable && rustup override set stable + - uses: dtolnay/rust-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 @@ -101,20 +89,6 @@ jobs: exit 1; } - msrv: - name: MSRV (rmg-core @ 1.68) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - submodules: false - - uses: dtolnay/rust-toolchain@1.68.0 - - uses: Swatinem/rust-cache@v2 - with: - workspaces: | - . - - name: cargo check (rmg-core) - run: cargo check -p rmg-core --all-targets rustdoc: name: Rustdoc (rmg-core warnings gate) @@ -123,13 +97,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: false - - uses: dtolnay/rust-toolchain@stable - with: - toolchain: stable - - name: rustup override stable - run: rustup toolchain install stable && rustup override set stable + - uses: dtolnay/rust-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..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 stable + MSRV toolchains, clippy/rustfmt, Node, and gh. - - Post-create installs MSRV 1.68.0 and wasm target. +- 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..8f87bcd 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" @@ -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..19cc1ab --- /dev/null +++ b/crates/rmg-core/src/constants.rs @@ -0,0 +1,19 @@ +//! 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 817918f..fdb1780 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,30 @@ 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(); + // 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, 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 +260,39 @@ 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 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, - parent: self.last_snapshot.as_ref().map(|s| s.hash), + parents, + plan_digest: empty_digest, + 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..e443fb2 100644 --- a/crates/rmg-core/src/lib.rs +++ b/crates/rmg-core/src/lib.rs @@ -34,6 +34,7 @@ /// 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; @@ -48,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. diff --git a/crates/rmg-core/src/snapshot.rs b/crates/rmg-core/src/snapshot.rs index c2156fb..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,16 +31,25 @@ use crate::tx::TxId; /// Snapshot returned after a successful commit. /// -/// The `hash` value is deterministic and reflects the entire canonicalised -/// graph state (root + payloads). +/// 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 hash derived from the entire graph state. + /// Canonical commit hash derived from state_root + 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 +131,35 @@ 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, + policy_id: u32, +) -> 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.update(&policy_id.to_le_bytes()); + h.finalize().into() +} 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-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-geom/src/broad/aabb_tree.rs b/crates/rmg-geom/src/broad/aabb_tree.rs new file mode 100644 index 0000000..2933970 --- /dev/null +++ b/crates/rmg-geom/src/broad/aabb_tree.rs @@ -0,0 +1,80 @@ +use crate::types::aabb::Aabb; +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)` all-pairs sweep. +/// +/// 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, +} + +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 + } +} diff --git a/crates/rmg-geom/src/broad/mod.rs b/crates/rmg-geom/src/broad/mod.rs new file mode 100644 index 0000000..7395d7a --- /dev/null +++ b/crates/rmg-geom/src/broad/mod.rs @@ -0,0 +1,13 @@ +//! 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; diff --git a/crates/rmg-geom/src/lib.rs b/crates/rmg-geom/src/lib.rs index b3d3b5d..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: @@ -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/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/crates/rmg-geom/tests/geom_broad_tests.rs b/crates/rmg-geom/tests/geom_broad_tests.rs new file mode 100644 index 0000000..7ee8049 --- /dev/null +++ b/crates/rmg-geom/tests/geom_broad_tests.rs @@ -0,0 +1,45 @@ +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() { + // 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 = 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]); +} + +#[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)]); +} diff --git a/crates/rmg-wasm/Cargo.toml b/crates/rmg-wasm/Cargo.toml index 4e298c0..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.68" +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 24f3627..e0763c3 100644 --- a/docs/decision-log.md +++ b/docs/decision-log.md @@ -35,3 +35,22 @@ - 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 = 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. + - `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. + +## 2025-10-29 — Toolchain strategy: floor raised to 1.71.1 + +- 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 512cb63..76bec18 100644 --- a/docs/execution-plan.md +++ b/docs/execution-plan.md @@ -33,12 +33,25 @@ 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). -- 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). +- 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 (single matrix; no separate floor job). > 2025-10-28 — Pre-commit auto-format flag update @@ -49,7 +62,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. diff --git a/docs/spec-knots-in-time.md b/docs/spec-knots-in-time.md new file mode 100644 index 0000000..4d40e97 --- /dev/null +++ b/docs/spec-knots-in-time.md @@ -0,0 +1,121 @@ +# 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 (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: + +- 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..499964f --- /dev/null +++ b/docs/spec-timecube.md @@ -0,0 +1,132 @@ +# 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. + 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"]