Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,8 +89,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:
Expand Down
15 changes: 15 additions & 0 deletions crates/aingle_ai/src/ineru/surprise_gate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
11 changes: 10 additions & 1 deletion crates/aingle_ai/src/nested_learning/transaction_level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 4 additions & 2 deletions crates/aingle_cortex/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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;
Expand Down
12 changes: 8 additions & 4 deletions crates/aingle_cortex/src/p2p/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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());
}
}
10 changes: 6 additions & 4 deletions crates/aingle_cortex/src/p2p/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 6 additions & 2 deletions crates/aingle_cortex/src/rest/audit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<AuditEntry>(&line) {
entries.push(entry);
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -298,7 +302,7 @@ mod tests {
}
// Should have evicted some entries
assert!(log.len() <= 15);
assert!(log.len() > 0);
assert!(!log.is_empty());
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions crates/aingle_cortex/src/rest/proof_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();

Expand Down
41 changes: 32 additions & 9 deletions crates/aingle_cortex/src/service/ingest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
{
Expand All @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
39 changes: 32 additions & 7 deletions crates/aingle_cortex/src/service/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
]
Expand All @@ -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()));
Expand All @@ -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::<Vec<_>>(), ["A"]);
assert_eq!(ag.today.iter().map(|t| t.text.as_str()).collect::<Vec<_>>(), ["B"]);
assert_eq!(ag.upcoming.iter().map(|t| t.text.as_str()).collect::<Vec<_>>(), ["C"]);
assert_eq!(
ag.overdue
.iter()
.map(|t| t.text.as_str())
.collect::<Vec<_>>(),
["A"]
);
assert_eq!(
ag.today.iter().map(|t| t.text.as_str()).collect::<Vec<_>>(),
["B"]
);
assert_eq!(
ag.upcoming
.iter()
.map(|t| t.text.as_str())
.collect::<Vec<_>>(),
["C"]
);
}

#[tokio::test]
Expand All @@ -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::<Vec<_>>(), ["B", "A", "C"]);
assert_eq!(
all.iter().map(|t| t.text.as_str()).collect::<Vec<_>>(),
["B", "A", "C"]
);

let doing = super::list_tasks(&state, Some("doing")).await;
assert_eq!(doing.len(), 1);
Expand All @@ -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"));
}

Expand Down
4 changes: 2 additions & 2 deletions crates/aingle_cortex/tests/data_integrity_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 10 additions & 0 deletions crates/aingle_graph/src/dag/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self> {
match s.to_lowercase().as_str() {
"dot" | "graphviz" => Some(Self::Dot),
Expand Down
Loading
Loading