From 00854436ba99e439b3642718a51681d4e9d628fc Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Wed, 29 Jul 2026 10:53:08 +0200 Subject: [PATCH 1/3] ci: enforce the lint bar the project already declares MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/ci.yml | 20 ++++++++- crates/aingle_ai/src/ineru/surprise_gate.rs | 15 +++++++ .../src/nested_learning/transaction_level.rs | 11 ++++- crates/aingle_cortex/src/main.rs | 6 ++- crates/aingle_cortex/src/p2p/config.rs | 12 ++++-- crates/aingle_cortex/src/p2p/manager.rs | 10 +++-- crates/aingle_cortex/src/rest/audit.rs | 8 +++- crates/aingle_cortex/src/rest/proof_api.rs | 4 +- crates/aingle_cortex/src/service/ingest.rs | 41 +++++++++++++++---- crates/aingle_cortex/src/service/tasks.rs | 39 ++++++++++++++---- .../tests/data_integrity_test.rs | 4 +- crates/aingle_graph/src/dag/export.rs | 10 +++++ crates/aingle_ingest/src/lib.rs | 36 ++++++++++++---- crates/aingle_ingest/src/tasks.rs | 22 ++++++++-- crates/aingle_logic/src/engine.rs | 2 +- crates/aingle_logic/src/rule.rs | 4 +- .../tests/e2e_multi_node_tests.rs | 6 +-- crates/aingle_viz/src/dag.rs | 4 +- 18 files changed, 201 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 697eb6cb..8158b9f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,8 +86,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: diff --git a/crates/aingle_ai/src/ineru/surprise_gate.rs b/crates/aingle_ai/src/ineru/surprise_gate.rs index 48a21b8f..677eccd0 100644 --- a/crates/aingle_ai/src/ineru/surprise_gate.rs +++ b/crates/aingle_ai/src/ineru/surprise_gate.rs @@ -130,6 +130,21 @@ impl SurpriseGate { } /// Get adaptive threshold based on recent statistics + /// + /// NOT `clamp`, and not by accident. `f32::min(NaN, 1.0)` returns `1.0` — + /// `min`/`max` discard NaN and return the other operand — so a NaN mean or + /// std yields a threshold of `1.0` here. `NaN.clamp(0.1, 1.0)` returns NaN, + /// and a NaN threshold is worse than either bound: every comparison against + /// it is false, so the gate would stop firing and do so silently. + /// `get_std()` can produce NaN from an empty history or from a negative + /// variance caused by floating-point error. + /// + /// The right fix is neither expression: branch on `is_nan()` explicitly and + /// pin the chosen fallback with a test. Which fallback is correct — today's + /// 1.0, meaning the gate almost never fires, or 0.1, meaning it almost + /// always does — belongs to whoever owns this gate, so it is left as a + /// decision rather than made silently by a lint fix. + #[allow(clippy::manual_clamp)] pub fn adaptive_threshold(&self) -> f32 { // Use mean + 1 std as adaptive threshold let std = self.get_std(); diff --git a/crates/aingle_ai/src/nested_learning/transaction_level.rs b/crates/aingle_ai/src/nested_learning/transaction_level.rs index 59d14ed0..19d8350d 100644 --- a/crates/aingle_ai/src/nested_learning/transaction_level.rs +++ b/crates/aingle_ai/src/nested_learning/transaction_level.rs @@ -198,7 +198,16 @@ impl TransactionClassifier { let min_dist = sorted.first().copied().unwrap_or(1.0); let second_dist = sorted.get(1).copied().unwrap_or(min_dist); - // Confidence based on separation + // Confidence based on separation. + // + // NOT `clamp`: `max`/`min` discard NaN and `clamp` propagates it, so the + // two differ exactly when the input is NaN. Here the division is guarded + // by `second_dist > 0.0`, so NaN should be unreachable — but that rests + // on the `partial_cmp().unwrap()` in the sort above, which PANICS on a + // NaN distance rather than producing one. Substituting `clamp` would be + // safe and would also settle nothing; the real work is making that sort + // total (`total_cmp`) and then deciding what a NaN distance means. + #[allow(clippy::manual_clamp)] if second_dist > 0.0 { (1.0 - min_dist / second_dist).max(0.0).min(1.0) } else { 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/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 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/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/src/dag/export.rs b/crates/aingle_graph/src/dag/export.rs index c8c0190e..11b02bc2 100644 --- a/crates/aingle_graph/src/dag/export.rs +++ b/crates/aingle_graph/src/dag/export.rs @@ -42,6 +42,16 @@ pub enum ExportFormat { impl ExportFormat { /// Parse from string (case-insensitive). + /// + /// Shadows `std::str::FromStr::from_str`, which clippy rightly flags: a + /// caller writing `ExportFormat::from_str` cannot tell which one they get, + /// and this one returns `Option` where the trait returns `Result`. + /// + /// The fix is to implement `FromStr` properly with an error type and drop + /// this inherent method. It is cheap — there is exactly one caller outside + /// this module's own tests — but it changes a public signature, so it is a + /// deliberate change with its own review and not a line in a lint sweep. + #[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option { match s.to_lowercase().as_str() { "dot" | "graphviz" => Some(Self::Dot), 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..7763da26 100644 --- a/crates/aingle_ingest/src/tasks.rs +++ b/crates/aingle_ingest/src/tasks.rs @@ -123,7 +123,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 +231,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 +259,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/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/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_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() } From 487f365ade13c471c79d0e71084f52911d114472 Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Wed, 29 Jul 2026 12:04:44 +0200 Subject: [PATCH 2/3] ci: run the checks on every pull request, not just those onto main and dev MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- .github/workflows/ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8158b9f2..a63f4e72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,8 +3,11 @@ 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: CARGO_TERM_COLOR: always From 2ca262fd2ce74dfdeda46733acbccdde14c33f4a Mon Sep 17 00:00:00 2001 From: ApiliumDevTeam Date: Wed, 29 Jul 2026 12:23:48 +0200 Subject: [PATCH 3/3] fix(ingest): rewrite a fallthrough with `?`, as clippy 1.97 asks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- crates/aingle_ingest/src/tasks.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/aingle_ingest/src/tasks.rs b/crates/aingle_ingest/src/tasks.rs index 7763da26..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());