leindex 2.1.1

LeIndex MCP and semantic code search engine for AI tools and large codebases
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
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
//! Feature flag infrastructure for controlled rollout of experimental features.
//!
//! Feature flags allow enabling/disabling functionality at runtime without
//! recompilation. Flags are read from environment variables with the
//! `LEINDEX_FEATURE_` prefix, or from the optional config file.
//!
//! ## Usage
//!
//! ```rust,ignore
//! use leindex::feature_flags::FeatureFlag;
//!
//! if FeatureFlag::NeuralSearch.is_enabled() {
//!     // Use neural search path
//! }
//! ```

use std::collections::HashMap;
use std::env;
use std::sync::{Mutex, OnceLock};

/// All supported feature flags.
///
/// Each variant maps to an environment variable `LEINDEX_FEATURE_<NAME>`.
/// Flags default to `false` unless explicitly marked as default-on.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum FeatureFlag {
    /// Enable neural (ONNX) embedding search at runtime.
    NeuralSearch,
    /// Enable the System-One typed-decision MCP gate (experimental).
    SystemOne,
    /// Enable remote embedding API (OpenAI, etc.).
    RemoteEmbeddings,
    /// Enable cross-language symbol resolution.
    CrossLanguageResolution,
    /// Enable experimental HNSW algorithm parameters.
    ExperimentalHnsw,
    /// Enable streaming MCP notifications.
    StreamingMcp,
    /// Enable global index auto-sync.
    GlobalAutoSync,
    /// Enable the daemon-client MIME protocol path (WS3).
    ///
    /// When enabled, `leindex mcp --stdio` acts as a thin shim that discovers
    /// (or spawns) the user-scoped `leindexd` daemon and forwards MCP/JSON-RPC
    /// frames to it over a Unix socket. When disabled, the stdio server runs
    /// the full inline engine (legacy v1.9.x behaviour). Default ON as a stable
    /// rollout capability; set
    /// `LEINDEX_FEATURE_DAEMON_CLIENT=false` (or `LEINDEX_LEGACY=1`) to
    /// revert — the flag doubles as a rollout-KILL.
    ///
    /// This runtime flag works alongside the `daemon-client` Cargo feature:
    /// the Cargo feature compiles the shim code path; this env flag controls
    /// whether the compiled code is used at runtime. When the Cargo feature
    /// is disabled, this flag has no effect (there is no shim code to run).
    DaemonClient,
    /// Enable the one-time legacy→CAS store migration sweep (WS4 Task 10).
    ///
    /// When enabled, `LeIndex::new` converts a legacy full-copy `.leindex/`
    /// store to the content-addressed generation layout on first run. The
    /// sweep is destructive (stale generations, completed jobs, and redundant
    /// full-copy artifacts are removed), so it ships behind this flag and a
    /// backup warning rather than running unconditionally.
    GenerationMigration,
    /// Wire the search/symbol/deep-analyze read path onto leased mmap
    /// generations (WS4 Task 14) instead of the legacy heap-mirror store.
    ///
    /// When enabled and the project has a current generation, MCP read-path
    /// handlers acquire a [`GenerationLease`](crate::storage::generation::lease::GenerationLease),
    /// drop the legacy heap-mirror structures, and read from the generation's
    /// content-addressed mmap layers. When disabled, handlers keep reading
    /// from the legacy heap-mirror path so the two can be compared
    /// bit-for-bit (VAL-EQUIV-001/002/003). Default ON since the v2.0.0
    /// rollout phase 8 (VAL-ROLLOUT-012); set
    /// `LEINDEX_FEATURE_GENERATION_READERS=false` (or `LEINDEX_LEGACY=1`) to
    /// revert.
    GenerationReaders,
    /// Route indexing through the fair bounded scheduler (WS5): heavy work is
    /// executed as stepped [`BoundedJob`](crate::scheduler::budget::BoundedJob)s
    /// behind the DRR queues and admission gate instead of one unstepped
    /// `spawn_blocking` call. The compiled default is ON; disabling it keeps
    /// the legacy spawn_blocking + error-at-cap indexing path.
    ///
    /// **Status:** declared but NOT consumed — blocked, by design. There is no
    /// honest consumer: `IndexJob::step` invokes `PhaseExecutor::run_phase`
    /// wholesale and ignores `WorkBudget`, and the LeIndex pipeline phases
    /// (`run_scan`, `run_parse`, `run_lexical`, `run_neural`) are monolithic
    /// `&mut self` methods — there is no bounded stepping surface to route
    /// through. Wiring this flag requires splitting the phase executor into
    /// stepped jobs first; wrapping whole-phase methods would be decorative,
    /// not bounded or fair. Ruling record: PR #90 (merge 237c3d36), spec D6 —
    /// retained in the repository's local .agent-docs/ archive.
    BoundedScheduler,
    /// Enable the streaming scan stage (WS6-9 Task 1): scan walks files lazily,
    /// hashing via a fixed 64KiB buffer, writing metadata records to CAS-staged
    /// scan blob without retaining source bodies.
    ///
    /// The feature flag gates `run_scan` in
    /// `src/cli/leindex/indexing/mod.rs`: the streaming path hashes the existing
    /// configured source inventory through `stream_scan_paths` without retaining
    /// bodies and propagates hard I/O failures. Default OFF preserves the legacy
    /// path. Coverage: `test_scan_route_flag_selects_both_routes` and
    /// `test_streaming_scan_flag_indexes_fixture_and_searches`.
    StreamingScan,
    /// Enable the streaming parse stage (WS6-9 Task 2): parse chunks bounded
    /// by file count and aggregate bytes through `stream_parse_parallel`.
    ///
    /// Consumer: `run_parse` in `src/cli/leindex/indexing/mod.rs` dispatches
    /// on `parse_route_for_current_flag()`; the streaming route feeds the same
    /// production `ParallelParser` in `ParseBudget`-bounded chunks, producing
    /// the identical `ParsingResult` sequence the legacy route returns. Default
    /// OFF preserves the legacy whole-set parse. Coverage:
    /// `test_parse_route_flag_selects_both_routes`,
    /// `test_chunk_file_inputs_respects_file_limit`,
    /// `test_chunk_file_inputs_respects_byte_limit_and_keeps_order`,
    /// `test_stream_parse_parallel_preserves_order_and_result_shape`, and
    /// `test_streaming_parse_flag_indexes_fixture_and_searches`.
    StreamingParse,
    /// Enable the compact PDG persistence stage (WS6-9 Task 3): per-file graph
    /// fragments to CAS adjacency without whole-PDG clone.
    StreamingPdg,
    /// Consumer: `build_lexical_embedder` in `src/cli/index_builder/mod.rs`
    /// selects the production streaming TF-IDF builder through
    /// `tfidf_route_for_current_flag()` in
    /// `src/cli/leindex/indexing/streaming/routes.rs`. The streaming route
    /// reuses production tokenization and writes the same vocabulary, IDF
    /// values, and vectors as the legacy builder. Default OFF preserves the
    /// legacy route. Coverage: `test_tfidf_route_flag_selects_both_routes`,
    /// `test_streaming_vocab_and_rows_match_production_embedder`, and
    /// `test_streaming_tfidf_flag_indexes_fixture_and_searches`.
    StreamingTfidf,
    /// Enable the global content-addressed embedding cache (WS10 Task 1-2).
    /// The cache stores embedding vectors at user-level (e.g.
    /// `~/.leindex/embed-cache/`) keyed by a 6-tuple CacheKey (model digest,
    /// tokenizer digest, prompt role/version, pooling, normalization, output
    /// dimensions, content hash). Cross-project dedup is automatic.
    GlobalEmbedCache,
    /// Leiden community detection over the PDG (roadmap Part IV). Kill
    /// with LEINDEX_FEATURE_COMMUNITY_DETECTION=false to skip computation
    /// and serving of community metadata.
    CommunityDetection,
    /// Enable SCIP precision ingestion when a supported external indexer is available.
    PrecisionIngest,
    /// Enable the embedding-cache debug escape hatch (WS10 privacy remediation).
    ///
    /// When ON *and* the caller provides source text to `GlobalEmbeddingCache::put`,
    /// the source text is appended to the row bytes after the vector payload for
    /// development troubleshooting. Default OFF — privacy gate preserved: no
    /// source text is persisted unless this flag (or the legacy
    /// `LEINDEX_EMBED_CACHE_DEBUG` env var, or `CacheConfig::debug_mode`) is set.
    DebugEscapeHatch,
    /// Enable the WS11 validated model profile as the production default
    /// (WS11 Task 7). When ON, the embed worker uses the model bake-off
    /// winner (CodeRankEmbed 137M, INT8 quantized, no reranker) instead of
    /// the legacy FP16 Qwen3 + reranker baseline. Default ON as the validated
    /// production model; set
    /// `LEINDEX_FEATURE_VALIDATED_MODEL=false` (or `LEINDEX_LEGACY=1`) to
    /// revert to the legacy FP16 Qwen3 behavior.
    ValidatedModel,
    /// Engram: a persistent, content-addressed phrase-book of neural query
    /// embeddings (`~/.leindex/engram/`). Repeat queries skip the embedder
    /// entirely (no worker spawn, no model digest, no network round trip).
    /// New capability: defaults OFF until enabled per deployment.
    Engram,
    /// Matryoshka (MRL) dimension slicing: persist neural vectors truncated to
    /// 128/256/512 dimensions in sidecar stores so short nodes are searched at
    /// their own granularity instead of at the full 768-d native width.
    ///
    /// Experimental, so it defaults OFF per AGENTS.md progressive rollout: even
    /// a configured `[search] matryoshka` policy stays inert until
    /// `LEINDEX_FEATURE_MATRYOSHKA=1` is exported. The retrieval and persistence
    /// code is written to be a no-op at the native dimension, so OFF reproduces
    /// the pre-Matryoshka behavior byte for byte.
    Matryoshka,
    /// Enable structural (AST-pattern) search via ast-grep.
    ///
    /// Consumer: the `structural` branch of `leindex_explore`. It compiles an
    /// ast-grep pattern for a language and matches it against LeIndex's own
    /// tree-sitter grammars. New capability, so it defaults OFF per AGENTS.md
    /// progressive rollout: even in a build with the `structural-search`
    /// Cargo feature, the branch refuses to run until
    /// `LEINDEX_FEATURE_STRUCTURAL_SEARCH=1` is exported.
    StructuralSearch,
}

