eidetic-engine 0.15.1

Durable, local-first, explainable memory for coding agents.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
//! Self-explaining config + advisory config-lint (bd-2vq2z.15).
//!
//! `ee` has many config knobs (search weights, profiles, budgets, embeddings,
//! mesh) but no surface that explains what a setting *does*, its effective
//! value, which layer set it, its valid range, and — crucially — whether it
//! actually affects runtime today. The analyst was misled by
//! `search.semantic_weight` (the name implies neural semantics, but the default
//! vector tier is deterministic hash embedding). This module ends that class of
//! silent-misconfig confusion.
//!
//! Two surfaces are built on this core:
//! - `ee config explain [<key>] [--json]` — describe a knob: effect, effective
//!   value, source layer (`cli|environment|project|user|default`, reusing the
//!   merge machinery's [`crate::config::merge::ConfigValueSource`]), valid
//!   range, and honest runtime status (active / forward-looking / inert);
//! - a config-lint advisory check inside `ee doctor` — flag suspicious config
//!   (a weight set but the tier it weights is non-neural, an embedding model
//!   path that does not exist, an env var `ee` does not consume, contradictory
//!   settings). **Advisory only — config-lint never flips core health.**
//!
//! This module is the pure, deterministic policy core: the knob registry, the
//! explain assembly, and the lint rules as pure functions over config facts —
//! unit-testable without a live config. The CLI/doctor wiring adapts the merged
//! config to these cores in the follow-on leaves.

use serde::Serialize;
use serde_json::{Value, json};

use crate::config::merge::{
    ConfigValueSource, MESH_ENABLED_KEY, PACK_DEFAULT_MAX_TOKENS_KEY, PACK_MMR_LAMBDA_KEY,
    SEARCH_DEFAULT_SPEED_KEY, SEARCH_GRAPH_WEIGHT_KEY, SEARCH_LEXICAL_WEIGHT_KEY,
    SEARCH_RERANK_KEY, SEARCH_RERANK_TOP_K_KEY, SEARCH_SEMANTIC_WEIGHT_KEY,
    STORAGE_DATABASE_PATH_KEY,
};

/// Schema identifier for the config-explain block.
pub const CONFIG_EXPLAIN_SCHEMA_V1: &str = "ee.config_explain.v1";

/// Severity of every config-lint finding. Config-lint is advisory: it informs,
/// it never degrades the top-line `ee doctor`/`ee status` health verdict.
pub const CONFIG_LINT_SEVERITY: &str = "advisory";

/// Whether this knob actually affects runtime behavior today — the
/// truth-in-labeling axis the analyst needed.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ConfigRuntimeStatus {
    /// Wired and affecting runtime behavior now.
    Active,
    /// Parsed and accepted, but not yet wired into live behavior (reserved). An
    /// honest "you can set this, but it does not do what the name implies yet".
    ForwardLooking,
    /// Currently has no effect given other active settings.
    Inert,
}

impl ConfigRuntimeStatus {
    /// Stable machine token.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Active => "active",
            Self::ForwardLooking => "forward_looking",
            Self::Inert => "inert",
        }
    }
}

/// A static description of one config knob: what it does, its range, and its
/// honest runtime status. The registry of these is the backbone of
/// `ee config explain`.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ConfigKnob {
    /// The fully-qualified config key (e.g. `search.semantic_weight`).
    pub key: &'static str,
    /// Coarse category for grouping (`search`, `pack`, `storage`, `mesh`, ...).
    pub category: &'static str,
    /// What the knob affects at runtime, in one sentence.
    pub effect: &'static str,
    /// The valid range / accepted values, human-readable.
    pub valid_range: &'static str,
    /// Whether it actually affects runtime today.
    pub status: ConfigRuntimeStatus,
    /// A truth-in-labeling caveat when the name could mislead, else `None`.
    pub caveat: Option<&'static str>,
}

