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