impl FeatureFlag {
    /// Returns the environment variable name for this flag.
    pub fn env_var(&self) -> &'static str {
        match self {
            Self::NeuralSearch => "LEINDEX_FEATURE_NEURAL_SEARCH",
            Self::SystemOne => "LEINDEX_FEATURE_SYSTEM_ONE",
            Self::RemoteEmbeddings => "LEINDEX_FEATURE_REMOTE_EMBEDDINGS",
            Self::CrossLanguageResolution => "LEINDEX_FEATURE_CROSS_LANGUAGE",
            Self::ExperimentalHnsw => "LEINDEX_FEATURE_EXPERIMENTAL_HNSW",
            Self::StreamingMcp => "LEINDEX_FEATURE_STREAMING_MCP",
            Self::GlobalAutoSync => "LEINDEX_FEATURE_GLOBAL_AUTO_SYNC",
            Self::DaemonClient => "LEINDEX_FEATURE_DAEMON_CLIENT",
            Self::GenerationMigration => "LEINDEX_FEATURE_GENERATION_MIGRATION",
            Self::GenerationReaders => "LEINDEX_FEATURE_GENERATION_READERS",
            Self::BoundedScheduler => "LEINDEX_FEATURE_BOUNDED_SCHEDULER",
            Self::StreamingScan => "LEINDEX_FEATURE_STREAMING_SCAN",
            Self::StreamingParse => "LEINDEX_FEATURE_STREAMING_PARSE",
            Self::StreamingPdg => "LEINDEX_FEATURE_STREAMING_PDG",
            Self::StreamingTfidf => "LEINDEX_FEATURE_STREAMING_TFIDF",
            Self::GlobalEmbedCache => "LEINDEX_FEATURE_GLOBAL_EMBED_CACHE",
            Self::CommunityDetection => "LEINDEX_FEATURE_COMMUNITY_DETECTION",
            Self::PrecisionIngest => "LEINDEX_FEATURE_PRECISION_INGEST",
            Self::DebugEscapeHatch => "LEINDEX_FEATURE_EMBED_CACHE_DEBUG",
            Self::ValidatedModel => "LEINDEX_FEATURE_VALIDATED_MODEL",
            Self::Engram => "LEINDEX_FEATURE_ENGRAM",
            Self::Matryoshka => "LEINDEX_FEATURE_MATRYOSHKA",
            Self::StructuralSearch => "LEINDEX_FEATURE_STRUCTURAL_SEARCH",
        }
    }

    /// Returns whether this flag is enabled by default (without env override).
    ///
    /// Defaults are intentionally per-feature: stable rollout capabilities may
    /// default ON as rollout-KILLs, while new consumers can remain opt-in until
    /// their route has passed its own acceptance gates. Do not infer the default
    /// for one flag from the broader v2.0 rollout state.
    pub fn default_value(&self) -> bool {
        match self {
            // Stable production capabilities default ON and remain individually
            // revertible through their flags. New consumer routes are listed only
            // after their own rollout gates pass; the PR-86 scan, parse, TF-IDF,
            // and neural consumers intentionally remain opt-in below.
            Self::StreamingMcp
            | Self::GlobalAutoSync
            | Self::NeuralSearch
            | Self::RemoteEmbeddings
            // Stable architecture capabilities with successful rollout gates
            // remain default ON and individually revertible. StreamingScan,
            // StreamingParse, and StreamingTfidf intentionally
            // remain OFF until their consumers complete a separate rollout.
            | Self::DaemonClient
            | Self::GenerationReaders
            | Self::BoundedScheduler
            | Self::StreamingPdg
            | Self::GlobalEmbedCache
            | Self::CommunityDetection
            | Self::ValidatedModel
            // SCIP precision ingest degrades silently to the Tier-0 PDG when
            // no external indexer is discoverable, so defaulting ON is safe:
            // machines without indexers behave exactly as before, machines
            // with them get precise edges. Rollout-KILL via explicit "false".
            | Self::PrecisionIngest => true,
            // Everything else defaults off
            _ => false,
        }
    }

    /// Checks whether this feature flag is currently enabled.
    ///
    /// Reads the corresponding env var. Values "1", "true", "yes" enable.
    /// Values "0", "false", "no" disable. If unset, uses `default_value()`.
    ///
    /// A test-only override (set via [`set_flag_override_for_test`]) takes
    /// precedence so a single process can exercise both sides of a feature.
    pub fn is_enabled(&self) -> bool {
        if let Some(map) = TEST_OVERRIDES
            .lock()
            .unwrap_or_else(|poisoned| poisoned.into_inner())
            .as_ref()
        {
            if let Some(v) = map.get(self) {
                return *v;
            }
        }
        // Phase-9 legacy fallback: when LEINDEX_LEGACY=1 is active, all
        // v2.0.0 rollout flags revert to OFF (legacy behavior). This allows
        // users to opt back to legacy code paths during the fallback window
        // without setting each flag individually.
        if legacy_mode_enabled() && is_legacy_revertible(self) {
            return false;
        }
        flag_store().get(self)
    }

    /// Returns a human-readable description of the flag.
    pub fn description(&self) -> &'static str {
        match self {
            Self::NeuralSearch => "Enable neural (ONNX) embedding search at runtime",
            Self::SystemOne => "Enable the System-One typed-decision MCP gate (experimental)",
            Self::RemoteEmbeddings => "Enable remote embedding API (OpenAI, etc.)",
            Self::CrossLanguageResolution => "Enable cross-language symbol resolution",
            Self::ExperimentalHnsw => "Enable experimental HNSW algorithm parameters",
            Self::StreamingMcp => "Enable streaming MCP notifications",
            Self::GlobalAutoSync => "Enable global index auto-sync",
            Self::DaemonClient => "Stdio MCP shim forwards to user-scoped leindexd daemon (WS3)",
            Self::GenerationMigration => "Enable the one-time legacy→CAS store migration sweep",
            Self::GenerationReaders => {
                "Read-path handlers use leased mmap generations instead of heap mirrors"
            }
            Self::BoundedScheduler => {
                "Route heavy work through the fair bounded scheduler (DRR + admission)"
            }
            Self::StreamingScan => {
                "Streaming scan stage: lazy file walk, 64KiB hash buffer, no source retention"
            }
            Self::StreamingParse => {
                "Streaming parse stage: bounded chunks, per-file persist, tree drop"
            }
            Self::StreamingPdg => {
                "Compact PDG persistence: per-file fragments to CAS, no whole-PDG clone"
            }
            Self::StreamingTfidf => "Streaming two-pass TF-IDF with direct CAS-staged row writes",
            Self::GlobalEmbedCache => {
                "Global content-addressed embedding cache with cross-project dedup"
            }
            Self::CommunityDetection => {
                "Leiden community detection over the PDG (project_map grouping, impact boundaries)"
            }
            Self::PrecisionIngest => {
                "SCIP precision ingestion (default on) with silent Tier-0 fallback when no indexer is available; set LEINDEX_FEATURE_PRECISION_INGEST=false to disable"
            }
            Self::DebugEscapeHatch => {
                "Embedding-cache debug escape hatch: store source text alongside rows"
            }
            Self::ValidatedModel => {
                "Use WS11 validated model profile (CodeRankEmbed-INT8, no reranker)"
            }
            Self::Engram => {
                "Engram: persistent phrase-book of neural query embeddings (repeat queries skip the embedder)"
            }
            Self::Matryoshka => {
                "Persist/search neural vectors at sliced MRL dimensions (128/256/512; experimental)"
            }
            Self::StructuralSearch => {
                "Structural (AST-pattern) search: ast-grep patterns matched against LeIndex's tree-sitter grammars"
            }
        }
    }
}

