Skip to main content

packset_core/
extract.rs

1//! Explicit keep-lines and hard filters. Deterministic; no model on write.
2
3/// A write the MemoryAgentBench seat protocol will ingest.
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub enum SeatWrite {
6    Lesson(String),
7    Preference(String),
8    Accept(String),
9}
10
11/// Remember / Prefer / Accept only. Raw context, habits, and dumps refuse.
12#[must_use]
13pub fn admit_seat_write(text: &str) -> Option<SeatWrite> {
14    let last = text.trim().lines().last()?.trim();
15    let lower = last.to_ascii_lowercase();
16    if let Some(rest) = strip_prefix_ci(&lower, last, "accept:") {
17        let id = rest
18            .trim()
19            .trim_matches(|c: char| c == ':' || c.is_whitespace());
20        if id.len() >= 4 {
21            return Some(SeatWrite::Accept(id.to_string()));
22        }
23        return None;
24    }
25    match claim_from_user(text)? {
26        ("lesson", claim) => Some(SeatWrite::Lesson(claim)),
27        ("preference", claim) => Some(SeatWrite::Preference(claim)),
28        _ => None,
29    }
30}
31
32/// Return (kind, claim) for an explicit keep directive.
33pub fn claim_from_user(text: &str) -> Option<(&'static str, String)> {
34    let last = text.trim().lines().last()?.trim();
35    if last.ends_with('?') {
36        return None;
37    }
38    let lower = last.to_ascii_lowercase();
39    // A dispatch over three prefixes, not an early return: `?` would collapse
40    // the first arm and leave the other two unreachable.
41    #[allow(clippy::question_mark)]
42    let (kind, rest) = if let Some(r) = strip_prefix_ci(&lower, last, "remember:") {
43        ("lesson", r)
44    } else if let Some(r) = strip_prefix_ci(&lower, last, "from now on:") {
45        ("habit", r)
46    } else if let Some(r) = strip_prefix_ci(&lower, last, "prefer:") {
47        ("preference", r)
48    } else {
49        return None;
50    };
51    let claim = rest
52        .trim_start_matches([':', ' ', ','])
53        .trim()
54        .trim_end_matches(['.', ',', ';', ':']);
55    if claim.len() < 8 {
56        return None;
57    }
58    Some((kind, claim.to_string()))
59}
60
61fn strip_prefix_ci<'a>(lower: &str, orig: &'a str, prefix: &str) -> Option<&'a str> {
62    if lower.starts_with(prefix) {
63        Some(&orig[prefix.len()..])
64    } else {
65        None
66    }
67}
68
69/// Tool stdout, listings, and fetched bodies are not atoms.
70pub fn is_tool_dump(text: &str) -> bool {
71    let t = text.trim();
72    if t.is_empty() {
73        return true;
74    }
75    let lower = t.to_ascii_lowercase();
76    if lower.contains("```") && (lower.contains("stdout") || lower.contains("stderr")) {
77        return true;
78    }
79    if lower.starts_with("<!doctype") || lower.starts_with("<html") {
80        return true;
81    }
82    let lines: Vec<&str> = t.lines().collect();
83    if lines.len() >= 8
84        && lines
85            .iter()
86            .filter(|l| l.starts_with('-') || l.starts_with("drwx") || l.starts_with("total "))
87            .count()
88            >= 6
89    {
90        return true;
91    }
92    false
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn remember_line() {
101        let (k, c) = claim_from_user("Remember: always pin the review set").unwrap();
102        assert_eq!(k, "lesson");
103        assert!(c.contains("pin the review set"));
104    }
105
106    #[test]
107    fn note_that_and_remember_that_are_not_claims() {
108        assert!(claim_from_user("Note that the test failed on line 12").is_none());
109        assert!(claim_from_user("Remember that: pin the review set").is_none());
110        assert!(claim_from_user("Prefer conventional commits").is_none());
111        assert!(claim_from_user("From now on, file a ticket first").is_none());
112    }
113
114    #[test]
115    fn seat_write_is_remember_prefer_or_accept() {
116        assert!(matches!(
117            admit_seat_write("Remember: pin the review set"),
118            Some(SeatWrite::Lesson(_))
119        ));
120        assert!(matches!(
121            admit_seat_write("Prefer: conventional commits always"),
122            Some(SeatWrite::Preference(_))
123        ));
124        assert!(matches!(
125            admit_seat_write("Accept: ab12cd"),
126            Some(SeatWrite::Accept(id)) if id == "ab12cd"
127        ));
128        assert!(admit_seat_write("From now on: file a ticket first").is_none());
129        assert!(admit_seat_write("The user lives in Berlin and likes tea.").is_none());
130    }
131
132    #[test]
133    fn listing_is_dump() {
134        let blob = (0..8)
135            .map(|i| format!("- file{i}.rs"))
136            .collect::<Vec<_>>()
137            .join("\n");
138        assert!(is_tool_dump(&blob));
139        assert!(!is_tool_dump("Remember: keep the habit."));
140    }
141}