1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
[]
= "moadim"
= "1.7.2"
= "2021"
# Floor set by the transitive proc-macro build dependency `darling 0.23`
# (pulled in via `rmcp-macros` <- `rmcp`), which itself declares
# `rust-version = "1.88.0"`. Bumping this is a deliberate, CHANGELOG-worthy
# change — see CONTRIBUTING.md.
= "1.88"
= "Loop engine for AI agents — routines over REST, MCP, and a built-in web UI"
= "MIT"
= "https://github.com/moadim-io/daemon"
= "https://moadim.io"
= ["scheduler", "mcp", "automation", "rest"]
= ["command-line-utilities", "web-programming::http-server"]
[]
= "1"
= "0.8"
= "0.4"
= { = "4", = ["derive"] }
= "4"
= "3"
= "1.0.2"
= "6"
= "0.11"
= "0.1"
= "0.4"
= { = "1", = ["derive"] }
= "1"
= "1.1"
= { = "2.0.0", = [
"server",
"macros",
"transport-io",
"transport-streamable-http-server",
] }
= "1"
= { = "1", = ["full"] }
= { = "0.5", = ["limit", "util"] }
= { = "0.7", = ["catch-panic", "compression-gzip"] }
= { = "5", = ["axum_extras"] }
= { = "9", = ["axum"] }
= { = "1", = ["v4"] }
= "1"
# `flock`-based advisory locking for the shared `~/.claude.json` prune (src/utils/claude_json.rs).
# Unix-only target dependency since the daemon's launch mechanism (tmux sessions) is Unix-only.
[]
= "0.2"
[[]]
= "moadim"
= "src/main.rs"
[]
= "1"
[]
= "deny"
[]
= "deny"
= "deny"
# Reject single-character identifiers (threshold 1, the lint default).
= "deny"
# Require backticks around code-like items (type names, paths, acronyms) in docs.
= "deny"
# Require inline format args (`{x}`) over positional (`{}`, x) where possible.
= "deny"
# Reject leftover `dbg!` debugging macros so they never ship to production.
= "deny"
# Reject leftover `todo!()`/`unimplemented!()` stubs so they never ship to
# production — a stray one would panic the daemon on that code path.
= "deny"
= "deny"
# Reject `async fn`s that never `.await` anything — they don't need to be
# async, and being async needlessly propagates async-ness to callers.
= "deny"
# Prefer `map_or`/`map_or_else`/`is_ok_and` over `.map(f).unwrap_or(_)`.
= "deny"
# Require a trailing `;` on the last statement of a `()`-returning block so
# statement-only blocks read consistently and intent stays explicit.
= "deny"
# Prefer `let PATTERN = expr else { diverge }` over a `match` whose only
# non-binding arm diverges (return/continue/break), keeping the happy path
# unindented.
= "deny"
# Prefer naming the method directly (`Result::ok`, `ToString::to_string`) over
# wrapping it in a closure that only forwards the receiver
# (`|e| e.ok()`, `|s| s.to_string()`).
= "deny"
# Require every `#[allow(...)]` suppression to carry a `reason = "..."` so
# reviewers can tell at a glance why the lint was silenced, and silenced lints
# stay documented as the code evolves.
= "deny"
# Reject `if !condition { A } else { B }` in favour of `if condition { B } else { A }`
# to eliminate double-negation and reduce cognitive load when reading branches.
= "deny"
# Forbid `.unwrap()` in production code: a panic in a long-running daemon process
# kills the server without a graceful shutdown, swallowing the in-flight request
# and leaving connected agents in an undefined state. All error paths must instead
# propagate via `?` or be handled explicitly so the structured error responses and
# logging this daemon provides remain intact. Production code is already clean, so
# `deny` locks in that state and keeps every future error path deliberate.
# Test code is exempt because test modules are gated with `#[cfg(test)]` and
# legitimately use `.unwrap()` to assert invariants.
= "deny"
# Prefer `for x in &collection` / `for x in &mut collection` over the explicit
# `for x in collection.iter()` / `.iter_mut()` form. The reference syntax is
# shorter, is the idiomatic way to iterate without consuming, and avoids the
# reader having to confirm that `.iter()` isn't secretly `.into_iter()` (which
# would move). It states "borrow and iterate" at a glance.
= "deny"
# Companion to `explicit_iter_loop` above: reject `for x in collection.into_iter()`
# in favour of the equivalent `for x in collection`. `.into_iter()` here is
# redundant — a plain `for` loop already calls it implicitly — so spelling it
# out adds noise without adding meaning. The codebase is already clean, so
# `deny` locks that in.
= "deny"
# Reject `use some::module::*;` glob imports. Explicit imports make it clear
# at the call site which module a name comes from, avoid silent breakage or
# ambiguity when the globbed module adds a new item, and keep `rust-analyzer`
# "go to definition" unambiguous.
= "deny"
# Reject `match` arms with identical bodies — either the arm is dead code
# already covered by another pattern, or the duplication is an unintentional
# bug where the bodies were meant to differ.
= "deny"
# Prefer `Self`/`Self::Variant` over spelling out the concrete type name
# inside its own `impl` block. `Self` always tracks the current type, so
# renaming the type requires zero edits inside the `impl` block, and it's
# the idiomatic way to say "same type as the one being implemented".
= "deny"
# Reject `{:?}` (Debug) formatting of a `Path`/`PathBuf` in user-facing output
# (log lines, error messages). Debug-formats a path with surrounding quotes and
# escaped separators/backslashes, which reads oddly and diverges from every
# other path already printed via `.display()` in this codebase. `Path::display`
# is the correct, consistent way to show a filesystem path to a human.
= "deny"
# Reject a `&mut` parameter that's never actually mutated through. A stale
# `&mut` overstates what the function does to its caller (and to the borrow
# checker, needlessly ruling out concurrent shared borrows) — the signature
# should say `&T` once the mutation it was written for is gone. The codebase
# is already clean, so `deny` keeps every future `&mut` parameter honest.
= "deny"
# Prefer `String::new()` over `"".into()`/`"".to_string()`/`"".to_owned()` for
# constructing an empty `String` — it says "empty string" directly instead of
# making the reader infer it from an empty literal plus a conversion call.
= "deny"
# Reject an `else` branch after an `if` whose every path already diverges
# (return/break/continue/panic) — the `else` adds a level of nesting for
# nothing, since control flow never falls through to it. Dropping it keeps the
# non-diverging code unindented, matching this codebase's existing preference
# for flat, early-return control flow (see `manual_let_else`, `if_not_else`
# above). The codebase is already clean, so `deny` locks that in.
= "deny"
# A type deriving `PartialEq` whose fields are all themselves `Eq` almost always
# satisfies `Eq` too (reflexivity holds for everything but float fields, which
# this codebase avoids in these structs). Deriving it costs nothing and unlocks
# the type as a `HashSet`/`BTreeMap` key or `HashMap` value in `==`-free contexts
# without a later, easy-to-forget follow-up PR. The codebase is already clean,
# so `deny` locks that in.
= "deny"
# Reject a by-value function argument that the body never consumes (never moves, mutates through
# ownership, or otherwise needs to own) — it should take a reference instead. A needless owned
# parameter forces every caller to clone or otherwise part with a value the callee never actually
# needed to own. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject collecting an iterator into a `Vec`/collection only to immediately
# iterate over it again (or check its length/emptiness) — the intermediate
# collection is pure allocation overhead the iterator chain never needed. The
# codebase is already clean, so `deny` locks that in.
= "deny"
# Prefer `.copied()` over `.cloned()` when iterating `Copy` items (e.g.
# `&i32`, `&bool`) — cloning a `Copy` type is a needlessly indirect way to say
# "copy this value"; `.copied()` states the intent directly and optimizes
# identically. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject `.push_str(&format!(...))` in favour of `write!`/`writeln!` directly into the `String`.
# The `format!` call allocates a throwaway `String` only to immediately copy its contents into
# the target and drop it, so it's pure overhead that `write!` avoids by writing straight into the
# existing buffer. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a `match` whose only non-wildcard arm destructures a single pattern, with the rest
# falling to a catch-all `else`-like arm — `if let ... else` says the same thing without the
# unused generality of `match`, keeping the two-way branch as readable as a plain `if`. The
# codebase is already clean, so `deny` locks that in.
= "deny"
# Reject an item (`use`, `const`, etc.) declared after a statement in the same block — items are
# scoped to the whole block from the start regardless of where they're written, so placing one
# mid-block after other code misleads the reader into thinking order matters here. Hoisting it to
# the top of the block states that up front. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a `&self` method that never reads `self` — it should be an associated function instead, so
# the signature doesn't overstate what the method needs from its receiver. The handful of MCP tool
# handlers that don't touch `self` are exempted with a documented `#[allow]`: `#[tool_router]`
# dispatches every handler through `self.method(...)` uniformly, so `&self` there is a framework
# requirement, not a mistake. The rest of the codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a `&T` parameter where `T` is `Copy` and no bigger than a pointer — passing it by value is
# at least as cheap and states the function doesn't need the caller's own reference. The codebase
# is already clean, so `deny` locks that in.
= "deny"
# Require `_`-separated digit groups in long integer literals (e.g. `424_242`) so a reader can
# judge magnitude at a glance instead of counting digits one by one. The codebase is already
# clean, so `deny` locks that in.
= "deny"
# Reject a case-sensitive `.ends_with(".ext")`/`.starts_with(...)` file-extension check in favour
# of `Path::extension()` compared with `eq_ignore_ascii_case`. A literal `.md` check silently
# mismatches an uppercase or mixed-case extension even though macOS's and Windows' default
# filesystems treat the two names as the same file, so the check's pass/fail result can disagree
# with what's actually on disk. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject `std::mem::forget` in favour of `drop` or letting the value go out of scope. In a
# long-running daemon, a forgotten value's `Drop` impl never runs — for file handles, locks, and
# other RAII guards that means leaked descriptors/locks that accumulate over the process's
# lifetime instead of a single one-off leak in a short-lived program. The one legitimate use in
# this codebase (a test that manually closes a fd and must stop `File::drop` from closing it
# again) is exempted with a documented `#[allow]`. The rest of the codebase is already clean, so
# `deny` locks that in.
= "deny"
# Reject a raw string literal (`r#"..."#`) whose `#` delimiters aren't needed because the string
# body contains no unescaped `"` that would otherwise terminate it early. The extra hashes are
# pure noise once nothing inside requires them. The codebase is already clean, so `deny` locks
# that in.
= "deny"
# Reject `as` casts that widen without loss (e.g. `u32 as i64`) in favour of `From`/`Into`. An
# `as` cast silently stays legal (and silently starts truncating) if the source or target type
# ever changes size; `i64::from(x)` is the same widening but fails to compile the moment it would
# no longer be lossless. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a function call passed directly as the fallback argument to `unwrap_or`/`ok_or`/
# `and`/`or` and friends (e.g. `opt.unwrap_or(expensive())`) in favour of the lazy `_else`
# form (`opt.unwrap_or_else(expensive)`). The eager form always evaluates the fallback, even
# on the common path where the value is already present — silent, needless work (or a needless
# allocation) on every call. The lazy form only runs it when actually needed. The codebase is
# already clean, so `deny` locks that in.
= "deny"
# Reject `_` as a `match`/`tokio::select!` arm pattern when the matched value's type is `()` —
# spelling out `()` states the value's type explicitly instead of leaving the reader to check that
# `_` isn't quietly discarding something meaningful. The codebase is already clean, so `deny` locks
# that in.
= "deny"
# Reject `"literal".as_bytes()` in favour of a `b"literal"` byte-string literal — the byte string
# is the same bytes without the runtime call, and states "this is bytes" at the point of
# declaration instead of at a conversion tacked on afterward. The codebase is already clean, so
# `deny` locks that in.
= "deny"
# Reject a `&Option<T>` parameter in favour of `Option<&T>` — the former forces every caller to
# already own (or clone into) an `Option`, while the latter accepts a plain `&T` wrapped in `Some`
# just as easily and is the idiomatic way to say "an optional borrow". The codebase is already
# clean, so `deny` locks that in.
= "deny"
# Reject `.map(|x| format!(...)).collect::<String>()` in favour of folding a `write!`/`writeln!`
# directly into a `String` buffer. Like `format_push_string` above, each `format!` call allocates a
# throwaway `String` only to have `collect` immediately copy its contents into the result and drop
# it — pure overhead a fold avoids by writing straight into the accumulator. The codebase is
# already clean, so `deny` locks that in.
= "deny"
# Reject a binding whose name is a near-miss of another binding already in scope (e.g. `txt` next to
# `text`) — a reader (or a future edit) can mix the two up since they differ by only a couple of
# characters. Fixed the four violations this surfaced: a `ContentBlock::Text(txt) => txt.text.clone()`
# match arm repeated across the MCP route tests, shadowing an existing local also named `text`;
# renamed the binding to `block`. No behavior change.
= "deny"
# Reject an or-pattern repeated across multiple match arms/parameters (e.g. `Foo::A | Foo::B =>
# ..., Foo::C | Foo::D => ...` written as separate arms with duplicated bodies) in favour of
# merging into one arm with a single nested or-pattern. Keeping the duplicates in sync by hand is
# easy to get wrong as arms are added or reordered. The codebase is already clean, so `deny` locks
# that in.
= "deny"
# Reject a scope with 4+ single-character bindings in play at once — past the first couple, a
# reader has to hold each letter's meaning in their head instead of reading a name. One violation:
# `ui/src/routines/filter_tests.rs`'s `is_active_detects_each_facet` test had six (`q`, `s`, `a`,
# `m`, `r`, `t`), one per `RoutineFilter` facet under test; renamed to `query_filter`,
# `status_filter`, `agent_filter`, `machine_filter`, `repo_filter`, `tag_filter`. No behavior change.
= "deny"
# Reject a `.clone()` whose result is immediately moved with no other use of the original value
# still pending — the clone buys nothing over moving the original directly, so it's pure wasted
# allocation (and, for a `Callback`/`UseStateHandle`, an extra `Rc` refcount bump) on every call.
# This surfaced 31 violations, all in test/UI code: intermediate `let x = x.clone();` shadows built
# for a `move` closure that turned out to be the value's last use, and a couple of `field.clone()`
# reads passed straight into a constructor that never touched the original again. Fixed by moving
# the original instead. The codebase is otherwise already clean, so `deny` locks that in.
= "deny"
# Reject a string literal that looks like a `format!`-family placeholder (`"{name}"`) sitting
# outside a formatting macro — usually a leftover `format!`/`println!` arg that got moved into a
# plain string and silently stopped interpolating. Fixes the 2 violations this surfaced in
# `routines/command.rs::substitute`, both intentional `String::replace` placeholder tokens (not
# formatting-macro arguments), which are now annotated with a scoped `#[allow]` explaining why.
= "deny"
# Reject a local array literal over 512000 bytes — a value that size belongs on the heap
# (`Vec`/`Box`), not the stack. A daemon process runs long-lived worker threads with a fixed,
# comparatively small stack, so an oversized stack array is a latent stack-overflow risk that only
# surfaces under the right call depth, unlike a heap allocation which fails safely. The codebase is
# already clean, so `deny` locks that in.
= "deny"
# Require an underscore between a numeric literal and its type suffix (e.g. `500_u64`, not
# `500u64`) — without it, the suffix reads as part of the digits at a glance, and a reviewer has
# to look twice to see where the number ends and the type begins. Fixed the 27 violations this
# surfaced (all `0u8`/`0u64`/`Nu64`-style literals in test code and one production site in
# `routines/cleanup/disk_cap.rs`) via `cargo clippy --fix`. No behavior change.
= "deny"
# Reject the `unreachable!()` macro. A daemon process that hits an `unreachable!()` panics
# the whole server instead of returning a structured error, and a match arm that looks
# provably impossible today can become reachable after an innocuous refactor elsewhere —
# the panic then only surfaces at runtime, in production. Fixed the one violation this
# surfaced: `build/ui.rs::base64_encode`'s length-3 `match` had a `_ => unreachable!()`
# catch-all for chunk lengths other than 1/2/3 (impossible from `chunks(3)`); rewritten
# without a `match` by indexing the guaranteed-present first byte directly and reading the
# optional second/third bytes via `.get()`, so there is no catch-all arm to guard. The rest
# of the codebase is already clean, so `deny` locks that in.
= "deny"
# Reject `std::process::exit`/`std::process::abort` outside of `fn main` (the lint already
# exempts `main` itself, where `src/main.rs`'s CLI dispatch legitimately calls `process::exit`
# to set the process's exit code). Calling either from anywhere else terminates the process
# immediately, skipping unwinding — so any `Drop` impl further up the call stack (an in-flight
# routine's cleanup, a lock guard, a file handle) never runs. In a long-running daemon that's the
# same "silent leak on the way out" risk `mem_forget` above already guards against. The codebase
# is already clean outside `main`, so `deny` locks that in.
= "deny"
# Forbid `.expect()` in production code, same rationale as `unwrap_used` above: a panic in a
# long-running daemon process kills the server without a graceful shutdown, and `.expect()` panics
# exactly like `.unwrap()` does — it only adds a custom message to the same abrupt failure mode.
# Denying `unwrap_used` alone left this an unguarded back door: swap `.unwrap()` for
# `.expect("message")` and the panic-safety guarantee silently stops applying. Test code keeps
# using `.expect()` freely (`allow-expect-in-tests = true` in clippy.toml, mirroring
# `allow-unwrap-in-tests`) since panicking is the desired failure mode there.
#
# Fixed the 25 violations this surfaced across `src/`. Most (a missing parent dir, a
# `toml`/`serde_json` re-serialization, a loopback socket address, the process's own executable
# path, a piped file handle) were converted to proper `Result` propagation — `?`, `ok_or_else`, or
# a `let else` early return for a `()`-returning best-effort function — matching this crate's
# existing `unwrap_used`-driven error-handling style. A handful are genuinely provable "can't
# happen" invariants (an id checked to exist earlier in the same function with the lock held
# continuously since; `Stdio::piped()` set a few lines above the matching `.take()`; a `?` a line
# above that already returns on `None`) and are kept as `.expect()` behind a scoped, reasoned
# `#[allow]`, the same pattern this file already uses for `mem_forget`/`clippy::zombie_processes`.
# `build.rs` (and the `src/build/` modules it pulls in) carries its own crate-level
# `#![allow(clippy::expect_used, reason = "...")]`: it's a build script, not the daemon binary, so
# a `.expect()` panic there just aborts `cargo build` with a message — the intended failure mode,
# same class of exemption as tests. The rest of the codebase is already clean, so `deny` locks
# that in.
= "deny"
# Forbid `panic!()` in production code: same rationale as `unwrap_used`/`expect_used`
# above — an unhandled panic kills this long-running daemon process outright instead of
# returning a structured error. The codebase is already clean, so `deny` locks that in.
# Test code is exempt via `allow-panic-in-tests` in clippy.toml, same as the other two.
= "deny"
# Reject a `let` binding whose explicit type annotation exactly matches what the compiler
# would already infer — the annotation adds nothing beyond what the initializer already states,
# so it's noise a reader has to cross-check against the inferred type instead of trusting. The
# codebase is already clean, so `deny` locks that in.
= "deny"
# Reject `if !cond { panic!("msg") }` in favour of `assert!(cond, "msg")` — the assert form states
# the invariant being checked directly instead of making the reader invert the condition to find
# it, and collapses two lines into one. Test code is exempt via `allow-panic-in-tests` in
# clippy.toml, same as `panic` above (an inverted `assert!` in a test reads the same either way).
# The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a `#[test]` function declared outside a `#[cfg(test)]` module. This is the compiler-
# enforced half of this repo's existing `*_tests.rs` convention: `.githooks/pre-push` step 1
# already greps for tests living outside a `#[cfg(test)] mod foo_tests;` sibling, but that check is
# text-pattern-based and only runs locally pre-push. This lint checks the same invariant from the
# AST instead, so it also runs in `cargo clippy` (CI's `lint.yml` job) and can't be skipped by a
# contributor who bypasses git hooks. The codebase is already clean, so `deny` locks that in.
= "deny"
# Require every `unsafe` block to carry a `// SAFETY: ...` comment immediately above it explaining
# why the block upholds the invariants `unsafe` asks the author to uphold. The codebase already
# follows this convention almost everywhere (env-var overrides in single-threaded tests, the
# `flock` FFI call in `utils/claude_json.rs`), but nothing enforced it, so it could silently lapse
# as new `unsafe` blocks were added. Fixed the 39 violations this surfaced — all in test helpers
# (`std::env::set_var`/`remove_var` restoring a previous env var after a single-threaded test, and
# one `libc::kill` cleaning up a test-spawned child process) — by adding the same `// SAFETY:`
# comment already used at the many documented call sites. No behavior change. `deny` locks in that
# every future `unsafe` block stays documented.
= "deny"
# Reject a bare `#[ignore]` on a test with no explanation of why it's skipped — the same rationale
# as `allow_attributes_without_reason` above, applied to test-skipping instead of lint-suppression:
# a silently ignored test rots invisibly (nobody notices it stopped running, or whether it's still
# safe to re-enable) unless the reason is written down next to it, e.g.
# `#[ignore = "requires a live tmux session"]`. No `#[ignore]` exists in this codebase today, so
# `deny` locks in that any future one must justify itself.
= "deny"
# Reject `File::open(path)?.read_to_end(...)`/`.read_to_string(...)` in favour of
# `fs::read`/`fs::read_to_string`, which open, read, and close the file in one call instead of
# three separate steps a caller has to get right by hand. The two sites this surfaced both seek to
# an offset before reading a log's tail (not the whole file), where the single-call helpers don't
# apply — each is annotated with a documented `#[allow]` explaining why, the same pattern already
# used for `mem_forget`/`clippy::zombie_processes` above. The rest of the codebase is already
# clean, so `deny` locks that in.
= "deny"
# Require a `# Panics` doc section on any function that can transitively panic (calls `.unwrap()`/
# `.expect()`, or a bare `panic!()`/`assert!()`), so a caller can tell without reading the body.
# `unwrap_used`/`expect_used`/`panic` above already ban all three outside tests and the handful of
# documented, `#[allow]`-scoped exemptions for a genuinely unavoidable panic — this closes the loop
# on that convention: an exemption must now also surface in the function's public docs, not just a
# comment next to the call site. The codebase is already clean, so `deny` locks that in.
= "deny"
# Require an `# Errors` doc section on any function returning `Result` explaining when it errors,
# the `Err`-side companion to `missing_panics_doc` above. A caller deciding whether to `?` a result
# or handle it inline should be able to tell what can go wrong from the signature's docs alone,
# without reading the body (or every function it transitively calls). The codebase is already
# clean, so `deny` locks that in.
= "deny"
# Reject a single-character string literal used as a `.replace`/`.split`/`.trim_matches`-style
# pattern (e.g. `"o"`) in favour of the equivalent `char` literal (`'o'`) — the `char` pattern
# skips the generic multi-byte substring-search machinery `&str` patterns go through, so it's a
# free micro-optimization alongside this file's other already-enabled perf lints
# (`cloned_instead_of_copied`, `format_push_string`, `format_collect`). The codebase is already
# clean, so `deny` locks that in.
= "deny"
# Reject a chain of similar binary-op expressions (e.g. `a.x + a.x + a.y`) where one operand looks
# like a copy-paste that should have referenced a different variable/field — the same bug class
# `redundant_clone` above guards elsewhere in the file, just for arithmetic/comparison groupings
# instead of clones. This is a real correctness lint (catches silent logic bugs), not a style
# preference. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a struct field whose name starts with the struct's own name (e.g. `Flag::flag_type`) —
# the prefix is redundant at every access site, since the struct name is already spelled out right
# before the field name (`flag.flag_type` repeats "flag"). Fixed the one violation this surfaced:
# `Flag::flag_type` (`routines/flags.rs`) renamed to `Flag::category`, matching its doc comment
# ("Free-text category"). The wire format is unchanged — the field already carries
# `#[serde(rename = "type")]`. The rest of the codebase is already clean, so `deny` locks that in.
= "deny"
# Reject `.to_string()`/`.to_owned()`-style conversions on a value that's already the target type
# (e.g. cloning a `String` via `.to_string()` instead of `.clone()`, or a `Vec` via `.to_vec()`
# instead of `.clone()`) — the indirect spelling obscures that it's a plain clone and, for some
# receiver types, is measurably slower than calling `Clone::clone` directly. The codebase is
# already clean, so `deny` locks that in.
= "deny"
# Reject `.to_string()` on a `&&T` (a reference to a reference — e.g. iterating `&Vec<&str>`
# yields `&&str`) in favour of calling `.to_string()` on the dereferenced `&T` directly. The
# extra `&` forces an unnecessary auto-deref through `Display`/`ToString` on every call; spelling
# it on the inner reference is the same result without that indirection. The codebase is already
# clean, so `deny` locks that in.
= "deny"
# Reject an `as`-cast from a signed integer to an unsigned one (e.g. `i64 as u64`), which silently
# wraps a negative value into a huge positive one instead of erroring. Fixed the 2 violations this
# surfaced, both a Unix-timestamp `i64` (`DateTime::timestamp()`) cast to the `u64` seconds this
# crate's timestamps are stored as: `logging::format_json_line`'s `ts_local` field and
# `ical::feed`'s snooze-deadline comparison. Neither value can go negative in practice (both come
# from "now" or a future cron fire, always post-1970), but the cast gave no indication of that
# invariant to a reader — converted to `u64::try_from(...)`, clamped to `0`/`false` on the
# theoretical negative case instead of silently wrapping. No behavior change. The codebase is
# otherwise already clean, so `deny` locks that in.
= "deny"
# Reject a `match`/`if let` that manually converts an `Option` to a `Result`
# (`Some(x) => Ok(x), None => Err(e)`) instead of `.ok_or(e)`. The manual form
# spells out in several lines what `.ok_or` says in one, and invites the two
# arms to drift out of sync on a future edit. The codebase is already clean,
# so `deny` locks that in.
= "deny"
# Reject a `match`/`if let` that manually re-derives what `Option::is_some_and`/`is_none_or` or
# `Result::is_ok_and`/`is_err_and` already compute (e.g. `match opt { Some(x) => pred(x), None =>
# false }`) instead of calling the combinator directly. Same rationale as `manual_ok_or` above: the
# manual form spells out in several lines what the combinator says in one, and invites the arms to
# drift out of sync on a future edit. The codebase is already clean, so `deny` locks that in.
= "deny"
# Reject an `as`-cast from an unsigned integer to a signed one (e.g. `u64 as i64`), the mirror
# image of `cast_sign_loss` above: a value past the target's positive range silently wraps into a
# negative one instead of erroring. Fixed the 2 violations this surfaced:
# `utils::time::format_local`'s Unix-seconds `u64` cast to the `i64` `chrono::Local::timestamp_opt`
# takes, and a test's `u32` child-process id cast to the `libc::pid_t` (`i32`) `libc::kill` takes.
# Neither value can realistically exceed the target's range today, but the cast gave no indication
# of that invariant to a reader — converted to `<target>::try_from(...)`, clamped to the target's
# `MAX` on the theoretical overflow case instead of silently wrapping. No behavior change. The rest
# of the codebase is already clean, so `deny` locks that in.
= "deny"
# Reject an `as`-cast from an integer to a floating-point type that can't represent every value of
# the source exactly (e.g. `u64 as f64`, `i64 as f64`) — past 2^52 the cast silently rounds to the
# nearest representable `f64`, the same "no indication of the invariant" gap `cast_sign_loss` and
# `cast_possible_wrap` above already close for the other integer-cast directions. Fixed the 2
# violations this surfaced: `cli::query::humanize_bytes`'s `u64` byte count cast to `f64` for
# display, which is deliberately an approximation (already rounded to one decimal place) and is
# kept as `as f64` behind a scoped, reasoned `#[allow]`; and a test's `delay_ms` shim parameter,
# narrowed from `u64` to `u32` and converted via `f64::from` so the cast is lossless by
# construction instead of merely lossless in practice. No behavior change. The rest of the
# codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a function whose body could run in `const` context but isn't marked `const fn`. A
# non-`const` function can't be called from a `const`/`static` initializer or another `const fn`,
# closing off callers unnecessarily when nothing about the body actually requires runtime
# evaluation. Fixed the 7 violations this surfaced (`cli::liveness_exit_code`,
# `machine::MachineSource::label`, `routes::mcp::MoadimMcp::new`, `routines::cleanup::is_expired`,
# `routines::flags::FlagScope::suffix`, `routines::model::bool_true`,
# `routines::service_log_tail::LogWithMeta::empty`) by adding `const`. No behavior change. The rest
# of the codebase is already clean, so `deny` locks that in.
= "deny"
# Reject a direct `==`/`!=` comparison between floating-point values. Binary floating-point can't
# represent most decimal fractions exactly, so two values computed by equivalent-looking paths
# (e.g. `0.1 + 0.2` vs `0.3`) can differ in the last bit and silently fail an exact-equality check
# that was meant to test "close enough". Callers who really do need bit-exact comparison (e.g.
# comparing a value against itself, or against a literal it was assigned from verbatim) should use
# `f64::to_bits`/`==` explicitly, or an epsilon-based comparison otherwise. The codebase is already
# clean, so `deny` locks that in.
= "deny"
# Reject an `if let Some(PAT) = expr` / `if let None = expr` (or the `matches!`-eligible enum-variant
# equivalent) whose pattern only tests a single unit-like variant with no bindings extracted — a
# direct `==` comparison (the type must derive `PartialEq`, which every such enum in this codebase
# already does per `derive_partial_eq_without_eq` above) says the same thing without the reader
# having to confirm no fields are actually being destructured. The codebase is already clean, so
# `deny` locks that in.
= "deny"
# Reject `.sort()`/`.sort_by()`/`.sort_by_key()` on a slice of primitives (integers, floats,
# chars, bools) in favour of the `_unstable` variant. A stable sort only matters when equal
# elements must keep their relative order, which is meaningless for primitives — two `5`s are
# indistinguishable regardless of which one came first. The unstable variant is faster and,
# unlike the stable one, sorts in place without allocating a scratch buffer. The codebase is
# already clean, so `deny` locks that in.
= "deny"