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/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/nested_learning/mod.rs b/crates/aingle_ai/src/nested_learning/mod.rs index 7021b764..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" diff --git a/crates/aingle_ai/src/nested_learning/transaction_level.rs b/crates/aingle_ai/src/nested_learning/transaction_level.rs index 59d14ed0..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 } @@ -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_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_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/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 56cb94ee..4868c854 100644 --- a/crates/aingle_cortex/src/p2p/config.rs +++ b/crates/aingle_cortex/src/p2p/config.rs @@ -139,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; @@ -175,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/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/rest/audit.rs b/crates/aingle_cortex/src/rest/audit.rs index 3f6a8e66..3b25af9e 100644 --- a/crates/aingle_cortex/src/rest/audit.rs +++ b/crates/aingle_cortex/src/rest/audit.rs @@ -63,7 +63,7 @@ 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().flatten() { + for line in reader.lines().map_while(Result::ok) { if let Ok(entry) = serde_json::from_str::(&line) { entries.push(entry); } @@ -188,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 { @@ -298,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 daf030df..e8ba89a0 100644 --- a/crates/aingle_cortex/src/rest/proof_api.rs +++ b/crates/aingle_cortex/src/rest/proof_api.rs @@ -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/service/ingest.rs b/crates/aingle_cortex/src/service/ingest.rs index 123ab517..7dda89c6 100644 --- a/crates/aingle_cortex/src/service/ingest.rs +++ b/crates/aingle_cortex/src/service/ingest.rs @@ -107,34 +107,6 @@ const INGEST_FILENAMES: &[&str] = &[ "vagrantfile", ]; -/// Whether an absent path is absent because a cloud client evicted the file's -/// contents to reclaim disk — NOT because anyone deleted it. -/// -/// Vaults live in synced folders, and the common clients differ in a way that -/// matters here. OneDrive and Google Drive keep the name and hydrate on read, so -/// the file still passes `is_file()` and nothing downstream notices. -/// **macOS/iCloud Drive does not keep the name**: `Notes.md` is replaced by a -/// sibling stub `.Notes.md.icloud`, so the entry vanishes from the directory and -/// `is_file()` is false — identical, to that test alone, to a deletion. -/// -/// Retracting on that signature signs a removal into the DAG for something the -/// user never removed, and signs the re-insert when they next open the note: -/// history that did not happen, in the one structure whose whole value is that -/// it did. Until the file returns, grounded retrieval also answers as though the -/// note does not exist. -/// -/// The opposite error is far cheaper — a stale entry that survives until a real -/// deletion event arrives — so this deliberately errs toward keeping. -/// -/// Not gated on `target_os`: a vault authored on macOS carries these stubs -/// wherever it is synced, and elsewhere the name simply does not occur. -fn is_cloud_evicted(abs: &std::path::Path) -> bool { - let (Some(dir), Some(name)) = (abs.parent(), abs.file_name().and_then(|n| n.to_str())) else { - return false; - }; - dir.join(format!(".{name}.icloud")).is_file() -} - /// Returns whether `path` should be ingested as a text file: a broad extension /// allowlist plus a few extensionless names, minus a denylist of generated, /// minified, or lock files that carry no semantic signal even when they slip @@ -1035,17 +1007,6 @@ pub async fn ingest_paths( || !is_ingestable_file(&abs) || ignores.is_ignored(&abs); - // Absent because a cloud client reclaimed the disk, not because anyone - // deleted it. Leave the index exactly as it is: see [`is_cloud_evicted`] - // for why a retraction here is worse than a stale entry. - if !excluded && !abs.is_file() && is_cloud_evicted(&abs) { - tracing::debug!( - path = %rel, - "skipping retraction: the file is evicted by a cloud client, not deleted" - ); - continue; - } - // Gone (deleted, moved to `.trash`, renamed away) or no longer eligible: // retract whatever this source previously contributed. A no-op for a path // that was never ingested, so a spurious event costs one registry lookup. @@ -1397,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). { @@ -1428,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. @@ -1464,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( @@ -2229,69 +2213,6 @@ mod tests { ); } - #[tokio::test] - async fn ingest_paths_does_not_retract_a_file_the_cloud_client_evicted() { - // A vault kept in a cloud-synced folder is the common case, and macOS - // reclaims disk by EVICTING file contents: `gone.md` is replaced by a - // sibling stub `.gone.md.icloud`, so the name leaves the directory and - // `is_file()` is false. Indistinguishable from a deletion by that test - // alone — and treating it as one signs a retraction into the DAG for - // something the user never deleted, then signs the re-insert when they - // next open the note. False history, plus a window in which grounded - // retrieval silently answers without the note. - // - // Being wrong the other way is cheap by comparison: a stale entry until - // a real deletion event arrives. - let dir = tempfile::tempdir().unwrap(); - let root = dir.path().to_str().unwrap(); - write( - dir.path(), - "gone.md", - "# Gone\n\nWe use [[sled]].\n\n- [ ] Doomed task\n", - ); - let state = enabled_state().await; - ingest_path(&state, root, None).await.unwrap(); - - // Exactly what an eviction leaves behind: no `gone.md`, a stub beside it. - std::fs::remove_file(dir.path().join("gone.md")).unwrap(); - write(dir.path(), ".gone.md.icloud", ""); - - let report = ingest_paths(&state, root, &["gone.md".to_string()], None) - .await - .unwrap(); - - assert_eq!( - report.files_removed, 0, - "an evicted file is not a deletion and must not be retracted" - ); - - // Everything the note contributed is still there: triples, its task, its - // registry row, and its chunks in grounded retrieval. - { - let graph = state.graph.read().await; - let left = graph - .find(TriplePattern::any().with_subject(NodeId::named("gone.md"))) - .unwrap(); - assert!( - !left.is_empty(), - "an evicted note lost its triples — that retraction never happened" - ); - } - let tasks = crate::service::tasks::list_tasks(&state, None).await; - assert!(!tasks.is_empty(), "an evicted note's tasks were tombstoned"); - let sources = list_sources(&state).await.unwrap(); - assert!(sources.iter().any(|s| s.path == "gone.md")); - let g = crate::service::ground::ground(&state, "We use sled", 5) - .await - .unwrap(); - assert!( - g.answer_context.iter().any(|c| c.source == "gone.md"), - "an evicted note dropped out of grounded retrieval, so the AI would \ - answer as if it did not exist; got: {:?}", - g.answer_context - ); - } - #[tokio::test] async fn ingest_paths_retracting_an_unknown_path_is_a_noop() { let dir = tempfile::tempdir().unwrap(); 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 0a3c6b67..e2253f0d 100644 --- a/crates/aingle_cortex/tests/data_integrity_test.rs +++ b/crates/aingle_cortex/tests/data_integrity_test.rs @@ -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 { 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 970d8ee9..5a03056e 100644 --- a/crates/aingle_graph/src/dag/mod.rs +++ b/crates/aingle_graph/src/dag/mod.rs @@ -33,7 +33,7 @@ pub use action::{ pub use backend::SledDagBackend; pub use backend::{DagBackend, MemoryDagBackend}; pub use canonical::{CanonicalAction, CANONICAL_SPEC, HASH_ALG, SIGNATURE_ALG, SIGNED_MESSAGE}; -pub use export::{DagGraph, ExportFormat}; +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_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/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/node.rs b/crates/aingle_minimal/src/node.rs index 3bef02a1..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(); } diff --git a/crates/aingle_minimal/src/ota.rs b/crates/aingle_minimal/src/ota.rs index 01ac4c7b..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()); } 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_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_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/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_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_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/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/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/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/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"]