/// Helper to compute the effective neural search enablement flag.
///
/// This combines the runtime feature flag (`LEINDEX_FEATURE_NEURAL_SEARCH`)
/// with the user-level config knob (`leindex.toml` `[neural] enabled`).
/// Conservative semantics: the config can disable neural (set to false),
/// but the runtime flag cannot enable what the config disabled.
pub fn is_neural_enabled(config_value: bool) -> bool {
    crate::feature_flags::FeatureFlag::NeuralSearch.is_enabled() && config_value
}

/// Environment variable name for the legacy fallback umbrella switch (phase 9).
///
/// When set to "1", "true", "yes", or "on", every legacy-revertible
/// rollout feature flag is forced OFF, restoring its legacy path where one
/// exists. GA features predating v2.0.0 are not included.
///
/// VAL-ROLLOUT-013: legacy paths reachable during fallback window.
pub const LEGACY_ENV: &str = "LEINDEX_LEGACY";

/// Returns `true` when the legacy fallback umbrella is active (`LEINDEX_LEGACY=1`).
///
/// When active, `FeatureFlag::is_enabled()` returns `false` for every flag
/// in `LEGACY_REVERTIBLE_FLAGS`, reverting those features to their legacy
/// paths. This is a convenience switch during the phase-9 fallback window.
pub fn legacy_mode_enabled() -> bool {
    match env::var(LEGACY_ENV) {
        Ok(v) => matches!(
            v.to_lowercase().as_str(),
            "1" | "true" | "yes" | "on" | "enable" | "enabled"
        ),
        Err(_) => false,
    }
}

