handoff-mcp-server 0.24.5 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,71 @@ use std::collections::HashMap;
2
2
  use std::path::Path;
3
3
 
4
4
  use anyhow::{Context, Result};
5
- use serde::{Deserialize, Serialize};
5
+ use serde::de::{self, SeqAccess};
6
+ use serde::{Deserialize, Deserializer, Serialize};
7
+
8
+ /// Convert a weekday name to its number (0=Sun..6=Sat).
9
+ pub fn weekday_to_num(s: &str) -> Option<u32> {
10
+ match s.to_lowercase().as_str() {
11
+ "sun" | "sunday" => Some(0),
12
+ "mon" | "monday" => Some(1),
13
+ "tue" | "tuesday" => Some(2),
14
+ "wed" | "wednesday" => Some(3),
15
+ "thu" | "thursday" => Some(4),
16
+ "fri" | "friday" => Some(5),
17
+ "sat" | "saturday" => Some(6),
18
+ _ => None,
19
+ }
20
+ }
21
+
22
+ fn deserialize_weekdays<'de, D>(deserializer: D) -> std::result::Result<Vec<u32>, D::Error>
23
+ where
24
+ D: Deserializer<'de>,
25
+ {
26
+ struct WeekdayVisitor;
27
+
28
+ impl<'de> de::Visitor<'de> for WeekdayVisitor {
29
+ type Value = Vec<u32>;
30
+
31
+ fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
32
+ f.write_str("an array of weekday numbers (0-6) or names (\"sun\"..\"sat\")")
33
+ }
34
+
35
+ fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Vec<u32>, A::Error>
36
+ where
37
+ A: SeqAccess<'de>,
38
+ {
39
+ let mut vals = Vec::new();
40
+ while let Some(elem) = seq.next_element::<toml::Value>()? {
41
+ match &elem {
42
+ toml::Value::Integer(n) => {
43
+ let n = *n as u32;
44
+ if n > 6 {
45
+ return Err(de::Error::custom(format!(
46
+ "weekday number {n} out of range 0-6"
47
+ )));
48
+ }
49
+ vals.push(n);
50
+ }
51
+ toml::Value::String(s) => {
52
+ let n = weekday_to_num(s).ok_or_else(|| {
53
+ de::Error::custom(format!("unknown weekday name: \"{s}\""))
54
+ })?;
55
+ vals.push(n);
56
+ }
57
+ _ => {
58
+ return Err(de::Error::custom(
59
+ "closed_weekdays elements must be integers or strings",
60
+ ));
61
+ }
62
+ }
63
+ }
64
+ Ok(vals)
65
+ }
66
+ }
67
+
68
+ deserializer.deserialize_seq(WeekdayVisitor)
69
+ }
6
70
 
7
71
  #[derive(Debug, Clone, Serialize, Deserialize)]
8
72
  pub struct Config {
@@ -66,9 +130,16 @@ pub struct SettingsConfig {
66
130
  #[serde(default = "default_memory_dup_threshold")]
67
131
  pub memory_dup_threshold: f64,
68
132
  /// BM25 relevance floor for `memory_query`; scores below are not returned.
69
- /// Default 0.5.
133
+ /// Default 2.0.
70
134
  #[serde(default = "default_memory_query_min_score")]
71
135
  pub memory_query_min_score: f64,
136
+ /// Relative threshold (0.0–1.0) for `memory_query`: after the absolute
137
+ /// `min_score` floor, a candidate is dropped unless its score is at least
138
+ /// `top_score × relative_threshold`. Prevents low-relevance "tail" matches
139
+ /// from riding a strong top hit. 0.0 disables (keep everything above
140
+ /// `min_score`). Default 0.3.
141
+ #[serde(default = "default_memory_query_relative_threshold")]
142
+ pub memory_query_relative_threshold: f64,
72
143
  /// Maximum number of memories `memory_query` returns per call. Default 5.
73
144
  #[serde(default = "default_memory_query_limit")]
74
145
  pub memory_query_limit: u32,
@@ -114,7 +185,12 @@ pub struct CalendarConfig {
114
185
  #[serde(default, skip_serializing_if = "Option::is_none")]
115
186
  pub work_hours_per_day: Option<f64>,
116
187
  /// Weekday numbers (0=Sun..6=Sat) that are non-working.
117
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
188
+ /// Accepts integers or weekday names ("sun", "sat", etc.) in TOML.
189
+ #[serde(
190
+ default,
191
+ skip_serializing_if = "Vec::is_empty",
192
+ deserialize_with = "deserialize_weekdays"
193
+ )]
118
194
  pub closed_weekdays: Vec<u32>,
119
195
  /// Specific YYYY-MM-DD dates that are non-working (override weekdays).
120
196
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
@@ -155,7 +231,11 @@ pub struct AssigneeConfig {
155
231
  pub color: Option<String>,
156
232
  #[serde(default, skip_serializing_if = "Option::is_none")]
157
233
  pub work_hours_per_day: Option<f64>,
158
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
234
+ #[serde(
235
+ default,
236
+ skip_serializing_if = "Vec::is_empty",
237
+ deserialize_with = "deserialize_weekdays"
238
+ )]
159
239
  pub closed_weekdays: Vec<u32>,
