Skip to main content

wenlan_types/
memory_type.rs

1// SPDX-License-Identifier: Apache-2.0
2//! JSON Schema description strings for the `memory_type` parameter.
3//!
4//! The canonical type set is owned by [`crate::sources::MemoryType`] and
5//! returned by [`MemoryType::all_values`]. This module exposes the prose
6//! description strings that downstream tool schemas (MCP `capture`,
7//! MCP `recall`, future MCP/Tauri schemas) advertise to clients.
8//!
9//! The drift tests below iterate over the canonical enum and assert each
10//! description string lists every variant. Adding a variant to
11//! [`crate::sources::MemoryType`] but forgetting to extend the description
12//! prose fails CI here — not silently in production.
13
14/// JSON Schema `description` for the `memory_type` parameter on memory-write
15/// tools (e.g. MCP `capture`). Lists the two-level filter values (profile /
16/// knowledge) and the precise types, plus the "auto-classified if omitted"
17/// hint that steers agents away from guessing.
18pub const MEMORY_TYPE_CAPTURE_DESCRIPTION: &str =
19    "\"profile\" (about the user) or \"knowledge\" (about the world) — or precise: \
20     \"identity\", \"preference\", \"decision\", \"lesson\", \"gotcha\", \"fact\" — \
21     auto-classified if omitted";
22
23/// JSON Schema `description` for the `memory_type` parameter on memory-read
24/// filter tools (e.g. MCP `recall`, `list_pending`). Same vocabulary as
25/// capture, framed as a filter.
26pub const MEMORY_TYPE_FILTER_DESCRIPTION: &str =
27    "Filter by type. Two-level filter: \"profile\" (user-facing) or \
28     \"knowledge\" (world-facing), or precise: \"identity\", \"preference\", \
29     \"decision\", \"lesson\", \"gotcha\", \"fact\".";
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34    use crate::sources::MemoryType;
35
36    #[test]
37    fn capture_description_lists_every_canonical_type() {
38        for ty in MemoryType::all_values() {
39            let needle = format!("\"{ty}\"");
40            assert!(
41                MEMORY_TYPE_CAPTURE_DESCRIPTION.contains(&needle),
42                "MEMORY_TYPE_CAPTURE_DESCRIPTION missing \"{ty}\""
43            );
44        }
45    }
46
47    #[test]
48    fn filter_description_lists_every_canonical_type() {
49        for ty in MemoryType::all_values() {
50            let needle = format!("\"{ty}\"");
51            assert!(
52                MEMORY_TYPE_FILTER_DESCRIPTION.contains(&needle),
53                "MEMORY_TYPE_FILTER_DESCRIPTION missing \"{ty}\""
54            );
55        }
56    }
57
58    /// "goal" is a legacy alias folded to Identity by `MemoryType::FromStr`.
59    /// It must NOT be advertised in any description surface, or clients will
60    /// believe they can store with `memory_type = "goal"` and get an
61    /// unexpected fold.
62    #[test]
63    fn descriptions_omit_legacy_goal() {
64        for desc in [
65            MEMORY_TYPE_CAPTURE_DESCRIPTION,
66            MEMORY_TYPE_FILTER_DESCRIPTION,
67        ] {
68            assert!(
69                !contains_word(desc, "goal"),
70                "description must not advertise legacy \"goal\": {desc}"
71            );
72        }
73    }
74
75    /// Word-boundary contains: returns true iff `needle` appears as a
76    /// standalone alphanumeric token (not a substring of a longer word).
77    /// Used by drift tests to ensure "goal" rejection doesn't false-match
78    /// on "goals" (plural English noun) elsewhere in prose.
79    pub(crate) fn contains_word(haystack: &str, needle: &str) -> bool {
80        haystack
81            .split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
82            .any(|tok| tok == needle)
83    }
84
85    #[test]
86    fn contains_word_rejects_partial_matches() {
87        assert!(contains_word("goal", "goal"));
88        assert!(contains_word("/ goal /", "goal"));
89        assert!(contains_word("goal,", "goal"));
90        assert!(contains_word("(goal)", "goal"));
91        assert!(!contains_word("goals", "goal"));
92        assert!(!contains_word("their goals here", "goal"));
93        assert!(!contains_word("subgoal", "goal"));
94    }
95}