/// The set of rollout flags that `LEINDEX_LEGACY=1` reverts to legacy behavior.
///
/// GA features (`NeuralSearch`, `StreamingMcp`, `GlobalAutoSync`) are NOT
/// included: they were production before v2.0.0 and the legacy fallback applies
/// only to v2.0.0 resource-architecture flags.
const LEGACY_REVERTIBLE_FLAGS: &[FeatureFlag] = &[
    FeatureFlag::DaemonClient,
    FeatureFlag::GenerationReaders,
    FeatureFlag::BoundedScheduler,
    FeatureFlag::StreamingScan,
    FeatureFlag::StreamingParse,
    FeatureFlag::StreamingPdg,
    FeatureFlag::StreamingTfidf,
    FeatureFlag::GlobalEmbedCache,
    FeatureFlag::CommunityDetection,
    FeatureFlag::PrecisionIngest,
    FeatureFlag::ValidatedModel,
];

/// Check whether a given flag is one that `LEINDEX_LEGACY=1` would revert.
pub fn is_legacy_revertible(flag: &FeatureFlag) -> bool {
    LEGACY_REVERTIBLE_FLAGS.contains(flag)
}

/// Internal store that caches env-var lookups in a OnceLock for zero-cost reads.
struct FlagStore {
    values: HashMap<FeatureFlag, bool>,
}

impl FlagStore {
    fn new() -> Self {
        let mut values = HashMap::new();
        for flag in [
            FeatureFlag::NeuralSearch,
            FeatureFlag::SystemOne,
            FeatureFlag::RemoteEmbeddings,
            FeatureFlag::CrossLanguageResolution,
            FeatureFlag::ExperimentalHnsw,
            FeatureFlag::StreamingMcp,
            FeatureFlag::GlobalAutoSync,
            FeatureFlag::DaemonClient,
            FeatureFlag::GenerationMigration,
            FeatureFlag::GenerationReaders,
            FeatureFlag::BoundedScheduler,
            FeatureFlag::StreamingScan,
            FeatureFlag::StreamingParse,
            FeatureFlag::StreamingPdg,
            FeatureFlag::StreamingTfidf,
            FeatureFlag::GlobalEmbedCache,
            FeatureFlag::DebugEscapeHatch,
            FeatureFlag::ValidatedModel,
            FeatureFlag::CommunityDetection,
            FeatureFlag::PrecisionIngest,
            FeatureFlag::Engram,
            FeatureFlag::StructuralSearch,
            FeatureFlag::Matryoshka,
        ] {
            let enabled = match env::var(flag.env_var()) {
                Ok(v) => matches!(
                    v.to_lowercase().as_str(),
                    "1" | "true" | "yes" | "on" | "enable" | "enabled"
                ),
                Err(_) => flag.default_value(),
            };
            values.insert(flag, enabled);
        }
        Self { values }
    }

