Skip to content

Commit a771189

Browse files
authored
Merge pull request #13 from flyingrobots/echo/core-math-canonical-zero
core(math): canonicalize -0.0 in rotations; add MulAssign for Mat4
2 parents 75261ee + 5ea8a8f commit a771189

26 files changed

+665
-90
lines changed

.devcontainer/post-create.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,7 @@ echo "[devcontainer] Priming cargo registry cache (optional)..."
1717
cargo fetch || true
1818

1919
echo "[devcontainer] Done. Run 'cargo test -p rmg-core' or 'make ci-local' to validate."
20+
if [ -f Makefile ]; then
21+
echo "[devcontainer] Installing git hooks (make hooks)"
22+
make hooks || true
23+
fi

.githooks/pre-commit

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
33

4-
5-
if [[ "${SKIP_HOOKS:-}" == 1 ]]; then
6-
exit 0
7-
fi
8-
94
# 1) PRNG coupling guard (existing logic)
105
PRNG_FILE="crates/rmg-core/src/math/prng.rs"
116
if git diff --cached --name-only | grep -qx "$PRNG_FILE"; then
@@ -54,13 +49,13 @@ fi
5449
_auto_fmt="${ECHO_AUTO_FMT:-1}"
5550
case "${_auto_fmt}" in
5651
1|true|TRUE|yes|YES|on|ON)
57-
echo "pre-commit: ECHO_AUTO_FMT=${_auto_fmt}running cargo fmt (auto-fix)"
58-
cargo fmt --all || { echo "pre-commit: cargo fmt failed" >&2; exit 1; }
59-
# Re-stage only staged files that were reformatted
60-
if STAGED=$(git diff --cached --name-only); then
61-
if [[ -n "$STAGED" ]]; then
62-
echo "$STAGED" | xargs -r git add --
63-
fi
52+
echo "pre-commit: ECHO_AUTO_FMT=${_auto_fmt}checking format"
53+
if ! cargo fmt --all -- --check; then
54+
echo "pre-commit: running cargo fmt to apply changes" >&2
55+
cargo fmt --all || { echo "pre-commit: cargo fmt failed" >&2; exit 1; }
56+
echo "pre-commit: rustfmt updated files. Aborting commit to preserve index integrity (partial staging safe)." >&2
57+
echo "Hint: review changes, restage (e.g., 'git add -p' or 'git add -A'), then commit again." >&2
58+
exit 1
6459
fi
6560
;;
6661
0|false|FALSE|no|NO|off|OFF)
@@ -72,12 +67,12 @@ case "${_auto_fmt}" in
7267
;;
7368
esac
7469

75-
# 4) Docs guard (scaled): only require docs when core public API changed
70+
# 4) Docs guard: require docs updates on any Rust changes
7671
STAGED=$(git diff --cached --name-only)
77-
CORE_API_CHANGED=$(echo "$STAGED" | grep -E '^crates/rmg-core/src/.*\.rs$' | grep -v '/tests/' || true)
78-
if [[ -n "$CORE_API_CHANGED" ]]; then
79-
echo "$STAGED" | grep -Fx 'docs/execution-plan.md' >/dev/null || { echo 'pre-commit: docs/execution-plan.md must be updated when core API changes.' >&2; exit 1; }
80-
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; }
72+
RUST_CHANGED=$(echo "$STAGED" | grep -E '\\.rs$' || true)
73+
if [[ -n "$RUST_CHANGED" ]]; then
74+
echo "$STAGED" | grep -Fx 'docs/execution-plan.md' >/dev/null || { echo 'pre-commit: docs/execution-plan.md must be updated when Rust files change.' >&2; exit 1; }
75+
echo "$STAGED" | grep -Fx 'docs/decision-log.md' >/dev/null || { echo 'pre-commit: docs/decision-log.md must be updated when Rust files change.' >&2; exit 1; }
8176
fi
8277

8378
# 5) Lockfile guard: ensure lockfile version is v3 (current cargo format)

.githooks/pre-push

Lines changed: 34 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env bash
22
set -euo pipefail
3-
PINNED="${PINNED:-1.71.1}"
4-
# MSRV floor for library checks (override with MSRV env)
5-
MSRV="${MSRV:-1.71.1}"
3+
# Resolve the pinned toolchain from rust-toolchain.toml, fallback to explicit env or a sane default
4+
PINNED_FROM_FILE=$(awk -F '"' '/^channel/ {print $2}' rust-toolchain.toml 2>/dev/null || echo "")
5+
PINNED="${PINNED:-${PINNED_FROM_FILE:-1.90.0}}"
66

77
for cmd in cargo rustup rg; do
88
if ! command -v "$cmd" >/dev/null 2>&1; then
@@ -13,64 +13,46 @@ done
1313