/// The config-knob registry. Covers the knobs with real semantic nuance and the
/// analyst's exact trap; extend as knobs gain explain coverage. Sorted by key so
/// `ee config explain` (no key) output is byte-stable.
#[must_use]
pub fn config_knobs() -> &'static [ConfigKnob] {
    // Keep sorted by `key`.
    &[
        ConfigKnob {
            key: MESH_ENABLED_KEY,
            category: "mesh",
            effect: "Enables the optional machine-to-machine memory mesh; off by default and never required for local operation.",
            valid_range: "true | false",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: PACK_DEFAULT_MAX_TOKENS_KEY,
            category: "pack",
            effect: "Default token budget for `ee pack` when --max-tokens is not given.",
            valid_range: "positive integer (tokens)",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: PACK_MMR_LAMBDA_KEY,
            category: "pack",
            effect: "MMR diversity/relevance tradeoff during pack selection: higher favors relevance, lower favors diversity.",
            valid_range: "0.0 ..= 1.0",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: SEARCH_DEFAULT_SPEED_KEY,
            category: "search",
            effect: "Default latency/quality tradeoff for search; maps to the frankensearch embedder stack tier.",
            valid_range: "fast | balanced | thorough",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: SEARCH_GRAPH_WEIGHT_KEY,
            category: "search",
            effect: "Fusion weight applied to graph-proximity signal when combining ranked result lists.",
            valid_range: "0.0 ..= 1.0",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: SEARCH_LEXICAL_WEIGHT_KEY,
            category: "search",
            effect: "Fusion weight applied to the lexical (BM25/FTS) tier when combining ranked result lists.",
            valid_range: "0.0 ..= 1.0",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: SEARCH_RERANK_KEY,
            category: "search",
            effect: "Controls whether the local reranker is auto-used when an available model is registered, or fully disabled before model lookup.",
            valid_range: "auto | off",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: SEARCH_RERANK_TOP_K_KEY,
            category: "search",
            effect: "Candidate pool size collected for reranking before truncating to the requested search limit.",
            valid_range: "positive integer",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
        ConfigKnob {
            key: SEARCH_SEMANTIC_WEIGHT_KEY,
            category: "search",
            effect: "Intended fusion weight for the vector (semantic) tier when combining ranked result lists.",
            valid_range: "0.0 ..= 1.0",
            // The analyst's trap: this setting is active, but the name does
            // not guarantee a neural vector tier.
            status: ConfigRuntimeStatus::Active,
            caveat: Some(
                "Does not imply neural semantic search by itself: this weights the active vector tier, which may be a neural model or the deterministic hash fallback. Check `ee index status` for the active embedding mode (see ADR 0070 and the bundled-embeddings work, bd-1et0v).",
            ),
        },
        ConfigKnob {
            key: STORAGE_DATABASE_PATH_KEY,
            category: "storage",
            effect: "Filesystem path to the durable ee memory database (the source of truth).",
            valid_range: "filesystem path",
            status: ConfigRuntimeStatus::Active,
            caveat: None,
        },
    ]
}

/// Look up a knob by exact key.
#[must_use]
pub fn knob_for_key(key: &str) -> Option<&'static ConfigKnob> {
    config_knobs().iter().find(|knob| knob.key == key)
}

/// A resolved explanation of one config knob: its static description plus the
/// effective value and the layer that set it.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ConfigExplanation {
    /// The static knob description.
    pub knob: ConfigKnob,
    /// The current effective value rendered as a string, when resolved.
    pub effective_value: Option<String>,
    /// The layer that supplied the effective value (`cli|...|default`).
    pub source_layer: &'static str,
    /// Advisory lint findings relevant to this key.
    pub lint_findings: Vec<ConfigLintFinding>,
}

impl ConfigExplanation {
    /// Render the `ee.config_explain.v1` block for one knob.
    #[must_use]
    pub fn data_json(&self) -> Value {
        json!({
            "schema": CONFIG_EXPLAIN_SCHEMA_V1,
            "key": self.knob.key,
            "category": self.knob.category,
            "effect": self.knob.effect,
            "validRange": self.knob.valid_range,
            "status": self.knob.status.as_str(),
            "caveat": self.knob.caveat,
            "effectiveValue": self.effective_value,
            "sourceLayer": self.source_layer,
            "lintFindings": self
                .lint_findings
                .iter()
                .map(ConfigLintFinding::data_json)
                .collect::<Vec<_>>(),
        })
    }

