1use std::fmt::Write as _;
9
10const PLATFORM_DEFAULT_SOURCE: &str = "platform default config path";
11
12#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct SettingWarning {
21 pub section: String,
24 pub key: String,
26 pub did_you_mean: Option<&'static str>,
28}
29
30impl std::fmt::Display for SettingWarning {
31 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 if self.section.is_empty() {
34 write!(f, "unknown config section [{}]", self.key)?;
35 } else {
36 write!(f, "unknown setting [{}].{}", self.section, self.key)?;
37 }
38 match self.did_you_mean {
39 Some(near) => write!(f, " — did you mean `{near}`?"),
40 None => write!(f, " (ignored)"),
41 }
42 }
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub enum SettingScope {
48 Shared,
50 Cli,
52 Mcp,
54}
55
56impl SettingScope {
57 pub const fn as_str(self) -> &'static str {
59 match self {
60 Self::Shared => "shared",
61 Self::Cli => "CLI",
62 Self::Mcp => "MCP",
63 }
64 }
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub struct SettingDoc {
70 pub section: &'static str,
72 pub key: &'static str,
74 pub value_type: &'static str,
76 pub default: &'static str,
78 pub description: &'static str,
80 pub example: &'static str,
89 pub scope: SettingScope,
91}
92
93#[derive(Clone, Copy, Debug)]
95pub struct SettingsHelp {
96 docs: &'static [SettingDoc],
97 config_path_precedence: &'static [&'static str],
98}
99
100impl SettingsHelp {
101 pub const fn docs(self) -> &'static [SettingDoc] {
103 self.docs
104 }
105
106 pub const fn config_path_precedence(self) -> &'static [&'static str] {
108 self.config_path_precedence
109 }
110
111 fn sections(self) -> impl Iterator<Item = &'static str> {
116 self.docs
117 .iter()
118 .enumerate()
119 .filter(|(i, doc)| *i == 0 || self.docs[i - 1].section != doc.section)
120 .map(|(_, doc)| doc.section)
121 }
122
123 fn keys_in(self, section: &str) -> impl Iterator<Item = &'static str> {
125 self.docs
126 .iter()
127 .filter(move |doc| doc.section == section)
128 .map(|doc| doc.key)
129 }
130
131 pub fn unknown_in(self, table: &toml::Table) -> Vec<SettingWarning> {
141 let mut out = Vec::new();
142 for (name, value) in table {
143 let Some(section) = self.sections().find(|s| s == name) else {
144 out.push(SettingWarning {
145 section: String::new(),
146 key: name.clone(),
147 did_you_mean: nearest(name, self.sections()),
148 });
149 continue;
150 };
151 let Some(entries) = value.as_table() else {
154 continue;
155 };
156 for key in entries.keys() {
157 if self.keys_in(section).any(|k| k == key) {
158 continue;
159 }
160 out.push(SettingWarning {
161 section: section.to_string(),
162 key: key.clone(),
163 did_you_mean: nearest(key, self.keys_in(section)),
164 });
165 }
166 }
167 out
168 }
169
170 pub fn render_config_example(self, set: bool) -> String {
187 let mut output = String::new();
191 for line in [
192 "# plugmem — every supported config.toml key, with its default.",
193 "#",
194 "# GENERATED from the settings catalogue. Do not edit: run",
195 "# cargo run -p plugmem-host --bin config_example",
196 "# and commit the result. A test fails when this file and the",
197 "# catalogue disagree, so an edit here is undone by the next run.",
198 "#",
199 "# Every key below is commented out and shows the default this build",
200 "# uses. Uncomment only what you mean to change — a config that sets",
201 "# everything explicitly freezes today's defaults, and never picks up",
202 "# a better one.",
203 "#",
204 "# What each setting is FOR, and when changing it is a good idea,",
205 "# lives in crates/plugmem-host/SETTINGS.md. This file is the shape;",
206 "# that file is the reasoning.",
207 "#",
208 ] {
209 output.push_str(line);
210 output.push('\n');
211 }
212 output.push_str("# Config file precedence, highest first:\n");
213 for (index, source) in self.config_path_precedence.iter().enumerate() {
214 let _ = writeln!(output, "# {}. {source}", index + 1);
215 }
216
217 let mut section = None;
218 for doc in self.docs {
219 if section != Some(doc.section) {
220 let _ = write!(output, "\n[{}]\n", doc.section);
221 section = Some(doc.section);
222 }
223 let scope = match doc.scope {
224 SettingScope::Shared => String::new(),
225 other => format!(", read only by {}", other.as_str()),
226 };
227 let _ = writeln!(
228 output,
229 "# {} — {}\n# {}, default: {}{}",
230 doc.key, doc.description, doc.value_type, doc.default, scope
231 );
232 let prefix = if set { "" } else { "# " };
233 let _ = writeln!(output, "{prefix}{} = {}", doc.key, doc.example);
234 }
235 output
236 }
237
238 pub fn render_human(self) -> String {
240 let mut output = String::from("plugmem settings\n\n");
241 output.push_str("Config file precedence:\n");
242 for (index, source) in self.config_path_precedence.iter().enumerate() {
243 if *source == PLATFORM_DEFAULT_SOURCE {
244 match crate::default_config_path() {
245 Some(path) => {
246 let _ = writeln!(output, " {}. {}", index + 1, path.display());
247 }
248 None => {
249 let _ = writeln!(output, " {}. {source} (unavailable)", index + 1);
250 }
251 }
252 } else {
253 let _ = writeln!(output, " {}. {source}", index + 1);
254 }
255 }
256 output.push('\n');
257
258 let mut section = None;
259 for doc in self.docs {
260 if section != Some(doc.section) {
261 if section.is_some() {
262 output.push('\n');
263 }
264 let _ = writeln!(output, "[{}]", doc.section);
265 section = Some(doc.section);
266 }
267 let _ = writeln!(
268 output,
269 " {} ({}, default: {}) — {} [{}]",
270 doc.key,
271 doc.value_type,
272 doc.default,
273 doc.description,
274 doc.scope.as_str()
275 );
276 }
277
278 output
279 }
280}
281
282fn nearest(typo: &str, candidates: impl Iterator<Item = &'static str>) -> Option<&'static str> {
294 let budget = 1 + typo.chars().count() / 4;
295 candidates
296 .map(|c| (edit_distance(typo, c), c))
297 .filter(|(d, _)| *d <= budget)
298 .min_by_key(|(d, _)| *d)
299 .map(|(_, c)| c)
300}
301
302fn edit_distance(a: &str, b: &str) -> usize {
307 let b: Vec<char> = b.chars().collect();
308 let mut prev: Vec<usize> = (0..=b.len()).collect();
309 let mut row = vec![0; b.len() + 1];
310 for (i, ca) in a.chars().enumerate() {
311 row[0] = i + 1;
312 for (j, cb) in b.iter().enumerate() {
313 let cost = usize::from(ca != *cb);
314 row[j + 1] = (prev[j] + cost).min(prev[j + 1] + 1).min(row[j] + 1);
315 }
316 core::mem::swap(&mut prev, &mut row);
317 }
318 prev[b.len()]
319}
320
321const CONFIG_PATH_PRECEDENCE: &[&str] = &[
322 "--config PATH",
323 "$PLUGMEM_CONFIG",
324 "platform default config path",
325 "built-in defaults",
326];
327
328const DOCS: &[SettingDoc] = &[
329 SettingDoc {
330 section: "database",
331 key: "path",
332 value_type: "path string",
333 default: "platform data directory/memory.plugmem",
334 example: "\"/var/lib/plugmem/memory.plugmem\"",
335 description: "Persistent database file; an explicit --db or open path and PLUGMEM_DB override it",
336 scope: SettingScope::Shared,
337 },
338 SettingDoc {
339 section: "workspace",
340 key: "dir",
341 value_type: "path string",
342 default: "unset (one database, no workspace)",
343 example: "\"/var/lib/plugmem/memories\"",
344 description: "Directory of named databases; unset means the single-database default",
345 scope: SettingScope::Shared,
346 },
347 SettingDoc {
348 section: "workspace",
349 key: "max_open",
350 value_type: "positive integer",
351 default: "16",
352 example: "16",
353 description: "Hard limit on open workspace databases; an inactive least-recently-used entry is closed, all-active returns Busy",
354 scope: SettingScope::Shared,
355 },
356 SettingDoc {
357 section: "workspace",
358 key: "idle_timeout_ms",
359 value_type: "non-negative integer",
360 default: "60000",
361 example: "60000",
362 description: "Close a workspace database unused this long, releasing its lock; 0 never closes",
363 scope: SettingScope::Shared,
364 },
365 SettingDoc {
366 section: "engine",
367 key: "dim",
368 value_type: "non-negative integer",
369 default: "0",
370 example: "768",
371 description: "Embedding dimension; 0 disables vector storage",
372 scope: SettingScope::Shared,
373 },
374 SettingDoc {
375 section: "engine",
376 key: "max_bytes",
377 value_type: "non-negative integer",
378 default: "2147483648",
379 example: "2147483648",
380 description: "Ceiling applied to each byte pool separately, not to their sum",
381 scope: SettingScope::Shared,
382 },
383 SettingDoc {
384 section: "engine",
385 key: "max_text",
386 value_type: "non-negative integer",
387 default: "4096",
388 example: "4096",
389 description: "Maximum fact text length in bytes",
390 scope: SettingScope::Shared,
391 },
392 SettingDoc {
393 section: "engine",
394 key: "max_blob",
395 value_type: "non-negative integer",
396 default: "65536",
397 example: "65536",
398 description: "Maximum single blob length in bytes",
399 scope: SettingScope::Shared,
400 },
401 SettingDoc {
402 section: "recall",
403 key: "bm25_k1",
404 value_type: "number > 0",
405 default: "1.2",
406 example: "1.2",
407 description: "BM25 term-frequency saturation: higher lets a repeated word keep counting",
408 scope: SettingScope::Shared,
409 },
410 SettingDoc {
411 section: "recall",
412 key: "bm25_b",
413 value_type: "number in [0, 1]",
414 default: "0.75",
415 example: "0.75",
416 description: "BM25 length normalisation: 0 ignores fact length, 1 penalises long facts fully",
417 scope: SettingScope::Shared,
418 },
419 SettingDoc {
420 section: "recall",
421 key: "rrf_k",
422 value_type: "integer >= 1",
423 default: "60",
424 example: "60",
425 description: "Reciprocal-rank-fusion constant: larger flattens the gap between rank 1 and rank 10",
426 scope: SettingScope::Shared,
427 },
428 SettingDoc {
429 section: "recall",
430 key: "w_bm25",
431 value_type: "number >= 0",
432 default: "1.0",
433 example: "1.0",
434 description: "Weight of the lexical source in the fused score; 0 switches it off",
435 scope: SettingScope::Shared,
436 },
437 SettingDoc {
438 section: "recall",
439 key: "w_vec",
440 value_type: "number >= 0",
441 default: "1.0",
442 example: "1.0",
443 description: "Weight of the vector source; 0 switches it off (and costs nothing when dim = 0)",
444 scope: SettingScope::Shared,
445 },
446 SettingDoc {
447 section: "recall",
448 key: "w_graph",
449 value_type: "number >= 0",
450 default: "1.0",
451 example: "1.0",
452 description: "Weight of the entity-graph source; 0 switches off relational expansion",
453 scope: SettingScope::Shared,
454 },
455 SettingDoc {
456 section: "recall",
457 key: "w_time",
458 value_type: "number >= 0",
459 default: "1.0",
460 example: "1.0",
461 description: "Weight of the temporal source (the recorded_at window); 0 switches it off",
462 scope: SettingScope::Shared,
463 },
464 SettingDoc {
465 section: "recall",
466 key: "w_recency",
467 value_type: "number >= 0",
468 default: "0.25",
469 example: "0.25",
470 description: "How much a fact's age discounts it, on top of the sources above",
471 scope: SettingScope::Shared,
472 },
473 SettingDoc {
474 section: "recall",
475 key: "half_life_days",
476 value_type: "integer >= 1",
477 default: "180",
478 example: "180",
479 description: "Age at which the recency discount has halved; larger keeps old facts competitive",
480 scope: SettingScope::Shared,
481 },
482 SettingDoc {
483 section: "recall",
484 key: "graph_depth",
485 value_type: "non-negative integer",
486 default: "2",
487 example: "2",
488 description: "Default hops the graph source may follow from an anchor entity; a recall's own `graph_depth` overrides it. Uncapped — the walk is bounded by its entity and edge caps, not by depth",
489 scope: SettingScope::Shared,
490 },
491 SettingDoc {
492 section: "recall",
493 key: "graph_decay",
494 value_type: "number in (0, 1]",
495 default: "0.5",
496 example: "0.5",
497 description: "How much each extra hop discounts a fact reached through the graph",
498 scope: SettingScope::Shared,
499 },
500 SettingDoc {
501 section: "recall",
502 key: "hnsw_ef_search",
503 value_type: "integer >= 1",
504 default: "64",
505 example: "64",
506 description: "Default HNSW beam width; higher is more accurate and slower. A recall's own `ef` overrides it, and it does nothing while the index is still flat",
507 scope: SettingScope::Shared,
508 },
509 SettingDoc {
510 section: "recall",
511 key: "similar_cos",
512 value_type: "number in [0, 1]",
513 default: "0.85",
514 example: "0.85",
515 description: "Cosine above which remember reports an existing fact as possibly conflicting (it never revises on its own)",
516 scope: SettingScope::Shared,
517 },
518 SettingDoc {
519 section: "recall",
520 key: "similar_jaccard",
521 value_type: "number in [0, 1]",
522 default: "0.5",
523 example: "0.5",
524 description: "Token overlap above which remember reports a possible conflict, for memories with no vectors",
525 scope: SettingScope::Shared,
526 },
527 SettingDoc {
528 section: "index",
529 key: "hnsw_ef_construction",
530 value_type: "integer >= hnsw_m (16 by default)",
531 default: "200",
532 example: "200",
533 description: "Beam width while building the vector graph: higher builds a better index, slower",
534 scope: SettingScope::Shared,
535 },
536 SettingDoc {
537 section: "index",
538 key: "flat_to_hnsw",
539 value_type: "integer >= 1",
540 default: "24000",
541 example: "24000",
542 description: "Vector count at which maintenance stops scanning flat and builds the HNSW graph",
543 scope: SettingScope::Shared,
544 },
545 SettingDoc {
546 section: "embedder",
547 key: "enabled",
548 value_type: "boolean",
549 default: "automatic",
550 example: "true",
551 description: "Enable or disable creation and use of the configured OpenAI-compatible embedder",
552 scope: SettingScope::Shared,
553 },
554 SettingDoc {
555 section: "embedder",
556 key: "url",
557 value_type: "string",
558 default: "unset",
559 example: "\"http://localhost:11434/v1/embeddings\"",
560 description: "OpenAI-compatible /v1/embeddings endpoint",
561 scope: SettingScope::Shared,
562 },
563 SettingDoc {
564 section: "embedder",
565 key: "model",
566 value_type: "string",
567 default: "unset",
568 example: "\"nomic-embed-text\"",
569 description: "Embedding model name",
570 scope: SettingScope::Shared,
571 },
572 SettingDoc {
573 section: "embedder",
574 key: "space_id",
575 value_type: "string",
576 default: "model",
577 example: "\"nomic-embed-text@v1\"",
578 description: "Stable semantic-space identity; change it only for incompatible vectors and reembed explicitly",
579 scope: SettingScope::Shared,
580 },
581 SettingDoc {
582 section: "embedder",
583 key: "api_key_env",
584 value_type: "string",
585 default: "unset",
586 example: "\"OPENAI_API_KEY\"",
587 description: "Environment variable containing the bearer token",
588 scope: SettingScope::Shared,
589 },
590 SettingDoc {
591 section: "embedder",
592 key: "on_error",
593 value_type: "\"fail\" | \"degrade\"",
594 default: "fail",
595 example: "\"fail\"",
596 description: "Unreachable provider: fail the verb, or store/answer without a vector and suspend the embedder",
597 scope: SettingScope::Shared,
598 },
599 SettingDoc {
600 section: "embedder",
601 key: "timeout_ms",
602 value_type: "non-negative integer",
603 default: "10000",
604 example: "10000",
605 description: "Deadline for one embeddings request end to end; 0 waits indefinitely",
606 scope: SettingScope::Shared,
607 },
608 SettingDoc {
609 section: "embedder",
610 key: "retry_after_ms",
611 value_type: "non-negative integer",
612 default: "unset (1s doubling to retry_max_ms)",
613 example: "0",
614 description: "Fixed wait before a suspended embedder is called again; 0 waits for an explicit resume",
615 scope: SettingScope::Shared,
616 },
617 SettingDoc {
618 section: "embedder",
619 key: "retry_max_ms",
620 value_type: "non-negative integer",
621 default: "60000",
622 example: "60000",
623 description: "Ceiling the default doubling retry grows to; ignored when retry_after_ms is set",
624 scope: SettingScope::Shared,
625 },
626 SettingDoc {
627 section: "maintenance",
628 key: "snapshot_every_ops",
629 value_type: "non-negative integer",
630 default: "1024",
631 example: "1024",
632 description: "Snapshot after this many mutations",
633 scope: SettingScope::Shared,
634 },
635 SettingDoc {
636 section: "maintenance",
637 key: "snapshot_journal_bytes",
638 value_type: "non-negative integer",
639 default: "4194304",
640 example: "4194304",
641 description: "Snapshot when the journal reaches this size",
642 scope: SettingScope::Shared,
643 },
644 SettingDoc {
645 section: "maintenance",
646 key: "maintain_every_forgets",
647 value_type: "non-negative integer",
648 default: "off",
649 example: "100",
650 description: "Run policy maintenance after this many forgets",
651 scope: SettingScope::Shared,
652 },
653 SettingDoc {
654 section: "maintenance",
655 key: "fsync",
656 value_type: "\"each_op\" | \"on_snapshot\"",
657 default: "each_op",
658 example: "\"each_op\"",
659 description: "When journal appends reach the disk. \"each_op\": every acknowledged write \
660survives a power cut. \"on_snapshot\": faster, an OS crash may lose the journal tail since the \
661last snapshot",
662 scope: SettingScope::Shared,
663 },
664 SettingDoc {
665 section: "maintenance",
666 key: "batch_size",
667 value_type: "positive integer",
668 default: "128",
669 example: "128",
670 description: "CLI import facts per embedding request and journal fsync",
671 scope: SettingScope::Cli,
672 },
673 SettingDoc {
674 section: "server",
675 key: "workers",
676 value_type: "positive integer",
677 default: "half of available cores",
678 example: "4",
679 description: "MCP worker threads",
680 scope: SettingScope::Mcp,
681 },
682];
683
684static SETTINGS_HELP: SettingsHelp = SettingsHelp {
685 docs: DOCS,
686 config_path_precedence: CONFIG_PATH_PRECEDENCE,
687};
688
689pub const fn settings_help() -> &'static SettingsHelp {
691 &SETTINGS_HELP
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697
698 #[test]
699 fn edit_distance_holds_at_the_degenerate_ends() {
700 assert_eq!(edit_distance("", ""), 0);
705 assert_eq!(edit_distance("", "dim"), 3);
706 assert_eq!(edit_distance("dim", ""), 3);
707
708 assert_eq!(edit_distance("a", "a"), 0);
710 assert_eq!(edit_distance("a", "b"), 1);
711 assert_eq!(edit_distance("a", ""), 1);
712 assert_eq!(edit_distance(" ", ""), 1);
713 assert_eq!(edit_distance(" ", "a"), 1);
714
715 assert_eq!(edit_distance("dim", "dm"), 1, "deletion");
717 assert_eq!(edit_distance("dim", "diim"), 1, "insertion");
718 assert_eq!(edit_distance("dim", "dir"), 1, "substitution");
719
720 assert_eq!(edit_distance("ключ", "ключ"), 0);
723 assert_eq!(edit_distance("ключ", "клуч"), 1);
724 assert_eq!(edit_distance("ключ", ""), 4);
725
726 for (a, b) in [("dim", "max_text"), ("", "fsync"), ("a", "workers")] {
728 assert_eq!(edit_distance(a, b), edit_distance(b, a), "{a} vs {b}");
729 }
730 }
731
732 #[test]
733 fn a_suggestion_is_offered_only_when_it_is_worth_offering() {
734 let engine = || settings_help().keys_in("engine");
735
736 assert_eq!(nearest("dm", engine()), Some("dim"));
738 assert_eq!(nearest("max_txt", engine()), Some("max_text"));
739
740 let recall = || settings_help().keys_in("recall");
743 assert_eq!(nearest("w_vector", recall()), Some("w_vec"));
744 assert_eq!(nearest("similar_cosine", recall()), Some("similar_cos"));
745
746 assert_eq!(nearest("half_life", recall()), None);
751
752 assert_eq!(nearest("a", engine()), None);
756 assert_eq!(nearest("", engine()), None);
757 assert_eq!(nearest(" ", engine()), None);
758 assert_eq!(nearest("completely_unrelated", engine()), None);
759 }
760
761 fn toml_of(lines: &[&str]) -> toml::Table {
764 lines.join("\n").parse().expect("valid TOML fixture")
765 }
766
767 #[test]
768 fn unknown_sections_and_keys_are_reported_with_their_context() {
769 let table = toml_of(&[
770 "[engine]",
771 "dim = 8",
772 "max_txt = 10",
773 "",
774 "[embedder]",
775 "enabled = false",
776 "",
777 "[engin]",
778 "dim = 4",
779 ]);
780
781 let found = settings_help().unknown_in(&table);
782 assert_eq!(
784 found,
785 vec![
786 SettingWarning {
787 section: String::new(),
788 key: "engin".to_string(),
789 did_you_mean: Some("engine"),
790 },
791 SettingWarning {
792 section: "engine".to_string(),
793 key: "max_txt".to_string(),
794 did_you_mean: Some("max_text"),
795 },
796 ]
797 );
798 assert!(
799 found[0]
800 .to_string()
801 .contains("unknown config section [engin]")
802 );
803 assert!(found[1].to_string().contains("[engine].max_txt"));
804 }
805
806 #[test]
807 fn keys_a_wrapper_owns_are_not_warned_about() {
808 let table = toml_of(&[
812 "[maintenance]",
813 "batch_size = 256",
814 "",
815 "[server]",
816 "workers = 4",
817 ]);
818 assert_eq!(settings_help().unknown_in(&table), vec![]);
819 }
820
821 #[test]
822 fn a_clean_config_warns_about_nothing() {
823 let mut text = String::new();
824 let mut section = "";
825 for doc in DOCS {
826 if doc.section != section {
827 let _ = writeln!(text, "[{}]", doc.section);
828 section = doc.section;
829 }
830 let _ = writeln!(text, "{} = 0", doc.key);
833 }
834 let table: toml::Table = text.parse().unwrap();
835 assert_eq!(
836 settings_help().unknown_in(&table),
837 vec![],
838 "the catalogue must accept everything it documents"
839 );
840 }
841
842 #[test]
843 fn every_documented_setting_has_a_complete_description() {
844 assert!(!DOCS.is_empty());
845 for doc in DOCS {
846 assert!(!doc.section.is_empty());
847 assert!(!doc.key.is_empty());
848 assert!(!doc.value_type.is_empty());
849 assert!(!doc.default.is_empty());
850 assert!(!doc.description.is_empty());
851 assert!(!doc.example.is_empty(), "{}.{}", doc.section, doc.key);
852 }
853 }
854
855 fn committed_example() -> String {
864 let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
865 .join("..")
866 .join("..")
867 .join("config.example.toml");
868 let text = std::fs::read_to_string(&path)
869 .unwrap_or_else(|e| panic!("reading {}: {e}", path.display()));
870 text.replace("\r\n", "\n")
871 }
872
873 #[test]
874 fn the_committed_config_example_matches_the_catalogue() {
875 assert_eq!(
880 committed_example(),
881 settings_help().render_config_example(false),
882 "config.example.toml is stale — run \
883 `cargo run -p plugmem-host --bin config_example` and commit it"
884 );
885 }
886
887 #[test]
888 fn the_example_is_a_config_the_loader_accepts() {
889 let table: toml::Table = settings_help()
894 .render_config_example(true)
895 .parse()
896 .expect("the generated example must be valid TOML");
897 let settings = crate::Settings::from_table(Some(&table))
898 .expect("the generated example must load through the real settings loader");
899 assert_eq!(
900 settings.warnings,
901 vec![],
902 "the example names a key no surface claims"
903 );
904 assert_eq!(settings.config.dim, 768);
907 assert_eq!(settings.embed_error_policy, crate::EmbedErrorPolicy::Fail);
908 assert!(settings.embedder.is_some());
909 }
910
911 #[test]
912 fn every_key_of_the_committed_example_is_commented_out() {
913 let table: toml::Table = committed_example()
916 .parse()
917 .expect("the committed example must be valid TOML");
918 for (section, entries) in &table {
919 assert!(
920 entries.as_table().is_some_and(toml::Table::is_empty),
921 "[{section}] has an active key; the example must be all comments"
922 );
923 }
924 }
925
926 #[test]
927 fn human_help_contains_every_documented_key() {
928 let rendered = settings_help().render_human();
929 for doc in DOCS {
930 assert!(
931 rendered.contains(doc.key),
932 "missing {}.{}",
933 doc.section,
934 doc.key
935 );
936 }
937 }
938}