Skip to main content

ev/
cmd.rs

1use crate::canonical::compute_id;
2use crate::store::Store;
3use crate::tick::{Check, Ground, Liveness, Tick};
4use crate::verify::verify;
5use std::path::Path;
6use std::process::ExitCode;
7
8pub fn init(repo: &Path) -> ExitCode {
9    let store = Store::at(repo);
10    match store.init() {
11        Ok(true) => {
12            println!("created .evolving/  (content-addressed chain + results cache)");
13            ExitCode::SUCCESS
14        }
15        Ok(false) => {
16            println!(".evolving/ already exists (no-op)");
17            ExitCode::SUCCESS
18        }
19        Err(e) => {
20            eprintln!("error: could not create .evolving/: {e}");
21            ExitCode::FAILURE
22        }
23    }
24}
25pub fn show(repo: &Path, id: &str) -> ExitCode {
26    let store = Store::at(repo);
27    let path = store.ticks_dir().join(id);
28    if !path.is_file() {
29        eprintln!("error: no tick with id {id}");
30        return ExitCode::FAILURE;
31    }
32    match std::fs::read_to_string(&path) {
33        Ok(text) => {
34            // print as-is (the on-disk pretty JSON: hashed payload + bookkeeping).
35            println!("{text}");
36            ExitCode::SUCCESS
37        }
38        Err(e) => {
39            eprintln!("error: reading {id}: {e}");
40            ExitCode::FAILURE
41        }
42    }
43}
44pub fn decide(repo: &Path, decision: &str, args: &[String]) -> ExitCode {
45    match crate::capture::run(repo, decision, args) {
46        Ok(t) => {
47            println!("recorded {} ({} ground(s))", t.id, t.grounds.len());
48            ExitCode::SUCCESS
49        }
50        Err(e) => {
51            eprintln!("error: {e}");
52            ExitCode::FAILURE
53        }
54    }
55}
56
57pub fn guard(repo: &Path, a: crate::guard::GuardArgs) -> ExitCode {
58    match crate::guard::run(repo, a) {
59        Ok(t) => {
60            println!("bound; wrote child {}", t.id);
61            ExitCode::SUCCESS
62        }
63        Err(e) => {
64            eprintln!("error: {e}");
65            ExitCode::FAILURE
66        }
67    }
68}
69
70pub fn verify_cmd(repo: &Path, self_test: bool) -> ExitCode {
71    if self_test {
72        return self_test_golden();
73    }
74    let store = Store::at(repo);
75    match verify(&store) {
76        Ok(v) if v.is_empty() => {
77            println!("✓ chain intact: every id == hash(payload), lineage forward-only");
78            println!("✓ every tick validates against the closed schema (R1) and check shape (R2)");
79            ExitCode::SUCCESS
80        }
81        Ok(v) => {
82            for line in &v {
83                println!("✗ {line}");
84            }
85            eprintln!("{} violation(s)", v.len());
86            ExitCode::FAILURE
87        }
88        Err(e) => {
89            eprintln!("error: reading store: {e}");
90            ExitCode::FAILURE
91        }
92    }
93}
94
95/// Reproduce the two frozen golden vectors; non-zero if either id drifts.
96fn self_test_golden() -> ExitCode {
97    let genesis = Tick {
98        id: String::new(),
99        parent_id: "".into(),
100        observe: "evaluating retrieval backend".into(),
101        decision: "freeze the retrieval schema for v2".into(),
102        grounds: vec![
103            Ground {
104                claim: "team still wants a frozen schema".into(),
105                supports: "chosen".into(),
106                check: Some(Check::Person {
107                    reference: "Q3 infra review".into(),
108                }),
109            },
110            Ground {
111                claim: "pgvector would lock our schema".into(),
112                supports: "rejected:pgvector".into(),
113                check: None,
114            },
115        ],
116        status: "live".into(),
117        held_since: "".into(),
118        blame: "Wang Yu".into(),
119    };
120    let case1 = Tick {
121        id: String::new(),
122        parent_id: "7b21f0a4c8de".into(),
123        observe: "multi-pod restore-safety counter — chat-room R2289→R2290".into(),
124        decision: "restore-safety counter DB-backed; reject Redis".into(),
125        grounds: vec![
126            Ground {
127                claim: "Argus introduces no Redis; multi-pod coord via existing DB".into(),
128                supports: "chosen".into(),
129                check: Some(Check::Test {
130                    reference: "pytest tests/test_redis_absent.py".into(),
131                    verified_at_sha: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
132                    counter_test:
133                        "pytest tests/test_redis_absent.py::test_redis_injection_flips_red".into(),
134                    liveness: Liveness {
135                        platforms: vec!["linux-ci".into()],
136                        triggered_by: vec!["pyproject.toml".into()],
137                        surfaces: vec!["pyproject-deps".into()],
138                    },
139                }),
140            },
141            Ground {
142                claim: "team still wants 0-Redis posture".into(),
143                supports: "chosen".into(),
144                check: Some(Check::Person {
145                    reference: "Q3 infra review".into(),
146                }),
147            },
148            Ground {
149                claim: "Redis would add a new infra dependency".into(),
150                supports: "rejected:Redis".into(),
151                check: None,
152            },
153        ],
154        status: "live".into(),
155        held_since: "".into(),
156        blame: "Wang Yu".into(),
157    };
158    let mut ok = true;
159    for (name, t, want) in [
160        ("genesis", &genesis, "e2b337f53a1f"),
161        ("case1", &case1, "638c47b0c9dd"),
162    ] {
163        let got = compute_id(t);
164        let pass = got == want;
165        ok &= pass;
166        println!(
167            "{} {name}: {got} (want {want})",
168            if pass { "✓" } else { "✗" }
169        );
170    }
171    if ok {
172        ExitCode::SUCCESS
173    } else {
174        ExitCode::FAILURE
175    }
176}