    /// Attach advisory config-lint findings for this key, preserving only
    /// findings that actually refer to the explained key.
    #[must_use]
    pub fn with_lint_findings(mut self, findings: &[ConfigLintFinding]) -> Self {
        self.lint_findings = findings
            .iter()
            .filter(|finding| finding.key == self.knob.key)
            .cloned()
            .collect();
        self
    }
}

/// Build an explanation for `key`, given its effective value and the source
/// layer the merge machinery attributed to it. Returns `None` for an unknown
/// key (the caller surfaces an honest "no explain coverage for <key>").
#[must_use]
pub fn explain(
    key: &str,
    effective_value: Option<String>,
    source: Option<ConfigValueSource>,
) -> Option<ConfigExplanation> {
    knob_for_key(key).map(|knob| ConfigExplanation {
        knob: *knob,
        effective_value,
        source_layer: source.map_or("unknown", ConfigValueSource::as_str),
        lint_findings: Vec::new(),
    })
}

/// A config-lint finding. Always advisory; carries a stable code and the key it
/// concerns so doctor can render it without degrading health.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigLintFinding {
    /// Stable machine code for the finding class.
    pub code: &'static str,
    /// The config key the finding concerns.
    pub key: String,
    /// Advisory, human-readable explanation.
    pub message: String,
    /// Always [`CONFIG_LINT_SEVERITY`] (`"advisory"`).
    pub severity: &'static str,
}

impl ConfigLintFinding {
    fn advisory(code: &'static str, key: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            code,
            key: key.into(),
            message: message.into(),
            severity: CONFIG_LINT_SEVERITY,
        }
    }

    /// Render the advisory finding in the `ee.config_explain.v1` shape.
    #[must_use]
    pub fn data_json(&self) -> Value {
        json!({
            "code": self.code,
            "key": self.key,
            "message": self.message,
            "severity": self.severity,
        })
    }
}

/// Lint code: a semantic weight is set but the active vector tier is non-neural.
pub const LINT_WEIGHT_WITHOUT_NEURAL_TIER: &str = "config_semantic_weight_without_neural_tier";
/// Lint code: an embedding-model path is configured but does not exist on disk.
pub const LINT_EMBEDDING_MODEL_PATH_MISSING: &str = "config_embedding_model_path_missing";
/// Lint code: an environment variable was set that `ee` does not consume.
pub const LINT_UNKNOWN_ENV_VAR: &str = "config_unknown_env_var";
/// Lint code: `search.mode = lexical` while a semantic weight is also set.
pub const LINT_CONTRADICTORY_LEXICAL_MODE: &str = "config_contradictory_lexical_mode";

/// Advisory: `search.semantic_weight` is set but the active vector tier is not
/// neural, so the weight does not buy neural semantics (the analyst trap).
#[must_use]
pub fn lint_semantic_weight_without_neural_tier(
    semantic_weight: Option<f64>,
    neural_tier_active: bool,
) -> Option<ConfigLintFinding> {
    match semantic_weight {
        Some(weight) if !neural_tier_active => Some(ConfigLintFinding::advisory(
            LINT_WEIGHT_WITHOUT_NEURAL_TIER,
            SEARCH_SEMANTIC_WEIGHT_KEY,
            format!(
                "search.semantic_weight={weight} is set, but the active vector tier is deterministic hash, not neural — this does not enable neural semantic search. Check `ee index status` for the active embedding mode."
            ),
        )),
        _ => None,
    }
}

/// Advisory: an embedding-model path is configured but does not exist.
#[must_use]
pub fn lint_embedding_model_path_missing(
    key: &str,
    configured_path: Option<&str>,
    path_exists: bool,
) -> Option<ConfigLintFinding> {
    match configured_path {
        Some(path) if !path_exists => Some(ConfigLintFinding::advisory(
            LINT_EMBEDDING_MODEL_PATH_MISSING,
            key,
            format!("configured embedding model path does not exist: {path}"),
        )),
        _ => None,
    }
}

