1use std::fmt::Write as _;
8
9const PLATFORM_DEFAULT_SOURCE: &str = "platform default config path";
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum SettingScope {
14 Shared,
16 Cli,
18 Mcp,
20}
21
22impl SettingScope {
23 pub const fn as_str(self) -> &'static str {
25 match self {
26 Self::Shared => "shared",
27 Self::Cli => "CLI",
28 Self::Mcp => "MCP",
29 }
30 }
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
35pub struct SettingDoc {
36 pub section: &'static str,
38 pub key: &'static str,
40 pub value_type: &'static str,
42 pub default: &'static str,
44 pub description: &'static str,
46 pub scope: SettingScope,
48}
49
50#[derive(Clone, Copy, Debug)]
52pub struct SettingsHelp {
53 docs: &'static [SettingDoc],
54 config_path_precedence: &'static [&'static str],
55}
56
57impl SettingsHelp {
58 pub const fn docs(self) -> &'static [SettingDoc] {
60 self.docs
61 }
62
63 pub const fn config_path_precedence(self) -> &'static [&'static str] {
65 self.config_path_precedence
66 }
67
68 pub fn render_human(self) -> String {
70 let mut output = String::from("plugmem settings\n\n");
71 output.push_str("Config file precedence:\n");
72 for (index, source) in self.config_path_precedence.iter().enumerate() {
73 if *source == PLATFORM_DEFAULT_SOURCE {
74 match crate::default_config_path() {
75 Some(path) => {
76 let _ = writeln!(output, " {}. {}", index + 1, path.display());
77 }
78 None => {
79 let _ = writeln!(output, " {}. {source} (unavailable)", index + 1);
80 }
81 }
82 } else {
83 let _ = writeln!(output, " {}. {source}", index + 1);
84 }
85 }
86 output.push('\n');
87
88 let mut section = None;
89 for doc in self.docs {
90 if section != Some(doc.section) {
91 if section.is_some() {
92 output.push('\n');
93 }
94 let _ = writeln!(output, "[{}]", doc.section);
95 section = Some(doc.section);
96 }
97 let _ = writeln!(
98 output,
99 " {} ({}, default: {}) — {} [{}]",
100 doc.key,
101 doc.value_type,
102 doc.default,
103 doc.description,
104 doc.scope.as_str()
105 );
106 }
107
108 output
109 }
110}
111
112const CONFIG_PATH_PRECEDENCE: &[&str] = &[
113 "--config PATH",
114 "$PLUGMEM_CONFIG",
115 "platform default config path",
116 "built-in defaults",
117];
118
119const DOCS: &[SettingDoc] = &[
120 SettingDoc {
121 section: "database",
122 key: "path",
123 value_type: "path string",
124 default: "platform data directory/memory.plugmem",
125 description: "Persistent database file; explicit --db/constructor path and PLUGMEM_DB override it",
126 scope: SettingScope::Shared,
127 },
128 SettingDoc {
129 section: "engine",
130 key: "dim",
131 value_type: "non-negative integer",
132 default: "0",
133 description: "Embedding dimension; 0 disables vector storage",
134 scope: SettingScope::Shared,
135 },
136 SettingDoc {
137 section: "engine",
138 key: "max_bytes",
139 value_type: "non-negative integer",
140 default: "2147483648",
141 description: "Total byte-pool ceiling",
142 scope: SettingScope::Shared,
143 },
144 SettingDoc {
145 section: "engine",
146 key: "max_text",
147 value_type: "non-negative integer",
148 default: "4096",
149 description: "Maximum fact text length in bytes",
150 scope: SettingScope::Shared,
151 },
152 SettingDoc {
153 section: "engine",
154 key: "max_blob",
155 value_type: "non-negative integer",
156 default: "65536",
157 description: "Maximum single blob length in bytes",
158 scope: SettingScope::Shared,
159 },
160 SettingDoc {
161 section: "engine",
162 key: "shards_facts",
163 value_type: "power-of-two integer",
164 default: "1024",
165 description: "Facts arena shard count",
166 scope: SettingScope::Shared,
167 },
168 SettingDoc {
169 section: "engine",
170 key: "shards_entities",
171 value_type: "power-of-two integer",
172 default: "256",
173 description: "Entities arena shard count",
174 scope: SettingScope::Shared,
175 },
176 SettingDoc {
177 section: "engine",
178 key: "shards_edges",
179 value_type: "power-of-two integer",
180 default: "512",
181 description: "Edges arena shard count",
182 scope: SettingScope::Shared,
183 },
184 SettingDoc {
185 section: "engine",
186 key: "shards_temporal",
187 value_type: "power-of-two integer",
188 default: "512",
189 description: "Temporal arena shard count",
190 scope: SettingScope::Shared,
191 },
192 SettingDoc {
193 section: "engine",
194 key: "shards_postings",
195 value_type: "power-of-two integer",
196 default: "2048",
197 description: "BM25 postings arena shard count",
198 scope: SettingScope::Shared,
199 },
200 SettingDoc {
201 section: "embedder",
202 key: "kind",
203 value_type: "string",
204 default: "none",
205 description: "Embedding provider: none, ollama, openai, lmstudio, vllm or llamacpp",
206 scope: SettingScope::Shared,
207 },
208 SettingDoc {
209 section: "embedder",
210 key: "url",
211 value_type: "string",
212 default: "unset",
213 description: "OpenAI-compatible /v1/embeddings endpoint",
214 scope: SettingScope::Shared,
215 },
216 SettingDoc {
217 section: "embedder",
218 key: "model",
219 value_type: "string",
220 default: "unset",
221 description: "Embedding model name",
222 scope: SettingScope::Shared,
223 },
224 SettingDoc {
225 section: "embedder",
226 key: "api_key_env",
227 value_type: "string",
228 default: "unset",
229 description: "Environment variable containing the bearer token",
230 scope: SettingScope::Shared,
231 },
232 SettingDoc {
233 section: "maintenance",
234 key: "snapshot_every_ops",
235 value_type: "non-negative integer",
236 default: "1024",
237 description: "Snapshot after this many mutations",
238 scope: SettingScope::Shared,
239 },
240 SettingDoc {
241 section: "maintenance",
242 key: "snapshot_journal_bytes",
243 value_type: "non-negative integer",
244 default: "4194304",
245 description: "Snapshot when the journal reaches this size",
246 scope: SettingScope::Shared,
247 },
248 SettingDoc {
249 section: "maintenance",
250 key: "maintain_every_forgets",
251 value_type: "non-negative integer",
252 default: "off",
253 description: "Run physical tombstone maintenance after this many forgets",
254 scope: SettingScope::Shared,
255 },
256 SettingDoc {
257 section: "maintenance",
258 key: "batch_size",
259 value_type: "positive integer",
260 default: "128",
261 description: "CLI import facts per embedding request and journal fsync",
262 scope: SettingScope::Cli,
263 },
264 SettingDoc {
265 section: "server",
266 key: "workers",
267 value_type: "positive integer",
268 default: "half of available cores",
269 description: "MCP worker threads",
270 scope: SettingScope::Mcp,
271 },
272];
273
274static SETTINGS_HELP: SettingsHelp = SettingsHelp {
275 docs: DOCS,
276 config_path_precedence: CONFIG_PATH_PRECEDENCE,
277};
278
279pub const fn settings_help() -> &'static SettingsHelp {
281 &SETTINGS_HELP
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 #[test]
289 fn every_documented_setting_has_a_complete_description() {
290 assert!(!DOCS.is_empty());
291 for doc in DOCS {
292 assert!(!doc.section.is_empty());
293 assert!(!doc.key.is_empty());
294 assert!(!doc.value_type.is_empty());
295 assert!(!doc.default.is_empty());
296 assert!(!doc.description.is_empty());
297 }
298 }
299
300 #[test]
301 fn human_help_contains_every_documented_key() {
302 let rendered = settings_help().render_human();
303 for doc in DOCS {
304 assert!(
305 rendered.contains(doc.key),
306 "missing {}.{}",
307 doc.section,
308 doc.key
309 );
310 }
311 }
312}