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}