    fn get(&self, flag: &FeatureFlag) -> bool {
        *self.values.get(flag).unwrap_or(&false)
    }
}

static FLAG_STORE: OnceLock<FlagStore> = OnceLock::new();

/// Test-only overrides. When `Some`, values in the map shadow the env-derived
/// store for the flagged features. Serialized by a `Mutex` so `is_enabled`
/// callers never observe a torn map.
static TEST_OVERRIDES: Mutex<Option<HashMap<FeatureFlag, bool>>> = Mutex::new(None);

/// Test-only: force a flag's effective value regardless of the environment.
///
/// Overrides take precedence over the (OnceLock-cached) env-derived store, so
/// tests can exercise both sides of a flag in one process without env
/// mutation races. Production callers must not use this.
#[doc(hidden)]
pub fn set_flag_override_for_test(flag: FeatureFlag, value: bool) {
    let mut guard = TEST_OVERRIDES
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    guard.get_or_insert_with(HashMap::new).insert(flag, value);
}

/// Test-only: clear all overrides, restoring env-derived flag behavior.
#[doc(hidden)]
pub fn clear_flag_overrides_for_test() {
    let mut guard = TEST_OVERRIDES
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner());
    *guard = None;
}

/// Serializes flag-override toggling across tests in one process.
///
/// `set_flag_override_for_test`/`clear_flag_overrides_for_test` mutate a
/// process-global store, so tests that exercise a flag on both sides must hold
/// this lock for their whole duration to avoid racing sibling tests.
#[doc(hidden)]
pub static FLAG_TEST_LOCK: Mutex<()> = Mutex::new(());

/// Take [`FLAG_TEST_LOCK`], ignoring poison.
///
/// A test that panics while holding the lock poisons it; with a plain
/// `lock().unwrap()` every later test that takes the lock then fails too, so
/// one failure (or one timing flake) cascades into unrelated ones. The guarded
/// state is just the override map, which each test resets itself, so poison
/// carries no information worth propagating.
#[doc(hidden)]
pub fn lock_flag_tests() -> std::sync::MutexGuard<'static, ()> {
    FLAG_TEST_LOCK
        .lock()
        .unwrap_or_else(|poisoned| poisoned.into_inner())
}

/// Clears all overrides when dropped, including on unwind.
#[doc(hidden)]
pub struct FlagOverrideReset;

impl Drop for FlagOverrideReset {
    fn drop(&mut self) {
        clear_flag_overrides_for_test();
    }
}

/// Run `f` with `flag` overridden to `value`, restoring the env-derived state
/// afterwards. Serialized by [`FLAG_TEST_LOCK`] so tests never race the shared
/// override store.
#[doc(hidden)]
pub fn with_flag_override(flag: FeatureFlag, value: bool, f: impl FnOnce()) {
    let _g = lock_flag_tests();
    // Reset even if `f` panics, so a failing test cannot leak its override
    // into the next one.
    let _reset = FlagOverrideReset;
    set_flag_override_for_test(flag, value);
    f();
}

fn flag_store() -> &'static FlagStore {
    FLAG_STORE.get_or_init(FlagStore::new)
}

/// Log the state of all feature flags at startup (spec §12.3).
///
/// Each flag is logged with its name, env-var, and effective ON/OFF state.
/// Called from the daemon and the inline server entry points so the user can
/// audit which workstream features are active in their deployment. The roll
/// out plan (§12.3) requires flag state to be visible at daemon start.
pub fn log_flag_state() {
    let flags = all_flags();
    let active: Vec<&FeatureFlag> = flags
        .iter()
        .filter(|(_, enabled)| *enabled)
        .map(|(flag, _)| flag)
        .collect();
    let inactive_count = flags.len() - active.len();
    let legacy = legacy_mode_enabled();
    tracing::info!(
        "Feature flags at startup: {} active, {} inactive{}. \
         Active: [{}]",
        active.len(),
        inactive_count,
        if legacy {
            " (LEINDEX_LEGACY=1 — v2.0.0 rollout reverted)"
        } else {
            ""
        },
        active
            .iter()
            .map(|f| format!(
                "{} ({})",
                f.env_var()
                    .strip_prefix("LEINDEX_FEATURE_")
                    .unwrap_or(f.env_var()),
                f.is_enabled()
            ))
            .collect::<Vec<_>>()
            .join(", "),
    );
}