1414
echo "🐰 BunBun 🐇"
1515

16-
if [[ "${SKIP_HOOKS:-}" == 1 ]]; then
17-
exit 0
18-
fi
1916

20-
echo "[pre-push] fmt (default toolchain)"
17+
echo "[pre-push] fmt (stable)"
2118
cargo fmt --all -- --check
2219

23-
echo "[pre-push] clippy (workspace, default toolchain)"
20+
echo "[pre-push] clippy (workspace, stable)"
2421
cargo clippy --all-targets -- -D warnings -D missing_docs
2522

26-
echo "[pre-push] tests (workspace, default toolchain)"
23+
echo "[pre-push] tests (workspace, stable)"
2724
cargo test --workspace
2825

29-
echo "[pre-push] Testing against MSRV ${MSRV} (core libraries)"
30-
# If any participating crate declares a rust-version greater than MSRV, skip MSRV checks entirely.
31-
CORE_RV=$(awk -F '"' '/^rust-version/ {print $2}' crates/rmg-core/Cargo.toml 2>/dev/null || echo "")
32-
GEOM_RV=$(awk -F '"' '/^rust-version/ {print $2}' crates/rmg-geom/Cargo.toml 2>/dev/null || echo "")
33-
if { [[ -n "$CORE_RV" ]] && printf '%s\n%s\n' "$MSRV" "$CORE_RV" | sort -V | tail -n1 | grep -qx "$CORE_RV" && [[ "$CORE_RV" != "$MSRV" ]]; } \
34-
|| { [[ -n "$GEOM_RV" ]] && printf '%s\n%s\n' "$MSRV" "$GEOM_RV" | sort -V | tail -n1 | grep -qx "$GEOM_RV" && [[ "$GEOM_RV" != "$MSRV" ]]; }; then
35-
echo "[pre-push] Skipping MSRV block: one or more crates declare rust-version > ${MSRV} (core=${CORE_RV:-unset}, geom=${GEOM_RV:-unset})"
36-
else
37-
if ! rustup run "$MSRV" cargo -V >/dev/null 2>&1; then
38-
echo "[pre-push] MSRV toolchain ${MSRV} not installed. Install via: rustup toolchain install ${MSRV}" >&2
39-
exit 1
40-
fi
41-
# Only run MSRV tests for crates that declare rust-version <= MSRV; skip otherwise.
42-
msrv_ok() {
43-
local crate="$1"
44-
local rv
45-
rv=$(awk -F '"' '/^rust-version/ {print $2}' "crates/${crate}/Cargo.toml" 2>/dev/null || echo "")
46-
if [[ -z "$rv" ]]; then
47-
return 0
48-
fi
49-
# If declared rust-version is greater than MSRV, skip.
50-
if printf '%s\n%s\n' "$MSRV" "$rv" | sort -V | tail -n1 | grep -qx "$rv" && [[ "$rv" != "$MSRV" ]]; then
51-
echo "[pre-push] Skipping MSRV test for ${crate} (rust-version ${rv} > MSRV ${MSRV})"
52-
return 1
53-
fi
54-
# If crate depends on workspace rmg-core whose rust-version exceeds MSRV, skip as well
55-
if grep -qE '^rmg-core\s*=\s*\{[^}]*path\s*=\s*"\.\./rmg-core"' "crates/${crate}/Cargo.toml" 2>/dev/null; then
56-
local core_rv
57-
core_rv=$(awk -F '"' '/^rust-version/ {print $2}' "crates/rmg-core/Cargo.toml" 2>/dev/null || echo "")
58-
if [[ -n "$core_rv" ]] && printf '%s\n%s\n' "$MSRV" "$core_rv" | sort -V | tail -n1 | grep -qx "$core_rv" && [[ "$core_rv" != "$MSRV" ]]; then
59-
echo "[pre-push] Skipping MSRV test for ${crate} (depends on rmg-core ${core_rv} > MSRV ${MSRV})"
60-
return 1
61-
fi
62-
fi
63-
return 0
64-
}
65-
if msrv_ok rmg-core; then cargo +"$MSRV" test -p rmg-core --all-targets; fi
66-
if msrv_ok rmg-geom; then cargo +"$MSRV" test -p rmg-geom --all-targets; fi
67-
fi
26+
# MSRV lane removed: policy is stable everywhere.
6827

