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