/// Advisory: an `EE_*`-adjacent environment variable was set that `ee` does not
/// consume (e.g. the `EMBEDDING_MODEL` trap). `recognized` is whether the env
/// registry recognizes the variable.
#[must_use]
pub fn lint_unknown_env_var(var_name: &str, recognized: bool) -> Option<ConfigLintFinding> {
    if recognized {
        None
    } else {
        Some(ConfigLintFinding::advisory(
            LINT_UNKNOWN_ENV_VAR,
            var_name,
            format!(
                "environment variable `{var_name}` is set but `ee` does not consume it; it has no effect. See `ee capabilities --json` data.envOverrides[] for the variables ee honors."
            ),
        ))
    }
}

/// Advisory: `search.mode = lexical` while a semantic weight is also configured
/// — the semantic weight cannot take effect under lexical-only mode.
#[must_use]
pub fn lint_contradictory_lexical_mode(
    mode: Option<&str>,
    semantic_weight: Option<f64>,
) -> Option<ConfigLintFinding> {
    match (mode, semantic_weight) {
        (Some("lexical"), Some(weight)) => Some(ConfigLintFinding::advisory(
            LINT_CONTRADICTORY_LEXICAL_MODE,
            SEARCH_SEMANTIC_WEIGHT_KEY,
            format!(
                "search mode is `lexical`, so search.semantic_weight={weight} has no effect. Set mode to `hybrid` to use the vector tier."
            ),
        )),
        _ => None,
    }
}

/// Config facts the lint rules operate over. Pure inputs gathered by the doctor
/// wiring from the merged config + filesystem + env registry.
#[derive(Clone, Debug, Default)]
pub struct ConfigLintFacts {
    /// Effective `search.semantic_weight`, if set.
    pub semantic_weight: Option<f64>,
    /// Whether the active vector tier is a neural model (vs hash fallback).
    pub neural_tier_active: bool,
    /// Effective `search.mode`, if set.
    pub search_mode: Option<String>,
    /// A configured embedding-model path key + value + existence, if any.
    pub embedding_model_path: Option<(String, String, bool)>,
    /// Set-but-unrecognized environment variables: (name, recognized).
    pub env_vars: Vec<(String, bool)>,
}

