Skip to main content

devscout_rs/mapcmd/
options.rs

1// `MapOptions` -- see `mapcmd.rs`'s own module header for `hash_reuse`. Kept
2// in its own sibling file (not inline in `mapcmd.rs`) because that file sits
3// within two lines of its `tools/size-ratchet.toml` ceiling; adding
4// `no_semantic` here, rather than growing the frozen file, keeps new code
5// out of a capped file by moving the struct it belongs to into a sibling
6// module.
7//
8// `compiler_facts_artifact_present` lives here for the same reason: it is
9// `map_repo`'s rebuild-trigger input for the `--no-semantic` rollback lever,
10// and `mapcmd.rs` has too little line budget left to host it directly.
11
12use std::path::Path;
13
14/// Options for `map_repo` -- see `mapcmd.rs`'s own module header.
15#[derive(Debug, Clone, Copy, Default)]
16pub struct MapOptions {
17    /// Content-hash reuse (`mapcmd.rs`'s own module header). `MapOptions::default()`
18    /// keeps `false` (mtime keying); the binary's env default is `true` -- see
19    /// `from_env`.
20    pub hash_reuse: bool,
21    /// The compiler-fact enrichment consumer's rollback lever (`devscout map
22    /// --no-semantic`): when `true`, `map_repo` never loads or applies an
23    /// admitted compiler-facts artifact even when one is present, so a
24    /// `--no-semantic` run always produces the exact syntax-only graph a
25    /// build with no artifact admitted would -- a zero-cost, always-available
26    /// escape hatch back to pre-enrichment behavior. A CLI flag, not an
27    /// environment switch, following this crate's own `--no-guess`-shaped
28    /// precedent rather than `SCOUT_MTIME_REUSE`'s env-switch shape, so the
29    /// lever is visible on the command line that produced a graph.
30    pub no_semantic: bool,
31}
32
33impl MapOptions {
34    /// Binary default: hash reuse ON, `no_semantic` OFF (an admitted artifact,
35    /// when present, is applied). Exactly `SCOUT_MTIME_REUSE=1` drops back to
36    /// mtime keying; anything else (including unset) keeps hash reuse. The
37    /// retired opt-in `SCOUT_HASH_REUSE` is deliberately not read any more --
38    /// it named what is now the default. A free function rather than folded
39    /// into `map_repo` itself so tests (and any future caller) can construct
40    /// `MapOptions` directly -- deterministic, no process-env mutation shared
41    /// across parallel `cargo test` threads.
42    pub fn from_env() -> Self {
43        MapOptions {
44            hash_reuse: std::env::var("SCOUT_MTIME_REUSE").as_deref() != Ok("1"),
45            no_semantic: false,
46        }
47    }
48}
49
50/// Whether an admitted compiler-facts artifact sits on disk at `root`,
51/// checked by presence alone (`Path::exists`), never by loading or parsing
52/// it. `map_repo`'s rebuild trigger needs this independently of whether
53/// `MapOptions::no_semantic` is set: when the flag forces `semantic_layer` to
54/// `None`, `semantic_layer.is_some()` can never fire, so a `--no-semantic`
55/// run over an otherwise-unchanged tree with a still-enriched `graph.json` on
56/// disk would leave that stale enriched graph in place forever (the rollback
57/// lever's whole point is to undo exactly that). Presence-only, not
58/// freshness or parseability, so a rebuild is triggered even for a stale or
59/// malformed artifact -- the same "fires safely, not perfectly" cost
60/// `mapcmd.rs`'s own trigger comment already accepts for its other two
61/// disjuncts.
62pub(crate) fn compiler_facts_artifact_present(root: &Path) -> bool {
63    crate::graph::compiler_facts_json_path(root).exists()
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn default_keeps_semantic_enrichment_on() {
72        assert!(!MapOptions::default().no_semantic);
73        assert!(!MapOptions::from_env().no_semantic);
74    }
75
76    #[test]
77    fn no_semantic_is_independently_settable() {
78        let opts = MapOptions {
79            hash_reuse: true,
80            no_semantic: true,
81        };
82        assert!(opts.hash_reuse);
83        assert!(opts.no_semantic);
84    }
85
86    #[test]
87    fn compiler_facts_artifact_present_checks_disk_only_never_parses() {
88        let n = std::sync::atomic::AtomicU64::new(0);
89        let dir = std::env::temp_dir().join(format!(
90            "scout-mapopts-test-{}-{}",
91            std::process::id(),
92            n.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
93        ));
94        std::fs::create_dir_all(&dir).expect("create temp dir");
95        assert!(!compiler_facts_artifact_present(&dir));
96
97        let path = crate::graph::compiler_facts_json_path(&dir);
98        if let Some(parent) = path.parent() {
99            std::fs::create_dir_all(parent).expect("create artifact parent dir");
100        }
101        // Deliberately malformed bytes -- presence, not parseability, is what
102        // this function checks.
103        std::fs::write(&path, b"not json").expect("write artifact stub");
104        assert!(compiler_facts_artifact_present(&dir));
105
106        std::fs::remove_dir_all(&dir).ok();
107    }
108}