6928
# Rustdoc warnings guard (public crates)
70-
echo "[pre-push] rustdoc warnings gate (rmg-core @ $PINNED)"
71-
RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p rmg-core --no-deps
72-
echo "[pre-push] rustdoc warnings gate (rmg-geom @ $PINNED)"
73-
RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p rmg-geom --no-deps
29+
required_crates=(rmg-core rmg-geom)
30+
optional_crates=(rmg-ffi rmg-wasm)
31+
missing_required=0
32+
33+
for krate in "${required_crates[@]}"; do
34+
if [ -f "crates/${krate}/Cargo.toml" ]; then
35+
echo "[pre-push] rustdoc warnings gate (${krate} @ $PINNED)"
36+
RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p "${krate}" --no-deps
37+
else
38+
echo "[pre-push] ERROR: required crate missing: crates/${krate}/Cargo.toml" >&2
39+
missing_required=1
40+
fi
41+
done
42+
43+
for krate in "${optional_crates[@]}"; do
44+
if [ -f "crates/${krate}/Cargo.toml" ]; then
45+
echo "[pre-push] rustdoc warnings gate (${krate} @ $PINNED)"
46+
RUSTDOCFLAGS="-D warnings" cargo +"$PINNED" doc -p "${krate}" --no-deps
47+
else
48+
echo "[pre-push] skipping ${krate}: missing crates/${krate}/Cargo.toml"
49+
fi
50+
done
51+
52+
if [ "$missing_required" -ne 0 ]; then
53+
echo "[pre-push] One or more required crates are missing; aborting push." >&2
54+
exit 1
55+
fi
7456

7557
# Banned patterns
7658
echo "[pre-push] scanning banned patterns"

.github/workflows/ci.yml

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
- uses: actions/checkout@v4
1616
with:
1717
submodules: false
18-
- uses: dtolnay/rust-toolchain@1.71.1
18+
- uses: dtolnay/rust-toolchain@1.90.0
1919
- uses: Swatinem/rust-cache@v2
2020
with:
2121
workspaces: |
@@ -30,7 +30,7 @@ jobs:
3030
- uses: actions/checkout@v4
3131
with:
3232
submodules: false
33-
- uses: dtolnay/rust-toolchain@1.71.1
33+
- uses: dtolnay/rust-toolchain@1.90.0
3434
with:
3535
components: clippy
3636
- uses: Swatinem/rust-cache@v2
@@ -47,7 +47,7 @@ jobs:
4747
- uses: actions/checkout@v4
4848
with:
4949
submodules: false
50-
- uses: dtolnay/rust-toolchain@1.71.1
50+
- uses: dtolnay/rust-toolchain@1.90.0
5151
- uses: Swatinem/rust-cache@v2
5252
with:
5353
workspaces: |
@@ -89,15 +89,47 @@ jobs:
8989
exit 1;
9090
}
9191
92+
# MSRV job removed per policy: use @stable everywhere
9293

9394
rustdoc:
94-
name: Rustdoc (rmg-core warnings gate)
95+
name: Rustdoc (warnings gate)
9596
runs-on: ubuntu-latest
9697
steps:
9798
- uses: actions/checkout@v4
9899
with:
99100
submodules: false
100-
- uses: dtolnay/rust-toolchain@1.71.1
101+
- uses: dtolnay/rust-toolchain@1.90.0
101102
- uses: Swatinem/rust-cache@v2
102-
- name: rustdoc warnings gate
103+
- name: rustdoc warnings gate (rmg-core)
103104
run: RUSTDOCFLAGS="-D warnings" cargo doc -p rmg-core --no-deps
105+
- name: rustdoc warnings gate (rmg-geom)
106+
run: RUSTDOCFLAGS="-D warnings" cargo doc -p rmg-geom --no-deps
107+
- name: rustdoc warnings gate (rmg-ffi)
108+
run: |
109+
if [ -f crates/rmg-ffi/Cargo.toml ]; then RUSTDOCFLAGS="-D warnings" cargo doc -p rmg-ffi --no-deps; fi
110+
- name: rustdoc warnings gate (rmg-wasm)
111+
run: |
112+
if [ -f crates/rmg-wasm/Cargo.toml ]; then RUSTDOCFLAGS="-D warnings" cargo doc -p rmg-wasm --no-deps; fi
113+
114+
audit:
115+
name: Security Audit
116+
runs-on: ubuntu-latest
117+
steps:
118+
- uses: actions/checkout@v4
119+
- uses: dtolnay/rust-toolchain@1.90.0
120+
- name: Install cargo-audit (latest)
121+
run: cargo install cargo-audit --locked
122+
- name: Run cargo audit
123+
env:
124+
CARGO_TERM_COLOR: always
125+
run: cargo audit --deny warnings
126+
127+
deny:
128+
name: Dependency Policy (cargo-deny)
129+
runs-on: ubuntu-latest
130+
steps:
131+
- uses: actions/checkout@v4
132+
- uses: dtolnay/rust-toolchain@stable
133+
- uses: Swatinem/rust-cache@v2
134+
- name: Run cargo-deny
135+
uses: EmbarkStudios/cargo-deny-action@v1
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Security Audit
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- "echo/**"
8+
- "feat/**"
9+
pull_request:
10+
11+
jobs:
12+
audit:
13+
name: Cargo Audit (stable)
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
with:
18+
submodules: false
19+
- uses: dtolnay/rust-toolchain@1.90.0
20+
- uses: actions/cache@v4
21+
with:
22+
path: |
23+
~/.cargo/registry
24+
~/.cargo/git
25+
~/.cargo/bin/cargo-audit
26+
key: audit-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
27+
- name: Install cargo-audit
28+
run: |
29+
if ! command -v cargo-audit >/dev/null; then
30+
cargo install cargo-audit --locked
31+
fi
32+
- name: Run cargo audit
33+
run: cargo audit --deny warnings

