Skip to main content

max / makenotwork

wam: adopt universal clippy/lint baseline; warnings to zero Four let-else conversions (manual_let_else) and write! for the SQL filter builder (format_push_string). clippy --all-targets clean, cargo fmt clean, tests green (21 passed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-23 13:14 UTC
Commit: 7cc64f21fe9d09dec63ff607439ead10426a8132
Parent: 1fb932f
6 files changed, +108 insertions, -79 deletions
@@ -23,3 +23,35 @@ reqwest = { version = "0.13", default-features = false, features = ["json", "rus
23 23 tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "time"] }
24 24 urlencoding = "2"
25 25 uuid = { version = "1", features = ["v4"] }
26 +
27 + [lints.rust]
28 + unused = "warn"
29 + unreachable_pub = "warn"
30 +
31 + [lints.clippy]
32 + pedantic = { level = "warn", priority = -1 }
33 + # Allow-list tuned from a measured breakdown across server/multithreaded/pter
34 + # (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
35 + # else in `pedantic` stays a warning. Keep this block identical across repos.
36 + module_name_repetitions = "allow"
37 + # Doc lints — no docs-completeness push underway.
38 + missing_errors_doc = "allow"
39 + missing_panics_doc = "allow"
40 + doc_markdown = "allow"
41 + # Numeric casts — endemic and mostly intentional in size/byte math.
42 + cast_possible_truncation = "allow"
43 + cast_sign_loss = "allow"
44 + cast_precision_loss = "allow"
45 + cast_possible_wrap = "allow"
46 + cast_lossless = "allow"
47 + # Subjective structure/style nags — high churn, low signal.
48 + must_use_candidate = "allow"
49 + too_many_lines = "allow"
50 + struct_excessive_bools = "allow"
51 + similar_names = "allow"
52 + items_after_statements = "allow"
53 + single_match_else = "allow"
54 + # Frequent false-positives in TUI/router-heavy code — added from the buckets breakdown.
55 + match_same_arms = "allow"
56 + unnecessary_wraps = "allow"
57 + type_complexity = "allow"
M wam/src/api.rs +21 -24
@@ -21,13 +21,13 @@ use crate::types::{Channel, NewTicket, Priority, Status, Ticket};
21 21
22 22 /// Shared state: SQLite connection + node identity.
23 23 #[derive(Clone)]
24 - pub struct AppState {
24 + pub(crate) struct AppState {
25 25 pub db: Arc<Mutex<Connection>>,
26 26 pub node_id: String,
27 27 }
28 28
29 29 /// Start the HTTP server, optionally syncing with peers.
30 - pub async fn serve(
30 + pub(crate) async fn serve(
31 31 conn: Connection,
32 32 port: u16,
33 33 peers: Vec<String>,
@@ -88,7 +88,7 @@ async fn create_ticket(
88 88 }
89 89
90 90 #[derive(Debug, Deserialize, Default)]
91 - pub struct ListQuery {
91 + pub(crate) struct ListQuery {
92 92 pub status: Option<String>,
93 93 pub priority: Option<String>,
94 94 pub channel: Option<String>,
@@ -142,7 +142,7 @@ async fn get_ticket(State(state): State<AppState>, Path(id): Path<String>) -> im
142 142 }
143 143
144 144 #[derive(Debug, Deserialize)]
145 - pub struct UpdateBody {
145 + pub(crate) struct UpdateBody {
146 146 pub status: Option<String>,
147 147 }
148 148
@@ -154,27 +154,21 @@ async fn update_ticket(
154 154 ) -> impl IntoResponse {
155 155 let conn = state.db.lock().await;
156 156
157 - let ticket = match db::get_ticket(&conn, &id) {
158 - Ok(t) => t,
159 - Err(_) => {
160 - return (
161 - StatusCode::NOT_FOUND,
162 - Json(serde_json::json!({"error": "ticket not found"})),
163 - )
164 - .into_response();
165 - }
157 + let Ok(ticket) = db::get_ticket(&conn, &id) else {
158 + return (
159 + StatusCode::NOT_FOUND,
160 + Json(serde_json::json!({"error": "ticket not found"})),
161 + )
162 + .into_response();
166 163 };
167 164
168 165 if let Some(status_str) = body.status {
169 - let status = match status_str.parse::<Status>() {
170 - Ok(s) => s,
171 - Err(_) => {
172 - return (
173 - StatusCode::BAD_REQUEST,
174 - Json(serde_json::json!({"error": format!("invalid status: {status_str}")})),
175 - )
176 - .into_response();
177 - }
166 + let Ok(status) = status_str.parse::<Status>() else {
167 + return (
168 + StatusCode::BAD_REQUEST,
169 + Json(serde_json::json!({"error": format!("invalid status: {status_str}")})),
170 + )
171 + .into_response();
178 172 };
179 173 if let Err(e) = db::update_status(&conn, &ticket.id, status) {
180 174 return (
@@ -198,7 +192,7 @@ async fn update_ticket(
198 192 // -- Sync endpoints -----------------------------------------------------------
199 193
200 194 #[derive(Debug, Deserialize)]
201 - pub struct SyncPullQuery {
195 + pub(crate) struct SyncPullQuery {
202 196 /// RFC3339 timestamp. Returns tickets updated after this time.
203 197 pub since: String,
204 198 }
@@ -338,7 +332,10 @@ async fn sync_with_peer(
338 332 .await?
339 333 .json()
340 334 .await?;
341 - let accepted = resp.get("accepted").and_then(|v| v.as_u64()).unwrap_or(0);
335 + let accepted = resp
336 + .get("accepted")
337 + .and_then(serde_json::Value::as_u64)
338 + .unwrap_or(0);
342 339 if accepted > 0 {
343 340 eprintln!("sync: pushed {accepted} ticket(s) to {peer_url}");
344 341 }
M wam/src/cli.rs +4 -4
@@ -6,13 +6,13 @@ use crate::types::{Channel, Priority, Status};
6 6
7 7 #[derive(Parser)]
8 8 #[command(name = "wam", about = "Whack-a-Mole -- distributed ticket manager")]
9 - pub struct Cli {
9 + pub(crate) struct Cli {
10 10 #[command(subcommand)]
11 11 pub command: Option<Command>,
12 12 }
13 13
14 14 #[derive(Subcommand)]
15 - pub enum Command {
15 + pub(crate) enum Command {
16 16 /// Create a new ticket
17 17 Create {
18 18 /// Ticket title
@@ -103,13 +103,13 @@ pub enum Command {
103 103 }
104 104
105 105 #[derive(Clone, Copy, Debug, clap::ValueEnum)]
106 - pub enum ExportFormat {
106 + pub(crate) enum ExportFormat {
107 107 Json,
108 108 Csv,
109 109 }
110 110
111 111 /// Parse a duration string like `90d`, `12h`, `30m` into `chrono::Duration`.
112 - pub fn parse_duration(s: &str) -> Result<chrono::Duration, String> {
112 + pub(crate) fn parse_duration(s: &str) -> Result<chrono::Duration, String> {
113 113 let s = s.trim();
114 114 if s.is_empty() {
115 115 return Err("duration is empty".to_string());
M wam/src/db.rs +30 -26
@@ -1,5 +1,7 @@
1 1 //! SQLite data layer for tickets with sync support.
2 2
3 + use std::fmt::Write as _;
4 +
3 5 use chrono::{DateTime, Utc};
4 6 use color_eyre::eyre::{Result, WrapErr, eyre};
5 7 use rusqlite::{Connection, Row, params};
@@ -7,7 +9,7 @@ use rusqlite::{Connection, Row, params};
7 9 use crate::types::{Channel, NewTicket, Priority, Status, Ticket};
8 10
9 11 /// Open (or create) the WAM database and run migrations.
10 - pub fn open_db() -> Result<Connection> {
12 + pub(crate) fn open_db() -> Result<Connection> {
11 13 let dirs = directories::ProjectDirs::from("work", "makenot", "wam")
12 14 .ok_or_else(|| eyre!("cannot determine data directory"))?;
13 15 let data_dir = dirs.data_dir();
@@ -25,7 +27,7 @@ pub fn open_db() -> Result<Connection> {
25 27
26 28 /// Open an in-memory database for testing.
27 29 #[cfg(test)]
28 - pub fn open_memory() -> Result<Connection> {
30 + pub(crate) fn open_memory() -> Result<Connection> {
29 31 let conn = Connection::open_in_memory()?;
30 32 migrate(&conn)?;
31 33 Ok(conn)
@@ -95,7 +97,7 @@ fn migrate(conn: &Connection) -> Result<()> {
95 97 }
96 98
97 99 /// Get or create this node's persistent identity.
98 - pub fn get_or_create_node_id(conn: &Connection) -> Result<String> {
100 + pub(crate) fn get_or_create_node_id(conn: &Connection) -> Result<String> {
99 101 let existing: Option<String> = conn
100 102 .query_row("SELECT value FROM meta WHERE key = 'node_id'", [], |row| {
101 103 row.get(0)
@@ -133,11 +135,9 @@ fn row_to_ticket(row: &Row) -> rusqlite::Result<Ticket> {
133 135 source: row.get("source")?,
134 136 source_ref: row.get("source_ref")?,
135 137 created_at: DateTime::parse_from_rfc3339(&created_str)
136 - .map(|dt| dt.with_timezone(&Utc))
137 - .unwrap_or_else(|_| Utc::now()),
138 + .map_or_else(|_| Utc::now(), |dt| dt.with_timezone(&Utc)),
138 139 updated_at: DateTime::parse_from_rfc3339(&updated_str)
139 - .map(|dt| dt.with_timezone(&Utc))
140 - .unwrap_or_else(|_| Utc::now()),
140 + .map_or_else(|_| Utc::now(), |dt| dt.with_timezone(&Utc)),
141 141 resolved_at: resolved_str.and_then(|s| {
142 142 DateTime::parse_from_rfc3339(&s)
143 143 .map(|dt| dt.with_timezone(&Utc))
@@ -147,7 +147,7 @@ fn row_to_ticket(row: &Row) -> rusqlite::Result<Ticket> {
147 147 }
148 148
149 149 /// Create a new ticket. Returns the created ticket.
150 - pub fn create_ticket(conn: &Connection, new: &NewTicket, node_id: &str) -> Result<Ticket> {
150 + pub(crate) fn create_ticket(conn: &Connection, new: &NewTicket, node_id: &str) -> Result<Ticket> {
151 151 let id = uuid::Uuid::new_v4().to_string();
152 152 let now = Utc::now().to_rfc3339();
153 153
@@ -172,7 +172,7 @@ pub fn create_ticket(conn: &Connection, new: &NewTicket, node_id: &str) -> Resul
172 172 }
173 173
174 174 /// Get a ticket by exact ID or unique prefix match.
175 - pub fn get_ticket(conn: &Connection, id_prefix: &str) -> Result<Ticket> {
175 + pub(crate) fn get_ticket(conn: &Connection, id_prefix: &str) -> Result<Ticket> {
176 176 let mut stmt = conn.prepare("SELECT * FROM tickets WHERE id LIKE ?1 || '%'")?;
177 177 let tickets: Vec<Ticket> = stmt
178 178 .query_map(params![id_prefix], row_to_ticket)?
@@ -187,7 +187,7 @@ pub fn get_ticket(conn: &Connection, id_prefix: &str) -> Result<Ticket> {
187 187
188 188 /// Filter criteria for listing tickets.
189 189 #[derive(Default)]
190 - pub struct ListFilter<'a> {
190 + pub(crate) struct ListFilter<'a> {
191 191 pub status: Option<Status>,
192 192 /// Filter by painhours color band (see [`Priority`]).
193 193 pub priority: Option<Priority>,
@@ -202,25 +202,25 @@ pub struct ListFilter<'a> {
202 202 /// The painhours score depends on the current time and the resolve-freeze rule,
203 203 /// so it can't be expressed in SQL's `ORDER BY`; the band filter and ranking are
204 204 /// applied in Rust after the row-level filters run in the query.
205 - pub fn list_tickets(conn: &Connection, filter: &ListFilter) -> Result<Vec<Ticket>> {
205 + pub(crate) fn list_tickets(conn: &Connection, filter: &ListFilter) -> Result<Vec<Ticket>> {
206 206 let mut sql = String::from("SELECT * FROM tickets WHERE 1=1");
207 207 let mut bind_values: Vec<String> = Vec::new();
208 208
209 209 if let Some(status) = filter.status {
210 210 bind_values.push(status.to_string());
211 - sql.push_str(&format!(" AND status = ?{}", bind_values.len()));
211 + let _ = write!(sql, " AND status = ?{}", bind_values.len());
212 212 }
213 213 if let Some(channel) = filter.channel {
214 214 bind_values.push(channel.to_string());
215 - sql.push_str(&format!(" AND channel = ?{}", bind_values.len()));
215 + let _ = write!(sql, " AND channel = ?{}", bind_values.len());
216 216 }
217 217 if let Some(source) = filter.source {
218 218 bind_values.push(source.to_string());
219 - sql.push_str(&format!(" AND source = ?{}", bind_values.len()));
219 + let _ = write!(sql, " AND source = ?{}", bind_values.len());
220 220 }
221 221 if let Some(search) = filter.search {
222 222 bind_values.push(format!("%{search}%"));
223 - sql.push_str(&format!(" AND title LIKE ?{}", bind_values.len()));
223 + let _ = write!(sql, " AND title LIKE ?{}", bind_values.len());
224 224 }
225 225
226 226 let mut stmt = conn.prepare(&sql)?;
@@ -247,7 +247,7 @@ pub fn list_tickets(conn: &Connection, filter: &ListFilter) -> Result<Vec<Ticket
247 247 }
248 248
249 249 /// Update a ticket's status. Sets resolved_at when moving to Resolved.
250 - pub fn update_status(conn: &Connection, id: &str, status: Status) -> Result<()> {
250 + pub(crate) fn update_status(conn: &Connection, id: &str, status: Status) -> Result<()> {
251 251 let now = Utc::now().to_rfc3339();
252 252 let resolved_at = if status == Status::Resolved {
253 253 Some(now.clone())
@@ -267,7 +267,12 @@ pub fn update_status(conn: &Connection, id: &str, status: Status) -> Result<()>
267 267 }
268 268
269 269 /// Update a ticket's title and body. Bumps `updated_at`.
270 - pub fn update_ticket(conn: &Connection, id: &str, title: &str, body: Option<&str>) -> Result<()> {
270 + pub(crate) fn update_ticket(
271 + conn: &Connection,
272 + id: &str,
273 + title: &str,
274 + body: Option<&str>,
275 + ) -> Result<()> {
271 276 let now = Utc::now().to_rfc3339();
272 277 let rows = conn.execute(
273 278 "UPDATE tickets SET title = ?1, body = ?2, updated_at = ?3 WHERE id = ?4",
@@ -281,7 +286,7 @@ pub fn update_ticket(conn: &Connection, id: &str, title: &str, body: Option<&str
281 286
282 287 /// Aggregate stats across all tickets.
283 288 #[derive(Debug, Default)]
284 - pub struct Stats {
289 + pub(crate) struct Stats {
285 290 pub total: usize,
286 291 pub by_status: Vec<(Status, usize)>,
287 292 /// Open tickets bucketed by painhours color band.
@@ -292,7 +297,7 @@ pub struct Stats {
292 297 pub avg_resolution_seconds: Option<i64>,
293 298 }
294 299
295 - pub fn stats(conn: &Connection) -> Result<Stats> {
300 + pub(crate) fn stats(conn: &Connection) -> Result<Stats> {
296 301 let tickets = list_tickets(conn, &ListFilter::default())?;
297 302 let mut s = Stats {
298 303 total: tickets.len(),
@@ -354,7 +359,7 @@ pub fn stats(conn: &Connection) -> Result<Stats> {
354 359
355 360 /// Delete tickets older than `older_than` with the given status.
356 361 /// Returns the number of rows deleted.
357 - pub fn prune_tickets(
362 + pub(crate) fn prune_tickets(
358 363 conn: &Connection,
359 364 older_than: chrono::Duration,
360 365 status: Status,
@@ -370,7 +375,7 @@ pub fn prune_tickets(
370 375 // -- Sync operations ----------------------------------------------------------
371 376
372 377 /// Get all tickets updated after the given timestamp.
373 - pub fn tickets_since(conn: &Connection, since: &str) -> Result<Vec<Ticket>> {
378 + pub(crate) fn tickets_since(conn: &Connection, since: &str) -> Result<Vec<Ticket>> {
374 379 let mut stmt =
375 380 conn.prepare("SELECT * FROM tickets WHERE updated_at > ?1 ORDER BY updated_at ASC")?;
376 381 let tickets = stmt
@@ -381,7 +386,7 @@ pub fn tickets_since(conn: &Connection, since: &str) -> Result<Vec<Ticket>> {
381 386
382 387 /// Upsert a ticket from a peer. Last-writer-wins based on updated_at.
383 388 /// Returns true if the ticket was inserted or updated.
384 - pub fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool> {
389 + pub(crate) fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool> {
385 390 // Check if we have this ticket and if ours is newer
386 391 let existing_updated: Option<String> = conn
387 392 .query_row(
@@ -393,8 +398,7 @@ pub fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool>
393 398
394 399 if let Some(ref existing) = existing_updated {
395 400 let existing_dt = DateTime::parse_from_rfc3339(existing)
396 - .map(|dt| dt.with_timezone(&Utc))
397 - .unwrap_or_else(|_| Utc::now());
401 + .map_or_else(|_| Utc::now(), |dt| dt.with_timezone(&Utc));
398 402 if existing_dt >= ticket.updated_at {
399 403 return Ok(false); // Ours is same or newer
400 404 }
@@ -437,7 +441,7 @@ pub fn upsert_synced_ticket(conn: &Connection, ticket: &Ticket) -> Result<bool>
437 441 }
438 442
439 443 /// Get the sync cursor for a peer (last synced timestamp).
440 - pub fn get_sync_cursor(conn: &Connection, peer_url: &str) -> Result<Option<String>> {
444 + pub(crate) fn get_sync_cursor(conn: &Connection, peer_url: &str) -> Result<Option<String>> {
441 445 let cursor: Option<String> = conn
442 446 .query_row(
443 447 "SELECT last_synced FROM sync_cursors WHERE peer_url = ?1",
@@ -449,7 +453,7 @@ pub fn get_sync_cursor(conn: &Connection, peer_url: &str) -> Result<Option<Strin
449 453 }
450 454
451 455 /// Update the sync cursor for a peer.
452 - pub fn set_sync_cursor(conn: &Connection, peer_url: &str, last_synced: &str) -> Result<()> {
456 + pub(crate) fn set_sync_cursor(conn: &Connection, peer_url: &str, last_synced: &str) -> Result<()> {
453 457 conn.execute(
454 458 "INSERT INTO sync_cursors (peer_url, last_synced) VALUES (?1, ?2)
455 459 ON CONFLICT(peer_url) DO UPDATE SET last_synced = excluded.last_synced",
M wam/src/tui.rs +6 -10
@@ -244,7 +244,7 @@ impl App {
244 244
245 245 // -- Entry point --------------------------------------------------------------
246 246
247 - pub fn run(conn: Connection, node_id: String) -> Result<()> {
247 + pub(crate) fn run(conn: Connection, node_id: String) -> Result<()> {
248 248 let mut terminal = ratatui::init();
249 249 let mut app = App::new(conn, node_id)?;
250 250
@@ -526,12 +526,9 @@ fn render_list(app: &mut App, f: &mut Frame, area: Rect) {
526 526 }
527 527
528 528 fn render_detail(app: &App, f: &mut Frame, area: Rect) {
529 - let ticket = match app.selected_ticket() {
530 - Some(t) => t,
531 - None => {
532 - f.render_widget(Paragraph::new("No ticket selected"), area);
533 - return;
534 - }
529 + let Some(ticket) = app.selected_ticket() else {
530 + f.render_widget(Paragraph::new("No ticket selected"), area);
531 + return;
535 532 };
536 533
537 534 let mut lines = vec![
@@ -641,9 +638,8 @@ fn render_create_popup(app: &App, f: &mut Frame, area: Rect) {
641 638 }
642 639
643 640 fn render_edit_popup(app: &App, f: &mut Frame, area: Rect) {
644 - let buf = match app.edit_buf.as_ref() {
645 - Some(b) => b,
646 - None => return,
641 + let Some(buf) = app.edit_buf.as_ref() else {
642 + return;
647 643 };
648 644
649 645 let popup_width = 70.min(area.width.saturating_sub(4));
M wam/src/types.rs +15 -15
@@ -59,13 +59,13 @@ const BAND_HIGH: u32 = 50;
59 59 const BAND_MEDIUM: u32 = 20;
60 60
61 61 /// Default 1-5 guesstimate for both `pain` and `scale` when unspecified.
62 - pub const DEFAULT_FACTOR: u8 = 3;
62 + pub(crate) const DEFAULT_FACTOR: u8 = 3;
63 63
64 64 // -- Priority (color band) ----------------------------------------------------
65 65
66 66 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
67 67 #[serde(rename_all = "lowercase")]
68 - pub enum Priority {
68 + pub(crate) enum Priority {
69 69 Low,
70 70 Medium,
71 71 High,
@@ -73,7 +73,7 @@ pub enum Priority {
73 73 }
74 74
75 75 impl Priority {
76 - pub fn color(self) -> Color {
76 + pub(crate) fn color(self) -> Color {
77 77 match self {
78 78 Self::Low => Color::DarkGray,
79 79 Self::Medium => Color::White,
@@ -83,7 +83,7 @@ impl Priority {
83 83 }
84 84
85 85 /// Bucket a 0-100 painhours score into a color band.
86 - pub fn from_painhours(score: u32) -> Self {
86 + pub(crate) fn from_painhours(score: u32) -> Self {
87 87 if score >= BAND_CRITICAL {
88 88 Self::Critical
89 89 } else if score >= BAND_HIGH {
@@ -124,7 +124,7 @@ impl FromStr for Priority {
124 124
125 125 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
126 126 #[serde(rename_all = "lowercase")]
127 - pub enum Status {
127 + pub(crate) enum Status {
128 128 Open,
129 129 InProgress,
130 130 Resolved,
@@ -132,7 +132,7 @@ pub enum Status {
132 132 }
133 133
134 134 impl Status {
135 - pub fn indicator(self) -> char {
135 + pub(crate) fn indicator(self) -> char {
136 136 match self {
137 137 Self::Open => '*',
138 138 Self::InProgress => '>',
@@ -171,7 +171,7 @@ impl FromStr for Status {
171 171 /// Ticket channel: who is communicating with whom.
172 172 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
173 173 #[serde(rename_all = "lowercase")]
174 - pub enum Channel {
174 + pub(crate) enum Channel {
175 175 /// System-to-system: automated alerts, CI results, health checks.
176 176 /// No human response expected.
177 177 System,
@@ -207,7 +207,7 @@ impl FromStr for Channel {
207 207 // -- Ticket -------------------------------------------------------------------
208 208
209 209 #[derive(Debug, Clone, Serialize, Deserialize)]
210 - pub struct Ticket {
210 + pub(crate) struct Ticket {
211 211 pub id: String,
212 212 pub title: String,
213 213 pub body: Option<String>,
@@ -227,7 +227,7 @@ pub struct Ticket {
227 227
228 228 impl Ticket {
229 229 /// Human-readable age string (e.g. "3m", "2h", "5d").
230 - pub fn age(&self) -> String {
230 + pub(crate) fn age(&self) -> String {
231 231 let dur = Utc::now() - self.created_at;
232 232 if dur.num_days() > 0 {
233 233 format!("{}d", dur.num_days())
@@ -249,7 +249,7 @@ impl Ticket {
249 249
250 250 /// Age in whole weeks, rounded up (overestimating is fine), floored at 1 so
251 251 /// a brand-new ticket still contributes to its score.
252 - pub fn age_weeks(&self) -> u32 {
252 + pub(crate) fn age_weeks(&self) -> u32 {
253 253 let secs = (self.age_anchor() - self.created_at).num_seconds().max(0);
254 254 let weeks = (secs as f64 / (7.0 * 86_400.0)).ceil();
255 255 (weeks as u32).max(1)
@@ -258,7 +258,7 @@ impl Ticket {
258 258 /// The painhours score: a 0-100 urgency number operators sort by.
259 259 /// `round(100 * (1 - e^(-raw/K)))` where
260 260 /// `raw = pain^PAIN_EXP * scale^SCALE_EXP * age_weeks^AGE_EXP`.
261 - pub fn painhours(&self) -> u32 {
261 + pub(crate) fn painhours(&self) -> u32 {
262 262 let pain = (self.pain.clamp(1, 5) as f64).powf(PAIN_EXP);
263 263 let scale = (self.scale.clamp(1, 5) as f64).powf(SCALE_EXP);
264 264 let age = (self.age_weeks() as f64).powf(AGE_EXP);
@@ -267,17 +267,17 @@ impl Ticket {
267 267 }
268 268
269 269 /// Color band derived from the painhours score.
270 - pub fn band(&self) -> Priority {
270 + pub(crate) fn band(&self) -> Priority {
271 271 Priority::from_painhours(self.painhours())
272 272 }
273 273
274 274 /// Short ID prefix for display.
275 - pub fn short_id(&self) -> &str {
275 + pub(crate) fn short_id(&self) -> &str {
276 276 &self.id[..8.min(self.id.len())]
277 277 }
278 278
279 279 /// Short node ID for display.
280 - pub fn short_node(&self) -> &str {
280 + pub(crate) fn short_node(&self) -> &str {
281 281 &self.node_id[..8.min(self.node_id.len())]
282 282 }
283 283 }
@@ -285,7 +285,7 @@ impl Ticket {
285 285 // -- NewTicket (for create) ---------------------------------------------------
286 286
287 287 #[derive(Debug, Serialize, Deserialize)]
288 - pub struct NewTicket {
288 + pub(crate) struct NewTicket {
289 289 pub title: String,
290 290 #[serde(default)]
291 291 pub body: Option<String>,