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}