Skip to main content

lean_ctx/core/config/
schema.rs

1//! Auto-generated config schema from `Config` struct metadata.
2//!
3//! Used by `lean-ctx config schema` to emit JSON and by
4//! `lean-ctx config validate` to check user config.toml files.
5
6use serde::Serialize;
7use std::collections::BTreeMap;
8
9#[derive(Debug, Clone, Serialize)]
10pub struct ConfigSchema {
11    pub version: u32,
12    pub sections: BTreeMap<String, SectionSchema>,
13}
14
15#[derive(Debug, Clone, Serialize)]
16pub struct SectionSchema {
17    pub description: String,
18    pub keys: BTreeMap<String, KeySchema>,
19}
20
21#[derive(Debug, Clone, Serialize)]
22pub struct KeySchema {
23    #[serde(rename = "type")]
24    pub ty: String,
25    pub default: serde_json::Value,
26    pub description: String,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub values: Option<Vec<String>>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub env_override: Option<String>,
31}
32
33fn clean_f32(v: f32) -> serde_json::Value {
34    let clean: f64 = format!("{v}").parse().unwrap_or(v as f64);
35    serde_json::json!(clean)
36}
37
38fn key(ty: &str, default: serde_json::Value, desc: &str) -> KeySchema {
39    KeySchema {
40        ty: ty.to_string(),
41        default,
42        description: desc.to_string(),
43        values: None,
44        env_override: None,
45    }
46}
47
48fn key_enum(values: &[&str], default: &str, desc: &str) -> KeySchema {
49    KeySchema {
50        ty: "enum".to_string(),
51        default: serde_json::Value::String(default.to_string()),
52        description: desc.to_string(),
53        values: Some(values.iter().map(ToString::to_string).collect()),
54        env_override: None,
55    }
56}
57
58fn key_with_env(ty: &str, default: serde_json::Value, desc: &str, env: &str) -> KeySchema {
59    KeySchema {
60        ty: ty.to_string(),
61        default,
62        description: desc.to_string(),
63        values: None,
64        env_override: Some(env.to_string()),
65    }
66}
67
68fn key_enum_with_env(values: &[&str], default: &str, desc: &str, env: &str) -> KeySchema {
69    KeySchema {
70        ty: "enum".to_string(),
71        default: serde_json::Value::String(default.to_string()),
72        description: desc.to_string(),
73        values: Some(values.iter().map(ToString::to_string).collect()),
74        env_override: Some(env.to_string()),
75    }
76}
77
78impl ConfigSchema {
79    pub fn generate() -> Self {
80        let cfg = super::Config::default();
81        let mut sections = BTreeMap::new();
82
83        let mut root = BTreeMap::new();
84        root.insert(
85            "ultra_compact".into(),
86            key(
87                "bool",
88                serde_json::json!(false),
89                "Legacy flag for maximum compression (use compression_level instead)",
90            ),
91        );
92        root.insert(
93            "tee_mode".into(),
94            key_enum(
95                &["never", "failures", "always"],
96                "failures",
97                "Controls when shell output is tee'd to disk for later retrieval",
98            ),
99        );
100        root.insert(
101            "output_density".into(),
102            key_enum_with_env(
103                &["normal", "terse", "ultra"],
104                "normal",
105                "Controls how dense/compact MCP tool output is formatted",
106                "LEAN_CTX_OUTPUT_DENSITY",
107            ),
108        );
109        root.insert(
110            "checkpoint_interval".into(),
111            key(
112                "u32",
113                serde_json::json!(cfg.checkpoint_interval),
114                "Session checkpoint interval in minutes",
115            ),
116        );
117        root.insert(
118            "excluded_commands".into(),
119            key(
120                "string[]",
121                serde_json::json!(cfg.excluded_commands),
122                "Commands to exclude from shell hook interception",
123            ),
124        );
125        root.insert(
126            "passthrough_urls".into(),
127            key(
128                "string[]",
129                serde_json::json!(cfg.passthrough_urls),
130                "URLs to pass through without proxy interception",
131            ),
132        );
133        root.insert("slow_command_threshold_ms".into(), key("u64", serde_json::json!(cfg.slow_command_threshold_ms), "Commands taking longer than this (ms) are recorded in the slow log. Set to 0 to disable"));
134        root.insert(
135            "theme".into(),
136            key(
137                "string",
138                serde_json::json!(cfg.theme),
139                "Dashboard color theme",
140            ),
141        );
142        root.insert(
143            "buddy_enabled".into(),
144            key(
145                "bool",
146                serde_json::json!(cfg.buddy_enabled),
147                "Enable the buddy system for multi-agent coordination",
148            ),
149        );
150        root.insert(
151            "enable_wakeup_ctx".into(),
152            key(
153                "bool",
154                serde_json::json!(cfg.enable_wakeup_ctx),
155                "Append wakeup briefing (facts, session summary) to ctx_overview output. Set false to reduce context bloat when calling ctx_overview frequently.",
156            ),
157        );
158        root.insert(
159            "redirect_exclude".into(),
160            key(
161                "string[]",
162                serde_json::json!(cfg.redirect_exclude),
163                "URL patterns to exclude from proxy redirection",
164            ),
165        );
166        root.insert(
167            "disabled_tools".into(),
168            key(
169                "string[]",
170                serde_json::json!(cfg.disabled_tools),
171                "Tools to exclude from the MCP tool list",
172            ),
173        );
174        root.insert(
175            "rules_scope".into(),
176            key_enum(
177                &["both", "global", "project"],
178                "both",
179                "Where agent rule files are installed. Override via LEAN_CTX_RULES_SCOPE",
180            ),
181        );
182        root.insert(
183            "extra_ignore_patterns".into(),
184            key(
185                "string[]",
186                serde_json::json!(cfg.extra_ignore_patterns),
187                "Extra glob patterns to ignore in graph/overview/preload",
188            ),
189        );
190        root.insert(
191            "terse_agent".into(),
192            key_enum_with_env(
193                &["off", "lite", "full", "ultra"],
194                "off",
195                "Controls agent output verbosity via instructions injection",
196                "LEAN_CTX_TERSE_AGENT",
197            ),
198        );
199        root.insert(
200            "compression_level".into(),
201            key_enum_with_env(
202                &["off", "lite", "standard", "max"],
203                "off",
204                "Unified compression level for all output",
205                "LEAN_CTX_COMPRESSION",
206            ),
207        );
208        root.insert(
209            "allow_paths".into(),
210            key_with_env(
211                "string[]",
212                serde_json::json!(cfg.allow_paths),
213                "Additional paths allowed by PathJail (absolute)",
214                "LEAN_CTX_ALLOW_PATH",
215            ),
216        );
217        root.insert(
218            "content_defined_chunking".into(),
219            key(
220                "bool",
221                serde_json::json!(false),
222                "Enable Rabin-Karp chunking for cache-optimal output ordering",
223            ),
224        );
225        root.insert(
226            "minimal_overhead".into(),
227            key_with_env(
228                "bool",
229                serde_json::json!(false),
230                "Skip session/knowledge/gotcha blocks in MCP instructions",
231                "LEAN_CTX_MINIMAL",
232            ),
233        );
234        root.insert(
235            "shell_hook_disabled".into(),
236            key_with_env(
237                "bool",
238                serde_json::json!(false),
239                "Disable shell hook injection",
240                "LEAN_CTX_NO_HOOK",
241            ),
242        );
243        root.insert(
244            "shell_activation".into(),
245            key_enum_with_env(
246                &["always", "agents-only", "off"],
247                "always",
248                "Controls when the shell hook auto-activates aliases",
249                "LEAN_CTX_SHELL_ACTIVATION",
250            ),
251        );
252        root.insert(
253            "update_check_disabled".into(),
254            key_with_env(
255                "bool",
256                serde_json::json!(false),
257                "Disable the daily version check",
258                "LEAN_CTX_NO_UPDATE_CHECK",
259            ),
260        );
261        root.insert(
262            "bm25_max_cache_mb".into(),
263            key_with_env(
264                "u64",
265                serde_json::json!(cfg.bm25_max_cache_mb),
266                "Maximum BM25 cache file size in MB",
267                "LEAN_CTX_BM25_MAX_CACHE_MB",
268            ),
269        );
270        root.insert(
271            "graph_index_max_files".into(),
272            key(
273                "u64",
274                serde_json::json!(cfg.graph_index_max_files),
275                "Maximum files in graph index. 0 = unlimited (default). Set >0 to cap for constrained systems",
276            ),
277        );
278        root.insert(
279            "memory_profile".into(),
280            key_enum_with_env(
281                &["low", "balanced", "performance"],
282                "balanced",
283                "Controls RAM vs feature trade-off",
284                "LEAN_CTX_MEMORY_PROFILE",
285            ),
286        );
287        root.insert(
288            "memory_cleanup".into(),
289            key_enum_with_env(
290                &["aggressive", "shared"],
291                "aggressive",
292                "Controls how aggressively memory is freed when idle",
293                "LEAN_CTX_MEMORY_CLEANUP",
294            ),
295        );
296        root.insert(
297            "savings_footer".into(),
298            key_enum_with_env(
299                &["auto", "always", "never"],
300                "never",
301                "Controls visibility of token savings footers: never (default, suppress everywhere), always, auto (context-dependent)",
302                "LEAN_CTX_SAVINGS_FOOTER",
303            ),
304        );
305        root.insert(
306            "max_ram_percent".into(),
307            key_with_env(
308                "u8",
309                serde_json::json!(cfg.max_ram_percent),
310                "Maximum percentage of system RAM that lean-ctx may use (1-50, default 5)",
311                "LEAN_CTX_MAX_RAM_PERCENT",
312            ),
313        );
314        root.insert(
315            "project_root".into(),
316            key_with_env(
317                "string?",
318                serde_json::json!(null),
319                "Explicit project root directory. Prevents accidental home-directory scans",
320                "LEAN_CTX_PROJECT_ROOT",
321            ),
322        );
323        root.insert(
324            "proxy_enabled".into(),
325            key(
326                "bool?",
327                serde_json::json!(null),
328                "Enable/disable the proxy layer. null = auto-detect, true = force on, false = force off",
329            ),
330        );
331        root.insert(
332            "proxy_port".into(),
333            key(
334                "u16?",
335                serde_json::json!(null),
336                "Custom proxy port (default: 4444). Useful for multi-user systems. Env: LEAN_CTX_PROXY_PORT",
337            ),
338        );
339        root.insert(
340            "response_verbosity".into(),
341            key_enum_with_env(
342                &["normal", "compact", "minimal"],
343                "normal",
344                "Controls how verbose tool responses are",
345                "LEAN_CTX_RESPONSE_VERBOSITY",
346            ),
347        );
348        root.insert(
349            "allow_auto_reroot".into(),
350            key_with_env(
351                "bool",
352                serde_json::json!(false),
353                "Allow automatic project-root re-rooting when absolute paths outside the jail are seen",
354                "LEAN_CTX_ALLOW_REROOT",
355            ),
356        );
357        root.insert(
358            "sandbox_level".into(),
359            key_with_env(
360                "u8",
361                serde_json::json!(0),
362                "Sandbox strictness level (0=default, 1=strict, 2=paranoid)",
363                "LEAN_CTX_SANDBOX_LEVEL",
364            ),
365        );
366        root.insert(
367            "reference_results".into(),
368            key_with_env(
369                "bool",
370                serde_json::json!(false),
371                "Store large tool outputs as references instead of inline content",
372                "LEAN_CTX_REFERENCE_RESULTS",
373            ),
374        );
375        root.insert(
376            "agent_token_budget".into(),
377            key(
378                "usize",
379                serde_json::json!(0),
380                "Default per-agent token budget. 0 = unlimited",
381            ),
382        );
383        root.insert(
384            "shell_allowlist".into(),
385            key_with_env(
386                "array",
387                serde_json::json!([]),
388                "Optional shell command allowlist. When non-empty, only listed binaries are permitted",
389                "LEAN_CTX_SHELL_ALLOWLIST",
390            ),
391        );
392
393        sections.insert(
394            "root".into(),
395            SectionSchema {
396                description: "Top-level configuration keys".into(),
397                keys: root,
398            },
399        );
400
401        sections.insert(
402            "ide_paths".into(),
403            SectionSchema {
404                description: "Per-IDE allowed paths. Keys are agent names (cursor, codex, opencode, antigravity, etc.), values are arrays of paths to index for that agent".into(),
405                keys: BTreeMap::new(),
406            },
407        );
408
409        let mut lsp_keys = BTreeMap::new();
410        lsp_keys.insert(
411            "rust".into(),
412            key(
413                "string?",
414                serde_json::json!(null),
415                "Custom path to rust-analyzer binary",
416            ),
417        );
418        lsp_keys.insert(
419            "typescript".into(),
420            key(
421                "string?",
422                serde_json::json!(null),
423                "Custom path to typescript-language-server binary",
424            ),
425        );
426        lsp_keys.insert(
427            "python".into(),
428            key(
429                "string?",
430                serde_json::json!(null),
431                "Custom path to pylsp binary",
432            ),
433        );
434        lsp_keys.insert(
435            "go".into(),
436            key(
437                "string?",
438                serde_json::json!(null),
439                "Custom path to gopls binary",
440            ),
441        );
442        sections.insert(
443            "lsp".into(),
444            SectionSchema {
445                description: "LSP server binary overrides. Map language name to custom binary path"
446                    .into(),
447                keys: lsp_keys,
448            },
449        );
450
451        let mut archive = BTreeMap::new();
452        archive.insert(
453            "enabled".into(),
454            key(
455                "bool",
456                serde_json::json!(cfg.archive.enabled),
457                "Enable zero-loss compression archive",
458            ),
459        );
460        archive.insert(
461            "threshold_chars".into(),
462            key(
463                "usize",
464                serde_json::json!(cfg.archive.threshold_chars),
465                "Minimum output size (chars) to trigger archiving",
466            ),
467        );
468        archive.insert(
469            "max_age_hours".into(),
470            key(
471                "u64",
472                serde_json::json!(cfg.archive.max_age_hours),
473                "Maximum age of archived entries before cleanup",
474            ),
475        );
476        archive.insert(
477            "max_disk_mb".into(),
478            key(
479                "u64",
480                serde_json::json!(cfg.archive.max_disk_mb),
481                "Maximum total disk usage for the archive",
482            ),
483        );
484        sections.insert("archive".into(), SectionSchema {
485            description: "Settings for the zero-loss compression archive (large tool outputs saved to disk)".into(),
486            keys: archive,
487        });
488
489        let mut autonomy = BTreeMap::new();
490        autonomy.insert(
491            "enabled".into(),
492            key(
493                "bool",
494                serde_json::json!(cfg.autonomy.enabled),
495                "Enable autonomous background behaviors",
496            ),
497        );
498        autonomy.insert(
499            "auto_preload".into(),
500            key(
501                "bool",
502                serde_json::json!(cfg.autonomy.auto_preload),
503                "Auto-preload related files on first read",
504            ),
505        );
506        autonomy.insert(
507            "auto_dedup".into(),
508            key(
509                "bool",
510                serde_json::json!(cfg.autonomy.auto_dedup),
511                "Auto-deduplicate repeated reads",
512            ),
513        );
514        autonomy.insert(
515            "auto_related".into(),
516            key(
517                "bool",
518                serde_json::json!(cfg.autonomy.auto_related),
519                "Auto-load graph-related files",
520            ),
521        );
522        autonomy.insert(
523            "auto_consolidate".into(),
524            key(
525                "bool",
526                serde_json::json!(cfg.autonomy.auto_consolidate),
527                "Auto-consolidate knowledge periodically",
528            ),
529        );
530        autonomy.insert(
531            "silent_preload".into(),
532            key(
533                "bool",
534                serde_json::json!(cfg.autonomy.silent_preload),
535                "Suppress preload notifications in output",
536            ),
537        );
538        autonomy.insert(
539            "dedup_threshold".into(),
540            key(
541                "usize",
542                serde_json::json!(cfg.autonomy.dedup_threshold),
543                "Number of repeated reads before dedup triggers",
544            ),
545        );
546        autonomy.insert(
547            "consolidate_every_calls".into(),
548            key(
549                "u32",
550                serde_json::json!(cfg.autonomy.consolidate_every_calls),
551                "Consolidate knowledge every N tool calls",
552            ),
553        );
554        autonomy.insert(
555            "consolidate_cooldown_secs".into(),
556            key(
557                "u64",
558                serde_json::json!(cfg.autonomy.consolidate_cooldown_secs),
559                "Minimum seconds between consolidation runs",
560            ),
561        );
562        sections.insert(
563            "autonomy".into(),
564            SectionSchema {
565                description:
566                    "Controls autonomous background behaviors (preload, dedup, consolidation)"
567                        .into(),
568                keys: autonomy,
569            },
570        );
571
572        let mut providers = BTreeMap::new();
573        providers.insert(
574            "enabled".into(),
575            key(
576                "bool",
577                serde_json::json!(cfg.providers.enabled),
578                "Master switch for the provider subsystem (GitHub, GitLab, etc.)",
579            ),
580        );
581        providers.insert(
582            "auto_index".into(),
583            key(
584                "bool",
585                serde_json::json!(cfg.providers.auto_index),
586                "Auto-ingest provider results into BM25/embedding indexes",
587            ),
588        );
589        providers.insert(
590            "cache_ttl_secs".into(),
591            key(
592                "u64",
593                serde_json::json!(cfg.providers.cache_ttl_secs),
594                "Default cache TTL for provider results (seconds)",
595            ),
596        );
597        providers.insert(
598            "github.enabled".into(),
599            key(
600                "bool",
601                serde_json::json!(cfg.providers.github.enabled),
602                "Enable/disable GitHub provider",
603            ),
604        );
605        providers.insert(
606            "github.api_url".into(),
607            key(
608                "string",
609                serde_json::json!(cfg.providers.github.api_url),
610                "GitHub API base URL (for GitHub Enterprise)",
611            ),
612        );
613        providers.insert(
614            "gitlab.enabled".into(),
615            key(
616                "bool",
617                serde_json::json!(cfg.providers.gitlab.enabled),
618                "Enable/disable GitLab provider",
619            ),
620        );
621        providers.insert(
622            "gitlab.api_url".into(),
623            key(
624                "string",
625                serde_json::json!(cfg.providers.gitlab.api_url),
626                "GitLab API base URL (for self-hosted instances)",
627            ),
628        );
629        sections.insert(
630            "providers".into(),
631            SectionSchema {
632                description:
633                    "External context providers (GitHub, GitLab, Jira, etc.). Set tokens via env vars (GITHUB_TOKEN, GITLAB_TOKEN)."
634                        .into(),
635                keys: providers,
636            },
637        );
638
639        let mut loop_det = BTreeMap::new();
640        loop_det.insert(
641            "normal_threshold".into(),
642            key(
643                "u32",
644                serde_json::json!(cfg.loop_detection.normal_threshold),
645                "Repetitions before reducing output",
646            ),
647        );
648        loop_det.insert(
649            "reduced_threshold".into(),
650            key(
651                "u32",
652                serde_json::json!(cfg.loop_detection.reduced_threshold),
653                "Repetitions before further reducing output",
654            ),
655        );
656        loop_det.insert(
657            "blocked_threshold".into(),
658            key(
659                "u32",
660                serde_json::json!(cfg.loop_detection.blocked_threshold),
661                "Repetitions before blocking. 0 = disabled",
662            ),
663        );
664        loop_det.insert(
665            "window_secs".into(),
666            key(
667                "u64",
668                serde_json::json!(cfg.loop_detection.window_secs),
669                "Time window in seconds for loop detection",
670            ),
671        );
672        loop_det.insert(
673            "search_group_limit".into(),
674            key(
675                "u32",
676                serde_json::json!(cfg.loop_detection.search_group_limit),
677                "Maximum unique searches within a loop window",
678            ),
679        );
680        loop_det.insert(
681            "tool_total_limits".into(),
682            key(
683                "table",
684                serde_json::json!({"ctx_read": 100, "ctx_search": 80, "ctx_shell": 50, "ctx_semantic_search": 60}),
685                "Per-tool total call limits within a session. Keys are tool names, values are max calls",
686            ),
687        );
688        sections.insert(
689            "loop_detection".into(),
690            SectionSchema {
691                description: "Loop detection settings for preventing repeated identical tool calls"
692                    .into(),
693                keys: loop_det,
694            },
695        );
696
697        let mut updates = BTreeMap::new();
698        updates.insert(
699            "auto_update".into(),
700            key(
701                "bool",
702                serde_json::json!(cfg.updates.auto_update),
703                "Enable automatic updates (requires explicit opt-in)",
704            ),
705        );
706        updates.insert(
707            "check_interval_hours".into(),
708            key(
709                "u64",
710                serde_json::json!(cfg.updates.check_interval_hours),
711                "How often to check for updates (hours)",
712            ),
713        );
714        updates.insert(
715            "notify_only".into(),
716            key(
717                "bool",
718                serde_json::json!(cfg.updates.notify_only),
719                "Only notify about updates, don't install automatically",
720            ),
721        );
722        sections.insert(
723            "updates".into(),
724            SectionSchema {
725                description: "Automatic update configuration".into(),
726                keys: updates,
727            },
728        );
729
730        let mut boundary = BTreeMap::new();
731        boundary.insert(
732            "cross_project_search".into(),
733            key(
734                "bool",
735                serde_json::json!(cfg.boundary_policy.cross_project_search),
736                "Allow searching across project boundaries",
737            ),
738        );
739        boundary.insert(
740            "cross_project_import".into(),
741            key(
742                "bool",
743                serde_json::json!(cfg.boundary_policy.cross_project_import),
744                "Allow importing knowledge from other projects",
745            ),
746        );
747        boundary.insert(
748            "audit_cross_access".into(),
749            key(
750                "bool",
751                serde_json::json!(cfg.boundary_policy.audit_cross_access),
752                "Log audit events when cross-project access occurs",
753            ),
754        );
755        boundary.insert(
756            "universal_gotchas_enabled".into(),
757            key(
758                "bool",
759                serde_json::json!(cfg.boundary_policy.universal_gotchas_enabled),
760                "Load universal (cross-project) gotchas",
761            ),
762        );
763        sections.insert(
764            "boundary_policy".into(),
765            SectionSchema {
766                description: "Cross-project boundary and access control policies".into(),
767                keys: boundary,
768            },
769        );
770
771        let mut secret_det = BTreeMap::new();
772        secret_det.insert(
773            "enabled".into(),
774            key(
775                "bool",
776                serde_json::json!(cfg.secret_detection.enabled),
777                "Enable secret/credential detection in tool outputs",
778            ),
779        );
780        secret_det.insert(
781            "redact".into(),
782            key(
783                "bool",
784                serde_json::json!(cfg.secret_detection.redact),
785                "Redact detected secrets from output",
786            ),
787        );
788        secret_det.insert(
789            "custom_patterns".into(),
790            key(
791                "array",
792                serde_json::json!(cfg.secret_detection.custom_patterns),
793                "Additional regex patterns to detect as secrets",
794            ),
795        );
796        sections.insert(
797            "secret_detection".into(),
798            SectionSchema {
799                description: "Secret/credential detection and redaction settings".into(),
800                keys: secret_det,
801            },
802        );
803
804        let mut cloud = BTreeMap::new();
805        cloud.insert(
806            "contribute_enabled".into(),
807            key(
808                "bool",
809                serde_json::json!(cfg.cloud.contribute_enabled),
810                "Enable contributing anonymized stats to lean-ctx cloud",
811            ),
812        );
813        sections.insert(
814            "cloud".into(),
815            SectionSchema {
816                description: "Cloud feature settings".into(),
817                keys: cloud,
818            },
819        );
820
821        let mut proxy = BTreeMap::new();
822        proxy.insert(
823            "anthropic_upstream".into(),
824            key(
825                "string?",
826                serde_json::json!(cfg.proxy.anthropic_upstream),
827                "Custom upstream URL for Anthropic API proxy",
828            ),
829        );
830        proxy.insert(
831            "openai_upstream".into(),
832            key(
833                "string?",
834                serde_json::json!(cfg.proxy.openai_upstream),
835                "Custom upstream URL for OpenAI API proxy",
836            ),
837        );
838        proxy.insert(
839            "gemini_upstream".into(),
840            key(
841                "string?",
842                serde_json::json!(cfg.proxy.gemini_upstream),
843                "Custom upstream URL for Gemini API proxy",
844            ),
845        );
846        sections.insert(
847            "proxy".into(),
848            SectionSchema {
849                description: "Proxy upstream configuration for API routing".into(),
850                keys: proxy,
851            },
852        );
853
854        let mem = &cfg.memory;
855        let mut mem_knowledge = BTreeMap::new();
856        mem_knowledge.insert(
857            "max_facts".into(),
858            key(
859                "usize",
860                serde_json::json!(mem.knowledge.max_facts),
861                "Maximum number of knowledge facts stored per project",
862            ),
863        );
864        mem_knowledge.insert(
865            "max_patterns".into(),
866            key(
867                "usize",
868                serde_json::json!(mem.knowledge.max_patterns),
869                "Maximum number of patterns stored",
870            ),
871        );
872        mem_knowledge.insert(
873            "max_history".into(),
874            key(
875                "usize",
876                serde_json::json!(mem.knowledge.max_history),
877                "Maximum history entries retained",
878            ),
879        );
880        mem_knowledge.insert(
881            "contradiction_threshold".into(),
882            key(
883                "f32",
884                clean_f32(mem.knowledge.contradiction_threshold),
885                "Confidence threshold for contradiction detection",
886            ),
887        );
888        mem_knowledge.insert(
889            "recall_facts_limit".into(),
890            key(
891                "usize",
892                serde_json::json!(mem.knowledge.recall_facts_limit),
893                "Maximum facts returned per recall query",
894            ),
895        );
896        mem_knowledge.insert(
897            "rooms_limit".into(),
898            key(
899                "usize",
900                serde_json::json!(mem.knowledge.rooms_limit),
901                "Maximum number of rooms returned",
902            ),
903        );
904        mem_knowledge.insert(
905            "timeline_limit".into(),
906            key(
907                "usize",
908                serde_json::json!(mem.knowledge.timeline_limit),
909                "Maximum number of timeline entries returned",
910            ),
911        );
912        mem_knowledge.insert(
913            "relations_limit".into(),
914            key(
915                "usize",
916                serde_json::json!(mem.knowledge.relations_limit),
917                "Maximum number of relations returned",
918            ),
919        );
920        sections.insert(
921            "memory.knowledge".into(),
922            SectionSchema {
923                description: "Knowledge memory budgets (facts, patterns, gotchas)".into(),
924                keys: mem_knowledge,
925            },
926        );
927
928        let mut mem_episodic = BTreeMap::new();
929        mem_episodic.insert(
930            "max_episodes".into(),
931            key(
932                "usize",
933                serde_json::json!(mem.episodic.max_episodes),
934                "Maximum number of episodes retained",
935            ),
936        );
937        mem_episodic.insert(
938            "max_actions_per_episode".into(),
939            key(
940                "usize",
941                serde_json::json!(mem.episodic.max_actions_per_episode),
942                "Maximum actions tracked per episode",
943            ),
944        );
945        mem_episodic.insert(
946            "summary_max_chars".into(),
947            key(
948                "usize",
949                serde_json::json!(mem.episodic.summary_max_chars),
950                "Maximum characters in episode summary",
951            ),
952        );
953        sections.insert(
954            "memory.episodic".into(),
955            SectionSchema {
956                description: "Episodic memory budgets (session episodes)".into(),
957                keys: mem_episodic,
958            },
959        );
960
961        let mut mem_procedural = BTreeMap::new();
962        mem_procedural.insert(
963            "max_procedures".into(),
964            key(
965                "usize",
966                serde_json::json!(mem.procedural.max_procedures),
967                "Maximum number of learned procedures stored",
968            ),
969        );
970        mem_procedural.insert(
971            "min_repetitions".into(),
972            key(
973                "usize",
974                serde_json::json!(mem.procedural.min_repetitions),
975                "Minimum repetitions before a pattern is stored",
976            ),
977        );
978        mem_procedural.insert(
979            "min_sequence_len".into(),
980            key(
981                "usize",
982                serde_json::json!(mem.procedural.min_sequence_len),
983                "Minimum sequence length for procedure detection",
984            ),
985        );
986        mem_procedural.insert(
987            "max_window_size".into(),
988            key(
989                "usize",
990                serde_json::json!(mem.procedural.max_window_size),
991                "Maximum window size for pattern analysis",
992            ),
993        );
994        sections.insert(
995            "memory.procedural".into(),
996            SectionSchema {
997                description: "Procedural memory budgets (learned patterns)".into(),
998                keys: mem_procedural,
999            },
1000        );
1001
1002        let mut mem_lifecycle = BTreeMap::new();
1003        mem_lifecycle.insert(
1004            "decay_rate".into(),
1005            key(
1006                "f32",
1007                clean_f32(mem.lifecycle.decay_rate),
1008                "Rate at which knowledge confidence decays over time",
1009            ),
1010        );
1011        mem_lifecycle.insert(
1012            "low_confidence_threshold".into(),
1013            key(
1014                "f32",
1015                clean_f32(mem.lifecycle.low_confidence_threshold),
1016                "Threshold below which facts are considered low-confidence",
1017            ),
1018        );
1019        mem_lifecycle.insert(
1020            "stale_days".into(),
1021            key(
1022                "i64",
1023                serde_json::json!(mem.lifecycle.stale_days),
1024                "Days after which unused facts are considered stale",
1025            ),
1026        );
1027        mem_lifecycle.insert(
1028            "similarity_threshold".into(),
1029            key(
1030                "f32",
1031                clean_f32(mem.lifecycle.similarity_threshold),
1032                "Similarity threshold for deduplication",
1033            ),
1034        );
1035        sections.insert(
1036            "memory.lifecycle".into(),
1037            SectionSchema {
1038                description: "Knowledge lifecycle policy (decay, staleness, dedup)".into(),
1039                keys: mem_lifecycle,
1040            },
1041        );
1042
1043        let mut mem_gotcha = BTreeMap::new();
1044        mem_gotcha.insert(
1045            "max_gotchas_per_project".into(),
1046            key(
1047                "usize",
1048                serde_json::json!(mem.gotcha.max_gotchas_per_project),
1049                "Maximum gotchas stored per project",
1050            ),
1051        );
1052        mem_gotcha.insert(
1053            "retrieval_budget_per_room".into(),
1054            key(
1055                "usize",
1056                serde_json::json!(mem.gotcha.retrieval_budget_per_room),
1057                "Maximum gotchas retrieved per room per query",
1058            ),
1059        );
1060        mem_gotcha.insert(
1061            "default_decay_rate".into(),
1062            key(
1063                "f32",
1064                clean_f32(mem.gotcha.default_decay_rate),
1065                "Default decay rate for gotcha importance",
1066            ),
1067        );
1068        sections.insert(
1069            "memory.gotcha".into(),
1070            SectionSchema {
1071                description: "Gotcha memory settings (project-specific warnings and pitfalls)"
1072                    .into(),
1073                keys: mem_gotcha,
1074            },
1075        );
1076
1077        let mut mem_embeddings = BTreeMap::new();
1078        mem_embeddings.insert(
1079            "max_facts".into(),
1080            key(
1081                "usize",
1082                serde_json::json!(mem.embeddings.max_facts),
1083                "Maximum number of embedding facts stored",
1084            ),
1085        );
1086        sections.insert(
1087            "memory.embeddings".into(),
1088            SectionSchema {
1089                description: "Embeddings memory settings for semantic search".into(),
1090                keys: mem_embeddings,
1091            },
1092        );
1093
1094        let mut aliases = BTreeMap::new();
1095        aliases.insert(
1096            "command".into(),
1097            key(
1098                "string",
1099                serde_json::json!(""),
1100                "The command pattern to match (e.g. 'deploy')",
1101            ),
1102        );
1103        aliases.insert(
1104            "alias".into(),
1105            key(
1106                "string",
1107                serde_json::json!(""),
1108                "The alias definition to execute",
1109            ),
1110        );
1111        sections.insert("custom_aliases".into(), SectionSchema {
1112            description: "Custom command aliases (array of {command, alias} entries). Note: field names are 'command' and 'alias' (not 'name')".into(),
1113            keys: aliases,
1114        });
1115
1116        if let Some(root_section) = sections.get_mut("root") {
1117            root_section.keys.insert(
1118                "custom_aliases".into(),
1119                key(
1120                    "array",
1121                    serde_json::json!([]),
1122                    "Custom command aliases (array of {command, alias} entries)",
1123                ),
1124            );
1125        }
1126
1127        ConfigSchema {
1128            version: 1,
1129            sections,
1130        }
1131    }
1132
1133    /// All known TOML keys (dot-separated) for validation.
1134    pub fn known_keys(&self) -> Vec<String> {
1135        let mut keys = Vec::new();
1136        for (section, schema) in &self.sections {
1137            if section == "root" {
1138                for key_name in schema.keys.keys() {
1139                    keys.push(key_name.clone());
1140                }
1141            } else {
1142                if schema.keys.is_empty() {
1143                    keys.push(section.clone());
1144                }
1145                for key_name in schema.keys.keys() {
1146                    keys.push(format!("{section}.{key_name}"));
1147                }
1148            }
1149        }
1150        keys
1151    }
1152}