diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 697eb6cb..aac324cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,13 +3,33 @@ name: AIngle CI on: push: branches: [ main, dev ] + # No branch filter on pull_request, on purpose. Filtering to [main, dev] meant + # a PR onto any other branch ran NO checks at all — so a stack of dependent + # PRs was invisible to CI until each one was retargeted, which is exactly when + # it is too late to learn something. Every PR gets the same gate. pull_request: - branches: [ main, dev ] env: + # PINNED, not `stable`. On 2026-07-29 the runner moved to clippy 1.97, which + # added `question_mark`, and the lint gate went red with no commit behind it — + # a compiler release is not a change to this repository and must not be able + # to fail its CI. + # + # Pinning here rather than in a `rust-toolchain.toml`: the toolchain action + # exports RUSTUP_TOOLCHAIN, which takes precedence over that file, so the + # pin would be ignored in exactly the place it is needed. The file exists too, + # for local parity. + # + # To bump: raise this, run `cargo + clippy --workspace --all-targets -- + # -D warnings` and `cargo + fmt --all -- --check` locally first, and fix + # whatever the new release added in the same commit. + RUST_VERSION: "1.97.0" CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 - MSRV: "1.85" + # The version this project promises to build on, verified by the job below + # against the WHOLE workspace. Keep in step with `rust-version` in the root + # Cargo.toml, which is where consumers read it. + MSRV: "1.91" jobs: # MSRV (Minimum Supported Rust Version) check @@ -40,8 +60,11 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-msrv- + # `-p aingle_minimal` before, which is the one member with no wasmer in + # its dependency tree — so it passed at 1.85 while the workspace could + # not build below 1.91. A promise nothing checks is not a promise. - name: Check MSRV - run: cargo check -p aingle_minimal + run: cargo check --workspace --all-targets # Format check fmt: @@ -51,8 +74,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: + toolchain: ${{ env.RUST_VERSION }} components: rustfmt - name: Check formatting @@ -66,8 +90,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: + toolchain: ${{ env.RUST_VERSION }} components: clippy - name: Install system dependencies @@ -86,8 +111,26 @@ jobs: restore-keys: | ${{ runner.os }}-cargo-clippy- + # The bar CLAUDE.md declares as `make lint`, which nothing enforced. + # + # This step used to read `-p aingle_minimal --features rest -- -W + # clippy::all`: one crate of seventeen, the library target only, and + # warnings that did not fail anything. It reported green while 54 lints + # accumulated across the workspace — among them a benchmark that had not + # compiled in months, and assertions that compared unsigned counters + # against zero and therefore passed no matter what the code did. + # + # `--all-targets` is load-bearing, not thoroughness for its own sake: it + # is what covers tests, benches and examples, and it is the only reason + # the dead benchmark was found at all. - name: Run Clippy - run: cargo clippy -p aingle_minimal --features rest -- -W clippy::all + run: cargo clippy --workspace --all-targets -- -D warnings + + # No separate "did it really compile?" step: with `--workspace` and + # `-D warnings`, a member that fails to build makes THIS step exit + # non-zero. The companion-repo failure that inspired one was a job that + # went red for a lint and stayed red for a build script nobody read to + # the bottom of — a reading problem, not a missing check. # Build check build: @@ -97,7 +140,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | @@ -130,7 +175,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | @@ -162,7 +209,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | @@ -206,7 +255,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install cargo-audit run: cargo install cargo-audit @@ -224,8 +275,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: + toolchain: ${{ env.RUST_VERSION }} components: llvm-tools-preview - name: Install system dependencies @@ -267,7 +319,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | @@ -306,7 +360,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | @@ -335,7 +391,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3a83cbcb..63c391cc 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,6 +15,8 @@ concurrency: cancel-in-progress: true env: + # Pinned on purpose: see the note in ci.yml. + RUST_VERSION: "1.97.0" CARGO_TERM_COLOR: always jobs: @@ -25,7 +27,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6c64d3c..bd5c0652 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,8 @@ permissions: contents: write env: + # Pinned on purpose: see the note in ci.yml. + RUST_VERSION: "1.97.0" CARGO_TERM_COLOR: always jobs: @@ -50,8 +52,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master with: + toolchain: ${{ env.RUST_VERSION }} targets: ${{ matrix.target }} - name: Install system dependencies (Linux) @@ -163,7 +166,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install system dependencies run: | diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index bd585f82..8fb37bf7 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -17,6 +17,8 @@ on: workflow_dispatch: env: + # Pinned on purpose: see the note in ci.yml. + RUST_VERSION: "1.97.0" CARGO_TERM_COLOR: always jobs: @@ -28,7 +30,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install cargo-audit run: cargo install cargo-audit @@ -60,7 +64,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install cargo-deny run: cargo install cargo-deny @@ -109,7 +115,9 @@ jobs: - uses: actions/checkout@v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ env.RUST_VERSION }} - name: Install cargo-vet run: cargo install cargo-vet diff --git a/Cargo.lock b/Cargo.lock index cac5e12a..42268eef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,7 +99,7 @@ dependencies = [ [[package]] name = "aingle_ai" -version = "0.7.1" +version = "0.7.2" dependencies = [ "blake2", "candle-core 0.9.2", @@ -121,7 +121,7 @@ dependencies = [ [[package]] name = "aingle_contracts" -version = "0.7.1" +version = "0.7.2" dependencies = [ "blake3", "dashmap 6.1.0", @@ -140,7 +140,7 @@ dependencies = [ [[package]] name = "aingle_cortex" -version = "0.8.0" +version = "0.9.0" dependencies = [ "aingle_graph", "aingle_ingest", @@ -201,7 +201,7 @@ dependencies = [ [[package]] name = "aingle_graph" -version = "0.8.0" +version = "0.9.0" dependencies = [ "bincode", "blake3", @@ -225,7 +225,7 @@ dependencies = [ [[package]] name = "aingle_ingest" -version = "0.8.0" +version = "0.9.0" dependencies = [ "aingle_graph", "blake3", @@ -237,7 +237,7 @@ dependencies = [ [[package]] name = "aingle_logic" -version = "0.8.0" +version = "0.9.0" dependencies = [ "aingle_graph", "chrono", @@ -253,7 +253,7 @@ dependencies = [ [[package]] name = "aingle_minimal" -version = "0.7.1" +version = "0.7.2" dependencies = [ "async-io", "async-tungstenite", @@ -295,7 +295,7 @@ dependencies = [ [[package]] name = "aingle_raft" -version = "0.7.1" +version = "0.7.2" dependencies = [ "aingle_graph", "aingle_wal", @@ -316,7 +316,7 @@ dependencies = [ [[package]] name = "aingle_viz" -version = "0.7.1" +version = "0.7.2" dependencies = [ "aingle_graph", "aingle_minimal", @@ -338,7 +338,7 @@ dependencies = [ [[package]] name = "aingle_wal" -version = "0.7.1" +version = "0.7.2" dependencies = [ "bincode", "blake3", @@ -350,7 +350,7 @@ dependencies = [ [[package]] name = "aingle_zk" -version = "0.8.0" +version = "0.9.0" dependencies = [ "blake3", "bulletproofs", @@ -4264,7 +4264,7 @@ dependencies = [ [[package]] name = "ineru" -version = "0.7.5" +version = "0.7.6" dependencies = [ "bincode", "blake3", @@ -4524,7 +4524,7 @@ dependencies = [ [[package]] name = "kaneru" -version = "0.7.1" +version = "0.7.2" dependencies = [ "chrono", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 3c691e85..da696937 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,17 @@ +[workspace.package] +# The MSRV, declared ONCE. It said 1.83 in twelve crates, 1.85 in the CI +# environment and 1.83 in CLAUDE.md, and the truth was none of those: the +# workspace needs 1.91, because `wasmer` (via aingle_contracts) requires it +# and `cranelift` under it requires 1.90. Verified by bisection — 1.85 and +# 1.90 both fail, 1.91 builds. Nothing had ever checked, because the MSRV job +# ran `cargo check -p aingle_minimal`: the one member with no wasmer in its +# tree. +# +# Uniform rather than per-crate on purpose: a lower number on a crate that +# has not been built at that version is the same unverified promise this is +# replacing. Refine per crate only with a run behind each figure. +rust-version = "1.91" + [workspace] resolver = "2" members = [ diff --git a/crates/aingle/Cargo.toml b/crates/aingle/Cargo.toml index 31866633..e2dc2af3 100644 --- a/crates/aingle/Cargo.toml +++ b/crates/aingle/Cargo.toml @@ -33,7 +33,7 @@ aingle_sqlite = { version = "0.0.1", path = "../aingle_sqlite" } aingle_middleware_bytes = "=0.0.3" aingle_state = { version = "0.0.1", path = "../aingle_state" } aingle_types = { version = "0.0.1", path = "../aingle_types" } -aingle_cortex = { version = "0.8", path = "../aingle_cortex", default-features = false, features = ["rest"] } +aingle_cortex = { version = "0.9", path = "../aingle_cortex", default-features = false, features = ["rest"] } aingle_wasmer_host = "0.0.1" aingle_websocket = { version = "0.0.1", path = "../aingle_websocket" } wasmer = "=7.0.1" diff --git a/crates/aingle_ai/Cargo.toml b/crates/aingle_ai/Cargo.toml index 6f2e2d10..b29f9c45 100644 --- a/crates/aingle_ai/Cargo.toml +++ b/crates/aingle_ai/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_ai" -version = "0.7.1" +version = "0.7.2" description = "AI integration layer for AIngle - Ineru, Nested Learning, Kaneru" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "ai", "titans", "nested-learning", "memory"] categories = ["science", "algorithms"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [dependencies] # Serialization serde = { version = "1", features = ["derive"] } diff --git a/crates/aingle_ai/src/ineru/config.rs b/crates/aingle_ai/src/ineru/config.rs index b884d75e..779e8cff 100644 --- a/crates/aingle_ai/src/ineru/config.rs +++ b/crates/aingle_ai/src/ineru/config.rs @@ -135,12 +135,16 @@ mod tests { #[test] fn test_validation() { - let mut config = IneruConfig::default(); - config.window_size = 0; + let config = IneruConfig { + window_size: 0, + ..Default::default() + }; assert!(config.validate().is_err()); - config = IneruConfig::default(); - config.surprise_threshold = 1.5; + let config = IneruConfig { + surprise_threshold: 1.5, + ..Default::default() + }; assert!(config.validate().is_err()); } diff --git a/crates/aingle_ai/src/ineru/surprise_gate.rs b/crates/aingle_ai/src/ineru/surprise_gate.rs index 48a21b8f..f459e7a3 100644 --- a/crates/aingle_ai/src/ineru/surprise_gate.rs +++ b/crates/aingle_ai/src/ineru/surprise_gate.rs @@ -75,7 +75,17 @@ impl SurpriseGate { } /// Record a surprise value for calibration + /// + /// Non-finite samples are dropped. Welford's algorithm has no way back from + /// one: `mean += delta / n` with a NaN delta makes the mean NaN, every + /// later update keeps it NaN, and `adaptive_threshold` inherits it. A NaN + /// threshold is worse than any number, because every comparison against it + /// is false — the gate stops firing and nothing says so. pub fn record_surprise(&mut self, surprise: f32) { + if !surprise.is_finite() { + return; + } + // Add to recent window self.recent_surprises.push_back(surprise); if self.recent_surprises.len() > self.calibration_window { @@ -130,10 +140,23 @@ impl SurpriseGate { } /// Get adaptive threshold based on recent statistics + /// + /// Falls back to the configured [`Self::set_threshold`] value when the + /// running statistics cannot produce a usable number. `record_surprise` + /// already refuses non-finite samples, so this should be unreachable — but + /// it is the difference between a wrong threshold and a NaN one, and a NaN + /// threshold disables the gate silently. Deserialised state is not covered + /// by the entry guard either. pub fn adaptive_threshold(&self) -> f32 { // Use mean + 1 std as adaptive threshold let std = self.get_std(); - (self.mean_surprise + std).min(1.0).max(0.1) + let adaptive = self.mean_surprise + std; + + if !adaptive.is_finite() { + return self.threshold.clamp(0.1, 1.0); + } + + adaptive.clamp(0.1, 1.0) } /// Get statistics @@ -240,4 +263,55 @@ mod tests { let adaptive = gate.adaptive_threshold(); assert!(adaptive > 0.0 && adaptive < 1.0); } + + #[test] + fn a_non_finite_sample_cannot_poison_the_running_statistics() { + // Welford has no way back from a NaN: `mean += delta / n` with a NaN + // delta makes the mean NaN and every later update keeps it that way. + let mut gate = SurpriseGate::new(0.5); + for _ in 0..20 { + gate.record_surprise(0.4); + } + let before = gate.adaptive_threshold(); + + gate.record_surprise(f32::NAN); + gate.record_surprise(f32::INFINITY); + gate.record_surprise(f32::NEG_INFINITY); + + assert_eq!( + gate.adaptive_threshold(), + before, + "a non-finite sample must be dropped, not folded into the mean" + ); + } + + #[test] + fn the_adaptive_threshold_is_always_a_usable_number() { + // The failure this guards is silent: every comparison against a NaN + // threshold is false, so `should_update` would stop firing and the + // memory would quietly stop learning. + let mut gate = SurpriseGate::new(0.5); + gate.record_surprise(f32::NAN); + for s in [0.0, 1.0, 0.5, f32::NAN, 0.9] { + gate.record_surprise(s); + } + + let t = gate.adaptive_threshold(); + assert!(t.is_finite(), "threshold was {t}"); + assert!( + (0.1..=1.0).contains(&t), + "threshold {t} left its documented range" + ); + } + + #[test] + fn unusable_statistics_fall_back_to_the_configured_threshold() { + // Not to an invented constant: the value the caller set is the + // non-adaptive baseline this gate already has. + let mut gate = SurpriseGate::new(0.5); + gate.set_threshold(0.7); + gate.mean_surprise = f32::NAN; + + assert!((gate.adaptive_threshold() - 0.7).abs() < 1e-6); + } } diff --git a/crates/aingle_ai/src/kaneru/continuum_memory.rs b/crates/aingle_ai/src/kaneru/continuum_memory.rs index 4104c47e..828131fe 100644 --- a/crates/aingle_ai/src/kaneru/continuum_memory.rs +++ b/crates/aingle_ai/src/kaneru/continuum_memory.rs @@ -5,6 +5,8 @@ //! //! Non-discrete memory system with smooth interpolation. +use std::cmp::Reverse; + use super::{Experience, MemoryResult, Query}; use crate::types::{pattern_id, Embedding}; @@ -123,8 +125,7 @@ impl ContinuumMemory { pub fn compress(&mut self) { // Keep only top half by access count if self.entries.len() > 100 { - self.entries - .sort_by(|a, b| b.access_count.cmp(&a.access_count)); + self.entries.sort_by_key(|e| Reverse(e.access_count)); self.entries.truncate(self.entries.len() / 2); } } diff --git a/crates/aingle_ai/src/kaneru/mod.rs b/crates/aingle_ai/src/kaneru/mod.rs index 22b90603..ec2f073f 100644 --- a/crates/aingle_ai/src/kaneru/mod.rs +++ b/crates/aingle_ai/src/kaneru/mod.rs @@ -423,8 +423,10 @@ mod tests { }; let result = agent.query(&query); - // Should have some results - assert!(result.relevant_contexts.len() > 0 || result.memory_matches.len() >= 0); + // Should have some results. The second half used to read + // `memory_matches.len() >= 0`, which is true for every usize — so the + // assertion passed even when the query returned nothing at all. + assert!(!result.relevant_contexts.is_empty() || !result.memory_matches.is_empty()); } #[test] diff --git a/crates/aingle_ai/src/kaneru/self_modifier.rs b/crates/aingle_ai/src/kaneru/self_modifier.rs index 2200b30b..1489eaf4 100644 --- a/crates/aingle_ai/src/kaneru/self_modifier.rs +++ b/crates/aingle_ai/src/kaneru/self_modifier.rs @@ -359,8 +359,10 @@ mod tests { #[test] fn test_rule_evolution() { - let mut config = KaneruConfig::default(); - config.max_rules = 10; + let config = KaneruConfig { + max_rules: 10, + ..Default::default() + }; let mut modifier = SelfModifier::new(&config); // Simulate good outcomes to trigger rule creation diff --git a/crates/aingle_ai/src/nested_learning/mod.rs b/crates/aingle_ai/src/nested_learning/mod.rs index d661db94..352050df 100644 --- a/crates/aingle_ai/src/nested_learning/mod.rs +++ b/crates/aingle_ai/src/nested_learning/mod.rs @@ -93,7 +93,10 @@ impl NestedLearning { self.tx_count += 1; // 4. Check if optimizer-level update is needed - if self.tx_count % self.config.optimizer_update_interval == 0 { + if self + .tx_count + .is_multiple_of(self.config.optimizer_update_interval) + { debug!( tx_count = self.tx_count, "Triggering optimizer-level update" @@ -114,7 +117,10 @@ impl NestedLearning { self.block_count += 1; // Check if meta-level update is needed - if self.block_count % self.config.meta_update_interval == 0 { + if self + .block_count + .is_multiple_of(self.config.meta_update_interval) + { debug!( block_count = self.block_count, "Triggering meta-level update" @@ -259,7 +265,7 @@ mod tests { let tx = make_test_tx(1); let result = nested.process(&tx).unwrap(); - assert!(result.processed.features.len() > 0); + assert!(!result.processed.features.is_empty()); } #[test] @@ -267,7 +273,7 @@ mod tests { let config = NestedConfig::default(); let nested = NestedLearning::new(config); - let batch: Vec<_> = (0..10).map(|i| make_test_tx(i)).collect(); + let batch: Vec<_> = (0..10).map(make_test_tx).collect(); let plan = nested.get_validation_plan(&batch); assert_eq!(plan.order.len(), 10); @@ -275,8 +281,11 @@ mod tests { #[test] fn test_meta_level_update() { - let mut config = NestedConfig::default(); - config.meta_update_interval = 5; // Update every 5 blocks + // Update every 5 blocks + let config = NestedConfig { + meta_update_interval: 5, + ..Default::default() + }; let mut nested = NestedLearning::new(config); let stats = BlockStats::default(); diff --git a/crates/aingle_ai/src/nested_learning/transaction_level.rs b/crates/aingle_ai/src/nested_learning/transaction_level.rs index fd38a931..7a85a251 100644 --- a/crates/aingle_ai/src/nested_learning/transaction_level.rs +++ b/crates/aingle_ai/src/nested_learning/transaction_level.rs @@ -191,16 +191,29 @@ impl TransactionClassifier { return 0.5; } - // Find min and second min distance + // Find min and second min distance. + // + // `total_cmp`, not `partial_cmp().unwrap()`: the latter PANICS on a NaN + // distance, which `l2_distance` yields from a centroid holding one. A + // total order also sorts NaN last, so a corrupt centroid cannot pass + // itself off as the nearest one. let mut sorted = distances.clone(); - sorted.sort_by(|a, b| a.partial_cmp(b).unwrap()); + sorted.sort_by(f32::total_cmp); let min_dist = sorted.first().copied().unwrap_or(1.0); let second_dist = sorted.get(1).copied().unwrap_or(min_dist); + // Nothing usable to measure a separation with. Answer what this function + // already answers when there are no centroids at all, rather than let + // the `> 0.0` test below fall through to 1.0 — which would report FULL + // confidence precisely when there is no information. + if !min_dist.is_finite() || !second_dist.is_finite() { + return 0.5; + } + // Confidence based on separation if second_dist > 0.0 { - (1.0 - min_dist / second_dist).max(0.0).min(1.0) + (1.0 - min_dist / second_dist).clamp(0.0, 1.0) } else { 1.0 } @@ -294,7 +307,7 @@ mod tests { // Process and update let tx = make_tx(1); - let processed = tl.process(tx.clone()); + let _processed = tl.process(tx.clone()); let outcome = ValidationOutcome { valid: true, @@ -307,4 +320,34 @@ mod tests { let processed2 = tl.process(tx); assert!(processed2.confidence > 0.0); } + + #[test] + fn a_nan_centroid_does_not_panic_and_does_not_win() { + // `partial_cmp().unwrap()` panicked here on any NaN distance, taking the + // process down from inside a confidence calculation. + let mut classifier = TransactionClassifier::new(); + classifier.centroids.clear(); + classifier.centroids.insert("good".into(), vec![0.0; 16]); + classifier + .centroids + .insert("corrupt".into(), vec![f32::NAN; 16]); + + let c = classifier.confidence(&[0.5; 16]); + + assert!(c.is_finite(), "confidence was {c}"); + assert!((0.0..=1.0).contains(&c), "confidence {c} left its range"); + } + + #[test] + fn no_measurable_separation_reports_neutral_confidence_not_full() { + // With every distance unusable the old code fell through the + // `second_dist > 0.0` test and returned 1.0 — maximum confidence at the + // exact moment there was no information at all. + let mut classifier = TransactionClassifier::new(); + classifier.centroids.clear(); + classifier.centroids.insert("a".into(), vec![f32::NAN; 16]); + classifier.centroids.insert("b".into(), vec![f32::NAN; 16]); + + assert_eq!(classifier.confidence(&[0.5; 16]), 0.5); + } } diff --git a/crates/aingle_ai/src/types.rs b/crates/aingle_ai/src/types.rs index 502a3d3f..645e58a4 100644 --- a/crates/aingle_ai/src/types.rs +++ b/crates/aingle_ai/src/types.rs @@ -174,8 +174,8 @@ impl AiTransaction { // Entry type hash features (first 4 bytes as float) let type_hash = pattern_id(self.entry_type.as_bytes()); - for i in 0..4 { - features.push(type_hash[i] as f32 / 255.0); + for byte in type_hash.iter().take(4) { + features.push(*byte as f32 / 255.0); } // Agent features (first 4 bytes) @@ -287,6 +287,6 @@ mod tests { let features = tx.extract_features(); assert_eq!(features.len(), 16); - assert!(features.iter().all(|&f| f >= 0.0 && f <= 2.0)); + assert!(features.iter().all(|&f| (0.0..=2.0).contains(&f))); } } diff --git a/crates/aingle_contracts/Cargo.toml b/crates/aingle_contracts/Cargo.toml index 605b2cd6..8b6aada2 100644 --- a/crates/aingle_contracts/Cargo.toml +++ b/crates/aingle_contracts/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_contracts" -version = "0.7.1" +version = "0.7.2" description = "Smart Contracts DSL and WASM Runtime for AIngle" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "smart-contracts", "wasm", "dsl", "blockchain"] categories = ["wasm", "database"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = ["runtime"] runtime = ["dep:wasmer"] diff --git a/crates/aingle_contracts/src/contract.rs b/crates/aingle_contracts/src/contract.rs index bb437e1c..3739df7e 100644 --- a/crates/aingle_contracts/src/contract.rs +++ b/crates/aingle_contracts/src/contract.rs @@ -13,11 +13,12 @@ use crate::error::{ContractError, Result}; use crate::types::{Address, ContractId}; /// Function visibility/mutability -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum FunctionType { /// Read-only function (view) View, /// State-mutating function + #[default] Mutate, /// Payable function (can receive value) Payable, @@ -27,12 +28,6 @@ pub enum FunctionType { Constructor, } -impl Default for FunctionType { - fn default() -> Self { - Self::Mutate - } -} - /// Contract function definition #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ContractFunction { diff --git a/crates/aingle_contracts/src/types.rs b/crates/aingle_contracts/src/types.rs index cc7f6b24..0fcc3b1a 100644 --- a/crates/aingle_contracts/src/types.rs +++ b/crates/aingle_contracts/src/types.rs @@ -46,7 +46,7 @@ impl Address { hasher.update(b"aingle_deploy:"); hasher.update(deployer.as_bytes()); hasher.update(code_hash); - hasher.update(&nonce); + hasher.update(nonce); let hash: [u8; 32] = hasher.finalize().into(); Self(hash) } diff --git a/crates/aingle_cortex/Cargo.toml b/crates/aingle_cortex/Cargo.toml index a0529c0e..9ff1b99e 100644 --- a/crates/aingle_cortex/Cargo.toml +++ b/crates/aingle_cortex/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_cortex" -version = "0.8.0" +version = "0.9.0" description = "Córtex API - REST/GraphQL/SPARQL interface for AIngle semantic graphs" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "api", "graphql", "sparql", "rest"] categories = ["web-programming", "database"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = ["rest", "sparql", "auth", "dag"] rest = [] @@ -36,11 +35,11 @@ path = "src/main.rs" [dependencies] # Core AIngle crates -aingle_graph = { version = "0.8", path = "../aingle_graph", features = ["sled-backend"] } -aingle_logic = { version = "0.8", path = "../aingle_logic" } -aingle_zk = { version = "0.8", path = "../aingle_zk" } +aingle_graph = { version = "0.9", path = "../aingle_graph", features = ["sled-backend"] } +aingle_logic = { version = "0.9", path = "../aingle_logic" } +aingle_zk = { version = "0.9", path = "../aingle_zk" } ineru = { version = "0.7", path = "../ineru" } -aingle_ingest = { version = "0.8", path = "../aingle_ingest" } +aingle_ingest = { version = "0.9", path = "../aingle_ingest" } ignore = "0.4" # Web framework diff --git a/crates/aingle_cortex/src/cluster_init.rs b/crates/aingle_cortex/src/cluster_init.rs index 2486d014..36f5342e 100644 --- a/crates/aingle_cortex/src/cluster_init.rs +++ b/crates/aingle_cortex/src/cluster_init.rs @@ -103,11 +103,9 @@ impl ClusterConfig { i += 1; } } - "--cluster-tls-key" => { - if i + 1 < args.len() { - cfg.tls_key = Some(args[i + 1].clone()); - i += 1; - } + "--cluster-tls-key" if i + 1 < args.len() => { + cfg.tls_key = Some(args[i + 1].clone()); + i += 1; } _ => {} } @@ -406,7 +404,7 @@ pub async fn ensure_dag_ready(state: &mut crate::state::AppState, db_path: Optio // Set DAG author from cluster node ID (no-op when not in cluster mode). if let Some(node_id) = state.cluster_node_id { - state.dag_author = Some(aingle_graph::NodeId::named(&format!("node:{}", node_id))); + state.dag_author = Some(aingle_graph::NodeId::named(format!("node:{}", node_id))); } // Initialize the Ed25519 signing key for DAG actions. @@ -459,7 +457,7 @@ pub async fn ensure_dag_ready(state: &mut crate::state::AppState, db_path: Optio } #[cfg(not(unix))] { - if let Err(e) = std::fs::write(&key_path, &seed) { + if let Err(e) = std::fs::write(&key_path, seed) { tracing::error!("Failed to persist DAG signing key: {e}"); } } @@ -496,7 +494,7 @@ pub async fn init_cluster( bind_addr: &str, p2p_addr: &str, ) -> Result<(), Error> { - config.validate().map_err(|e| Error::Internal(e))?; + config.validate().map_err(Error::Internal)?; let wal_dir = config.wal_dir.as_deref().unwrap_or("wal"); let wal_path = std::path::Path::new(wal_dir); diff --git a/crates/aingle_cortex/src/main.rs b/crates/aingle_cortex/src/main.rs index 374b60f0..90bbc0e0 100644 --- a/crates/aingle_cortex/src/main.rs +++ b/crates/aingle_cortex/src/main.rs @@ -41,8 +41,10 @@ async fn main() -> Result<(), Box> { return Ok(()); } - let mut config = CortexConfig::default(); - config.embed_model = std::env::var("AINGLE_EMBED_MODEL").ok(); + let mut config = CortexConfig { + embed_model: std::env::var("AINGLE_EMBED_MODEL").ok(), + ..Default::default() + }; // Simple argument parsing let mut i = 1; diff --git a/crates/aingle_cortex/src/p2p/config.rs b/crates/aingle_cortex/src/p2p/config.rs index c44da9f4..4868c854 100644 --- a/crates/aingle_cortex/src/p2p/config.rs +++ b/crates/aingle_cortex/src/p2p/config.rs @@ -114,13 +114,11 @@ impl P2pConfig { i += 1; } } - "--p2p-peer" => { - if i + 1 < args.len() { - if let Ok(addr) = args[i + 1].parse::() { - cfg.manual_peers.push(addr); - } - i += 1; + "--p2p-peer" if i + 1 < args.len() => { + if let Ok(addr) = args[i + 1].parse::() { + cfg.manual_peers.push(addr); } + i += 1; } _ => {} } @@ -141,8 +139,10 @@ mod tests { #[test] fn rejects_invalid_port() { - let mut cfg = P2pConfig::default(); - cfg.port = 0; + let mut cfg = P2pConfig { + port: 0, + ..Default::default() + }; assert!(cfg.validate().is_err()); cfg.port = 80; @@ -177,8 +177,10 @@ mod tests { #[test] fn rejects_empty_seed() { - let mut cfg = P2pConfig::default(); - cfg.seed = Some(String::new()); + let cfg = P2pConfig { + seed: Some(String::new()), + ..Default::default() + }; assert!(cfg.validate().is_err()); } } diff --git a/crates/aingle_cortex/src/p2p/dag_sync.rs b/crates/aingle_cortex/src/p2p/dag_sync.rs index f5f6b0fa..7f9df964 100644 --- a/crates/aingle_cortex/src/p2p/dag_sync.rs +++ b/crates/aingle_cortex/src/p2p/dag_sync.rs @@ -19,7 +19,7 @@ pub fn collect_local_tips(graph: &GraphDB) -> (Vec, u64) { .tips_raw() .unwrap_or_default() .into_iter() - .map(|h| hex::encode(h)) + .map(hex::encode) .collect::>(); let count = dag_store.action_count() as u64; (tips, count) diff --git a/crates/aingle_cortex/src/p2p/manager.rs b/crates/aingle_cortex/src/p2p/manager.rs index f6912bea..461c1786 100644 --- a/crates/aingle_cortex/src/p2p/manager.rs +++ b/crates/aingle_cortex/src/p2p/manager.rs @@ -1057,10 +1057,12 @@ mod tests { #[tokio::test] async fn manager_starts_and_stops() { - let mut config = P2pConfig::default(); - config.enabled = true; - config.port = 0; // OS-assigned - config.data_dir = tempfile::TempDir::new().unwrap().into_path(); + let config = P2pConfig { + enabled: true, + port: 0, // OS-assigned + data_dir: tempfile::TempDir::new().unwrap().keep(), + ..Default::default() + }; let state = AppState::new().unwrap(); let manager = P2pManager::start(config, state).await.unwrap(); diff --git a/crates/aingle_cortex/src/proofs/store.rs b/crates/aingle_cortex/src/proofs/store.rs index 2bb6eb34..4ff1283e 100644 --- a/crates/aingle_cortex/src/proofs/store.rs +++ b/crates/aingle_cortex/src/proofs/store.rs @@ -282,7 +282,7 @@ impl ProofStore { .map_err(|e| VerificationError::InvalidProofData(e.to_string()))?; self.backend .put(&proof_id, &serialized) - .map_err(|e| VerificationError::InvalidProofData(e))?; + .map_err(VerificationError::InvalidProofData)?; // Update stats let mut stats = self.stats.write().await; diff --git a/crates/aingle_cortex/src/rest/audit.rs b/crates/aingle_cortex/src/rest/audit.rs index 2856ebd9..3b25af9e 100644 --- a/crates/aingle_cortex/src/rest/audit.rs +++ b/crates/aingle_cortex/src/rest/audit.rs @@ -63,11 +63,9 @@ impl AuditLog { // Read existing entries from JSONL file if let Ok(file) = std::fs::File::open(&path) { let reader = std::io::BufReader::new(file); - for line in reader.lines() { - if let Ok(line) = line { - if let Ok(entry) = serde_json::from_str::(&line) { - entries.push(entry); - } + for line in reader.lines().map_while(Result::ok) { + if let Ok(entry) = serde_json::from_str::(&line) { + entries.push(entry); } } // Keep only the last max_entries @@ -190,6 +188,10 @@ impl AuditLog { pub fn len(&self) -> usize { self.entries.len() } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } } impl Default for AuditLog { @@ -300,7 +302,7 @@ mod tests { } // Should have evicted some entries assert!(log.len() <= 15); - assert!(log.len() > 0); + assert!(!log.is_empty()); } #[test] diff --git a/crates/aingle_cortex/src/rest/dag.rs b/crates/aingle_cortex/src/rest/dag.rs index 0bdd3048..6afcc203 100644 --- a/crates/aingle_cortex/src/rest/dag.rs +++ b/crates/aingle_cortex/src/rest/dag.rs @@ -387,12 +387,10 @@ pub async fn get_dag_export( ) -> Result { use axum::response::IntoResponse; - let format = aingle_graph::dag::ExportFormat::from_str(&query.format).ok_or_else(|| { - Error::InvalidInput(format!( - "Unknown format '{}'. Use: dot, mermaid, json", - query.format - )) - })?; + let format: aingle_graph::dag::ExportFormat = query + .format + .parse() + .map_err(|e: aingle_graph::dag::UnknownExportFormat| Error::InvalidInput(e.to_string()))?; let graph = state.graph.read().await; let dag_graph = graph diff --git a/crates/aingle_cortex/src/rest/proof_api.rs b/crates/aingle_cortex/src/rest/proof_api.rs index 711aca50..e8ba89a0 100644 --- a/crates/aingle_cortex/src/rest/proof_api.rs +++ b/crates/aingle_cortex/src/rest/proof_api.rs @@ -124,7 +124,7 @@ pub async fn verify_proofs_batch( let results = state.proof_store.batch_verify(&request.proof_ids).await; let mut verifications = Vec::new(); - for (proof_id, result) in request.proof_ids.iter().zip(results.into_iter()) { + for (proof_id, result) in request.proof_ids.iter().zip(results) { // Every entry carries its own replay material: a batch verdict is no more // authoritative than a single one, so it must not be the only thing a // caller can act on. @@ -498,7 +498,7 @@ mod tests { proof_data: serde_json::json!({"test": "data"}), metadata: None, }; - submit_proof(AxumState(state.clone()), None, Json(request)) + let _ = submit_proof(AxumState(state.clone()), None, Json(request)) .await .unwrap(); } @@ -526,7 +526,7 @@ mod tests { metadata: None, }; - submit_proof(AxumState(state.clone()), None, Json(request)) + let _ = submit_proof(AxumState(state.clone()), None, Json(request)) .await .unwrap(); diff --git a/crates/aingle_cortex/src/rest/triples.rs b/crates/aingle_cortex/src/rest/triples.rs index 342b0006..d8a3972f 100644 --- a/crates/aingle_cortex/src/rest/triples.rs +++ b/crates/aingle_cortex/src/rest/triples.rs @@ -178,7 +178,7 @@ pub async fn create_triple( #[cfg(feature = "dag")] if let Some(ref raft) = state.raft { let dag_author = state.dag_author.clone().unwrap_or_else(|| { - aingle_graph::NodeId::named(&format!("node:{}", state.cluster_node_id.unwrap_or(0))) + aingle_graph::NodeId::named(format!("node:{}", state.cluster_node_id.unwrap_or(0))) }); // Get current tips, and a sequence number that continues this author's // recorded chain rather than restarting at 1 and evicting its own history @@ -457,7 +457,7 @@ pub async fn delete_triple( #[cfg(feature = "dag")] if let Some(ref raft) = state.raft { let dag_author = state.dag_author.clone().unwrap_or_else(|| { - aingle_graph::NodeId::named(&format!("node:{}", state.cluster_node_id.unwrap_or(0))) + aingle_graph::NodeId::named(format!("node:{}", state.cluster_node_id.unwrap_or(0))) }); let (parents, subject_for_dag, dag_seq) = { let graph = state.graph.read().await; diff --git a/crates/aingle_cortex/src/service/ingest.rs b/crates/aingle_cortex/src/service/ingest.rs index 05169000..7dda89c6 100644 --- a/crates/aingle_cortex/src/service/ingest.rs +++ b/crates/aingle_cortex/src/service/ingest.rs @@ -1358,19 +1358,37 @@ mod tests { let state = enabled_state().await; // Two open tasks. - write(dir.path(), "todos.md", "# Todos\n\n- [ ] Task A\n- [ ] Task B\n"); + write( + dir.path(), + "todos.md", + "# Todos\n\n- [ ] Task A\n- [ ] Task B\n", + ); ingest_path(&state, path, None).await.unwrap(); let rows = list_tasks(&state, None).await; assert_eq!(rows.len(), 2); assert!(rows.iter().all(|r| r.status == "todo")); // Complete A, keep B — re-ingest the changed note. - write(dir.path(), "todos.md", "# Todos\n\n- [x] Task A\n- [ ] Task B\n"); + write( + dir.path(), + "todos.md", + "# Todos\n\n- [x] Task A\n- [ ] Task B\n", + ); ingest_path(&state, path, None).await.unwrap(); let rows = list_tasks(&state, None).await; - assert_eq!(rows.len(), 2, "still exactly two tasks — no orphans or duplicates"); - assert_eq!(rows.iter().find(|r| r.text == "Task A").unwrap().status, "done"); - assert_eq!(rows.iter().find(|r| r.text == "Task B").unwrap().status, "todo"); + assert_eq!( + rows.len(), + 2, + "still exactly two tasks — no orphans or duplicates" + ); + assert_eq!( + rows.iter().find(|r| r.text == "Task A").unwrap().status, + "done" + ); + assert_eq!( + rows.iter().find(|r| r.text == "Task B").unwrap().status, + "todo" + ); // The old `status=todo` triple for A must be gone (exactly one remains). { @@ -1389,7 +1407,11 @@ mod tests { .with_predicate(Predicate::named("status")), ) .unwrap(); - assert_eq!(statuses.len(), 1, "no stale status triple should remain for A"); + assert_eq!( + statuses.len(), + 1, + "no stale status triple should remain for A" + ); } // Remove A from the note — its task node is retracted, B survives. @@ -1425,9 +1447,10 @@ mod tests { assert_eq!(rows.len(), 2); // Snapshot Q2's `card_due` triple id — it must NOT change when only Q1 is reviewed. - let q2_due_id_before = card_field_triple_id(&state, "card:deck.md#bbbbbbbbbbbb", "card_due") - .await - .expect("Q2 card_due before"); + let q2_due_id_before = + card_field_triple_id(&state, "card:deck.md#bbbbbbbbbbbb", "card_due") + .await + .expect("Q2 card_due before"); // Review Q1: reschedule it (new due + ef), keep its front text and id. write( diff --git a/crates/aingle_cortex/src/service/tasks.rs b/crates/aingle_cortex/src/service/tasks.rs index 6c0a0422..b781ed0f 100644 --- a/crates/aingle_cortex/src/service/tasks.rs +++ b/crates/aingle_cortex/src/service/tasks.rs @@ -244,7 +244,11 @@ mod tests { vec![ (format!("task:n.md#{id}"), "is_a".into(), "task".into()), (format!("task:n.md#{id}"), "status".into(), status.into()), - (format!("task:n.md#{id}"), "task_text".into(), id.to_uppercase()), + ( + format!("task:n.md#{id}"), + "task_text".into(), + id.to_uppercase(), + ), (format!("task:n.md#{id}"), date_pred.into(), date.into()), (format!("task:n.md#{id}"), "in_note".into(), "n.md".into()), ] @@ -258,7 +262,7 @@ mod tests { rows.extend(task("c", "todo", "deadline", "2026-07-28")); // upcoming (≤+7) rows.extend(task("d", "todo", "deadline", "2026-08-30")); // beyond horizon rows.extend(task("e", "done", "deadline", "2026-07-20")); // excluded (done) - // f: open but undated → excluded from agenda + // f: open but undated → excluded from agenda rows.push(("task:n.md#f".into(), "is_a".into(), "task".into())); rows.push(("task:n.md#f".into(), "status".into(), "todo".into())); rows.push(("task:n.md#f".into(), "task_text".into(), "F".into())); @@ -270,9 +274,24 @@ mod tests { let state = graph_with(&refs).await; let ag = super::agenda(&state, "2026-07-24", 7).await; - assert_eq!(ag.overdue.iter().map(|t| t.text.as_str()).collect::>(), ["A"]); - assert_eq!(ag.today.iter().map(|t| t.text.as_str()).collect::>(), ["B"]); - assert_eq!(ag.upcoming.iter().map(|t| t.text.as_str()).collect::>(), ["C"]); + assert_eq!( + ag.overdue + .iter() + .map(|t| t.text.as_str()) + .collect::>(), + ["A"] + ); + assert_eq!( + ag.today.iter().map(|t| t.text.as_str()).collect::>(), + ["B"] + ); + assert_eq!( + ag.upcoming + .iter() + .map(|t| t.text.as_str()) + .collect::>(), + ["C"] + ); } #[tokio::test] @@ -290,7 +309,10 @@ mod tests { let all = super::list_tasks(&state, None).await; assert_eq!(all.len(), 3); // sorted by due date: b(07-10) < a(07-20) < c(07-25) - assert_eq!(all.iter().map(|t| t.text.as_str()).collect::>(), ["B", "A", "C"]); + assert_eq!( + all.iter().map(|t| t.text.as_str()).collect::>(), + ["B", "A", "C"] + ); let doing = super::list_tasks(&state, Some("doing")).await; assert_eq!(doing.len(), 1); @@ -311,7 +333,10 @@ mod tests { ]) .await; let ag = super::agenda(&state, "2026-07-24", 7).await; - assert!(ag.today.iter().any(|t| t.text == "Z"), "scheduled-today belongs in today"); + assert!( + ag.today.iter().any(|t| t.text == "Z"), + "scheduled-today belongs in today" + ); assert!(!ag.upcoming.iter().any(|t| t.text == "Z")); } diff --git a/crates/aingle_cortex/src/service/validate.rs b/crates/aingle_cortex/src/service/validate.rs index a98cfff7..bc76afbf 100644 --- a/crates/aingle_cortex/src/service/validate.rs +++ b/crates/aingle_cortex/src/service/validate.rs @@ -262,7 +262,7 @@ mod tests { let s = v .as_str() .unwrap_or_else(|| panic!("expected hex, got {v}")); - assert!(s.len() % 2 == 0, "hex must be byte-aligned: {s:?}"); + assert!(s.len().is_multiple_of(2), "hex must be byte-aligned: {s:?}"); (0..s.len() / 2) .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).expect("hex digit")) .collect() diff --git a/crates/aingle_cortex/tests/data_integrity_test.rs b/crates/aingle_cortex/tests/data_integrity_test.rs index 70ddb0a7..e2253f0d 100644 --- a/crates/aingle_cortex/tests/data_integrity_test.rs +++ b/crates/aingle_cortex/tests/data_integrity_test.rs @@ -139,7 +139,7 @@ async fn test_graph_dag_triple_materialization_consistency() { let mut triple_ids = Vec::new(); for i in 0..50 { let triple = Triple::new( - NodeId::named(&format!("entity:{}", i)), + NodeId::named(format!("entity:{}", i)), Predicate::named("has_value"), Value::Integer(i * 100), ); @@ -166,7 +166,7 @@ async fn test_graph_dag_triple_materialization_consistency() { // Verify pattern queries return correct results for i in 0..50 { - let pattern = TriplePattern::subject(NodeId::named(&format!("entity:{}", i))); + let pattern = TriplePattern::subject(NodeId::named(format!("entity:{}", i))); let results = graph.find(pattern).unwrap(); assert_eq!( results.len(), @@ -185,8 +185,8 @@ async fn test_graph_dag_triple_materialization_consistency() { assert_eq!(graph.count(), 25); // Verify even ones remain, odd ones gone - for i in 0..50 { - let exists = graph.get(&triple_ids[i]).unwrap().is_some(); + for (i, id) in triple_ids.iter().enumerate().take(50) { + let exists = graph.get(id).unwrap().is_some(); if i % 2 == 0 { assert!(exists, "even triple {} should still exist", i); } else { @@ -209,7 +209,7 @@ async fn test_batch_insert_index_consistency() { let triples: Vec = (0..100) .map(|i| { Triple::new( - NodeId::named(&format!("batch:{}", i)), + NodeId::named(format!("batch:{}", i)), Predicate::named("batch_value"), Value::Integer(i), ) @@ -222,7 +222,7 @@ async fn test_batch_insert_index_consistency() { // Verify every triple is findable by subject pattern (uses SPO index) for i in 0..100 { - let pattern = TriplePattern::subject(NodeId::named(&format!("batch:{}", i))); + let pattern = TriplePattern::subject(NodeId::named(format!("batch:{}", i))); let results = graph.find(pattern).unwrap(); assert_eq!(results.len(), 1, "batch:{} should be findable via index", i); assert_eq!(results[0].object, Value::Integer(i)); @@ -238,7 +238,7 @@ async fn test_batch_insert_index_consistency() { let triples2: Vec = (0..100) .map(|i| { Triple::new( - NodeId::named(&format!("batch:{}", i)), + NodeId::named(format!("batch:{}", i)), Predicate::named("batch_value"), Value::Integer(i), ) @@ -270,7 +270,7 @@ async fn test_app_state_flush_restore_roundtrip() { let graph = state.graph.read().await; for i in 0..10 { let triple = Triple::new( - NodeId::named(&format!("node:{}", i)), + NodeId::named(format!("node:{}", i)), Predicate::named("value"), Value::String(format!("data-{}", i)), ); @@ -308,7 +308,7 @@ async fn test_app_state_flush_restore_roundtrip() { assert_eq!(graph.count(), 10, "graph triples should survive restart"); for i in 0..10 { - let pattern = TriplePattern::subject(NodeId::named(&format!("node:{}", i))); + let pattern = TriplePattern::subject(NodeId::named(format!("node:{}", i))); let results = graph.find(pattern).unwrap(); assert_eq!(results.len(), 1, "node:{} missing after restart", i); assert_eq!( @@ -496,7 +496,7 @@ async fn test_graph_sled_persistence_full_cycle() { let graph = GraphDB::sled(path_str).unwrap(); for i in 0..25 { let triple = Triple::new( - NodeId::named(&format!("persist:{}", i)), + NodeId::named(format!("persist:{}", i)), Predicate::named("data"), Value::Float(i as f64 * 1.5), ); @@ -512,7 +512,7 @@ async fn test_graph_sled_persistence_full_cycle() { assert_eq!(graph.count(), 25, "sled graph should persist"); for i in 0..25 { - let pattern = TriplePattern::subject(NodeId::named(&format!("persist:{}", i))); + let pattern = TriplePattern::subject(NodeId::named(format!("persist:{}", i))); let results = graph.find(pattern).unwrap(); assert_eq!(results.len(), 1, "persist:{} missing", i); match &results[0].object { diff --git a/crates/aingle_cortex/tests/rate_limiting_test.rs b/crates/aingle_cortex/tests/rate_limiting_test.rs index 19687210..a18c8972 100644 --- a/crates/aingle_cortex/tests/rate_limiting_test.rs +++ b/crates/aingle_cortex/tests/rate_limiting_test.rs @@ -281,7 +281,7 @@ async fn test_rate_limiter_gradual_refill() { // Should have succeeded ~3 times (with some tolerance for timing) assert!( - successes >= 2 && successes <= 4, + (2..=4).contains(&successes), "Should refill approximately 3 tokens, got {}", successes ); diff --git a/crates/aingle_graph/Cargo.toml b/crates/aingle_graph/Cargo.toml index 453ea2aa..fa437304 100644 --- a/crates/aingle_graph/Cargo.toml +++ b/crates/aingle_graph/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_graph" -version = "0.8.0" +version = "0.9.0" description = "Native GraphDB for AIngle - Semantic triple store with SPO indexes" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "graph", "rdf", "semantic", "triplestore"] categories = ["database", "data-structures"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = ["sled-backend"] # Storage backends diff --git a/crates/aingle_graph/src/dag/export.rs b/crates/aingle_graph/src/dag/export.rs index c8c0190e..349ede5d 100644 --- a/crates/aingle_graph/src/dag/export.rs +++ b/crates/aingle_graph/src/dag/export.rs @@ -40,14 +40,41 @@ pub enum ExportFormat { Json, } -impl ExportFormat { - /// Parse from string (case-insensitive). - pub fn from_str(s: &str) -> Option { +/// The string offered to [`ExportFormat`]'s `FromStr` named no known format. +/// +/// Carries the offending input and knows the accepted spellings, so a caller +/// does not have to keep its own copy of the list — which is how a REST handler +/// ends up advertising formats the parser no longer accepts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnknownExportFormat(pub String); + +impl std::fmt::Display for UnknownExportFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "unknown export format '{}'. Use: dot (or graphviz), mermaid (or md), json", + self.0 + ) + } +} + +impl std::error::Error for UnknownExportFormat {} + +/// Parse from string (case-insensitive). +/// +/// This was an inherent `from_str` returning `Option`, which shadowed +/// `std::str::FromStr::from_str` and returned a different shape from it: a +/// caller reading `ExportFormat::from_str(..)` could not tell which one they +/// were getting, and got no reason for the failure either way. +impl std::str::FromStr for ExportFormat { + type Err = UnknownExportFormat; + + fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { - "dot" | "graphviz" => Some(Self::Dot), - "mermaid" | "md" => Some(Self::Mermaid), - "json" => Some(Self::Json), - _ => None, + "dot" | "graphviz" => Ok(Self::Dot), + "mermaid" | "md" => Ok(Self::Mermaid), + "json" => Ok(Self::Json), + _ => Err(UnknownExportFormat(s.to_string())), } } } @@ -309,15 +336,28 @@ mod tests { #[test] fn test_export_format_parsing() { - assert_eq!(ExportFormat::from_str("dot"), Some(ExportFormat::Dot)); - assert_eq!(ExportFormat::from_str("DOT"), Some(ExportFormat::Dot)); - assert_eq!(ExportFormat::from_str("graphviz"), Some(ExportFormat::Dot)); + assert_eq!("dot".parse(), Ok(ExportFormat::Dot)); + assert_eq!("DOT".parse(), Ok(ExportFormat::Dot)); + assert_eq!("graphviz".parse(), Ok(ExportFormat::Dot)); + assert_eq!("mermaid".parse(), Ok(ExportFormat::Mermaid)); + assert_eq!("md".parse(), Ok(ExportFormat::Mermaid)); + assert_eq!("json".parse(), Ok(ExportFormat::Json)); assert_eq!( - ExportFormat::from_str("mermaid"), - Some(ExportFormat::Mermaid) + "xml".parse::(), + Err(UnknownExportFormat("xml".to_string())) ); - assert_eq!(ExportFormat::from_str("json"), Some(ExportFormat::Json)); - assert_eq!(ExportFormat::from_str("xml"), None); + } + + #[test] + fn an_unknown_format_says_what_it_would_have_accepted() { + // The list used to live in the REST handler, where it could drift away + // from the parser without anything noticing. + let e = "xml".parse::().unwrap_err(); + let msg = e.to_string(); + assert!(msg.contains("xml"), "{msg}"); + for accepted in ["dot", "graphviz", "mermaid", "md", "json"] { + assert!(msg.contains(accepted), "{accepted} missing from: {msg}"); + } } #[test] diff --git a/crates/aingle_graph/src/dag/mod.rs b/crates/aingle_graph/src/dag/mod.rs index 50da412d..5a03056e 100644 --- a/crates/aingle_graph/src/dag/mod.rs +++ b/crates/aingle_graph/src/dag/mod.rs @@ -29,13 +29,11 @@ pub mod tips; pub use action::{ DagAction, DagActionHash, DagPayload, MemoryOpKind, Provenance, TripleInsertPayload, }; -pub use canonical::{ - CanonicalAction, CANONICAL_SPEC, HASH_ALG, SIGNATURE_ALG, SIGNED_MESSAGE, -}; #[cfg(feature = "sled-backend")] pub use backend::SledDagBackend; pub use backend::{DagBackend, MemoryDagBackend}; -pub use export::{DagGraph, ExportFormat}; +pub use canonical::{CanonicalAction, CANONICAL_SPEC, HASH_ALG, SIGNATURE_ALG, SIGNED_MESSAGE}; +pub use export::{DagGraph, ExportFormat, UnknownExportFormat}; pub use pruning::{PruneResult, RetentionPolicy}; #[cfg(feature = "dag-sign")] pub use signing::{DagSigningKey, DagVerifyingKey, VerifyResult}; diff --git a/crates/aingle_graph/src/dag/store.rs b/crates/aingle_graph/src/dag/store.rs index b32ea183..b430cd02 100644 --- a/crates/aingle_graph/src/dag/store.rs +++ b/crates/aingle_graph/src/dag/store.rs @@ -7,6 +7,8 @@ //! In-memory indexes (author chain, affected triples) are rebuilt on startup //! from the backend, ensuring zero data loss across restarts. +use std::cmp::Reverse; + use super::action::{DagAction, DagActionHash, DagPayload, TripleInsertPayload}; use super::backend::DagBackend; use super::pruning::{PruneResult, RetentionPolicy}; @@ -369,7 +371,7 @@ impl DagStore { .collect(); // Sort by seq descending (most recent first) - entries.sort_by(|a, b| b.0.cmp(&a.0)); + entries.sort_by_key(|e| Reverse(e.0)); entries.truncate(limit); drop(idx); @@ -407,7 +409,7 @@ impl DagStore { } // Sort by timestamp descending - result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + result.sort_by_key(|a| Reverse(a.timestamp)); result.truncate(limit); Ok(result) @@ -440,7 +442,7 @@ impl DagStore { } } - result.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + result.sort_by_key(|a| Reverse(a.timestamp)); result.truncate(limit); Ok(result) @@ -713,10 +715,10 @@ impl DagStore { hash.copy_from_slice(&key[2..]); if let Some(action) = DagAction::from_bytes(value) { - if action.timestamp <= *ts { - if best.as_ref().map_or(true, |(_, t)| action.timestamp > *t) { - best = Some((DagActionHash(hash), action.timestamp)); - } + if action.timestamp <= *ts + && best.as_ref().is_none_or(|(_, t)| action.timestamp > *t) + { + best = Some((DagActionHash(hash), action.timestamp)); } } } @@ -1044,10 +1046,9 @@ fn topo_sort(mut collected: HashMap<[u8; 32], DagAction>) -> crate::Result Vec<[u8; 32]> { match payload { - DagPayload::TripleInsert { triples } => triples - .iter() - .map(|t| compute_triple_id_from_payload(t)) - .collect(), + DagPayload::TripleInsert { triples } => { + triples.iter().map(compute_triple_id_from_payload).collect() + } DagPayload::TripleDelete { triple_ids, .. } => triple_ids.clone(), DagPayload::Batch { ops } => ops.iter().flat_map(extract_affected_triple_ids).collect(), _ => vec![], diff --git a/crates/aingle_graph/src/dag/timetravel.rs b/crates/aingle_graph/src/dag/timetravel.rs index 95abfb82..152885a5 100644 --- a/crates/aingle_graph/src/dag/timetravel.rs +++ b/crates/aingle_graph/src/dag/timetravel.rs @@ -49,6 +49,7 @@ pub struct DagDiff { /// Errors from individual insert/delete operations are intentionally ignored: /// - Duplicate inserts are expected (same triple in multiple actions). /// - Deletes of already-deleted triples are expected after pruning. +/// /// These are not failures — they're inherent to replaying a DAG. pub(crate) fn replay_payload(db: &crate::GraphDB, payload: &DagPayload) -> crate::Result<()> { match payload { diff --git a/crates/aingle_graph/src/lib.rs b/crates/aingle_graph/src/lib.rs index ebfa4bfe..ef60a331 100644 --- a/crates/aingle_graph/src/lib.rs +++ b/crates/aingle_graph/src/lib.rs @@ -1108,10 +1108,8 @@ impl GraphDB { let mut deleted = 0usize; for triple in all { if let Some(name) = triple.subject.as_name() { - if name.starts_with(prefix) { - if self.delete(&triple.id())? { - deleted += 1; - } + if name.starts_with(prefix) && self.delete(&triple.id())? { + deleted += 1; } } } @@ -1456,7 +1454,7 @@ mod tests { for i in 0..10 { db.insert(Triple::new( - NodeId::named(&format!("node:{}", i)), + NodeId::named(format!("node:{}", i)), Predicate::named("has_value"), Value::integer(i as i64), )) @@ -1808,7 +1806,7 @@ mod tests { // DAG has one action let action = db.dag_action(&dag_hash).unwrap().unwrap(); assert_eq!(action.seq, 1); - assert!(action.is_genesis() == false || action.parents.is_empty()); + assert!(!action.is_genesis() || action.parents.is_empty()); // Tips point to the new action let tips = db.dag_tips().unwrap(); @@ -1874,7 +1872,7 @@ mod tests { for seq in 1..=5 { let triple = Triple::new( - NodeId::named(&format!("node:{}", seq)), + NodeId::named(format!("node:{}", seq)), Predicate::named("p"), Value::integer(seq), ); diff --git a/crates/aingle_graph/src/value.rs b/crates/aingle_graph/src/value.rs index dfa8836f..d948c28b 100644 --- a/crates/aingle_graph/src/value.rs +++ b/crates/aingle_graph/src/value.rs @@ -481,8 +481,8 @@ mod tests { let int_val = Value::integer(42); assert_eq!(int_val.as_integer(), Some(42)); - let float_val = Value::float(3.14); - assert_eq!(float_val.as_float(), Some(3.14)); + let float_val = Value::float(2.5); + assert_eq!(float_val.as_float(), Some(2.5)); } #[test] diff --git a/crates/aingle_graph/tests/graph_integration_tests.rs b/crates/aingle_graph/tests/graph_integration_tests.rs index 56860125..355e4600 100644 --- a/crates/aingle_graph/tests/graph_integration_tests.rs +++ b/crates/aingle_graph/tests/graph_integration_tests.rs @@ -152,7 +152,7 @@ fn test_batch_insertion_large() { let triples: Vec = (0..100) .map(|i| { Triple::new( - NodeId::named(&format!("node:{}", i)), + NodeId::named(format!("node:{}", i)), Predicate::named("has_id"), Value::integer(i as i64), ) @@ -247,7 +247,7 @@ fn test_query_with_limit() { // Insert 10 triples for i in 0..10 { db.insert(Triple::new( - NodeId::named(&format!("node:{}", i)), + NodeId::named(format!("node:{}", i)), Predicate::named("type"), Value::literal("test"), )) @@ -581,7 +581,7 @@ fn test_different_value_types() { db.insert(Triple::new( NodeId::named("node:1"), Predicate::named("float_val"), - Value::float(3.14), + Value::float(2.5), )) .unwrap(); diff --git a/crates/aingle_ingest/Cargo.toml b/crates/aingle_ingest/Cargo.toml index 21b1e49b..20453e06 100644 --- a/crates/aingle_ingest/Cargo.toml +++ b/crates/aingle_ingest/Cargo.toml @@ -1,13 +1,12 @@ [package] name = "aingle_ingest" -version = "0.8.0" +version = "0.9.0" description = "Structural extraction of triples and text chunks from markdown/code for AIngle" license = "Apache-2.0 OR LicenseRef-Commercial" edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [dependencies] -aingle_graph = { version = "0.8", path = "../aingle_graph", features = ["dag"] } +aingle_graph = { version = "0.9", path = "../aingle_graph", features = ["dag"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" regex = "1.12" diff --git a/crates/aingle_ingest/src/chunk.rs b/crates/aingle_ingest/src/chunk.rs index dc540327..b79f71f4 100644 --- a/crates/aingle_ingest/src/chunk.rs +++ b/crates/aingle_ingest/src/chunk.rs @@ -188,10 +188,7 @@ mod tests { // 50 lines that are each ~1KB: a full 50-line window would exceed the // cap, so the window must close early on the byte budget. let one = "a".repeat(1024); - let content = std::iter::repeat(one) - .take(50) - .collect::>() - .join("\n"); + let content = std::iter::repeat_n(one, 50).collect::>().join("\n"); let chunks = chunk_fixed("data.txt", &content, "h", 50); assert!( @@ -240,7 +237,7 @@ mod tests { // Regression: a small markdown note splits by heading into one chunk per // section, none of them byte-split. let content = "# A\n\nalpha\n\n# B\n\nbeta\n"; - let chunks = chunk_markdown("n.md", &content, "h"); + let chunks = chunk_markdown("n.md", content, "h"); assert_eq!(chunks.len(), 2); assert!(chunks[0].text.contains("alpha")); diff --git a/crates/aingle_ingest/src/lib.rs b/crates/aingle_ingest/src/lib.rs index ef339115..ee41b25b 100644 --- a/crates/aingle_ingest/src/lib.rs +++ b/crates/aingle_ingest/src/lib.rs @@ -155,7 +155,10 @@ mod tests { .iter() .any(|t| t.predicate == "status" && t.object == ObjectValue::Text("done".into()))); assert_eq!( - ex.triples.iter().filter(|t| t.predicate == "status").count(), + ex.triples + .iter() + .filter(|t| t.predicate == "status") + .count(), 2, "exactly the two real tasks become task nodes" ); @@ -180,7 +183,10 @@ mod tests { // A `🔁 every …` task records its recurrence as a `recur` fact on the // same stable task node (encoded compactly, e.g. `1m`), so MCP/queries // can see the repeat cadence. The reschedule itself is app-side. - let ex = extract("todos.md", "- [ ] rent \u{1F4C5} 2026-08-01 \u{1F501} every month\n"); + let ex = extract( + "todos.md", + "- [ ] rent \u{1F4C5} 2026-08-01 \u{1F501} every month\n", + ); let recur = ex .triples .iter() @@ -206,7 +212,10 @@ mod tests { .find(|t| t.predicate == "is_a" && t.object == ObjectValue::Text("card".into())) .expect("card is_a triple"); let subj = isa.subject.clone(); - assert_eq!(subj, "card:deck.md#cafef00dcafe", "id= in comment is sticky"); + assert_eq!( + subj, "card:deck.md#cafef00dcafe", + "id= in comment is sticky" + ); let has = |p: &str, o: ObjectValue| { ex.triples .iter() @@ -293,7 +302,10 @@ mod tests { - [ ] another real task\n"; let ex = extract("doc.md", md); assert_eq!( - ex.triples.iter().filter(|t| t.predicate == "status").count(), + ex.triples + .iter() + .filter(|t| t.predicate == "status") + .count(), 2, "only the two real tasks become task nodes" ); @@ -305,9 +317,8 @@ mod tests { .triples .iter() .any(|t| t.predicate == "tagged" && t.object == ObjectValue::Text("notatag".into()))); - assert!(!ex.triples.iter().any( - |t| t.predicate == "has_section" && t.object == ObjectValue::Text("not a heading".into()) - )); + assert!(!ex.triples.iter().any(|t| t.predicate == "has_section" + && t.object == ObjectValue::Text("not a heading".into()))); // The genuine heading outside the fence survives. assert!(ex .triples @@ -326,7 +337,11 @@ mod tests { .filter(|t| t.predicate == "status") .map(|t| t.subject.clone()) .collect(); - assert_eq!(subjects.len(), 2, "identical task text must not collapse to one node"); + assert_eq!( + subjects.len(), + 2, + "identical task text must not collapse to one node" + ); assert!(ex .triples .iter() @@ -340,7 +355,10 @@ mod tests { #[test] fn task_lines_still_yield_note_links_and_tags() { // A task line's wikilinks/tags must still attach to the note itself. - let ex = extract("todos.md", "- [ ] Follow up on [[sled]] about #durability\n"); + let ex = extract( + "todos.md", + "- [ ] Follow up on [[sled]] about #durability\n", + ); assert!(ex.triples.iter().any(|t| t.subject == "todos.md" && t.predicate == "links_to" && t.object == ObjectValue::Node("sled".into()))); diff --git a/crates/aingle_ingest/src/tasks.rs b/crates/aingle_ingest/src/tasks.rs index af7bf0de..e571a1ec 100644 --- a/crates/aingle_ingest/src/tasks.rs +++ b/crates/aingle_ingest/src/tasks.rs @@ -106,10 +106,9 @@ pub fn parse_task(line: &str) -> Option { let ch = c[1].chars().next().unwrap(); let rest = c.get(2).map_or(String::new(), |m| m.as_str().to_string()); (status_from_checkbox(ch), rest) - } else if let Some(c) = KEYWORD.captures(line) { - (status_from_keyword(&c[1]), c[2].to_string()) } else { - return None; + let c = KEYWORD.captures(line)?; + (status_from_keyword(&c[1]), c[2].to_string()) }; let priority = PRIORITY.captures(&rest).and_then(|c| c[1].chars().next()); @@ -123,7 +122,11 @@ pub fn parse_task(line: &str) -> Option { .filter(|d| valid_date(d)); // Recurrence: encode `every ` as `` (e.g. `3d`). let recur = RECUR.captures(&rest).map(|c| { - let n: u32 = c.get(1).and_then(|m| m.as_str().parse().ok()).unwrap_or(1).max(1); + let n: u32 = c + .get(1) + .and_then(|m| m.as_str().parse().ok()) + .unwrap_or(1) + .max(1); let unit = match c[2].to_ascii_lowercase().as_str() { "day" => 'd', "week" => 'w', @@ -227,9 +230,15 @@ mod tests { assert_eq!(q.text, "standup"); // Week / year encodings; `every` optional. - assert_eq!(t("- [ ] x \u{1F501} every week").recur.as_deref(), Some("1w")); + assert_eq!( + t("- [ ] x \u{1F501} every week").recur.as_deref(), + Some("1w") + ); assert_eq!(t("- [ ] x \u{1F501} 2 weeks").recur.as_deref(), Some("2w")); - assert_eq!(t("- [ ] x \u{1F501} every 5 years").recur.as_deref(), Some("5y")); + assert_eq!( + t("- [ ] x \u{1F501} every 5 years").recur.as_deref(), + Some("5y") + ); // No recurrence → None. assert_eq!(t("- [ ] plain task").recur, None); @@ -249,7 +258,11 @@ mod tests { // visible in the text (nothing silently lost). let p = t("- [ ] file taxes \u{1F4C5} 2026-13-45"); assert_eq!(p.deadline, None); - assert!(p.text.contains("2026-13-45"), "invalid date stays in text: {}", p.text); + assert!( + p.text.contains("2026-13-45"), + "invalid date stays in text: {}", + p.text + ); // A valid date still parses and is stripped. let q = t("- [ ] pay \u{1F4C5} 2026-02-15 \u{23F3} 2026-02-01"); assert_eq!(q.deadline.as_deref(), Some("2026-02-15")); diff --git a/crates/aingle_logic/Cargo.toml b/crates/aingle_logic/Cargo.toml index d2d6a683..c6dbb501 100644 --- a/crates/aingle_logic/Cargo.toml +++ b/crates/aingle_logic/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_logic" -version = "0.8.0" +version = "0.9.0" description = "Proof-of-Logic validation engine for AIngle semantic graphs" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "logic", "reasoning", "validation", "rules"] categories = ["algorithms", "data-structures"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = [] # Enable SPARQL-like query rules @@ -21,7 +20,7 @@ owl = [] [dependencies] # Graph database -aingle_graph = { version = "0.8", path = "../aingle_graph" } +aingle_graph = { version = "0.9", path = "../aingle_graph" } # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/crates/aingle_logic/src/engine.rs b/crates/aingle_logic/src/engine.rs index 1b2c7cfc..2ed47b75 100644 --- a/crates/aingle_logic/src/engine.rs +++ b/crates/aingle_logic/src/engine.rs @@ -348,7 +348,7 @@ impl RuleEngine { /// * `bindings` - Mutable `Bindings` to accumulate variable assignments. /// * `depth` - Current recursion depth to prevent stack overflow and enforce `max_depth`. /// * `visited` - A `HashSet` to keep track of goals already visited in the current proof path - /// to detect and prevent inference loops. + /// to detect and prevent inference loops. /// * `result` - The mutable `BackwardChainResult` to record proof steps. /// /// # Returns diff --git a/crates/aingle_logic/src/rule.rs b/crates/aingle_logic/src/rule.rs index e89b7f07..f01c8a26 100644 --- a/crates/aingle_logic/src/rule.rs +++ b/crates/aingle_logic/src/rule.rs @@ -6,6 +6,8 @@ //! Rules are the fundamental building blocks of logical validation. //! They define conditions that must be met and actions to take. +use std::cmp::Reverse; + use aingle_graph::{NodeId, Predicate, Triple, Value}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -623,7 +625,7 @@ impl RuleSet { /// Get all enabled rules sorted by priority pub fn enabled_sorted(&self) -> Vec<&Rule> { let mut rules: Vec<_> = self.rules.iter().filter(|r| r.enabled).collect(); - rules.sort_by(|a, b| b.priority.cmp(&a.priority)); + rules.sort_by_key(|r| Reverse(r.priority)); rules } diff --git a/crates/aingle_minimal/Cargo.toml b/crates/aingle_minimal/Cargo.toml index d59a0cb3..b09e98bc 100644 --- a/crates/aingle_minimal/Cargo.toml +++ b/crates/aingle_minimal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_minimal" -version = "0.7.1" +version = "0.7.2" description = "Ultra-light AIngle node for IoT devices (<1MB RAM)" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "iot", "dag", "embedded"] categories = ["embedded", "network-programming"] edition = "2021" -rust-version = "1.85" - +rust-version.workspace = true [features] default = ["std", "coap", "sqlite"] std = [] diff --git a/crates/aingle_minimal/src/config.rs b/crates/aingle_minimal/src/config.rs index 94459c36..dc5a6e75 100644 --- a/crates/aingle_minimal/src/config.rs +++ b/crates/aingle_minimal/src/config.rs @@ -44,7 +44,7 @@ use std::time::Duration; /// let mut config = Config::test_mode(); /// config.power_mode = PowerMode::Low; /// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum PowerMode { /// Full performance with no power-saving measures. /// @@ -54,6 +54,7 @@ pub enum PowerMode { /// A balance between performance and power consumption. /// /// This is the default mode, suitable for most use cases. + #[default] Balanced, /// Prioritizes low power usage by reducing activity. /// @@ -65,12 +66,6 @@ pub enum PowerMode { Critical, } -impl Default for PowerMode { - fn default() -> Self { - Self::Balanced - } -} - /// Defines the network transport to be used by the node. /// /// Different transports are optimized for different environments. CoAP is ideal @@ -321,12 +316,14 @@ impl GossipConfig { /// ``` #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] +#[derive(Default)] pub enum StorageBackendType { /// Use SQLite for storage. /// /// SQLite is a lightweight, single-file database that's ideal for IoT devices /// and embedded systems. It has minimal dependencies and works well on /// resource-constrained platforms. + #[default] Sqlite, /// Use RocksDB for storage. /// @@ -341,13 +338,6 @@ pub enum StorageBackendType { Memory, } -impl Default for StorageBackendType { - fn default() -> Self { - // SQLite is the default for IoT compatibility. - Self::Sqlite - } -} - impl std::fmt::Display for StorageBackendType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -1027,8 +1017,11 @@ mod tests { #[test] fn test_config_validate_invalid_memory() { - let mut config = Config::default(); - config.memory_limit = 100; // Too small + // Too small + let config = Config { + memory_limit: 100, + ..Default::default() + }; assert!(config.validate().is_err()); } diff --git a/crates/aingle_minimal/src/error.rs b/crates/aingle_minimal/src/error.rs index 4a3249ed..c20f2355 100644 --- a/crates/aingle_minimal/src/error.rs +++ b/crates/aingle_minimal/src/error.rs @@ -740,7 +740,7 @@ mod tests { let _: &dyn StdError = &error; // Test source() for Io error - let io_err = std::io::Error::new(std::io::ErrorKind::Other, "test"); + let io_err = std::io::Error::other("test"); let error = Error::Io(io_err); assert!(StdError::source(&error).is_some()); } diff --git a/crates/aingle_minimal/src/network.rs b/crates/aingle_minimal/src/network.rs index a2d31677..15fd7e1d 100644 --- a/crates/aingle_minimal/src/network.rs +++ b/crates/aingle_minimal/src/network.rs @@ -12,6 +12,8 @@ //! - **QUIC**: Future support for reliable transport //! - **Mesh**: Future support for mesh networking +use std::cmp::Reverse; + use crate::config::{GossipConfig, TransportConfig}; use crate::discovery::Discovery; use crate::error::{Error, Result}; @@ -565,7 +567,7 @@ impl Network { /// Get peers for gossip (best quality first) pub fn gossip_peers(&self) -> Vec { let mut peers: Vec<_> = self.active_peers().into_iter().collect(); - peers.sort_by(|a, b| b.quality.cmp(&a.quality)); + peers.sort_by_key(|p| Reverse(p.quality)); peers .into_iter() .take(self.gossip_config.max_peers) @@ -905,8 +907,10 @@ mod tests { #[test] fn test_gossip_timing() { - let mut config = GossipConfig::default(); - config.loop_delay = Duration::from_millis(10); + let config = GossipConfig { + loop_delay: Duration::from_millis(10), + ..Default::default() + }; let mut gossip = GossipManager::new(config); assert!(!gossip.should_gossip()); // Just created @@ -1166,8 +1170,10 @@ mod tests { #[test] fn test_network_gossip_peers_sorting() { let config = TransportConfig::Memory; - let mut gossip = GossipConfig::default(); - gossip.max_peers = 10; + let gossip = GossipConfig { + max_peers: 10, + ..Default::default() + }; let mut network = Network::new(config, gossip, "test-node".to_string()); // Add multiple peers @@ -1189,8 +1195,10 @@ mod tests { #[test] fn test_network_gossip_peers_limited() { let config = TransportConfig::Memory; - let mut gossip = GossipConfig::default(); - gossip.max_peers = 2; + let gossip = GossipConfig { + max_peers: 2, + ..Default::default() + }; let mut network = Network::new(config, gossip, "test-node".to_string()); // Add more peers than limit @@ -1260,8 +1268,10 @@ mod tests { #[test] fn test_gossip_manager_complete_failure() { - let mut config = GossipConfig::default(); - config.loop_delay = Duration::from_millis(10); + let config = GossipConfig { + loop_delay: Duration::from_millis(10), + ..Default::default() + }; let mut manager = GossipManager::new(config); @@ -1589,9 +1599,11 @@ mod tests { #[test] fn test_gossip_manager_config() { - let mut config = GossipConfig::default(); - config.loop_delay = Duration::from_millis(100); - config.max_peers = 5; + let config = GossipConfig { + loop_delay: Duration::from_millis(100), + max_peers: 5, + ..Default::default() + }; let manager = GossipManager::new(config.clone()); diff --git a/crates/aingle_minimal/src/node.rs b/crates/aingle_minimal/src/node.rs index 6b9e4b18..3f89b76f 100644 --- a/crates/aingle_minimal/src/node.rs +++ b/crates/aingle_minimal/src/node.rs @@ -569,7 +569,7 @@ impl MinimalNode { while self.running.load(Ordering::SeqCst) { // Sync discovered peers periodically (every 100 iterations = ~1 second) discovery_sync_counter = discovery_sync_counter.wrapping_add(1); - if discovery_sync_counter % 100 == 0 { + if discovery_sync_counter.is_multiple_of(100) { self.network.sync_discovered_peers(); } @@ -1037,9 +1037,11 @@ mod tests { let addr: std::net::SocketAddr = "192.168.1.100:5683".parse().unwrap(); node.add_peer(addr); - // Verify peer was added via stats + // Verify peer was added via stats. `peer_count` is unsigned, so the + // old `>= 0` here held for every possible value including "nothing was + // added" — the one outcome this test exists to rule out. let stats = node.stats().unwrap(); - assert!(stats.peer_count >= 0); // At least no error + assert_eq!(stats.peer_count, 1); } #[test] @@ -1052,9 +1054,9 @@ mod tests { node.add_peer("192.168.1.101:5683".parse().unwrap()); node.add_peer("192.168.1.102:5683".parse().unwrap()); - // Stats should work + // Stats should work, and report all three. let stats = node.stats().unwrap(); - assert!(stats.peer_count >= 0); + assert_eq!(stats.peer_count, 3); } #[test] @@ -1126,9 +1128,11 @@ mod tests { // Sleep a bit to ensure uptime is measurable std::thread::sleep(std::time::Duration::from_millis(10)); - let stats = node.stats().unwrap(); - // Uptime should be at least 0 (could be 0 if very fast) - assert!(stats.uptime_secs >= 0); + // Uptime is whole seconds, so a node this young legitimately reports 0 + // and there is no lower bound to assert: `>= 0` on a u64 was always + // true. What this test does establish is that `stats()` succeeds on a + // freshly started node, which is the `unwrap` below. + let _stats = node.stats().unwrap(); } #[test] diff --git a/crates/aingle_minimal/src/ota.rs b/crates/aingle_minimal/src/ota.rs index 02f44a83..af76b91e 100644 --- a/crates/aingle_minimal/src/ota.rs +++ b/crates/aingle_minimal/src/ota.rs @@ -558,7 +558,7 @@ mod hex { } pub fn decode(s: &str) -> Result, String> { - if s.len() % 2 != 0 { + if !s.len().is_multiple_of(2) { return Err("Invalid hex string length".to_string()); } @@ -711,7 +711,7 @@ mod tests { }; let cloned = update.clone(); assert_eq!(cloned.version, "2.0.0"); - assert_eq!(cloned.critical, true); + assert!(cloned.critical); let debug = format!("{:?}", update); assert!(debug.contains("UpdateInfo")); diff --git a/crates/aingle_minimal/src/power.rs b/crates/aingle_minimal/src/power.rs index 8f0a231f..18280b38 100644 --- a/crates/aingle_minimal/src/power.rs +++ b/crates/aingle_minimal/src/power.rs @@ -590,9 +590,11 @@ mod tests { std::thread::sleep(Duration::from_millis(100)); let stats = pm.get_stats(); - // At least one metric should be non-zero - assert!(stats.uptime_secs >= 0); - assert!(stats.time_in_balanced >= 0); + // These counters are unsigned, so the old `>= 0` pair held always. Assert + // the invariant that can actually break instead: time spent in one + // profile cannot exceed the total uptime, and energy is never negative. + assert!(stats.time_in_balanced <= stats.uptime_secs); + assert!(stats.energy_consumed_mwh >= 0.0); } #[test] diff --git a/crates/aingle_minimal/src/types.rs b/crates/aingle_minimal/src/types.rs index 5de73a15..78750b06 100644 --- a/crates/aingle_minimal/src/types.rs +++ b/crates/aingle_minimal/src/types.rs @@ -732,7 +732,7 @@ mod hex { } pub fn decode(s: &str) -> Result, FromHexError> { - if s.len() % 2 != 0 { + if !s.len().is_multiple_of(2) { return Err(FromHexError::OddLength); } (0..s.len()) diff --git a/crates/aingle_minimal/tests/e2e_multi_node_tests.rs b/crates/aingle_minimal/tests/e2e_multi_node_tests.rs index ef5af902..2861ac23 100644 --- a/crates/aingle_minimal/tests/e2e_multi_node_tests.rs +++ b/crates/aingle_minimal/tests/e2e_multi_node_tests.rs @@ -662,15 +662,15 @@ fn test_ring_topology_simulation() { let mut nodes_with_peers: Vec<_> = nodes.into_iter().collect(); // Add peers in ring fashion - for i in 0..RING_SIZE { + for (i, node) in nodes_with_peers.iter_mut().enumerate() { let prev = if i == 0 { RING_SIZE - 1 } else { i - 1 }; let next = (i + 1) % RING_SIZE; let prev_addr: SocketAddr = format!("192.168.1.{}:5683", prev + 1).parse().unwrap(); let next_addr: SocketAddr = format!("192.168.1.{}:5683", next + 1).parse().unwrap(); - nodes_with_peers[i].add_peer(prev_addr); - nodes_with_peers[i].add_peer(next_addr); + node.add_peer(prev_addr); + node.add_peer(next_addr); } // Verify each node has exactly 2 peers diff --git a/crates/aingle_minimal/tests/iot_integration_tests.rs b/crates/aingle_minimal/tests/iot_integration_tests.rs index 9bd70f68..c72532f8 100644 --- a/crates/aingle_minimal/tests/iot_integration_tests.rs +++ b/crates/aingle_minimal/tests/iot_integration_tests.rs @@ -260,10 +260,11 @@ fn test_power_stats_tracking() { pm.set_power_profile(PowerProfile::LowPower); let stats = pm.get_stats(); - // Stats should be valid (non-negative) - assert!(stats.uptime_secs >= 0); - assert!(stats.time_in_high_performance >= 0); - assert!(stats.time_in_low_power >= 0); + // These counters are unsigned, so "non-negative" was true by construction + // and asserted nothing. The real invariant: time attributed to the profiles + // this test drove cannot exceed the total uptime. + assert!(stats.time_in_high_performance <= stats.uptime_secs); + assert!(stats.time_in_low_power <= stats.uptime_secs); } #[test] @@ -400,7 +401,9 @@ fn test_complete_iot_device_workflow() { // 8. Get statistics let power_stats = power_manager.get_stats(); - assert!(power_stats.uptime_secs >= 0); + // `uptime_secs` is unsigned; the old `>= 0` asserted nothing. Energy is the + // figure this end-to-end run can actually get wrong. + assert!(power_stats.energy_consumed_mwh >= 0.0); let ota_stats = ota_manager.stats(); assert_eq!(ota_stats.updates_applied, 0); diff --git a/crates/aingle_minimal/tests/transport_integration_tests.rs b/crates/aingle_minimal/tests/transport_integration_tests.rs index d406b5ae..5c2de25d 100644 --- a/crates/aingle_minimal/tests/transport_integration_tests.rs +++ b/crates/aingle_minimal/tests/transport_integration_tests.rs @@ -8,8 +8,6 @@ //! - BLE scanning, connection, and message exchange //! - Hardware wallet connection and signing workflow -use std::time::Duration; - // ============================================================================ // WebRTC Transport Tests // ============================================================================ diff --git a/crates/aingle_raft/Cargo.toml b/crates/aingle_raft/Cargo.toml index c197fb3b..4414e474 100644 --- a/crates/aingle_raft/Cargo.toml +++ b/crates/aingle_raft/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_raft" -version = "0.7.1" +version = "0.7.2" description = "Raft consensus for AIngle clustering" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "raft", "consensus", "clustering"] categories = ["database"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = [] dag = ["aingle_graph/dag"] @@ -28,7 +27,7 @@ tracing = "0.1" chrono = { version = "0.4", features = ["serde"] } futures-util = "0.3" anyerror = "0.1" -aingle_graph = { version = "0.8", path = "../aingle_graph", features = ["sled-backend"] } +aingle_graph = { version = "0.9", path = "../aingle_graph", features = ["sled-backend"] } ineru = { version = "0.7", path = "../ineru" } [dev-dependencies] diff --git a/crates/aingle_raft/src/log_store.rs b/crates/aingle_raft/src/log_store.rs index d3dd4d27..a19d495c 100644 --- a/crates/aingle_raft/src/log_store.rs +++ b/crates/aingle_raft/src/log_store.rs @@ -258,7 +258,7 @@ impl CortexLogStore { term: *term, data: data.clone(), }) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + .map_err(io::Error::other)?; } // Only update BTreeMap after all WAL writes succeed @@ -279,7 +279,7 @@ impl CortexLogStore { pub async fn last_log_id(&self) -> Option { let log = self.log.read().await; - log.values().last().map(|e| e.log_id.clone()) + log.values().last().map(|e| e.log_id) } } @@ -299,7 +299,7 @@ impl RaftLogReader for Arc { async fn read_vote(&mut self) -> Result, io::Error> { let v = self.vote.read().await; - Ok(v.clone()) + Ok(*v) } } @@ -315,14 +315,10 @@ impl RaftLogStorage for Arc { let log = self.log.read().await; let purged = self.purged_log_id.read().await; - let last_log_id = log - .values() - .last() - .map(|e| e.log_id.clone()) - .or_else(|| purged.clone()); + let last_log_id = log.values().last().map(|e| e.log_id).or_else(|| *purged); Ok(LogState { - last_purged_log_id: purged.clone(), + last_purged_log_id: *purged, last_log_id, }) } @@ -334,7 +330,7 @@ impl RaftLogStorage for Arc { async fn save_vote(&mut self, vote: &Vote) -> Result<(), io::Error> { CortexLogStore::persist_vote(&self.wal_dir, vote)?; let mut v = self.vote.write().await; - *v = Some(vote.clone()); + *v = Some(*vote); Ok(()) } @@ -347,7 +343,7 @@ impl RaftLogStorage for Arc { async fn read_committed(&mut self) -> Result, io::Error> { let c = self.committed.read().await; - Ok(c.clone()) + Ok(*c) } async fn append(&mut self, entries: I, callback: IOFlushed) -> Result<(), io::Error> @@ -384,7 +380,7 @@ impl RaftLogStorage for Arc { // Persist truncation boundary so recovery filters out stale entries let mut trunc = self.truncated_after.write().await; - *trunc = last_log_id.clone(); + *trunc = last_log_id; CortexLogStore::persist_truncated_after(&self.wal_dir, &last_log_id)?; Ok(()) @@ -400,7 +396,7 @@ impl RaftLogStorage for Arc { // Persist purge boundary let mut purged = self.purged_log_id.write().await; - *purged = Some(log_id.clone()); + *purged = Some(log_id); CortexLogStore::persist_purged(&self.wal_dir, &log_id)?; // Clean up old WAL segments that are entirely below the purge point diff --git a/crates/aingle_raft/src/network.rs b/crates/aingle_raft/src/network.rs index 0cce196b..453cd8e5 100644 --- a/crates/aingle_raft/src/network.rs +++ b/crates/aingle_raft/src/network.rs @@ -345,7 +345,7 @@ impl CortexNetworkConnection { .as_millis() ); - let num_chunks = (payload.len() + CHUNK_SIZE - 1) / CHUNK_SIZE; + let num_chunks = payload.len().div_ceil(CHUNK_SIZE); tracing::info!( target_node = self.target, total_bytes = total_size, diff --git a/crates/aingle_raft/src/snapshot_builder.rs b/crates/aingle_raft/src/snapshot_builder.rs index d3dff2e8..d15a3244 100644 --- a/crates/aingle_raft/src/snapshot_builder.rs +++ b/crates/aingle_raft/src/snapshot_builder.rs @@ -36,7 +36,7 @@ impl RaftSnapshotBuilder for CortexSnapshotBuilder { let triples = { let all = graph .find(aingle_graph::TriplePattern::any()) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| io::Error::other(e.to_string()))?; all.into_iter() .map(|t| TripleSnapshot { @@ -49,7 +49,7 @@ impl RaftSnapshotBuilder for CortexSnapshotBuilder { let ineru_ltm = memory .export_snapshot() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| io::Error::other(e.to_string()))?; // Drop locks before serialization to reduce hold time drop(graph); @@ -93,14 +93,12 @@ impl RaftSnapshotBuilder for CortexSnapshotBuilder { checksum: String::new(), }; - let data = snapshot - .to_bytes() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + let data = snapshot.to_bytes().map_err(io::Error::other)?; let snapshot_id = format!("snap-{}-{}", last_applied_term, last_applied_index); let meta = SnapshotMeta { - last_log_id: self.last_applied.clone(), + last_log_id: self.last_applied, last_membership: self.last_membership.clone(), snapshot_id, }; diff --git a/crates/aingle_raft/src/state_machine.rs b/crates/aingle_raft/src/state_machine.rs index 64ec27e4..c1f6cb8d 100644 --- a/crates/aingle_raft/src/state_machine.rs +++ b/crates/aingle_raft/src/state_machine.rs @@ -428,7 +428,7 @@ impl CortexStateMachine { /// Get the last applied log ID. pub async fn last_applied(&self) -> Option { let guard = self.last_applied.read().await; - guard.clone() + *guard } /// Get the count of applied mutations. @@ -447,7 +447,7 @@ impl RaftStateMachine for Arc { async fn applied_state(&mut self) -> Result<(Option, StoredMembershipOf), io::Error> { let la = self.last_applied.read().await; let membership = self.last_membership.read().await; - Ok((la.clone(), membership.clone())) + Ok((*la, membership.clone())) } async fn apply(&mut self, mut entries: Strm) -> Result<(), io::Error> @@ -460,7 +460,7 @@ impl RaftStateMachine for Arc { // Check for membership change if let Some(membership) = entry.get_membership() { let mut lm = self.last_membership.write().await; - *lm = StoredMembership::new(Some(entry.log_id.clone()), membership.clone()); + *lm = StoredMembership::new(Some(entry.log_id), membership.clone()); } // Apply the business logic @@ -482,7 +482,7 @@ impl RaftStateMachine for Arc { // marking entries as applied before they actually are (#1). { let mut la = self.last_applied.write().await; - *la = Some(entry.log_id.clone()); + *la = Some(entry.log_id); } // Send response to client if waiting (leader only) @@ -500,7 +500,7 @@ impl RaftStateMachine for Arc { CortexSnapshotBuilder { graph: Arc::clone(&self.graph), memory: Arc::clone(&self.memory), - last_applied: la.clone(), + last_applied: *la, last_membership: membership.clone(), proof_provider: self.proof_provider.clone(), } @@ -521,8 +521,7 @@ impl RaftStateMachine for Arc { // Build both new graph and new memory into temporaries FIRST, // then swap atomically only if both succeed (#7). - let new_graph = - GraphDB::memory().map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let new_graph = GraphDB::memory().map_err(|e| io::Error::other(e.to_string()))?; for ts in &cluster_snap.triples { let value = json_to_value(&ts.object); let triple = aingle_graph::Triple::new( @@ -532,7 +531,7 @@ impl RaftStateMachine for Arc { ); new_graph .insert(triple) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| io::Error::other(e.to_string()))?; } let new_memory = if !cluster_snap.ineru_ltm.is_empty() { @@ -597,7 +596,7 @@ impl RaftStateMachine for Arc { // Update metadata { let mut la = self.last_applied.write().await; - *la = meta.last_log_id.clone(); + *la = meta.last_log_id; } { let mut lm = self.last_membership.write().await; diff --git a/crates/aingle_viz/Cargo.toml b/crates/aingle_viz/Cargo.toml index 45e4f806..a3a6bbf3 100644 --- a/crates/aingle_viz/Cargo.toml +++ b/crates/aingle_viz/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_viz" -version = "0.7.1" +version = "0.7.2" description = "DAG Visualization for AIngle - Web-based graph explorer" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "visualization", "dag", "graph"] categories = ["visualization", "web-programming"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = [] @@ -30,7 +29,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # Graph data -aingle_graph = { version = "0.8", path = "../aingle_graph" } +aingle_graph = { version = "0.9", path = "../aingle_graph" } aingle_minimal = { version = "0.7", path = "../aingle_minimal", default-features = false, features = ["sqlite"] } # Utilities diff --git a/crates/aingle_viz/src/api.rs b/crates/aingle_viz/src/api.rs index 617dece9..99558bb9 100644 --- a/crates/aingle_viz/src/api.rs +++ b/crates/aingle_viz/src/api.rs @@ -844,8 +844,8 @@ mod tests { // Add test data for i in 0..5 { - let node = DagNodeBuilder::new(&format!("node{}", i), NodeType::Entry) - .label(&format!("Node {}", i)) + let node = DagNodeBuilder::new(format!("node{}", i), NodeType::Entry) + .label(format!("Node {}", i)) .build(); state.add_node(node).await.unwrap(); } diff --git a/crates/aingle_viz/src/dag.rs b/crates/aingle_viz/src/dag.rs index 9f4a1685..65dd7071 100644 --- a/crates/aingle_viz/src/dag.rs +++ b/crates/aingle_viz/src/dag.rs @@ -45,6 +45,8 @@ //! The [`DagView::to_d3_json`] method converts the DAG to a format compatible with //! D3.js force-directed graphs, making it easy to visualize in a web browser. +use std::cmp::Reverse; + use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -662,7 +664,7 @@ impl DagView { /// ``` pub fn recent_nodes(&self, limit: usize) -> Vec<&DagNode> { let mut nodes: Vec<_> = self.nodes.iter().collect(); - nodes.sort_by(|a, b| b.timestamp.cmp(&a.timestamp)); + nodes.sort_by_key(|n| Reverse(n.timestamp)); nodes.into_iter().take(limit).collect() } diff --git a/crates/aingle_viz/src/server.rs b/crates/aingle_viz/src/server.rs index 25e6418d..c0e4792a 100644 --- a/crates/aingle_viz/src/server.rs +++ b/crates/aingle_viz/src/server.rs @@ -729,8 +729,8 @@ mod tests { let mut dag = DagView::new(); for i in 0..5 { dag.add_node( - DagNodeBuilder::new(&format!("node{}", i), NodeType::Entry) - .label(&format!("Node {}", i)) + DagNodeBuilder::new(format!("node{}", i), NodeType::Entry) + .label(format!("Node {}", i)) .build(), ); } diff --git a/crates/aingle_wal/Cargo.toml b/crates/aingle_wal/Cargo.toml index 85b53903..862cb538 100644 --- a/crates/aingle_wal/Cargo.toml +++ b/crates/aingle_wal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_wal" -version = "0.7.1" +version = "0.7.2" description = "Write-Ahead Log for AIngle clustering and replication" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "wal", "replication", "clustering"] categories = ["database"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [dependencies] serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/aingle_wal/src/writer.rs b/crates/aingle_wal/src/writer.rs index 13b4cae1..eb35e9b1 100644 --- a/crates/aingle_wal/src/writer.rs +++ b/crates/aingle_wal/src/writer.rs @@ -73,12 +73,10 @@ impl WalWriter { let timestamp = Utc::now(); let prev_hash = { - let guard = self.last_hash.lock().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("WAL last_hash lock poisoned: {e}"), - ) - })?; + let guard = self + .last_hash + .lock() + .map_err(|e| io::Error::other(format!("WAL last_hash lock poisoned: {e}")))?; *guard }; @@ -93,12 +91,10 @@ impl WalWriter { }; { - let mut seg = self.current_segment.lock().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("WAL segment lock poisoned: {e}"), - ) - })?; + let mut seg = self + .current_segment + .lock() + .map_err(|e| io::Error::other(format!("WAL segment lock poisoned: {e}")))?; seg.append(&entry)?; seg.sync()?; @@ -112,12 +108,10 @@ impl WalWriter { // Update last_hash { - let mut guard = self.last_hash.lock().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("WAL last_hash lock poisoned: {e}"), - ) - })?; + let mut guard = self + .last_hash + .lock() + .map_err(|e| io::Error::other(format!("WAL last_hash lock poisoned: {e}")))?; *guard = entry.hash; } @@ -126,12 +120,10 @@ impl WalWriter { /// Flush the current segment to disk. pub fn sync(&self) -> io::Result<()> { - let mut seg = self.current_segment.lock().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("WAL segment lock poisoned: {e}"), - ) - })?; + let mut seg = self + .current_segment + .lock() + .map_err(|e| io::Error::other(format!("WAL segment lock poisoned: {e}")))?; seg.sync() } diff --git a/crates/aingle_zk/Cargo.toml b/crates/aingle_zk/Cargo.toml index cdccfa20..2fc97970 100644 --- a/crates/aingle_zk/Cargo.toml +++ b/crates/aingle_zk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aingle_zk" -version = "0.8.0" +version = "0.9.0" description = "Zero-Knowledge Proofs for AIngle - privacy-preserving cryptographic primitives" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "zkp", "zero-knowledge", "privacy", "cryptography"] categories = ["cryptography"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = ["std"] std = [] diff --git a/crates/aingle_zk/benches/full_benchmarks.rs b/crates/aingle_zk/benches/full_benchmarks.rs index d4b8c7a3..6a2fef2a 100644 --- a/crates/aingle_zk/benches/full_benchmarks.rs +++ b/crates/aingle_zk/benches/full_benchmarks.rs @@ -11,7 +11,7 @@ //! - Memory usage use aingle_zk::{ - aggregation::{aggregate_proofs, ProofAggregator}, + aggregation::aggregate_proofs, batch::{verify_schnorr_batch, BatchVerifier}, commitment::{HashCommitment, PedersenCommitment}, merkle::{MerkleTree, SparseMerkleTree}, @@ -442,12 +442,16 @@ fn benchmark_serialization(c: &mut Criterion) { /// Benchmark memory usage fn benchmark_memory(c: &mut Criterion) { - let mut group = c.benchmark_group("memory"); + // Not `mut`: this group records no measurements, it only prints sizes. + let group = c.benchmark_group("memory"); // Measure proof sizes let secret = Scalar::random(&mut OsRng); let public = RISTRETTO_BASEPOINT_POINT * secret; - let schnorr = SchnorrProof::prove_knowledge(&secret, b"test", &public); + // Argument order follows `prove_knowledge(secret, public_point, message)`. + // It was called with the last two swapped, so this benchmark had not + // compiled since that signature settled — invisible until `--all-targets`. + let schnorr = SchnorrProof::prove_knowledge(&secret, &public, b"test"); let schnorr_size = std::mem::size_of_val(&schnorr); println!("Schnorr proof in-memory size: {} bytes", schnorr_size); diff --git a/crates/aingle_zk/src/aggregation.rs b/crates/aingle_zk/src/aggregation.rs index 0976bb12..37a49a7b 100644 --- a/crates/aingle_zk/src/aggregation.rs +++ b/crates/aingle_zk/src/aggregation.rs @@ -423,7 +423,7 @@ mod tests { // Should have some size savings from aggregation let savings = agg.size_savings(); - assert!(savings >= 0.0 && savings <= 1.0); + assert!((0.0..=1.0).contains(&savings)); let ratio = agg.compression_ratio(); // Ratio could be < 1.0 if aggregated format has overhead @@ -506,8 +506,9 @@ mod tests { assert!(result.all_valid); assert_eq!(result.success_rate(), 1.0); assert!(result.avg_time_per_proof() >= 0.0); - // Verification time may be 0 if very fast (< 1ms) - assert!(result.verification_time_ms >= 0); + // No assertion on `verification_time_ms`: it is unsigned and legitimately + // 0 for a batch that verifies in under a millisecond, so every bound + // that could be written here is either always true or flaky. } #[test] diff --git a/crates/aingle_zk/src/merkle.rs b/crates/aingle_zk/src/merkle.rs index f69eb957..ad22e6f3 100644 --- a/crates/aingle_zk/src/merkle.rs +++ b/crates/aingle_zk/src/merkle.rs @@ -136,7 +136,7 @@ impl MerkleTree { while level_size > 1 { // Sibling index - let sibling_index = if current_index % 2 == 0 { + let sibling_index = if current_index.is_multiple_of(2) { current_index + 1 } else { current_index - 1 diff --git a/crates/aingle_zk/src/proof.rs b/crates/aingle_zk/src/proof.rs index 3d6d6d0d..0ec00bc1 100644 --- a/crates/aingle_zk/src/proof.rs +++ b/crates/aingle_zk/src/proof.rs @@ -939,7 +939,7 @@ mod tests { // Compute challenge c = H(R || P) let mut hasher = Sha256::new(); - hasher.update(&r_bytes); + hasher.update(r_bytes); hasher.update(public.compress().as_bytes()); let challenge: [u8; 32] = hasher.finalize().into(); let c = Scalar::from_bytes_mod_order(challenge); diff --git a/crates/ineru/Cargo.toml b/crates/ineru/Cargo.toml index 850a50c4..04bb57b4 100644 --- a/crates/ineru/Cargo.toml +++ b/crates/ineru/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ineru" -version = "0.7.5" +version = "0.7.6" description = "Ineru: Neural-inspired memory system for AIngle AI agents" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "ai", "memory", "neural", "agents"] categories = ["science", "data-structures"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = ["std"] std = [] diff --git a/crates/ineru/benches/memory_bench.rs b/crates/ineru/benches/memory_bench.rs index 25157415..7c2d1a75 100644 --- a/crates/ineru/benches/memory_bench.rs +++ b/crates/ineru/benches/memory_bench.rs @@ -187,7 +187,7 @@ fn bench_iot_mode(c: &mut Criterion) { let recent = memory.recall_recent(5); // 3. Periodic maintenance (every 100 readings) - if counter % 100 == 0 { + if counter.is_multiple_of(100) { let _ = memory.decay(); } diff --git a/crates/kaneru/Cargo.toml b/crates/kaneru/Cargo.toml index 2bfb982f..6107f079 100644 --- a/crates/kaneru/Cargo.toml +++ b/crates/kaneru/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "kaneru" -version = "0.7.1" +version = "0.7.2" description = "Kaneru: Unified Multi-Agent Execution System for AIngle AI agents" license = "Apache-2.0 OR LicenseRef-Commercial" repository = "https://github.com/ApiliumCode/aingle" @@ -10,8 +10,7 @@ authors = ["Apilium Technologies "] keywords = ["aingle", "ai", "agents", "rl", "iot"] categories = ["science", "network-programming"] edition = "2021" -rust-version = "1.83" - +rust-version.workspace = true [features] default = ["std"] std = [] diff --git a/crates/kaneru/benches/agent_bench.rs b/crates/kaneru/benches/agent_bench.rs index 197a34f5..1234c2d8 100644 --- a/crates/kaneru/benches/agent_bench.rs +++ b/crates/kaneru/benches/agent_bench.rs @@ -5,7 +5,7 @@ //! //! Run with: cargo bench -p kaneru -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; use kaneru::{Action, Agent, AgentConfig, Condition, Goal, Observation, Rule, SimpleAgent}; /// Benchmark agent creation diff --git a/crates/kaneru/examples/kaneru_orchestrator.rs b/crates/kaneru/examples/kaneru_orchestrator.rs index 3acaacb0..3b5f2d66 100644 --- a/crates/kaneru/examples/kaneru_orchestrator.rs +++ b/crates/kaneru/examples/kaneru_orchestrator.rs @@ -12,9 +12,8 @@ //! - Serialize/deserialize agent state use kaneru::{ - Action, ActionResult, ActionType, Goal, GoalSelectionStrategy, KaneruAgent, KaneruConfig, - LearningAlgorithm, LearningConfig, Observation, OperationMode, Outcome, PredictiveConfig, - Priority, + ActionResult, ActionType, Goal, GoalSelectionStrategy, KaneruAgent, KaneruConfig, + LearningAlgorithm, LearningConfig, Observation, OperationMode, Outcome, Priority, }; fn main() { diff --git a/crates/kaneru/src/agent.rs b/crates/kaneru/src/agent.rs index c935fff2..b1624975 100644 --- a/crates/kaneru/src/agent.rs +++ b/crates/kaneru/src/agent.rs @@ -872,10 +872,12 @@ impl Agent for SimpleAgent { self.stats.actions_executed += 1; // Store state-action pair for learning - if self.config.learning_enabled && self.last_observation.is_some() { - let state_id = StateId::from_observation(self.last_observation.as_ref().unwrap()); - let action_id = ActionId::from_action(&action); - self.last_state_action = Some((state_id, action_id)); + if self.config.learning_enabled { + if let Some(observation) = self.last_observation.as_ref() { + let state_id = StateId::from_observation(observation); + let action_id = ActionId::from_action(&action); + self.last_state_action = Some((state_id, action_id)); + } } // Simple execution - just log and return success diff --git a/crates/kaneru/src/coordination.rs b/crates/kaneru/src/coordination.rs index 2ec8f594..b2850418 100644 --- a/crates/kaneru/src/coordination.rs +++ b/crates/kaneru/src/coordination.rs @@ -36,6 +36,7 @@ use crate::{Action, AgentId, KaneruAgent, Observation, Outcome}; use serde::{Deserialize, Serialize}; +use std::cmp::Reverse; use std::collections::{HashMap, VecDeque}; /// A unique identifier for a `Message`. @@ -301,7 +302,7 @@ impl MessageBus { } self.queue = remaining; - messages.sort_by(|a, b| b.priority.cmp(&a.priority)); + messages.sort_by_key(|m| Reverse(m.priority)); messages } diff --git a/crates/kaneru/src/goal.rs b/crates/kaneru/src/goal.rs index 1e56ffa1..d2c0b0cf 100644 --- a/crates/kaneru/src/goal.rs +++ b/crates/kaneru/src/goal.rs @@ -375,7 +375,7 @@ mod tests { #[test] fn test_goal_status_clone() { let status = GoalStatus::Active; - let cloned = status.clone(); + let cloned = status; assert_eq!(status, cloned); } diff --git a/crates/kaneru/src/hierarchical/decomposition.rs b/crates/kaneru/src/hierarchical/decomposition.rs index e7ad4998..773b159f 100644 --- a/crates/kaneru/src/hierarchical/decomposition.rs +++ b/crates/kaneru/src/hierarchical/decomposition.rs @@ -330,7 +330,6 @@ impl DecompositionStrategy for ConditionalStrategy { #[cfg(test)] mod tests { use super::*; - use crate::types::ValueRange; #[test] fn test_default_rules_exist() { diff --git a/crates/kaneru/src/hierarchical/goal_solver.rs b/crates/kaneru/src/hierarchical/goal_solver.rs index 4296f40e..6abcb834 100644 --- a/crates/kaneru/src/hierarchical/goal_solver.rs +++ b/crates/kaneru/src/hierarchical/goal_solver.rs @@ -564,7 +564,6 @@ fn is_mutually_exclusive(g1: &GoalType, g2: &GoalType) -> bool { #[cfg(test)] mod tests { use super::*; - use crate::types::Timestamp; #[test] fn test_goal_solver_creation() { diff --git a/crates/kaneru/src/hierarchical/tests.rs b/crates/kaneru/src/hierarchical/tests.rs index fe596f56..229b755f 100644 --- a/crates/kaneru/src/hierarchical/tests.rs +++ b/crates/kaneru/src/hierarchical/tests.rs @@ -96,7 +96,7 @@ fn test_executable_goals() { let executable = solver.get_executable_goals(); // Only the simple task and subtask should be executable - assert!(executable.len() >= 1); + assert!(!executable.is_empty()); assert!(executable.iter().any(|g| g.id == id1)); } diff --git a/crates/kaneru/src/kaneru_agent.rs b/crates/kaneru/src/kaneru_agent.rs index c948d3fb..d212174d 100644 --- a/crates/kaneru/src/kaneru_agent.rs +++ b/crates/kaneru/src/kaneru_agent.rs @@ -383,7 +383,7 @@ impl KaneruAgent { self.learning.add_experience(exp); // 5. Perform experience replay - if self.stats.total_steps % 10 == 0 { + if self.stats.total_steps.is_multiple_of(10) { self.learning.replay_batch(32, &self.available_actions); } diff --git a/crates/kaneru/src/learning/engine.rs b/crates/kaneru/src/learning/engine.rs index a78328f1..bb813b5e 100644 --- a/crates/kaneru/src/learning/engine.rs +++ b/crates/kaneru/src/learning/engine.rs @@ -670,10 +670,12 @@ mod tests { #[test] fn test_epsilon_decay() { - let mut config = LearningConfig::default(); - config.epsilon = 1.0; - config.epsilon_decay = 0.9; - config.epsilon_min = 0.1; + let config = LearningConfig { + epsilon: 1.0, + epsilon_decay: 0.9, + epsilon_min: 0.1, + ..Default::default() + }; let mut engine = LearningEngine::new(config); diff --git a/crates/kaneru/src/predictive/anomaly.rs b/crates/kaneru/src/predictive/anomaly.rs index 57b7fff6..33768bb5 100644 --- a/crates/kaneru/src/predictive/anomaly.rs +++ b/crates/kaneru/src/predictive/anomaly.rs @@ -279,7 +279,7 @@ mod tests { detector.update(&Observation::sensor("flag", true)); detector.update(&Observation::sensor("flag", false)); - assert!(detector.history.len() > 0); + assert!(!detector.history.is_empty()); } #[test] diff --git a/crates/kaneru/src/types.rs b/crates/kaneru/src/types.rs index 34feffa2..9b84a614 100644 --- a/crates/kaneru/src/types.rs +++ b/crates/kaneru/src/types.rs @@ -293,8 +293,8 @@ mod tests { #[test] fn test_value_conversions() { - let v = Value::Float(3.14); - assert!((v.as_f64().unwrap() - 3.14).abs() < 0.001); + let v = Value::Float(2.5); + assert!((v.as_f64().unwrap() - 2.5).abs() < 0.001); let v = Value::Int(42); assert_eq!(v.as_i64().unwrap(), 42); @@ -372,7 +372,7 @@ mod tests { assert_eq!(Value::Bool(true).as_string(), "true"); assert_eq!(Value::Bool(false).as_string(), "false"); assert_eq!(Value::Int(42).as_string(), "42"); - assert_eq!(Value::Float(3.14).as_string(), "3.14"); + assert_eq!(Value::Float(2.5).as_string(), "2.5"); assert_eq!(Value::String("test".to_string()).as_string(), "test"); } @@ -393,9 +393,9 @@ mod tests { #[test] fn test_value_from_f64() { - let v: Value = 3.14f64.into(); + let v: Value = 2.5f64.into(); if let Value::Float(f) = v { - assert!((f - 3.14).abs() < 0.001); + assert!((f - 2.5).abs() < 0.001); } else { panic!("Expected Float"); } diff --git a/examples/iot_sensor_network/src/main.rs b/examples/iot_sensor_network/src/main.rs index ea457712..499cdf42 100644 --- a/examples/iot_sensor_network/src/main.rs +++ b/examples/iot_sensor_network/src/main.rs @@ -166,7 +166,7 @@ fn main() -> Result<(), Box> { let reading: SensorReading = serde_json::from_slice(&entry.content)?; println!( " {} - {} {:.2} {} at {}", - hash.to_string()[..8].to_string(), + &hash.to_string()[..8], reading.sensor_id, reading.value, reading.unit, diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..01d3e090 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,10 @@ +# Local parity with CI, which pins `RUST_VERSION` in .github/workflows/ci.yml — +# see the note there for why the pin lives in the workflow rather than only here. +# +# CI overrides this file (the toolchain action exports RUSTUP_TOOLCHAIN), so the +# two must be bumped together or a developer will verify against a version the +# runner does not use. That is how a lint gate goes red on a machine that +# reported it green. +[toolchain] +channel = "1.97.0" +components = ["clippy", "rustfmt"]