160
240
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
161
241
  pub closed_dates: Vec<String>,
@@ -252,7 +332,11 @@ fn default_memory_dup_threshold() -> f64 {
252
332
  }
253
333
 
254
334
  fn default_memory_query_min_score() -> f64 {
255
- 0.5
335
+ 2.0
336
+ }
337
+
338
+ fn default_memory_query_relative_threshold() -> f64 {
339
+ 0.3
256
340
  }
257
341
 
258
342
  fn default_memory_query_limit() -> u32 {
@@ -299,6 +383,7 @@ impl Default for SettingsConfig {
299
383
  memory_enabled: default_memory_enabled(),
300
384
  memory_dup_threshold: default_memory_dup_threshold(),
301
385
  memory_query_min_score: default_memory_query_min_score(),
386
+ memory_query_relative_threshold: default_memory_query_relative_threshold(),
302
387
  memory_query_limit: default_memory_query_limit(),
303
388
  memory_stale_days: default_memory_stale_days(),
304
389
  memory_injected_gc_days: default_memory_injected_gc_days(),
@@ -364,3 +449,130 @@ pub fn write_config(path: &Path, config: &Config) -> Result<()> {
364
449
  .with_context(|| format!("Failed to write config: {}", path.display()))?;
365
450
  Ok(())
366
451
  }
452
+
453
+ #[cfg(test)]
454
+ mod tests {
455
+ use super::*;
456
+
457
+ fn parse_config(toml_str: &str) -> Config {
458
+ toml::from_str(toml_str).unwrap()
459
+ }
460
+
461
+ #[test]
462
+ fn closed_weekdays_string_names() {
463
+ let cfg = parse_config(
464
+ r#"
465
+ [project]
466
+ name = "test"
467
+ [calendar]
468
+ closed_weekdays = ["sun", "sat"]
469
+ "#,
470
+ );
471
+ assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
472
+ }
473
+
474
+ #[test]
475
+ fn closed_weekdays_integer_values() {
476
+ let cfg = parse_config(
477
+ r#"
478
+ [project]
479
+ name = "test"
480
+ [calendar]
481
+ closed_weekdays = [0, 6]
482
+ "#,
483
+ );
484
+ assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
485
+ }
486
+
487
+ #[test]
488
+ fn closed_weekdays_mixed() {
489
+ let cfg = parse_config(
490
+ r#"
491
+ [project]
492
+ name = "test"
493
+ [calendar]
494
+ closed_weekdays = ["sun", 6]
495
+ "#,
496
+ );
497
+ assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
498
+ }
499
+
500
+ #[test]
501
+ fn closed_weekdays_empty() {
502
+ let cfg = parse_config(
503
+ r#"
504
+ [project]
505
+ name = "test"
506
+ "#,
507
+ );
508
+ assert!(cfg.calendar.closed_weekdays.is_empty());
509
+ }
510
+
511
+ #[test]
512
+ fn closed_weekdays_full_names() {
513
+ let cfg = parse_config(
514
+ r#"
515
+ [project]
516
+ name = "test"
517
+ [calendar]
518
+ closed_weekdays = ["sunday", "saturday"]
519
+ "#,
520
+ );
521
+ assert_eq!(cfg.calendar.closed_weekdays, vec![0, 6]);
522
+ }
523
+
524
+ #[test]
525
+ fn assignee_closed_weekdays_strings() {
526
+ let cfg = parse_config(
527
+ r#"
528
+ [project]
529
+ name = "test"
530
+ [assignees.alice]
531
+ closed_weekdays = ["mon", "fri"]
532
+ "#,
533
+ );
534
+ let alice = cfg.assignees.get("alice").unwrap();
535
+ assert_eq!(alice.closed_weekdays, vec![1, 5]);
536
+ }
537
+
538
+ #[test]
539
+ fn closed_weekdays_invalid_name() {
540
+ let result = toml::from_str::<Config>(
541
+ r#"
542
+ [project]
543
+ name = "test"
544
+ [calendar]
545
+ closed_weekdays = ["funday"]
546
+ "#,
547
+ );
548
+ assert!(result.is_err());
549
+ }
550
+
551
+ #[test]
552
+ fn closed_weekdays_out_of_range() {
553
+ let result = toml::from_str::<Config>(
554
+ r#"
555
+ [project]
556
+ name = "test"
557
+ [calendar]
558
+ closed_weekdays = [7]
559
+ "#,
560
+ );
561
+ assert!(result.is_err());
562
+ }
563
+
564
+ #[test]
565
+ fn round_trip_preserves_integer_format() {
566
+ let cfg = parse_config(
567
+ r#"
568
+ [project]
569
+ name = "test"
570
+ [calendar]
571
+ closed_weekdays = ["sun", "sat"]
572
+ "#,
573
+ );
574
+ let serialized = toml::to_string_pretty(&cfg).unwrap();
575
+ let re_parsed = parse_config(&serialized);
576
+ assert_eq!(re_parsed.calendar.closed_weekdays, vec![0, 6]);
577
+ }
578
+ }