From 11eafd9593f7e6c149733f72cb10fe11f5a61f8d Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Thu, 30 Jul 2026 11:53:10 +0200 Subject: [PATCH] =?UTF-8?q?release:=20aingle=200.9.0=20=E2=86=92=20main=20?= =?UTF-8?q?(PRs=20#144=E2=80=93#149)=20(#150)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: assert something in the tests that asserted nothing Nine assertions compared an unsigned counter against zero — `peer_count >= 0`, `uptime_secs >= 0`, `verification_time_ms >= 0`. Every one of them held for every possible value, including the failure each test exists to catch: `test_add_peer` passed whether or not the peer was added. Replace them with the claim the test is named for where there is one (peer counts), an invariant that can actually break where there is not (time in a power profile cannot exceed total uptime; energy is never negative), and nothing at all where the author's own comment says the value is legitimately zero — with a line explaining why no bound is asserted, so it does not get "fixed" back. Also: `3.14` as an arbitrary test float in eight places, which clippy reads as an approximation of PI. These tests are about storing a decimal, not about geometry, so use a number that does not claim to be a constant. And `aingle_zk`'s memory benchmark had not compiled since `prove_knowledge` settled on `(secret, public_point, message)` — it was still passing the last two swapped. Invisible to `cargo test`, which does not build benches. * style: apply cargo clippy --fix across the workspace Machine-applicable suggestions only, applied by the tool and left otherwise untouched: needless borrows, redundant closures, `is_empty` over length comparisons, `io::Error::other`, derivable impls, unused imports. Mechanical and behaviour-preserving by construction, but it is 40 files of churn in the engine, so it is a separate commit: drop it without losing the fixes in the commit before, which are not mechanical. Twenty-four lints remain that clippy declines to auto-fix because they need judgement — clamp (NaN handling differs), `sort_by_key` where the key borrows, `Default::default()` field reassignment, and a public `from_str` that shadows the trait method and cannot be renamed without breaking callers. Left for a later pass rather than rushed. * style: clear the clippy lints that do not change behaviour Twenty-one of the twenty-four remaining lints, split by the kind of decision each one represents rather than by the lint that reports it. Notation (15). Ten `Config::default()` values built by assigning fields afterwards become struct literals with `..Default::default()`. One loop that existed only to index an array becomes an iterator. One `unwrap` guarded by a separate `is_some` becomes `if let`. One doc list item that rustdoc read as a continuation gets its blank line. And one more assertion that asserted nothing: `!contexts.is_empty() || matches.len() >= 0` held for every usize, so the test passed even when the query returned nothing — the failure it exists to catch. Descending sorts (6). `sort_by(|a, b| b.k.cmp(&a.k))` becomes `sort_by_key(|x| Reverse(x.k))`, which is stable in the same way and removes the class of bug where `a` and `b` are quietly transposed. All six keys turned out to be `Copy`, so none needed the borrow-shaped exception this pass was ready to grant. Formatting is applied to the files this branch touches and to no others, so the diff stays readable. Three lints are deliberately NOT here, because they are decisions and not cleanup, and burying them in a commit called "style" is how a semantic change ships unreviewed. Both `clamp` sites change NaN behaviour, and the `from_str` one shapes a public API. Each has its own issue. * ci: enforce the lint bar the project already declares CLAUDE.md defines `make lint` as `cargo clippy --workspace --all-targets -- -D warnings`. CI ran `cargo clippy -p aingle_minimal --features rest -- -W clippy::all`: one member of seventeen, the library target only, and warnings that failed nothing. It reported green throughout. Behind it: a benchmark that had not compiled in months, assertions that compared unsigned counters against zero and so passed no matter what the code did, and a `flatten()` over `io::Lines` that spins forever on a repeated read error. `--all-targets` is what covers benches and integration tests, and is the only reason the dead benchmark surfaced. `Format Check` was separately red on main, so the aggregate `CI Success` gate has been failing for some time; `cargo fmt --all` here makes it pass and the widened clippy keeps it honest. Note the lints were an onion: with `-D warnings` a failing crate stops everything downstream from being linted at all, so the count grew from 24 to 54 as each layer was cleared. It took four passes to reach a clean run — worth knowing before trusting a single measurement. The three lints that are decisions rather than cleanup carry `#[allow(...)]` with the full reasoning at the site: two `clamp` substitutions that change NaN behaviour, and a public `from_str` that shadows the trait method. Removing each attribute is the definition of done for that decision. * ci: run the checks on every pull request, not just those onto main and dev `pull_request: branches: [main, dev]` meant a PR onto any other branch ran no checks at all. A stack of dependent PRs therefore had no CI until each was retargeted — which is after the review, not before it. Found by building such a stack: three of its five PRs showed no checks whatsoever, and nothing about the interface said why. * fix(ingest): rewrite a fallthrough with `?`, as clippy 1.97 asks `clippy::question_mark` is new in 1.97. Nobody touched this code; the toolchain moved under it. Which is the whole argument for pinning: this repository installs `dtolnay/rust-toolchain@stable`, so the gate added in this PR can go red from a compiler release with no commit behind it. Verified by installing 1.97.0 locally — clippy, rustfmt and the tests are all clean under the exact toolchain the runner used, which 1.96 could not have told me. * fix(ai): stop a single NaN from silently disabling two subsystems Both sites carried a clamp-shaped expression that clippy wanted rewritten. Following that advice mechanically would have introduced a bug in one and papered over a worse one in the other, so each is settled here instead. SurpriseGate. `min`/`max` discard NaN and return the other operand; `clamp` propagates it. But the real entry point is upstream: `record_surprise` feeds Welford's algorithm, and one non-finite sample makes the running mean NaN permanently — every later update keeps it, the adaptive threshold inherits it, and because every comparison against NaN is false the gate stops firing with nothing to show for it. Reject non-finite samples at the boundary, and fall back to the CONFIGURED threshold — the non-adaptive baseline this type already has — rather than to an invented constant, if unusable statistics arrive some other way (deserialised state is not covered by the entry guard). TransactionClassifier::confidence. `partial_cmp().unwrap()` panics on a NaN distance, which `l2_distance` produces from a corrupt centroid, so the process died inside a confidence calculation. `total_cmp` is total and also sorts NaN last, so a corrupt centroid can no longer pass itself off as the nearest one. And with every distance unusable the old code fell past the `second_dist > 0.0` test to return 1.0 — FULL confidence at the exact moment there is no information. It now answers 0.5, the same neutral value it already returns when there are no centroids at all. Five tests, each pinning a failure that was silent: a poisoned mean, a NaN threshold, the panic, and full confidence from no information. * refactor(dag): ExportFormat implements FromStr instead of shadowing it `ExportFormat::from_str` was an inherent method returning `Option`, which shadows `std::str::FromStr::from_str` and returns 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. Implement the trait with an error that carries the offending input and knows the accepted spellings. The list of valid formats lived in the REST handler, where it could drift away from the parser with nothing to catch it — it already advertised "dot, mermaid, json" while the parser also accepted "graphviz" and "md". Now the type owns it and a test asserts the message names every spelling the parser takes. One caller migrated. `cargo clippy --workspace --all-targets -- -D warnings` and `cargo fmt --all --check` are both clean with no `#[allow]` left anywhere from this series. * chore: pin the toolchain and declare an MSRV that is true Two blockers for tagging a release, both of the same shape as the lint gate: a number nothing verified. PIN. The workflows installed `dtolnay/rust-toolchain@stable`, so on 2026-07-29 the runner moved to clippy 1.97, `question_mark` arrived, and CI went red with no commit behind it. Sixteen installations across four workflows now take `RUST_VERSION`, declared once. Not a `rust-toolchain.toml` alone: that action exports RUSTUP_TOOLCHAIN, which takes precedence over the file, so the pin would be ignored exactly where it is needed. The file is here too, for local parity, and the note says they must move together. MSRV. Twelve crates said 1.83, the CI environment said 1.85, CLAUDE.md said 1.83, and the truth was none of them. Bisected: 1.85 fails (`cranelift` needs 1.90), 1.90 fails (`wasmer` needs 1.91), 1.91 builds. Nothing had ever noticed because the MSRV job ran `cargo check -p aingle_minimal` — the single member with no wasmer in its tree. It now checks the whole workspace, and the figure lives once in `[workspace.package]` with thirteen members inheriting it. Uniform rather than per-crate deliberately: a lower number on a crate never built at that version is the same unverified promise being removed here. Raising the declared MSRV also lets clippy suggest APIs that were too new before, which surfaced eight `manual_is_multiple_of` sites. Applied. Verified under both toolchains that now matter: clippy and rustfmt clean and 2248 tests green on the pinned 1.97.0, and `cargo check --workspace --all-targets` clean on the declared 1.91.0. * chore(release): bump workspace versions for aingle 0.9.0 --- .github/workflows/ci.yml | 86 ++++++++++++++++--- .github/workflows/docs.yml | 6 +- .github/workflows/release.yml | 9 +- .github/workflows/security.yml | 14 ++- Cargo.lock | 26 +++--- Cargo.toml | 14 +++ crates/aingle/Cargo.toml | 2 +- crates/aingle_ai/Cargo.toml | 5 +- crates/aingle_ai/src/ineru/config.rs | 12 ++- crates/aingle_ai/src/ineru/surprise_gate.rs | 76 +++++++++++++++- .../aingle_ai/src/kaneru/continuum_memory.rs | 5 +- crates/aingle_ai/src/kaneru/mod.rs | 6 +- crates/aingle_ai/src/kaneru/self_modifier.rs | 6 +- crates/aingle_ai/src/nested_learning/mod.rs | 21 +++-- .../src/nested_learning/transaction_level.rs | 51 ++++++++++- crates/aingle_ai/src/types.rs | 6 +- crates/aingle_contracts/Cargo.toml | 5 +- crates/aingle_contracts/src/contract.rs | 9 +- crates/aingle_contracts/src/types.rs | 2 +- crates/aingle_cortex/Cargo.toml | 13 ++- crates/aingle_cortex/src/cluster_init.rs | 14 ++- crates/aingle_cortex/src/main.rs | 6 +- crates/aingle_cortex/src/p2p/config.rs | 22 ++--- crates/aingle_cortex/src/p2p/dag_sync.rs | 2 +- crates/aingle_cortex/src/p2p/manager.rs | 10 ++- crates/aingle_cortex/src/proofs/store.rs | 2 +- crates/aingle_cortex/src/rest/audit.rs | 14 +-- crates/aingle_cortex/src/rest/dag.rs | 10 +-- crates/aingle_cortex/src/rest/proof_api.rs | 6 +- crates/aingle_cortex/src/rest/triples.rs | 4 +- crates/aingle_cortex/src/service/ingest.rs | 41 +++++++-- crates/aingle_cortex/src/service/tasks.rs | 39 +++++++-- crates/aingle_cortex/src/service/validate.rs | 2 +- .../tests/data_integrity_test.rs | 22 ++--- .../aingle_cortex/tests/rate_limiting_test.rs | 2 +- crates/aingle_graph/Cargo.toml | 5 +- crates/aingle_graph/src/dag/export.rs | 68 ++++++++++++--- crates/aingle_graph/src/dag/mod.rs | 6 +- crates/aingle_graph/src/dag/store.rs | 23 ++--- crates/aingle_graph/src/dag/timetravel.rs | 1 + crates/aingle_graph/src/lib.rs | 12 ++- crates/aingle_graph/src/value.rs | 4 +- .../tests/graph_integration_tests.rs | 6 +- crates/aingle_ingest/Cargo.toml | 7 +- crates/aingle_ingest/src/chunk.rs | 7 +- crates/aingle_ingest/src/lib.rs | 36 ++++++-- crates/aingle_ingest/src/tasks.rs | 27 ++++-- crates/aingle_logic/Cargo.toml | 7 +- crates/aingle_logic/src/engine.rs | 2 +- crates/aingle_logic/src/rule.rs | 4 +- crates/aingle_minimal/Cargo.toml | 5 +- crates/aingle_minimal/src/config.rs | 25 ++---- crates/aingle_minimal/src/error.rs | 2 +- crates/aingle_minimal/src/network.rs | 36 +++++--- crates/aingle_minimal/src/node.rs | 20 +++-- crates/aingle_minimal/src/ota.rs | 4 +- crates/aingle_minimal/src/power.rs | 8 +- crates/aingle_minimal/src/types.rs | 2 +- .../tests/e2e_multi_node_tests.rs | 6 +- .../tests/iot_integration_tests.rs | 13 +-- .../tests/transport_integration_tests.rs | 2 - crates/aingle_raft/Cargo.toml | 7 +- crates/aingle_raft/src/log_store.rs | 22 ++--- crates/aingle_raft/src/network.rs | 2 +- crates/aingle_raft/src/snapshot_builder.rs | 10 +-- crates/aingle_raft/src/state_machine.rs | 17 ++-- crates/aingle_viz/Cargo.toml | 7 +- crates/aingle_viz/src/api.rs | 4 +- crates/aingle_viz/src/dag.rs | 4 +- crates/aingle_viz/src/server.rs | 4 +- crates/aingle_wal/Cargo.toml | 5 +- crates/aingle_wal/src/writer.rs | 40 ++++----- crates/aingle_zk/Cargo.toml | 5 +- crates/aingle_zk/benches/full_benchmarks.rs | 10 ++- crates/aingle_zk/src/aggregation.rs | 7 +- crates/aingle_zk/src/merkle.rs | 2 +- crates/aingle_zk/src/proof.rs | 2 +- crates/ineru/Cargo.toml | 5 +- crates/ineru/benches/memory_bench.rs | 2 +- crates/kaneru/Cargo.toml | 5 +- crates/kaneru/benches/agent_bench.rs | 2 +- crates/kaneru/examples/kaneru_orchestrator.rs | 5 +- crates/kaneru/src/agent.rs | 10 ++- crates/kaneru/src/coordination.rs | 3 +- crates/kaneru/src/goal.rs | 2 +- .../kaneru/src/hierarchical/decomposition.rs | 1 - crates/kaneru/src/hierarchical/goal_solver.rs | 1 - crates/kaneru/src/hierarchical/tests.rs | 2 +- crates/kaneru/src/kaneru_agent.rs | 2 +- crates/kaneru/src/learning/engine.rs | 10 ++- crates/kaneru/src/predictive/anomaly.rs | 2 +- crates/kaneru/src/types.rs | 10 +-- examples/iot_sensor_network/src/main.rs | 2 +- rust-toolchain.toml | 10 +++ 94 files changed, 736 insertions(+), 394 deletions(-) create mode 100644 rust-toolchain.toml 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"]