CONTRIBUTING.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,22 @@ Echo is a deterministic, renderer-agnostic engine. We prioritize:
6565
- A minimal docs-guard: when core API files change, it requires updating `docs/execution-plan.md` and `docs/decision-log.md` (mirrors CI)
6666
- To auto-fix formatting on commit: `ECHO_AUTO_FMT=1 git commit -m "message"`
6767

68+
#### Partial Staging & rustfmt
69+
- rustfmt formats entire files, not only staged hunks. To preserve index integrity, our pre-commit hook now aborts the commit if running `cargo fmt` would change any files. It first checks with `cargo fmt --check`, and if changes are needed it applies them and exits with a helpful message.
70+
- Workflow when this happens:
71+
1) Review formatting changes: `git status` and `git diff`.
72+
2) Restage intentionally formatted files (e.g., `git add -A` or `git add -p`).
73+
3) Commit again.
74+
- Tips:
75+
- If you need to keep a partial-staged commit, do two commits: first commit the formatter-only changes, then commit your code changes.
76+
- You can switch to check-only with `ECHO_AUTO_FMT=0` (commit will still fail on formatting issues, but nothing is auto-applied).
77+
- Do not bypass hooks. The repo runs fmt, clippy, tests, and rustdoc on the pinned toolchain before push.
78+
- Toolchain: pinned to Rust 1.90.0. Ensure your local override matches:
79+
80+
- rustup toolchain install 1.90.0
81+
- rustup override set 1.90.0
82+
- When any Rust code changes (.rs anywhere), update both `docs/execution-plan.md` and `docs/decision-log.md` with intent and a brief rationale. The hook enforces this.
83+
6884
## Communication
6985
- Major updates should land in `docs/execution-plan.md` and `docs/decision-log.md`; rely on GitHub discussions or issues for longer-form proposals.
7086
- Respect the temporal theme—leave the codebase cleaner for the next timeline traveler.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ members = [
88
]
99
resolver = "2"
1010

11+
[workspace.dependencies]
12+
rmg-core = { version = "0.1.0", path = "crates/rmg-core" }
13+
1114
[profile.release]
1215
opt-level = "s"
1316
lto = true

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@
2424

2525
Most game engines are object-oriented state machines. Unity, Unreal, Godot all maintain mutable object hierarchies that update every frame. Echo says: "No, everything is a graph, and the engine rewrites that graph deterministically using typed transformation rules."
2626

27+
## Snapshot Hashes
28+
29+
Echo records two hashes during a commit:
30+
- `state_root`: deterministic hash of the reachable graph state under the current root.
31+
- `commit hash` (commit_id): hash of a canonical header including `state_root`, parents, and deterministic digests for plan/decisions/rewrites.
32+
33+
See `docs/spec-merkle-commit.md` for the precise encoding and invariants.
34+
2735
Echo is fundamentally **built different**.
2836

2937
RMG provides atomic, in-place edits of recursive meta-graphs with deterministic local scheduling and snapshot isolation.

crates/rmg-core/src/footprint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ use crate::ident::{EdgeId, Hash, NodeId};
1818
/// conflicts on boundary interfaces. The engine only requires stable equality
1919
/// and ordering; it does not rely on a specific bit layout.
2020
///
21-
/// For demos/tests, use [`pack_port_key`] to derive a deterministic 64‑bit key
22-
/// from a [`NodeId`], a `port_id`, and a direction flag.
21+
/// For demos/tests, use [`pack_port_key`](crate::footprint::pack_port_key) to derive a
22+
/// deterministic 64‑bit key from a [`NodeId`], a `port_id`, and a direction flag.
2323
pub type PortKey = u64;
2424

2525
/// Simple ordered set of 256‑bit ids based on `BTreeSet` for deterministic

crates/rmg-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
clippy::module_name_repetitions,
3131
clippy::use_self
3232
)]
33+
// Permit intentional name repetition for public API clarity (e.g., FooFoo types) and
34+
// functions named after their module for discoverability (e.g., `motion_rule`).
3335

3436
/// Deterministic math subsystem (Vec3, Mat4, Quat, PRNG).
3537
pub mod math;

0 commit comments

Comments
 (0)