/// Returns a list of all feature flags and their current state.
///
/// Useful for CLI output (`leindex feature-flags`) and debugging.
pub fn all_flags() -> Vec<(FeatureFlag, bool)> {
    [
        FeatureFlag::NeuralSearch,
        FeatureFlag::SystemOne,
        FeatureFlag::RemoteEmbeddings,
        FeatureFlag::CrossLanguageResolution,
        FeatureFlag::ExperimentalHnsw,
        FeatureFlag::StreamingMcp,
        FeatureFlag::GlobalAutoSync,
        FeatureFlag::DaemonClient,
        FeatureFlag::GenerationMigration,
        FeatureFlag::GenerationReaders,
        FeatureFlag::BoundedScheduler,
        FeatureFlag::StreamingScan,
        FeatureFlag::StreamingParse,
        FeatureFlag::StreamingPdg,
        FeatureFlag::StreamingTfidf,
        FeatureFlag::GlobalEmbedCache,
        FeatureFlag::CommunityDetection,
        FeatureFlag::PrecisionIngest,
        FeatureFlag::DebugEscapeHatch,
        FeatureFlag::ValidatedModel,
        FeatureFlag::Engram,
        FeatureFlag::Matryoshka,
        FeatureFlag::StructuralSearch,
    ]
    .into_iter()
    .map(|f| (f, f.is_enabled()))
    .collect()
}

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

    #[test]
    fn test_flag_env_var_names() {
        assert_eq!(
            FeatureFlag::NeuralSearch.env_var(),
            "LEINDEX_FEATURE_NEURAL_SEARCH"
        );
        assert_eq!(
            FeatureFlag::GlobalAutoSync.env_var(),
            "LEINDEX_FEATURE_GLOBAL_AUTO_SYNC"
        );
    }

    #[test]
    fn test_system_one_is_registered_in_production_flag_store() {
        assert_eq!(
            FeatureFlag::SystemOne.env_var(),
            "LEINDEX_FEATURE_SYSTEM_ONE"
        );
        assert!(!FeatureFlag::SystemOne.default_value());
        assert!(
            FlagStore::new()
                .values
                .contains_key(&FeatureFlag::SystemOne)
        );
        assert!(
            all_flags()
                .iter()
                .any(|(flag, _)| *flag == FeatureFlag::SystemOne)
        );
    }

    /// `effective_matryoshka()` requires this flag, so an unregistered variant
    /// means MRL can never be enabled in production: `FlagStore::get` returns
    /// `false` for absent entries, so `LEINDEX_FEATURE_MATRYOSHKA=1` would be
    /// silently ignored while `all_flags()` still advertised the flag.
    #[test]
    fn test_matryoshka_is_registered_in_production_flag_store() {
        assert_eq!(
            FeatureFlag::Matryoshka.env_var(),
            "LEINDEX_FEATURE_MATRYOSHKA"
        );
        assert!(!FeatureFlag::Matryoshka.default_value());
        assert!(
            FlagStore::new()
                .values
                .contains_key(&FeatureFlag::Matryoshka)
        );
    }

    #[test]
    fn test_default_values() {
        // NeuralSearch is a GA feature: its flag defaults ON so it acts as a
        // per-deployment rollout-KILL (explicit `false` disables; unset follows
        // normal config) rather than gating a not-yet-released capability.
        assert!(FeatureFlag::NeuralSearch.default_value());
        assert!(FeatureFlag::StreamingMcp.default_value());
    }

    #[test]
    fn test_engram_is_opt_in_and_listed() {
        // New capabilities default OFF (AGENTS.md progressive rollout).
        assert!(!FeatureFlag::Engram.default_value());
        assert_eq!(FeatureFlag::Engram.env_var(), "LEINDEX_FEATURE_ENGRAM");
        assert!(all_flags().iter().any(|(f, _)| *f == FeatureFlag::Engram));
        assert!(
            all_flags()
                .iter()
                .any(|(f, _)| *f == FeatureFlag::CommunityDetection)
        );
    }

    #[test]
    fn test_precision_ingest_defaults_on_with_kill_switch() {
        // Precision ingest defaults ON: without a discoverable external
        // indexer it degrades silently to Tier-0, so the default is safe.
        // The flag remains a rollout-KILL (explicit false disables).
        assert_eq!(
            FeatureFlag::PrecisionIngest.env_var(),
            "LEINDEX_FEATURE_PRECISION_INGEST"
        );
        assert!(FeatureFlag::PrecisionIngest.default_value());
    }

    #[test]
    fn test_generation_readers_now_default_on_after_rollout() {
        // v2.0.0 rollout phase 8: all section 16 acceptance gates pass.
        // GenerationReaders now defaults ON as the production read-path.
        // The flag acts as a rollout-KILL: explicit "0"/"false" reverts to
        // the legacy heap-mirror read-path for scoped rollback.
        assert_eq!(
            FeatureFlag::GenerationReaders.env_var(),
            "LEINDEX_FEATURE_GENERATION_READERS"
        );
        assert!(FeatureFlag::GenerationReaders.default_value());
    }

    #[test]
    fn test_debug_escape_hatch_default_off_and_named_correctly() {
        // The privacy escape hatch for the embedding cache MUST default OFF.
        // Source text must never be persisted unless the user explicitly opts
        // in via the env var, the feature flag, or CacheConfig::debug_mode.
        assert_eq!(
            FeatureFlag::DebugEscapeHatch.env_var(),
            "LEINDEX_FEATURE_EMBED_CACHE_DEBUG"
        );
        assert!(!FeatureFlag::DebugEscapeHatch.default_value());
        // is_enabled reads from the cached FlagStore, which reflects unset env
        // as default_value(). Other tests may have set the override so we use
        // default_value() to confirm the production default.
    }

    #[test]
    fn test_debug_escape_hatch_override_toggles_state() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        assert!(FeatureFlag::RemoteEmbeddings.default_value());
        assert!(!FeatureFlag::DebugEscapeHatch.default_value());
        assert!(!FeatureFlag::DebugEscapeHatch.is_enabled());
        set_flag_override_for_test(FeatureFlag::DebugEscapeHatch, true);
        assert!(FeatureFlag::DebugEscapeHatch.is_enabled());
        set_flag_override_for_test(FeatureFlag::DebugEscapeHatch, false);
        assert!(!FeatureFlag::DebugEscapeHatch.is_enabled());
    }

    #[test]
    fn test_override_toggles_flag() {
        // Serialize the shared override store across this test only.
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        assert!(FeatureFlag::RemoteEmbeddings.default_value());
        assert!(FeatureFlag::GenerationReaders.default_value());
        assert!(FeatureFlag::GenerationReaders.is_enabled());
        set_flag_override_for_test(FeatureFlag::GenerationReaders, false);
        assert!(!FeatureFlag::GenerationReaders.is_enabled());
        set_flag_override_for_test(FeatureFlag::GenerationReaders, true);
        assert!(FeatureFlag::GenerationReaders.is_enabled());
    }

    #[test]
    fn test_validated_model_flag_default_on_after_rollout() {
        // v2.0.0 rollout phase 8: all section 16 acceptance gates pass.
        // The WS11 validated model profile (CodeRankEmbed INT8, no reranker)
        // is now the production default. The flag acts as a rollout-KILL.
        assert_eq!(
            FeatureFlag::ValidatedModel.env_var(),
            "LEINDEX_FEATURE_VALIDATED_MODEL"
        );
        assert!(FeatureFlag::ValidatedModel.default_value());
    }

    #[test]
    fn test_validated_model_override_toggles() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        assert!(FeatureFlag::ValidatedModel.default_value());
        assert!(FeatureFlag::ValidatedModel.is_enabled());
        set_flag_override_for_test(FeatureFlag::ValidatedModel, false);
        assert!(!FeatureFlag::ValidatedModel.is_enabled());
        set_flag_override_for_test(FeatureFlag::ValidatedModel, true);
        assert!(FeatureFlag::ValidatedModel.is_enabled());
    }

    /// Selected stable rollout flags default ON after their gates pass. Each
    /// flag acts as a rollout-KILL: explicit "false" reverts that capability.
    #[test]
    fn test_rollout_flags_default_on_after_gates_passed() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        let rollout_flags = [
            FeatureFlag::RemoteEmbeddings,
            FeatureFlag::DaemonClient,
            FeatureFlag::GenerationReaders,
            FeatureFlag::BoundedScheduler,
            FeatureFlag::StreamingPdg,
            FeatureFlag::GlobalEmbedCache,
            FeatureFlag::ValidatedModel,
        ];
        for flag in &rollout_flags {
            assert!(
                flag.default_value(),
                "{} should default ON after gates pass (rollout-KILL semantics)",
                flag.env_var()
            );
            assert!(
                flag.is_enabled(),
                "{} should be enabled with no override",
                flag.env_var()
            );
        }
    }

    /// Consumer routes that have not completed their rollout remain opt-in.
    #[test]
    fn test_pr86_consumer_flags_default_off() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        assert!(FeatureFlag::RemoteEmbeddings.default_value());
        assert!(FeatureFlag::RemoteEmbeddings.is_enabled());
        for flag in [
            FeatureFlag::StreamingScan,
            FeatureFlag::StreamingParse,
            FeatureFlag::StreamingTfidf,
        ] {
            assert!(!flag.default_value(), "{} must default OFF", flag.env_var());
        }
    }

    #[test]
    fn test_daemon_client_flag_added_and_on_after_rollout() {
        assert_eq!(
            FeatureFlag::DaemonClient.env_var(),
            "LEINDEX_FEATURE_DAEMON_CLIENT"
        );
        assert!(FeatureFlag::DaemonClient.default_value());
    }

    #[test]
    fn test_daemon_client_override_toggles() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        assert!(FeatureFlag::DaemonClient.default_value());
        set_flag_override_for_test(FeatureFlag::DaemonClient, true);
        assert!(FeatureFlag::DaemonClient.is_enabled());
        set_flag_override_for_test(FeatureFlag::DaemonClient, false);
        assert!(!FeatureFlag::DaemonClient.is_enabled());
    }

    // ── Phase 9 legacy fallback tests (VAL-ROLLOUT-013) ──────────────

    /// `LEINDEX_LEGACY=1` disables only the legacy-revertible flags; the fresh
    /// default-on embedding capability is not part of that v2.0.0 fallback set.
    #[test]
    fn test_rollout_flags_default_off_with_legacy_mode() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        clear_flag_overrides_for_test();
        let prev = env::var(LEGACY_ENV).ok();
        // SAFETY: single-threaded test under FLAG_TEST_LOCK.
        unsafe { env::remove_var(LEGACY_ENV) };

        assert!(FeatureFlag::RemoteEmbeddings.default_value());
        assert!(FeatureFlag::RemoteEmbeddings.is_enabled());
        for flag in [
            FeatureFlag::DaemonClient,
            FeatureFlag::GenerationReaders,
            FeatureFlag::BoundedScheduler,
            FeatureFlag::StreamingPdg,
            FeatureFlag::GlobalEmbedCache,
            FeatureFlag::ValidatedModel,
            FeatureFlag::CommunityDetection,
            FeatureFlag::PrecisionIngest,
        ] {
            assert!(flag.default_value(), "{} should default ON", flag.env_var());
            assert!(
                flag.is_enabled(),
                "{} should be enabled by default",
                flag.env_var()
            );
        }

        // SAFETY: single-threaded test under FLAG_TEST_LOCK.
        unsafe { env::set_var(LEGACY_ENV, "1") };
        assert!(
            FeatureFlag::NeuralSearch.is_enabled(),
            "GA features unaffected by LEINDEX_LEGACY"
        );
        assert!(
            FeatureFlag::RemoteEmbeddings.is_enabled(),
            "fresh remote embeddings default is not in the legacy fallback set"
        );
        for flag in [
            FeatureFlag::DaemonClient,
            FeatureFlag::GenerationReaders,
            FeatureFlag::BoundedScheduler,
            FeatureFlag::StreamingScan,
            FeatureFlag::StreamingParse,
            FeatureFlag::StreamingPdg,
            FeatureFlag::StreamingTfidf,
            FeatureFlag::GlobalEmbedCache,
            FeatureFlag::ValidatedModel,
            FeatureFlag::CommunityDetection,
            FeatureFlag::PrecisionIngest,
        ] {
            assert!(
                !flag.is_enabled(),
                "{} should be OFF under LEINDEX_LEGACY=1",
                flag.env_var()
            );
        }

        // Restore.
        // SAFETY: single-threaded test under FLAG_TEST_LOCK.
        unsafe {
            match &prev {
                Some(v) => env::set_var(LEGACY_ENV, v),
                None => env::remove_var(LEGACY_ENV),
            }
        }
    }

    /// `LEINDEX_LEGACY=0` or unset keeps fresh default-on flags enabled without
    /// overrides while preserving opt-in defaults for unfinished consumers.
    #[test]
    fn test_legacy_unset_keeps_compiled_defaults() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        clear_flag_overrides_for_test();
        let prev = env::var(LEGACY_ENV).ok();

        // SAFETY: single-threaded test under FLAG_TEST_LOCK.
        unsafe { env::remove_var(LEGACY_ENV) };

        assert!(!legacy_mode_enabled());
        for flag in [
            FeatureFlag::RemoteEmbeddings,
            FeatureFlag::DaemonClient,
            FeatureFlag::GenerationReaders,
            FeatureFlag::BoundedScheduler,
            FeatureFlag::StreamingPdg,
            FeatureFlag::GlobalEmbedCache,
            FeatureFlag::ValidatedModel,
            FeatureFlag::CommunityDetection,
            FeatureFlag::PrecisionIngest,
        ] {
            assert!(flag.default_value(), "{} should default ON", flag.env_var());
            assert!(
                flag.is_enabled(),
                "{} should be enabled with no override",
                flag.env_var()
            );
        }
        for flag in [
            FeatureFlag::StreamingScan,
            FeatureFlag::StreamingParse,
            FeatureFlag::StreamingTfidf,
        ] {
            assert!(
                !flag.default_value(),
                "{} should remain opt-in",
                flag.env_var()
            );
            assert!(!flag.is_enabled(), "{} should remain OFF", flag.env_var());
        }

        // Setting to 0 also keeps defaults.
        // SAFETY: single-threaded test under FLAG_TEST_LOCK.
        unsafe { env::set_var(LEGACY_ENV, "0") };
        assert!(!legacy_mode_enabled());
        assert!(FeatureFlag::RemoteEmbeddings.is_enabled());

        // SAFETY: single-threaded test under FLAG_TEST_LOCK.
        unsafe {
            match &prev {
                Some(v) => env::set_var(LEGACY_ENV, v),
                None => env::remove_var(LEGACY_ENV),
            }
        }
    }

    #[test]
    fn test_structural_search_is_opt_in_and_listed() {
        // New capabilities default OFF (AGENTS.md progressive rollout): the
        // Cargo feature compiles the code, this flag decides whether the MCP
        // branch will run it.
        assert_eq!(
            FeatureFlag::StructuralSearch.env_var(),
            "LEINDEX_FEATURE_STRUCTURAL_SEARCH"
        );
        assert!(!FeatureFlag::StructuralSearch.default_value());
        assert!(
            !FeatureFlag::StructuralSearch.description().is_empty(),
            "every flag needs a description for log_flag_state"
        );
        // Both enumerations must carry the flag: `FlagStore` is what
        // `is_enabled` reads, `all_flags` is what `leindex feature-flags`
        // prints. A flag in one and not the other is a silent bug.
        assert!(
            FlagStore::new()
                .values
                .contains_key(&FeatureFlag::StructuralSearch),
            "StructuralSearch missing from the production flag store"
        );
        assert!(
            all_flags()
                .iter()
                .any(|(flag, _)| *flag == FeatureFlag::StructuralSearch),
            "StructuralSearch missing from all_flags"
        );
    }

    #[test]
    fn test_structural_search_override_toggles() {
        let _g = lock_flag_tests();
        let _reset = FlagOverrideReset;
        assert!(!FeatureFlag::StructuralSearch.is_enabled());
        set_flag_override_for_test(FeatureFlag::StructuralSearch, true);
        assert!(FeatureFlag::StructuralSearch.is_enabled());
        set_flag_override_for_test(FeatureFlag::StructuralSearch, false);
        assert!(!FeatureFlag::StructuralSearch.is_enabled());
    }

    /// `is_legacy_revertible` covers all v2.0.0 rollout flags.
    #[test]
    fn test_is_legacy_revertible() {
        assert!(is_legacy_revertible(&FeatureFlag::DaemonClient));
        assert!(is_legacy_revertible(&FeatureFlag::ValidatedModel));
        // GA features are NOT legacy-revertible.
        assert!(!is_legacy_revertible(&FeatureFlag::NeuralSearch));
        // Debug escape hatch is not a rollout flag.
        assert!(!is_legacy_revertible(&FeatureFlag::DebugEscapeHatch));
    }
}