/// Run all config-lint rules over the gathered facts, returning advisory
/// findings sorted by `(code, key)` for byte-stable output. Deterministic, and
/// — by construction — every finding is advisory; this function can never
/// produce a health-degrading result.
#[must_use]
pub fn run_config_lint(facts: &ConfigLintFacts) -> Vec<ConfigLintFinding> {
    let mut findings = Vec::new();

    if let Some(finding) =
        lint_semantic_weight_without_neural_tier(facts.semantic_weight, facts.neural_tier_active)
    {
        findings.push(finding);
    }
    if let Some(finding) =
        lint_contradictory_lexical_mode(facts.search_mode.as_deref(), facts.semantic_weight)
    {
        findings.push(finding);
    }
    if let Some((key, path, exists)) = &facts.embedding_model_path {
        if let Some(finding) = lint_embedding_model_path_missing(key, Some(path), *exists) {
            findings.push(finding);
        }
    }
    for (name, recognized) in &facts.env_vars {
        if let Some(finding) = lint_unknown_env_var(name, *recognized) {
            findings.push(finding);
        }
    }

    findings.sort_by(|a, b| a.code.cmp(b.code).then_with(|| a.key.cmp(&b.key)));
    findings
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn registry_is_sorted_by_key_for_stable_output() {
        let keys: Vec<&str> = config_knobs().iter().map(|knob| knob.key).collect();
        let mut sorted = keys.clone();
        sorted.sort_unstable();
        assert_eq!(keys, sorted, "config knob registry must stay sorted by key");
    }

    #[test]
    fn semantic_weight_knob_is_honest_about_neural() {
        let knob = knob_for_key(SEARCH_SEMANTIC_WEIGHT_KEY).expect("semantic_weight is covered");
        assert_eq!(knob.status, ConfigRuntimeStatus::Active);
        let caveat = knob.caveat.expect("semantic_weight must carry a caveat");
        assert!(
            caveat.contains("vector") && caveat.contains("neural"),
            "caveat must explain the vector-vs-neural truth"
        );
    }

    #[test]
    fn rerank_knobs_are_active_and_specific() {
        let mode = knob_for_key(SEARCH_RERANK_KEY).expect("search.rerank is covered");
        assert_eq!(mode.status, ConfigRuntimeStatus::Active);
        assert_eq!(mode.valid_range, "auto | off");
        assert!(
            mode.effect.contains("disabled before model lookup"),
            "rerank mode must document that off suppresses lookup"
        );

        let top_k = knob_for_key(SEARCH_RERANK_TOP_K_KEY).expect("rerank_top_k is covered");
        assert_eq!(top_k.status, ConfigRuntimeStatus::Active);
        assert_eq!(top_k.valid_range, "positive integer");
        assert!(
            top_k.effect.contains("Candidate pool size"),
            "rerank_top_k must describe the collect-limit effect"
        );
    }

    #[test]
    fn default_speed_knob_matches_parser_tokens() {
        let speed =
            knob_for_key(SEARCH_DEFAULT_SPEED_KEY).expect("search.default_speed is covered");
        assert_eq!(speed.status, ConfigRuntimeStatus::Active);
        assert_eq!(speed.valid_range, "fast | balanced | thorough");
        assert!(
            !speed.valid_range.contains("instant") && !speed.valid_range.contains("quality"),
            "config explain must not advertise query-schema speed tokens for search.default_speed"
        );
    }

    #[test]
    fn explain_reports_effect_value_and_source_layer() {
        let explanation = explain(
            SEARCH_SEMANTIC_WEIGHT_KEY,
            Some("0.45".to_owned()),
            Some(ConfigValueSource::Project),
        )
        .expect("known key explains");
        let value = explanation.data_json();
        assert_eq!(value["key"], SEARCH_SEMANTIC_WEIGHT_KEY);
        assert_eq!(value["effectiveValue"], "0.45");
        assert_eq!(value["sourceLayer"], "project");
        assert_eq!(value["status"], "active");
        assert!(value["caveat"].is_string());
        assert_eq!(
            value["lintFindings"].as_array().map(Vec::len),
            Some(0),
            "explanations always carry a deterministic lintFindings array"
        );
    }

    #[test]
    fn explain_unknown_key_is_none() {
        assert!(explain("does.not.exist", None, None).is_none());
    }

    #[test]
    fn explain_unset_value_falls_back_to_unknown_source() {
        let explanation =
            explain(MESH_ENABLED_KEY, None, None).expect("known key explains even when unset");
        assert_eq!(explanation.source_layer, "unknown");
        assert_eq!(explanation.data_json()["effectiveValue"], Value::Null);
    }

    #[test]
    fn explain_data_json_keeps_required_shape_when_values_are_absent() {
        let explanation = explain(MESH_ENABLED_KEY, None, None).expect("known key explains");
        let value = explanation.data_json();

        assert_eq!(value["schema"], CONFIG_EXPLAIN_SCHEMA_V1);
        assert_eq!(value["key"], MESH_ENABLED_KEY);
        assert_eq!(value["category"], "mesh");
        assert_eq!(value["status"], "active");
        assert_eq!(value["caveat"], Value::Null);
        assert_eq!(value["effectiveValue"], Value::Null);
        assert_eq!(value["sourceLayer"], "unknown");
        assert!(
            value["effect"]
                .as_str()
                .is_some_and(|text| !text.is_empty()),
            "effect must be populated for human-facing explain output"
        );
        assert!(
            value["validRange"]
                .as_str()
                .is_some_and(|text| !text.is_empty()),
            "valid range must be populated for human-facing explain output"
        );
        assert_eq!(
            value["lintFindings"].as_array().map(Vec::len),
            Some(0),
            "lintFindings is always present, even when empty"
        );
    }

    #[test]
    fn explain_source_layer_tokens_match_merge_sources() {
        for (source, expected) in [
            (ConfigValueSource::Cli, "cli"),
            (ConfigValueSource::Environment, "environment"),
            (ConfigValueSource::Project, "project"),
            (ConfigValueSource::User, "user"),
            (ConfigValueSource::Default, "default"),
        ] {
            let explanation = explain(MESH_ENABLED_KEY, Some("true".to_owned()), Some(source))
                .expect("known key explains");
            assert_eq!(explanation.source_layer, expected);
            assert_eq!(explanation.data_json()["sourceLayer"], expected);
        }
    }

    #[test]
    fn explain_can_attach_key_scoped_lint_findings() {
        let facts = ConfigLintFacts {
            semantic_weight: Some(0.45),
            neural_tier_active: false,
            search_mode: Some("lexical".to_owned()),
            embedding_model_path: Some((
                "embedding.model_path".to_owned(),
                "/no/such".to_owned(),
                false,
            )),
            env_vars: vec![("EMBEDDING_MODEL".to_owned(), false)],
        };
        let findings = run_config_lint(&facts);
        let explanation = explain(
            SEARCH_SEMANTIC_WEIGHT_KEY,
            Some("0.45".to_owned()),
            Some(ConfigValueSource::Project),
        )
        .expect("known key explains")
        .with_lint_findings(&findings);
        let json = explanation.data_json();
        let attached = json["lintFindings"]
            .as_array()
            .expect("lint findings array");
        assert_eq!(attached.len(), 2);
        assert!(
            attached
                .iter()
                .all(|finding| finding["key"] == SEARCH_SEMANTIC_WEIGHT_KEY),
            "only findings for the explained key are attached"
        );
        assert!(
            attached
                .iter()
                .all(|finding| finding["severity"] == CONFIG_LINT_SEVERITY),
            "config-explain lint findings remain advisory-only"
        );
    }

    #[test]
    fn explain_ignores_empty_and_non_matching_lint_findings() {
        let unrelated = ConfigLintFinding::advisory(
            LINT_UNKNOWN_ENV_VAR,
            "EMBEDDING_MODEL",
            "foreign env var has no effect",
        );

        let empty = explain(
            MESH_ENABLED_KEY,
            Some("false".to_owned()),
            Some(ConfigValueSource::User),
        )
        .expect("known key explains")
        .with_lint_findings(&[]);
        assert!(empty.lint_findings.is_empty());
        assert_eq!(
            empty.data_json()["lintFindings"].as_array().map(Vec::len),
            Some(0)
        );

        let unrelated = explain(
            MESH_ENABLED_KEY,
            Some("false".to_owned()),
            Some(ConfigValueSource::User),
        )
        .expect("known key explains")
        .with_lint_findings(&[unrelated]);
        assert!(
            unrelated.lint_findings.is_empty(),
            "findings for other keys/env vars must not leak into this key"
        );
        assert_eq!(
            unrelated.data_json()["lintFindings"]
                .as_array()
                .map(Vec::len),
            Some(0)
        );
    }

    #[test]
    fn lint_flags_semantic_weight_without_neural_tier() {
        let finding = lint_semantic_weight_without_neural_tier(Some(0.45), false)
            .expect("a weight without a neural tier is suspicious");
        assert_eq!(finding.code, LINT_WEIGHT_WITHOUT_NEURAL_TIER);
        assert_eq!(finding.severity, CONFIG_LINT_SEVERITY);
        // Not a finding once a neural tier is active.
        assert!(lint_semantic_weight_without_neural_tier(Some(0.45), true).is_none());
        // Not a finding when the weight is unset.
        assert!(lint_semantic_weight_without_neural_tier(None, false).is_none());
    }

    #[test]
    fn lint_flags_semantic_weight_boundaries_without_neural_tier() {
        for weight in [0.0, 1.0] {
            let finding = lint_semantic_weight_without_neural_tier(Some(weight), false)
                .expect("boundary weights still need the neural-tier honesty warning");
            assert_eq!(finding.code, LINT_WEIGHT_WITHOUT_NEURAL_TIER);
            assert_eq!(finding.key.as_str(), SEARCH_SEMANTIC_WEIGHT_KEY);
            assert!(
                finding
                    .message
                    .contains(&format!("search.semantic_weight={weight}")),
                "finding should report the exact configured boundary weight"
            );
        }
    }

    #[test]
    fn lint_flags_missing_embedding_model_path() {
        let finding = lint_embedding_model_path_missing(
            "embedding.model_path",
            Some("/no/such/model"),
            false,
        )
        .expect("a missing model path is suspicious");
        assert_eq!(finding.code, LINT_EMBEDDING_MODEL_PATH_MISSING);
        // Present path → no finding.
        assert!(
            lint_embedding_model_path_missing("embedding.model_path", Some("/exists"), true)
                .is_none()
        );
        // Unconfigured path → no finding, regardless of the path existence probe.
        assert!(lint_embedding_model_path_missing("embedding.model_path", None, false).is_none());
    }

    #[test]
    fn lint_flags_unknown_env_var() {
        let finding = lint_unknown_env_var("EMBEDDING_MODEL", false).expect("unknown var flagged");
        assert_eq!(finding.code, LINT_UNKNOWN_ENV_VAR);
        assert!(
            finding.message.contains("ee capabilities --json")
                && finding.message.contains("data.envOverrides[]"),
            "unknown-env lint must point at the shipped env-var discovery surface"
        );
        assert!(
            !finding.message.contains("ee config env"),
            "unknown-env lint must not point at the nonexistent `ee config env` command"
        );
        assert!(lint_unknown_env_var("EE_DB", true).is_none());
    }

    #[test]
    fn lint_flags_contradictory_lexical_mode() {
        let finding = lint_contradictory_lexical_mode(Some("lexical"), Some(0.45))
            .expect("lexical mode + semantic weight is contradictory");
        assert_eq!(finding.code, LINT_CONTRADICTORY_LEXICAL_MODE);
        assert!(lint_contradictory_lexical_mode(Some("hybrid"), Some(0.45)).is_none());
        assert!(lint_contradictory_lexical_mode(Some("lexical"), None).is_none());
    }

    #[test]
    fn run_config_lint_is_advisory_only_and_deterministic() {
        let facts = ConfigLintFacts {
            semantic_weight: Some(0.45),
            neural_tier_active: false,
            search_mode: Some("lexical".to_owned()),
            embedding_model_path: Some((
                "embedding.model_path".to_owned(),
                "/no/such".to_owned(),
                false,
            )),
            env_vars: vec![
                ("EMBEDDING_MODEL".to_owned(), false),
                ("EE_DB".to_owned(), true),
            ],
        };
        let findings = run_config_lint(&facts);
        // semantic-without-neural + contradictory-mode + missing-path + unknown-env.
        assert_eq!(findings.len(), 4);
        // Every finding is advisory — config-lint never degrades health.
        assert!(findings.iter().all(|f| f.severity == CONFIG_LINT_SEVERITY));
        // Deterministic ordering by (code, key).
        let again = run_config_lint(&facts);
        assert_eq!(findings, again);
    }

    #[test]
    fn run_config_lint_orders_same_code_findings_by_key() {
        let facts = ConfigLintFacts {
            semantic_weight: None,
            neural_tier_active: false,
            search_mode: None,
            embedding_model_path: None,
            env_vars: vec![
                ("EEZ_UNUSED".to_owned(), false),
                ("EEA_UNUSED".to_owned(), false),
                ("EE_REAL".to_owned(), true),
            ],
        };
        let findings = run_config_lint(&facts);

        assert_eq!(findings.len(), 2);
        assert_eq!(findings[0].code, LINT_UNKNOWN_ENV_VAR);
        assert_eq!(findings[0].key.as_str(), "EEA_UNUSED");
        assert_eq!(findings[1].code, LINT_UNKNOWN_ENV_VAR);
        assert_eq!(findings[1].key.as_str(), "EEZ_UNUSED");
    }

    #[test]
    fn run_config_lint_clean_config_has_no_findings() {
        let facts = ConfigLintFacts {
            semantic_weight: Some(0.45),
            neural_tier_active: true,
            search_mode: Some("hybrid".to_owned()),
            embedding_model_path: None,
            env_vars: vec![("EE_DB".to_owned(), true)],
        };
        assert!(run_config_lint(&facts).is_empty());
    }
}