1use super::{SettingKey, ValueKind};
8
9pub const SETTING_KEYS: &[SettingKey] = &[
26 SettingKey {
27 key: "agent_surface.max_items",
28 default: Some("0"),
29 kind: ValueKind::Unsigned,
30 },
31 SettingKey {
32 key: "agent_surface.max_output_bytes",
33 default: Some("0"),
34 kind: ValueKind::Unsigned,
35 },
36 SettingKey {
37 key: "agent_surface.truncate_content",
38 default: Some("0"),
39 kind: ValueKind::Unsigned,
40 },
41 SettingKey {
42 key: "cache.dir",
43 default: None,
44 kind: ValueKind::Path,
45 },
46 SettingKey {
47 key: "cli.max_instances",
48 default: None,
49 kind: ValueKind::Unsigned,
50 },
51 SettingKey {
52 key: "cli.no_input",
53 default: Some("false"),
54 kind: ValueKind::Bool,
55 },
56 SettingKey {
57 key: "cli.stdin_timeout_secs",
58 default: Some("60"),
59 kind: ValueKind::Unsigned,
60 },
61 SettingKey {
62 key: "db.busy_base_delay_ms",
63 default: Some("300"),
64 kind: ValueKind::Unsigned,
65 },
66 SettingKey {
67 key: "db.busy_retries",
68 default: Some("5"),
69 kind: ValueKind::Unsigned,
70 },
71 SettingKey {
72 key: "db.path",
73 default: None,
74 kind: ValueKind::Path,
75 },
76 SettingKey {
77 key: "db.query_timeout_ms",
78 default: Some("5000"),
79 kind: ValueKind::Unsigned,
80 },
81 SettingKey {
82 key: "display.tz",
83 default: Some("UTC"),
84 kind: ValueKind::Tz,
85 },
86 SettingKey {
87 key: "embedding.batch_size",
88 default: Some("32"),
89 kind: ValueKind::Unsigned,
90 },
91 SettingKey {
92 key: "embedding.dim",
93 default: Some("1024"),
94 kind: ValueKind::Unsigned,
95 },
96 SettingKey {
97 key: "embedding.model",
104 default: None,
105 kind: ValueKind::Text,
106 },
107 SettingKey {
108 key: "embedding.backend",
117 default: None,
118 kind: ValueKind::OneOf(&["auto", "openrouter", "open-router"]),
119 },
120 SettingKey {
121 key: "llm.backend",
126 default: None,
127 kind: ValueKind::OneOf(&["none", "openrouter", "open-router"]),
128 },
129 SettingKey {
130 key: "search.hybrid.max_graph_results",
134 default: Some("50"),
135 kind: ValueKind::Unsigned,
136 },
137 SettingKey {
138 key: "embedding.entity_cache_max_entries",
139 default: Some("10000"),
140 kind: ValueKind::Unsigned,
141 },
142 SettingKey {
143 key: "embedding.entity_cache_ttl_secs",
144 default: Some("3600"),
145 kind: ValueKind::Unsigned,
146 },
147 SettingKey {
148 key: "embedding.timeout_secs",
149 default: Some("300"),
150 kind: ValueKind::Unsigned,
151 },
152 SettingKey {
153 key: "enrich.circuit_breaker_reset_secs",
154 default: Some("60"),
155 kind: ValueKind::Unsigned,
156 },
157 SettingKey {
158 key: "enrich.entity_connect.default_limit",
159 default: Some("100"),
160 kind: ValueKind::Unsigned,
161 },
162 SettingKey {
163 key: "enrich.entity_connect.large_ns_limit",
164 default: Some("25"),
165 kind: ValueKind::Unsigned,
166 },
167 SettingKey {
168 key: "enrich.entity_description.corpus_top_k",
169 default: Some("8"),
170 kind: ValueKind::Unsigned,
171 },
172 SettingKey {
173 key: "enrich.entity_description.domain",
174 default: Some("auto"),
175 kind: ValueKind::Text,
176 },
177 SettingKey {
178 key: "enrich.entity_description.grounding_threshold",
179 default: Some("0.30"),
180 kind: ValueKind::Float,
181 },
182 SettingKey {
183 key: "enrich.entity_description.neighbour_top_k",
184 default: Some("12"),
185 kind: ValueKind::Unsigned,
186 },
187 SettingKey {
188 key: "enrich.entity_description.min_corpus_chars",
189 default: Some("40"),
190 kind: ValueKind::Unsigned,
191 },
192 SettingKey {
193 key: "enrich.entity_description.quality_sample",
194 default: Some("50"),
195 kind: ValueKind::Unsigned,
196 },
197 SettingKey {
198 key: "enrich.entity_description.snippet_chars",
199 default: Some("2000"),
200 kind: ValueKind::Unsigned,
201 },
202 SettingKey {
217 key: "enrich.entity_type.allowed_types",
218 default: None,
219 kind: ValueKind::Text,
220 },
221 SettingKey {
222 key: "enrich.entity_type.on_unknown_type",
223 default: Some("keep"),
224 kind: ValueKind::Text,
225 },
226 SettingKey {
227 key: "enrich.entity_type_validate.corpus_top_k",
228 default: Some("8"),
229 kind: ValueKind::Unsigned,
230 },
231 SettingKey {
232 key: "enrich.entity_type_validate.min_corpus_chars",
233 default: Some("40"),
234 kind: ValueKind::Unsigned,
235 },
236 SettingKey {
237 key: "enrich.entity_type_validate.neighbour_top_k",
238 default: Some("12"),
239 kind: ValueKind::Unsigned,
240 },
241 SettingKey {
242 key: "enrich.entity_type_validate.snippet_chars",
243 default: Some("2000"),
244 kind: ValueKind::Unsigned,
245 },
246 SettingKey {
247 key: "enrich.rate_limit_deadline_secs",
248 default: Some("3600"),
249 kind: ValueKind::Unsigned,
250 },
251 SettingKey {
252 key: "enrich.reembed_claim_batch",
253 default: Some("32"),
254 kind: ValueKind::Unsigned,
255 },
256 SettingKey {
257 key: "enrich.scan_page_size",
258 default: Some("512"),
259 kind: ValueKind::Unsigned,
260 },
261 SettingKey {
262 key: "enrich.yield_every_n_items",
263 default: Some("10"),
264 kind: ValueKind::Unsigned,
265 },
266 SettingKey {
267 key: "i18n.lang",
268 default: Some("en"),
269 kind: ValueKind::OneOf(&[
271 "en",
272 "EN",
273 "english",
274 "pt",
275 "PT",
276 "pt-BR",
277 "pt-br",
278 "portugues",
279 "portuguese",
280 ]),
281 },
282 SettingKey {
283 key: "ingest.low_memory",
284 default: Some("false"),
285 kind: ValueKind::Bool,
286 },
287 SettingKey {
288 key: "limits.max_entities_per_memory",
289 default: Some("50"),
290 kind: ValueKind::Unsigned,
291 },
292 SettingKey {
293 key: "limits.max_relations_per_memory",
294 default: Some("50"),
295 kind: ValueKind::Unsigned,
296 },
297 SettingKey {
298 key: "llm.fallback",
299 default: Some("none"),
300 kind: ValueKind::OneOf(&["none"]),
302 },
303 SettingKey {
304 key: "llm.max_host_concurrency",
305 default: None,
306 kind: ValueKind::Unsigned,
307 },
308 SettingKey {
309 key: "llm.model",
310 default: None,
311 kind: ValueKind::Text,
312 },
313 SettingKey {
314 key: "llm.openrouter_timeout_secs",
315 default: Some("600"),
316 kind: ValueKind::Unsigned,
317 },
318 SettingKey {
319 key: "llm.probe_timeout_ms",
320 default: Some("800"),
321 kind: ValueKind::Unsigned,
322 },
323 SettingKey {
324 key: "llm.skip_embedding_on_failure",
325 default: Some("false"),
326 kind: ValueKind::Bool,
327 },
328 SettingKey {
329 key: "llm.slot_no_wait",
330 default: Some("false"),
331 kind: ValueKind::Bool,
332 },
333 SettingKey {
334 key: "llm.slot_wait_secs",
335 default: Some("300"),
336 kind: ValueKind::Unsigned,
337 },
338 SettingKey {
339 key: "llm.worker_rss_mb",
340 default: Some("350"),
341 kind: ValueKind::Unsigned,
342 },
343 SettingKey {
344 key: "log.format",
345 default: Some("pretty"),
346 kind: ValueKind::OneOf(&["pretty", "json"]),
347 },
348 SettingKey {
349 key: "log.level",
350 default: Some("warn"),
351 kind: ValueKind::LogDirective,
352 },
353 SettingKey {
354 key: "log.retention_days",
355 default: Some("7"),
356 kind: ValueKind::Unsigned,
357 },
358 SettingKey {
359 key: "log.rotation",
360 default: Some("daily"),
361 kind: ValueKind::OneOf(&["daily", "hourly", "never"]),
362 },
363 SettingKey {
364 key: "log.to_file",
365 default: Some("false"),
366 kind: ValueKind::Bool,
367 },
368 SettingKey {
369 key: "namespace.default",
370 default: Some("global"),
371 kind: ValueKind::Text,
372 },
373 SettingKey {
374 key: "network.chat_url",
375 default: None,
376 kind: ValueKind::Url,
377 },
378 SettingKey {
379 key: "network.embed_url",
380 default: None,
381 kind: ValueKind::Url,
382 },
383 SettingKey {
384 key: "network.openrouter.chat_url",
385 default: Some(crate::constants::DEFAULT_OPENROUTER_CHAT_URL),
386 kind: ValueKind::Url,
387 },
388 SettingKey {
389 key: "network.openrouter.embeddings_url",
390 default: Some(crate::constants::DEFAULT_OPENROUTER_EMBEDDINGS_URL),
391 kind: ValueKind::Url,
392 },
393 SettingKey {
394 key: "parallelism.embed_runtime_threads",
395 default: None,
396 kind: ValueKind::Unsigned,
397 },
398 SettingKey {
399 key: "parallelism.max_total_workers",
400 default: Some("64"),
401 kind: ValueKind::Unsigned,
402 },
403 SettingKey {
404 key: "parallelism.rayon_threads",
405 default: None,
406 kind: ValueKind::Unsigned,
407 },
408 SettingKey {
409 key: "retry.disable",
410 default: Some("false"),
411 kind: ValueKind::Bool,
412 },
413 SettingKey {
414 key: "shutdown.ignore",
415 default: Some("false"),
416 kind: ValueKind::Bool,
417 },
418 SettingKey {
419 key: "system.max_load_per_ncpu",
420 default: Some("2.0"),
421 kind: ValueKind::Float,
422 },
423];
424
425pub fn setting_key_names() -> impl Iterator<Item = &'static str> {
430 SETTING_KEYS.iter().map(|entry| entry.key)
431}
432
433pub(super) const LEGACY_SETTING_KEYS: &[(&str, &str)] =
445 &[("db.default_path", "db.path"), ("paths.cache", "cache.dir")];
446
447const SUGGESTION_THRESHOLD: f64 = 0.7;
452
453pub fn is_known_setting(key: &str) -> bool {
455 setting_key_names().any(|known| known == key)
456}
457
458pub fn nearest_setting_key(key: &str) -> Option<&'static str> {
464 setting_key_names()
465 .map(|candidate| {
466 let score = rapidfuzz::distance::jaro_winkler::normalized_similarity(
467 key.chars(),
468 candidate.chars(),
469 );
470 (candidate, score)
471 })
472 .filter(|(_, score)| *score >= SUGGESTION_THRESHOLD)
473 .max_by(|a, b| a.1.total_cmp(&b.1))
474 .map(|(candidate, _)| candidate)
475}
476#[cfg(test)]
480mod setting_keys_drift_tests {
481 use super::SETTING_KEYS;
482
483 const SCANNED_SOURCES: &[&str] = &[
496 include_str!("../commands/enrich/extraction_descriptions.rs"),
497 include_str!("../commands/enrich/extraction_graph.rs"),
501 include_str!("../commands/enrich/events/entity_type_policy.rs"),
504 include_str!("../commands/enrich/prompts.rs"),
505 include_str!("../commands/enrich/quality_sample.rs"),
506 include_str!("../commands/enrich/scheduler.rs"),
507 include_str!("../commands/enrich/status.rs"),
508 include_str!("../commands/ingest/args.rs"),
509 include_str!("../constants/mod.rs"),
513 include_str!("../constants/identity.rs"),
514 include_str!("../constants/network.rs"),
515 include_str!("../constants/exit_codes.rs"),
516 include_str!("../constants/storage.rs"),
517 include_str!("../constants/embedding.rs"),
518 include_str!("../constants/enrich.rs"),
519 include_str!("../constants/search.rs"),
520 include_str!("../constants/runtime.rs"),
521 include_str!("../constants/limits.rs"),
522 include_str!("../embedder/getters.rs"),
523 include_str!("../i18n/mod.rs"),
524 include_str!("../lib.rs"),
525 include_str!("../llm_slots.rs"),
526 include_str!("../lock.rs"),
527 include_str!("../main.rs"),
528 include_str!("../namespace.rs"),
529 include_str!("../paths.rs"),
530 include_str!("../retry.rs"),
531 include_str!("../system_load.rs"),
532 include_str!("../tracing_init.rs"),
533 include_str!("../tz.rs"),
534 ];
535
536 const KEY_CALL_MARKERS: &[&str] = &["get_setting(\""];
542
543 const KEY_RESOLVER_MARKERS: &[&str] = &[
556 "resolve_u64(",
557 "resolve_usize(",
558 "resolve_bool(",
559 "resolve_string(",
560 ];
561
562 fn normalize(src: &str) -> String {
569 src.lines()
570 .filter(|line| !line.trim_start().starts_with("//"))
571 .flat_map(|line| line.chars().filter(|c| !c.is_whitespace()))
572 .collect()
573 }
574
575 pub(super) fn scan_keys(src: &str) -> Vec<String> {
580 let normalized = normalize(src);
581 let mut found = Vec::new();
582 for marker in KEY_CALL_MARKERS {
583 for cap in normalized.split(marker).skip(1) {
584 if let Some(end) = cap.find('"') {
585 if !cap[..end].is_empty() {
586 found.push(cap[..end].to_string());
587 }
588 }
589 }
590 }
591 for marker in KEY_RESOLVER_MARKERS {
592 for cap in normalized.split(marker).skip(1) {
593 let Some(comma) = cap.find(',') else { continue };
597 let close = cap.find(')').unwrap_or(usize::MAX);
598 if comma > close {
599 continue;
600 }
601 let rest = &cap[comma + 1..];
602 if !rest.starts_with('"') {
603 continue;
604 }
605 if let Some(end) = rest[1..].find('"') {
606 if end > 0 {
607 found.push(rest[1..1 + end].to_string());
608 }
609 }
610 }
611 }
612 found
613 }
614
615 #[test]
616 fn setting_keys_covers_every_literal_key_in_src() {
617 let registered: std::collections::HashSet<&str> =
618 SETTING_KEYS.iter().map(|e| e.key).collect();
619 let mut missing: Vec<String> = SCANNED_SOURCES
620 .iter()
621 .flat_map(|src| scan_keys(src))
622 .filter(|key| !registered.contains(key.as_str()))
623 .collect();
624 missing.sort();
625 missing.dedup();
626 assert!(
627 missing.is_empty(),
628 "setting keys read at runtime but missing from SETTING_KEYS: {missing:?}"
629 );
630 }
631
632 const COMPLETENESS_EXEMPT_DIRS: &[&str] = &["config"];
638 const COMPLETENESS_EXEMPT_FILES: &[&str] = &["runtime_config.rs"];
639
640 #[test]
641 fn scanned_sources_lists_every_file_that_reads_a_setting_key() {
642 let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
651 if !root.is_dir() {
652 eprintln!("skipping: {} is not a directory", root.display());
655 return;
656 }
657
658 let mut rust_files = Vec::new();
659 collect_rust_files(&root, &mut rust_files);
660 assert!(
661 !rust_files.is_empty(),
662 "walk found zero .rs files under {} — the walk itself is broken, \
663 which is exactly how this guard would go silently blind",
664 root.display()
665 );
666
667 let listed: String = SCANNED_SOURCES.concat();
668 let mut unlisted = Vec::new();
669 for path in &rust_files {
670 let Ok(rel) = path.strip_prefix(&root) else {
671 continue;
672 };
673 let exempt_dir = rel.components().any(|c| {
674 COMPLETENESS_EXEMPT_DIRS.contains(&c.as_os_str().to_string_lossy().as_ref())
675 });
676 let exempt_file = rel
677 .file_name()
678 .is_some_and(|f| COMPLETENESS_EXEMPT_FILES.contains(&f.to_string_lossy().as_ref()));
679 if exempt_dir || exempt_file {
680 continue;
681 }
682 let Ok(body) = std::fs::read_to_string(path) else {
683 continue;
684 };
685 if scan_keys(&body).is_empty() {
686 continue;
687 }
688 if !listed.contains(body.as_str()) {
691 unlisted.push(rel.display().to_string());
692 }
693 }
694 unlisted.sort();
695 assert!(
696 unlisted.is_empty(),
697 "these files read a setting key but are absent from SCANNED_SOURCES: {unlisted:?}"
698 );
699 }
700
701 fn collect_rust_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
703 let Ok(entries) = std::fs::read_dir(dir) else {
704 return;
705 };
706 for entry in entries.flatten() {
707 let path = entry.path();
708 if path.is_dir() {
709 collect_rust_files(&path, out);
710 } else if path.extension().is_some_and(|e| e == "rs") {
711 out.push(path);
712 }
713 }
714 }
715
716 #[test]
717 fn scanner_sees_resolve_calls_wrapped_across_lines() {
718 let src = r#"
721 let v = crate::runtime_config::resolve_u64(
722 None,
723 "made.up.key",
724 7,
725 );
726 "#;
727 assert_eq!(scan_keys(src), vec!["made.up.key".to_string()]);
728 }
729
730 #[test]
731 fn scanner_ignores_keys_quoted_inside_whole_line_comments() {
732 let src = " /// scans for get_setting(\"placeholder\") call sites\n";
734 assert!(scan_keys(src).is_empty());
735 }
736}