1use std::path::Path;
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
10pub struct GlobalSection {
11 #[serde(default)]
13 pub default_wiki: String,
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct WikiEntry {
19 pub name: String,
21 pub path: String,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub description: Option<String>,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub remote: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Defaults {
34 #[serde(default = "default_search_top_k")]
36 pub search_top_k: u32,
37 #[serde(default = "default_true")]
39 pub search_excerpt: bool,
40 #[serde(default)]
42 pub search_sections: bool,
43 #[serde(default = "default_page_mode")]
45 pub page_mode: String,
46 #[serde(default = "default_list_page_size")]
48 pub list_page_size: u32,
49 #[serde(default = "default_output_format")]
51 pub output_format: String,
52 #[serde(default = "default_facets_top_tags")]
54 pub facets_top_tags: u32,
55}
56
57impl Default for Defaults {
58 fn default() -> Self {
59 Self {
60 search_top_k: 10,
61 search_excerpt: true,
62 search_sections: false,
63 page_mode: "flat".into(),
64 list_page_size: 20,
65 output_format: "text".into(),
66 facets_top_tags: 10,
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, Default)]
73pub struct ReadConfig {
74 #[serde(default)]
76 pub no_frontmatter: bool,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct IndexConfig {
82 #[serde(default)]
84 pub auto_rebuild: bool,
85 #[serde(default = "default_true")]
87 pub auto_recovery: bool,
88 #[serde(default = "default_memory_budget_mb")]
90 pub memory_budget_mb: u32,
91 #[serde(default = "default_tokenizer")]
93 pub tokenizer: String,
94}
95
96impl Default for IndexConfig {
97 fn default() -> Self {
98 Self {
99 auto_rebuild: false,
100 auto_recovery: true,
101 memory_budget_mb: 50,
102 tokenizer: "en_stem".into(),
103 }
104 }
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct GraphConfig {
110 #[serde(default = "default_graph_format")]
112 pub format: String,
113 #[serde(default = "default_graph_depth")]
115 pub depth: u32,
116 #[serde(default)]
118 pub r#type: Vec<String>,
119 #[serde(default)]
121 pub output: String,
122 #[serde(default = "default_min_nodes_for_communities")]
124 pub min_nodes_for_communities: usize,
125 #[serde(default = "default_community_suggestions_limit")]
127 pub community_suggestions_limit: usize,
128 #[serde(default = "default_true")]
131 pub snapshot: bool,
132 #[serde(default = "default_snapshot_keep")]
134 pub snapshot_keep: u32,
135 #[serde(default = "default_snapshot_format")]
137 pub snapshot_format: String,
138 #[serde(default = "default_true")]
142 pub structural_algorithms: bool,
143 #[serde(default = "default_max_nodes_for_diameter")]
145 pub max_nodes_for_diameter: usize,
146}
147
148impl Default for GraphConfig {
149 fn default() -> Self {
150 Self {
151 format: "mermaid".into(),
152 depth: 3,
153 r#type: Vec::new(),
154 output: String::new(),
155 min_nodes_for_communities: default_min_nodes_for_communities(),
156 community_suggestions_limit: default_community_suggestions_limit(),
157 snapshot: true,
158 snapshot_keep: 3,
159 snapshot_format: "bincode+lz4".into(),
160 structural_algorithms: true,
161 max_nodes_for_diameter: default_max_nodes_for_diameter(),
162 }
163 }
164}
165
166fn default_min_nodes_for_communities() -> usize {
167 30
168}
169
170fn default_community_suggestions_limit() -> usize {
171 2
172}
173
174fn default_snapshot_keep() -> u32 {
175 3
176}
177
178fn default_snapshot_format() -> String {
179 "bincode+lz4".into()
180}
181fn default_max_nodes_for_diameter() -> usize {
182 2000
183}
184
185fn default_acp_max_sessions() -> usize {
186 20
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ServeConfig {
192 #[serde(default)]
194 pub http: bool,
195 #[serde(default = "default_http_port")]
197 pub http_port: u16,
198 #[serde(default = "default_http_allowed_hosts")]
200 pub http_allowed_hosts: Vec<String>,
201 #[serde(default)]
203 pub acp: bool,
204 #[serde(default = "default_max_restarts")]
206 pub max_restarts: u32,
207 #[serde(default = "default_restart_backoff")]
209 pub restart_backoff: u32,
210 #[serde(default = "default_heartbeat_secs")]
212 pub heartbeat_secs: u32,
213 #[serde(default = "default_acp_max_sessions")]
215 pub acp_max_sessions: usize,
216}
217
218impl Default for ServeConfig {
219 fn default() -> Self {
220 Self {
221 http: false,
222 http_port: 8080,
223 http_allowed_hosts: default_http_allowed_hosts(),
224 acp: false,
225 max_restarts: 10,
226 restart_backoff: 1,
227 heartbeat_secs: 60,
228 acp_max_sessions: default_acp_max_sessions(),
229 }
230 }
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct ValidationConfig {
236 #[serde(default = "default_type_strictness")]
238 pub type_strictness: String,
239}
240
241impl Default for ValidationConfig {
242 fn default() -> Self {
243 Self {
244 type_strictness: "loose".into(),
245 }
246 }
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
251pub struct LoggingConfig {
252 #[serde(default = "default_log_path")]
254 pub log_path: String,
255 #[serde(default = "default_log_rotation")]
257 pub log_rotation: String,
258 #[serde(default = "default_log_max_files")]
260 pub log_max_files: u32,
261 #[serde(default = "default_log_format")]
263 pub log_format: String,
264}
265
266impl Default for LoggingConfig {
267 fn default() -> Self {
268 Self {
269 log_path: default_log_path(),
270 log_rotation: "daily".into(),
271 log_max_files: 7,
272 log_format: "text".into(),
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize, Deserialize)]
279pub struct IngestConfig {
280 #[serde(default = "default_true")]
282 pub auto_commit: bool,
283}
284
285impl Default for IngestConfig {
286 fn default() -> Self {
287 Self { auto_commit: true }
288 }
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize)]
293pub struct HistoryConfig {
294 #[serde(default = "default_true")]
296 pub follow: bool,
297 #[serde(default = "default_history_limit")]
299 pub default_limit: u32,
300}
301
302impl Default for HistoryConfig {
303 fn default() -> Self {
304 Self {
305 follow: true,
306 default_limit: 10,
307 }
308 }
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct WatchConfig {
314 #[serde(default = "default_debounce_ms")]
316 pub debounce_ms: u32,
317}
318
319impl Default for WatchConfig {
320 fn default() -> Self {
321 Self { debounce_ms: 500 }
322 }
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct SuggestConfig {
328 #[serde(default = "default_suggest_limit")]
330 pub default_limit: u32,
331 #[serde(default = "default_suggest_min_score")]
333 pub min_score: f32,
334}
335
336impl Default for SuggestConfig {
337 fn default() -> Self {
338 Self {
339 default_limit: 5,
340 min_score: 0.1,
341 }
342 }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize)]
347pub struct SearchConfig {
348 #[serde(default = "default_search_status")]
350 pub status: std::collections::HashMap<String, f32>,
351}
352
353fn default_search_status() -> std::collections::HashMap<String, f32> {
354 [
355 ("active".into(), 1.0_f32),
356 ("draft".into(), 0.8),
357 ("archived".into(), 0.3),
358 ("unknown".into(), 0.9),
359 ]
360 .into_iter()
361 .collect()
362}
363
364impl Default for SearchConfig {
365 fn default() -> Self {
366 Self {
367 status: default_search_status(),
368 }
369 }
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct LintConfig {
375 #[serde(default = "default_stale_days")]
377 pub stale_days: u32,
378 #[serde(default = "default_stale_confidence_threshold")]
380 pub stale_confidence_threshold: f32,
381}
382
383impl Default for LintConfig {
384 fn default() -> Self {
385 Self {
386 stale_days: default_stale_days(),
387 stale_confidence_threshold: default_stale_confidence_threshold(),
388 }
389 }
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize, Default)]
394pub struct CustomPattern {
395 pub name: String,
397 pub pattern: String,
399 pub replacement: String,
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, Default)]
405pub struct RedactConfig {
406 #[serde(default)]
408 pub disable: Vec<String>,
409 #[serde(default)]
411 pub patterns: Vec<CustomPattern>,
412}
413
414#[derive(Debug, Clone, Serialize, Deserialize, Default)]
418pub struct GlobalConfig {
419 #[serde(default)]
421 pub global: GlobalSection,
422 #[serde(default)]
424 pub wikis: Vec<WikiEntry>,
425 #[serde(default)]
427 pub defaults: Defaults,
428 #[serde(default)]
430 pub read: ReadConfig,
431 #[serde(default)]
433 pub index: IndexConfig,
434 #[serde(default)]
436 pub graph: GraphConfig,
437 #[serde(default)]
439 pub serve: ServeConfig,
440 #[serde(default)]
442 pub validation: ValidationConfig,
443 #[serde(default)]
445 pub ingest: IngestConfig,
446 #[serde(default)]
448 pub history: HistoryConfig,
449 #[serde(default)]
451 pub suggest: SuggestConfig,
452 #[serde(default)]
454 pub search: SearchConfig,
455 #[serde(default)]
457 pub lint: LintConfig,
458 #[serde(default)]
460 pub logging: LoggingConfig,
461 #[serde(default)]
463 pub watch: WatchConfig,
464 #[serde(default)]
466 pub redact: RedactConfig,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize)]
471pub struct TypeEntry {
472 pub schema: String,
474 pub description: String,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, Default)]
482pub struct WikiConfig {
483 #[serde(default)]
485 pub name: String,
486 #[serde(default)]
488 pub description: String,
489 #[serde(default)]
491 pub types: std::collections::HashMap<String, TypeEntry>,
492 #[serde(default)]
494 pub defaults: Option<Defaults>,
495 #[serde(default)]
497 pub read: Option<ReadConfig>,
498 #[serde(default)]
500 pub validation: Option<ValidationConfig>,
501 #[serde(default)]
503 pub ingest: Option<IngestConfig>,
504 #[serde(default)]
506 pub graph: Option<GraphConfig>,
507 #[serde(default)]
509 pub history: Option<HistoryConfig>,
510 #[serde(default)]
512 pub suggest: Option<SuggestConfig>,
513 #[serde(default)]
515 pub search: Option<SearchConfig>,
516 #[serde(default)]
518 pub lint: Option<LintConfig>,
519 #[serde(default)]
521 pub redact: Option<RedactConfig>,
522 #[serde(default = "default_wiki_root")]
524 pub wiki_root: String,
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
529pub struct ResolvedConfig {
530 pub defaults: Defaults,
532 pub read: ReadConfig,
534 pub index: IndexConfig,
536 pub graph: GraphConfig,
538 pub serve: ServeConfig,
540 pub ingest: IngestConfig,
542 pub validation: ValidationConfig,
544 pub history: HistoryConfig,
546 pub suggest: SuggestConfig,
548 pub search: SearchConfig,
550 pub lint: LintConfig,
552 pub redact: RedactConfig,
554}
555
556fn default_search_top_k() -> u32 {
559 10
560}
561fn default_true() -> bool {
562 true
563}
564fn default_page_mode() -> String {
565 "flat".into()
566}
567fn default_list_page_size() -> u32 {
568 20
569}
570fn default_output_format() -> String {
571 "text".into()
572}
573fn default_facets_top_tags() -> u32 {
574 10
575}
576fn default_memory_budget_mb() -> u32 {
577 50
578}
579fn default_tokenizer() -> String {
580 "en_stem".into()
581}
582fn default_graph_format() -> String {
583 "mermaid".into()
584}
585fn default_graph_depth() -> u32 {
586 3
587}
588fn default_http_port() -> u16 {
589 8080
590}
591fn default_http_allowed_hosts() -> Vec<String> {
592 vec!["localhost".into(), "127.0.0.1".into(), "::1".into()]
593}
594fn default_max_restarts() -> u32 {
595 10
596}
597fn default_restart_backoff() -> u32 {
598 1
599}
600fn default_heartbeat_secs() -> u32 {
601 60
602}
603fn default_type_strictness() -> String {
604 "loose".into()
605}
606fn default_log_path() -> String {
607 let home = std::env::var("HOME").unwrap_or_else(|_| ".".into());
608 std::path::PathBuf::from(home)
609 .join(".llm-wiki")
610 .join("logs")
611 .to_string_lossy()
612 .into()
613}
614fn default_log_rotation() -> String {
615 "daily".into()
616}
617fn default_log_max_files() -> u32 {
618 7
619}
620fn default_log_format() -> String {
621 "text".into()
622}
623fn default_history_limit() -> u32 {
624 10
625}
626fn default_debounce_ms() -> u32 {
627 500
628}
629fn default_suggest_limit() -> u32 {
630 5
631}
632fn default_suggest_min_score() -> f32 {
633 0.1
634}
635fn default_stale_days() -> u32 {
636 90
637}
638fn default_stale_confidence_threshold() -> f32 {
639 0.4
640}
641fn default_wiki_root() -> String {
642 "wiki".to_string()
643}
644pub fn resolve(global: &GlobalConfig, per_wiki: &WikiConfig) -> ResolvedConfig {
648 ResolvedConfig {
649 defaults: per_wiki
650 .defaults
651 .clone()
652 .unwrap_or_else(|| global.defaults.clone()),
653 read: per_wiki.read.clone().unwrap_or_else(|| global.read.clone()),
654 index: global.index.clone(),
655 graph: per_wiki
656 .graph
657 .clone()
658 .unwrap_or_else(|| global.graph.clone()),
659 serve: global.serve.clone(),
660 ingest: per_wiki
661 .ingest
662 .clone()
663 .unwrap_or_else(|| global.ingest.clone()),
664 validation: per_wiki
665 .validation
666 .clone()
667 .unwrap_or_else(|| global.validation.clone()),
668 history: per_wiki
669 .history
670 .clone()
671 .unwrap_or_else(|| global.history.clone()),
672 suggest: per_wiki
673 .suggest
674 .clone()
675 .unwrap_or_else(|| global.suggest.clone()),
676 search: {
677 let mut merged = global.search.status.clone();
678 if let Some(wiki_search) = &per_wiki.search {
679 for (k, v) in &wiki_search.status {
680 merged.insert(k.clone(), *v);
681 }
682 }
683 SearchConfig { status: merged }
684 },
685 lint: per_wiki.lint.clone().unwrap_or_else(|| global.lint.clone()),
686 redact: per_wiki
687 .redact
688 .clone()
689 .unwrap_or_else(|| global.redact.clone()),
690 }
691}
692
693pub fn load_global(path: &Path) -> Result<GlobalConfig> {
695 if !path.exists() {
696 return Ok(GlobalConfig::default());
697 }
698 let content = std::fs::read_to_string(path)
699 .with_context(|| format!("failed to read {}", path.display()))?;
700 let config: GlobalConfig =
701 toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))?;
702 Ok(config)
703}
704
705pub fn load_wiki(wiki_root: &Path) -> Result<WikiConfig> {
707 let path = wiki_root.join("wiki.toml");
708 if !path.exists() {
709 return Ok(WikiConfig::default());
710 }
711 let content = std::fs::read_to_string(&path)
712 .with_context(|| format!("failed to read {}", path.display()))?;
713 let config: WikiConfig =
714 toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))?;
715 Ok(config)
716}
717
718pub fn save_global(config: &GlobalConfig, path: &Path) -> Result<()> {
720 if let Some(parent) = path.parent() {
721 std::fs::create_dir_all(parent)?;
722 }
723 let content = toml::to_string_pretty(config)?;
724 std::fs::write(path, content)?;
725 Ok(())
726}
727
728pub fn save_wiki(config: &WikiConfig, wiki_root: &Path) -> Result<()> {
730 let path = wiki_root.join("wiki.toml");
731 let content = toml::to_string_pretty(config)?;
732 std::fs::write(path, content)?;
733 Ok(())
734}
735
736pub fn set_global_config_value(global: &mut GlobalConfig, key: &str, value: &str) -> Result<()> {
738 match key {
739 "global.default_wiki" => global.global.default_wiki = value.into(),
740 "defaults.search_top_k" => global.defaults.search_top_k = value.parse()?,
741 "defaults.search_excerpt" => global.defaults.search_excerpt = value.parse()?,
742 "defaults.search_sections" => global.defaults.search_sections = value.parse()?,
743 "defaults.page_mode" => global.defaults.page_mode = value.into(),
744 "defaults.list_page_size" => global.defaults.list_page_size = value.parse()?,
745 "defaults.output_format" => global.defaults.output_format = value.into(),
746 "defaults.facets_top_tags" => global.defaults.facets_top_tags = value.parse()?,
747 "read.no_frontmatter" => global.read.no_frontmatter = value.parse()?,
748 "index.auto_rebuild" => global.index.auto_rebuild = value.parse()?,
749 "index.auto_recovery" => global.index.auto_recovery = value.parse()?,
750 "index.memory_budget_mb" => global.index.memory_budget_mb = value.parse()?,
751 "index.tokenizer" => global.index.tokenizer = value.into(),
752 "graph.format" => global.graph.format = value.into(),
753 "graph.depth" => global.graph.depth = value.parse()?,
754 "graph.output" => global.graph.output = value.into(),
755 "graph.snapshot" => global.graph.snapshot = value.parse()?,
756 "graph.snapshot_keep" => global.graph.snapshot_keep = value.parse()?,
757 "graph.snapshot_format" => global.graph.snapshot_format = value.into(),
758 "graph.structural_algorithms" => global.graph.structural_algorithms = value.parse()?,
759 "graph.max_nodes_for_diameter" => global.graph.max_nodes_for_diameter = value.parse()?,
760 "serve.http" => global.serve.http = value.parse()?,
761 "serve.http_port" => global.serve.http_port = value.parse()?,
762 "serve.http_allowed_hosts" => {
763 global.serve.http_allowed_hosts =
764 value.split(',').map(|s| s.trim().to_string()).collect();
765 }
766 "serve.acp" => global.serve.acp = value.parse()?,
767 "serve.max_restarts" => global.serve.max_restarts = value.parse()?,
768 "serve.restart_backoff" => global.serve.restart_backoff = value.parse()?,
769 "serve.heartbeat_secs" => global.serve.heartbeat_secs = value.parse()?,
770 "serve.acp_max_sessions" => global.serve.acp_max_sessions = value.parse()?,
771 "ingest.auto_commit" => global.ingest.auto_commit = value.parse()?,
772 "history.follow" => global.history.follow = value.parse()?,
773 "history.default_limit" => global.history.default_limit = value.parse()?,
774 "suggest.default_limit" => global.suggest.default_limit = value.parse()?,
775 "suggest.min_score" => global.suggest.min_score = value.parse()?,
776 "validation.type_strictness" => global.validation.type_strictness = value.into(),
777 "logging.log_path" => global.logging.log_path = value.into(),
778 "logging.log_rotation" => global.logging.log_rotation = value.into(),
779 "logging.log_max_files" => global.logging.log_max_files = value.parse()?,
780 "logging.log_format" => global.logging.log_format = value.into(),
781 "watch.debounce_ms" => global.watch.debounce_ms = value.parse()?,
782 _ => anyhow::bail!("unknown key: {key}"),
783 }
784 Ok(())
785}
786
787pub fn get_config_value(resolved: &ResolvedConfig, global: &GlobalConfig, key: &str) -> String {
789 match key {
790 "global.default_wiki" => global.global.default_wiki.clone(),
791 "defaults.search_top_k" => resolved.defaults.search_top_k.to_string(),
792 "defaults.search_excerpt" => resolved.defaults.search_excerpt.to_string(),
793 "defaults.search_sections" => resolved.defaults.search_sections.to_string(),
794 "defaults.page_mode" => resolved.defaults.page_mode.clone(),
795 "defaults.list_page_size" => resolved.defaults.list_page_size.to_string(),
796 "defaults.output_format" => resolved.defaults.output_format.clone(),
797 "defaults.facets_top_tags" => resolved.defaults.facets_top_tags.to_string(),
798 "read.no_frontmatter" => resolved.read.no_frontmatter.to_string(),
799 "index.auto_rebuild" => resolved.index.auto_rebuild.to_string(),
800 "index.auto_recovery" => global.index.auto_recovery.to_string(),
801 "index.memory_budget_mb" => global.index.memory_budget_mb.to_string(),
802 "index.tokenizer" => global.index.tokenizer.clone(),
803 "graph.format" => resolved.graph.format.clone(),
804 "graph.depth" => resolved.graph.depth.to_string(),
805 "graph.output" => resolved.graph.output.clone(),
806 "graph.snapshot" => resolved.graph.snapshot.to_string(),
807 "graph.snapshot_keep" => resolved.graph.snapshot_keep.to_string(),
808 "graph.snapshot_format" => resolved.graph.snapshot_format.clone(),
809 "graph.structural_algorithms" => resolved.graph.structural_algorithms.to_string(),
810 "graph.max_nodes_for_diameter" => resolved.graph.max_nodes_for_diameter.to_string(),
811 "serve.http" => resolved.serve.http.to_string(),
812 "serve.http_port" => resolved.serve.http_port.to_string(),
813 "serve.http_allowed_hosts" => resolved.serve.http_allowed_hosts.join(","),
814 "serve.acp" => resolved.serve.acp.to_string(),
815 "serve.max_restarts" => global.serve.max_restarts.to_string(),
816 "serve.restart_backoff" => global.serve.restart_backoff.to_string(),
817 "serve.heartbeat_secs" => global.serve.heartbeat_secs.to_string(),
818 "serve.acp_max_sessions" => global.serve.acp_max_sessions.to_string(),
819 "validation.type_strictness" => resolved.validation.type_strictness.clone(),
820 "logging.log_path" => global.logging.log_path.clone(),
821 "logging.log_rotation" => global.logging.log_rotation.clone(),
822 "logging.log_max_files" => global.logging.log_max_files.to_string(),
823 "logging.log_format" => global.logging.log_format.clone(),
824 "watch.debounce_ms" => global.watch.debounce_ms.to_string(),
825 "ingest.auto_commit" => resolved.ingest.auto_commit.to_string(),
826 "history.follow" => resolved.history.follow.to_string(),
827 "history.default_limit" => resolved.history.default_limit.to_string(),
828 "suggest.default_limit" => resolved.suggest.default_limit.to_string(),
829 "suggest.min_score" => resolved.suggest.min_score.to_string(),
830 _ => format!("unknown key: {key}"),
831 }
832}
833
834pub fn set_wiki_config_value(wiki_cfg: &mut WikiConfig, key: &str, value: &str) -> Result<()> {
836 match key {
837 "defaults.search_top_k" => {
838 wiki_cfg
839 .defaults
840 .get_or_insert_with(Defaults::default)
841 .search_top_k = value.parse()?;
842 }
843 "defaults.search_excerpt" => {
844 wiki_cfg
845 .defaults
846 .get_or_insert_with(Defaults::default)
847 .search_excerpt = value.parse()?;
848 }
849 "defaults.search_sections" => {
850 wiki_cfg
851 .defaults
852 .get_or_insert_with(Defaults::default)
853 .search_sections = value.parse()?;
854 }
855 "defaults.page_mode" => {
856 wiki_cfg
857 .defaults
858 .get_or_insert_with(Defaults::default)
859 .page_mode = value.into();
860 }
861 "defaults.list_page_size" => {
862 wiki_cfg
863 .defaults
864 .get_or_insert_with(Defaults::default)
865 .list_page_size = value.parse()?;
866 }
867 "defaults.output_format" => {
868 wiki_cfg
869 .defaults
870 .get_or_insert_with(Defaults::default)
871 .output_format = value.into();
872 }
873 "defaults.facets_top_tags" => {
874 wiki_cfg
875 .defaults
876 .get_or_insert_with(Defaults::default)
877 .facets_top_tags = value.parse()?;
878 }
879 "read.no_frontmatter" => {
880 wiki_cfg
881 .read
882 .get_or_insert_with(ReadConfig::default)
883 .no_frontmatter = value.parse()?;
884 }
885 "validation.type_strictness" => {
886 wiki_cfg
887 .validation
888 .get_or_insert_with(ValidationConfig::default)
889 .type_strictness = value.into();
890 }
891 "ingest.auto_commit" => {
892 wiki_cfg
893 .ingest
894 .get_or_insert_with(IngestConfig::default)
895 .auto_commit = value.parse()?;
896 }
897 "history.follow" => {
898 wiki_cfg
899 .history
900 .get_or_insert_with(HistoryConfig::default)
901 .follow = value.parse()?;
902 }
903 "history.default_limit" => {
904 wiki_cfg
905 .history
906 .get_or_insert_with(HistoryConfig::default)
907 .default_limit = value.parse()?;
908 }
909 "suggest.default_limit" => {
910 wiki_cfg
911 .suggest
912 .get_or_insert_with(SuggestConfig::default)
913 .default_limit = value.parse()?;
914 }
915 "suggest.min_score" => {
916 wiki_cfg
917 .suggest
918 .get_or_insert_with(SuggestConfig::default)
919 .min_score = value.parse()?;
920 }
921 "graph.format" => {
922 wiki_cfg
923 .graph
924 .get_or_insert_with(GraphConfig::default)
925 .format = value.into();
926 }
927 "graph.depth" => {
928 wiki_cfg
929 .graph
930 .get_or_insert_with(GraphConfig::default)
931 .depth = value.parse()?;
932 }
933 "graph.output" => {
934 wiki_cfg
935 .graph
936 .get_or_insert_with(GraphConfig::default)
937 .output = value.into();
938 }
939 "graph.snapshot" => {
940 wiki_cfg
941 .graph
942 .get_or_insert_with(GraphConfig::default)
943 .snapshot = value.parse()?;
944 }
945 "graph.snapshot_keep" => {
946 wiki_cfg
947 .graph
948 .get_or_insert_with(GraphConfig::default)
949 .snapshot_keep = value.parse()?;
950 }
951 "graph.snapshot_format" => {
952 wiki_cfg
953 .graph
954 .get_or_insert_with(GraphConfig::default)
955 .snapshot_format = value.into();
956 }
957 "graph.structural_algorithms" => {
958 wiki_cfg
959 .graph
960 .get_or_insert_with(GraphConfig::default)
961 .structural_algorithms = value.parse()?;
962 }
963 "graph.max_nodes_for_diameter" => {
964 wiki_cfg
965 .graph
966 .get_or_insert_with(GraphConfig::default)
967 .max_nodes_for_diameter = value.parse()?;
968 }
969 "global.default_wiki"
970 | "index.auto_rebuild"
971 | "index.auto_recovery"
972 | "index.memory_budget_mb"
973 | "index.tokenizer"
974 | "serve.http"
975 | "serve.http_port"
976 | "serve.http_allowed_hosts"
977 | "serve.acp"
978 | "serve.max_restarts"
979 | "serve.restart_backoff"
980 | "serve.heartbeat_secs"
981 | "serve.acp_max_sessions"
982 | "logging.log_path"
983 | "logging.log_rotation"
984 | "logging.log_max_files"
985 | "logging.log_format" => {
986 anyhow::bail!("{key} is a global-only key \u{2014} use --global");
987 }
988 "watch.debounce_ms" => {
989 anyhow::bail!("{key} is a global-only key \u{2014} use --global");
990 }
991 _ => anyhow::bail!("unknown key: {key}"),
992 }
993 Ok(())
994}