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