use crate::canonical::compute_id;
use crate::store::Store;
use crate::tick::{Check, Ground, Liveness, Tick};
use crate::verify::verify;
use std::path::Path;
use std::process::ExitCode;
fn render_source_ref(v: &serde_json::Value) -> String {
v.as_str()
.map(String::from)
.unwrap_or_else(|| v.to_string())
}
fn triggered_since(
repo: &std::path::Path,
ground: &crate::tick::Ground,
receipts: &[crate::receipt::Receipt],
) -> bool {
use crate::tick::Check;
let triggered_by = match &ground.check {
Some(Check::Test { liveness, .. }) => &liveness.triggered_by,
_ => return false,
};
let latest = receipts.iter().max_by(|a, b| a.ran_at.cmp(&b.ran_at));
match latest {
Some(r) => crate::liveness::changed_since(repo, &r.commit, triggered_by).unwrap_or(false),
None => false,
}
}
pub fn init(repo: &Path) -> ExitCode {
let store = Store::at(repo);
match store.init() {
Ok(true) => {
println!("created .evolving/ (content-addressed chain + results cache)");
ExitCode::SUCCESS
}
Ok(false) => {
println!(".evolving/ already exists (no-op)");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: could not create .evolving/: {e}");
ExitCode::FAILURE
}
}
}
pub fn show(repo: &Path, id: &str) -> ExitCode {
let store = Store::at(repo);
let path = store.ticks_dir().join(id);
if !path.is_file() {
eprintln!("error: no tick with id {id}");
return ExitCode::FAILURE;
}
match std::fs::read_to_string(&path) {
Ok(text) => {
println!("{text}");
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
if let Some(a) = v.get("authority").and_then(|x| x.as_str()) {
println!("authority: {a}");
}
if let Some(j) = v.get("jurisdiction").and_then(|x| x.as_str()) {
println!("jurisdiction: {j}");
}
if let Some(r) = v.get("source_ref") {
println!("source_ref: {}", render_source_ref(r));
}
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: reading {id}: {e}");
ExitCode::FAILURE
}
}
}
pub fn decide(repo: &Path, decision: Option<&str>, args: &[String]) -> ExitCode {
let (decision, args): (Option<&str>, Vec<String>) = match decision {
Some(d) if d.starts_with('-') => {
let mut v = vec![d.to_string()];
v.extend_from_slice(args);
(None, v)
}
other => (other, args.to_vec()),
};
match crate::capture::run(repo, decision, &args) {
Ok(t) => {
crate::events::append(&Store::at(repo), "decide", Some(&t.id), None);
println!("recorded {} ({} ground(s))", t.id, t.grounds.len());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
pub fn guard(repo: &Path, a: crate::guard::GuardArgs) -> ExitCode {
match crate::guard::run(repo, a) {
Ok(t) => {
crate::events::append(&Store::at(repo), "guard", Some(&t.id), None);
println!("bound; wrote child {}", t.id);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
pub fn verify_cmd(repo: &Path, self_test: bool) -> ExitCode {
if self_test {
return self_test_golden();
}
let store = Store::at(repo);
for w in crate::verify::unknown_key_warnings(&store).unwrap_or_default() {
eprintln!("{w}");
}
for w in crate::verify::imported_op_warnings(&store).unwrap_or_default() {
eprintln!("{w}");
}
match verify(&store) {
Ok(v) if v.is_empty() => {
println!("✓ chain intact: every id == hash(payload), lineage forward-only");
println!("✓ every tick validates against the closed schema (R1) and check shape (R2)");
ExitCode::SUCCESS
}
Ok(v) => {
for line in &v {
println!("✗ {line}");
}
eprintln!("{} violation(s)", v.len());
ExitCode::FAILURE
}
Err(e) => {
eprintln!("error: reading store: {e}");
ExitCode::FAILURE
}
}
}
fn latest_ran_at(receipts: &[crate::receipt::Receipt]) -> Option<String> {
receipts.iter().map(|r| r.ran_at.clone()).max()
}
fn live_ctx(
store: &Store,
staleness_days: u64,
live_origin_sha: Option<String>,
attest: Option<Vec<String>>,
) -> crate::verdict::Ctx {
crate::verdict::Ctx {
live_origin_sha,
selected: crate::selected::read(store).unwrap_or(None),
now_unix: time::OffsetDateTime::now_utc().unix_timestamp(),
staleness_secs: staleness_days as i64 * 86_400,
attest,
}
}
pub fn check(
repo: &Path,
exit_on_red: bool,
run: bool,
platform: &str,
offline: bool,
attest: Vec<String>,
) -> ExitCode {
use crate::verdict::{verdict_for, Verdict};
let store = Store::at(repo);
if !store.exists() {
eprintln!("error: no .evolving/ store here — run `ev init` first");
return ExitCode::FAILURE;
}
let files = match store.read_all() {
Ok(f) => f,
Err(e) => {
eprintln!("error: reading store: {e}");
return ExitCode::FAILURE;
}
};
let config = crate::config::read(&store);
if run {
for (_filename, raw) in &files {
let t = match crate::tick::from_value(raw) {
Ok(t) => t,
Err(_) => continue,
};
if t.status != "live" {
continue;
}
for g in &t.grounds {
if let Some(Check::Test {
reference,
counter_test,
liveness,
..
}) = &g.check
{
if liveness.platforms.iter().any(|p| p == platform) {
match crate::runner::run_check(
repo,
reference,
platform,
config.green_exit_code,
) {
Ok(mut rc) => {
if let Some(counter_test) = counter_test {
if let Ok(ct) = crate::runner::run_check(
repo,
counter_test,
platform,
config.green_exit_code,
) {
rc.falsifiable = Some(rc.result != ct.result);
}
}
if let Err(e) = crate::receipt::append(&store, &rc) {
eprintln!(
"warning: could not write receipt for {reference:?}: {e}"
);
}
}
Err(e) => eprintln!("warning: run failed for {reference:?}: {e}"),
}
}
}
}
}
}
let live_origin = crate::staleness::resolve(repo, &store, &config.staleness_ref, offline);
let attest = if attest.is_empty() {
None
} else {
Some(attest)
};
let ctx = live_ctx(&store, config.staleness_days, live_origin, attest);
let mut rows: Vec<String> = Vec::new();
let mut any_not_green = false;
let mut total_test_bindings = 0usize;
let mut harvested_unproven = 0usize;
for (filename, raw) in &files {
let t = match crate::tick::from_value(raw) {
Ok(t) => t,
Err(_) => continue, };
if t.status != "live" {
continue;
}
let mut verdicts = Vec::with_capacity(t.grounds.len());
for g in &t.grounds {
let receipts = match &g.check {
Some(Check::Test { reference, .. }) => {
crate::receipt::read_for(&store, reference).unwrap_or_default()
}
_ => Vec::new(),
};
let ts = triggered_since(repo, g, &receipts);
let mut v = verdict_for(g, &receipts, &ctx, ts);
if matches!(t.jurisdiction.as_deref(), Some("C") | Some("D"))
&& !matches!(v, Verdict::Green | Verdict::NotApplicable | Verdict::Exempt)
{
v = Verdict::Memo;
}
if !matches!(
v,
Verdict::Green | Verdict::NotApplicable | Verdict::Exempt | Verdict::Memo
) {
any_not_green = true;
}
if let Some(Check::Test { counter_test, .. }) = &g.check {
total_test_bindings += 1;
let harvested = counter_test.is_none();
let mut detail = match &v {
Verdict::NotRun { missing_platforms } => {
format!("missing: {}", missing_platforms.join(", "))
}
Verdict::Stale { reason } => reason.clone(),
_ => latest_ran_at(&receipts)
.map(|ts| format!("ran {ts}"))
.unwrap_or_else(|| "no receipt".into()),
};
if harvested {
harvested_unproven += 1;
detail = format!("harvested — falsifiability not proven; {detail}");
crate::events::append(&store, "harvested", Some(&t.id), Some(v.label()));
}
rows.push(format!(
"{}\t{filename}\t{:?}\t({detail})",
v.label(),
g.claim
));
crate::events::append(&store, "check", Some(&t.id), Some(v.label()));
}
verdicts.push((g, v));
}
let _ = crate::state::write_state(
&store,
&t.id,
&verdicts,
&config.staleness_ref,
ctx.live_origin_sha.as_deref(),
);
}
if rows.is_empty() {
println!("no test-bound grounds to check");
} else {
for r in &rows {
println!("{r}");
}
if harvested_unproven > 0 {
println!(
"harvested-unproven: {harvested_unproven} of {total_test_bindings} test bindings have no counter-test (run ev guard to add one)"
);
}
if !run {
println!("note: run `ev check --run` to execute each counter-test and prove its falsifiability");
}
}
if exit_on_red && any_not_green {
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
pub struct MigrateArgs {
pub sources: Vec<String>,
pub dry_run: bool,
pub reconcile: bool,
pub against: Option<String>,
pub blame: Option<String>,
pub bind_check: Option<String>,
pub platforms: Vec<String>,
pub triggered_by: Vec<String>,
pub surfaces: Vec<String>,
pub verified_at_sha: Option<String>,
pub jurisdiction_map: Option<String>,
}
fn parse_jurisdiction_map(path: &str) -> Result<std::collections::HashMap<String, String>, String> {
let text = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
let mut map = std::collections::HashMap::new();
for line in text.lines() {
let l = line.trim();
if l.is_empty() || l.starts_with('#') {
continue;
}
let mut tokens = l.split_whitespace();
match (tokens.next(), tokens.next(), tokens.next()) {
(Some(key), Some(bucket), None) => {
crate::tick::validate_jurisdiction(bucket)
.map_err(|e| format!("jurisdiction-map line {l:?}: {e}"))?;
map.insert(key.to_string(), bucket.to_string());
}
_ => {
return Err(format!(
"jurisdiction-map line {l:?}: expected `<source_key> <bucket>`"
))
}
}
}
Ok(map)
}
fn extract_source(spec: &str) -> Result<Vec<crate::migrate::MigrationRecord>, String> {
let (kind, path) = spec
.split_once(':')
.ok_or_else(|| format!("--source expects <kind>:<path>, got {spec:?}"))?;
let text = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
let recs = match kind {
"canonical" => crate::migrate::canonical_records(&text)?,
"gitlog" => crate::migrate::extract_gitlog(&text),
"to-human" => crate::migrate::extract_to_human(&text),
"decisions-immutable" => crate::migrate::extract_decisions_immutable(&text),
"escalation" => crate::migrate::extract_escalation(&text),
other => {
return Err(format!(
"unknown source kind {other:?} (expected canonical | gitlog | to-human | decisions-immutable | escalation)"
))
}
};
Ok(recs)
}
pub fn migrate(repo: &Path, a: MigrateArgs) -> ExitCode {
if let Some(selector) = &a.bind_check {
let sha = match crate::capture::resolve_sha(repo, &a.verified_at_sha) {
Ok(s) => s,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
match crate::migrate::bind_check(
selector.clone(),
sha,
a.platforms.clone(),
a.triggered_by.clone(),
a.surfaces.clone(),
) {
Ok(Check::Test {
reference,
liveness,
..
}) => {
println!(
"harvested check (falsifiability not proven; no counter-test): {reference:?} on [{}] triggered-by [{}] surface [{}]",
liveness.platforms.join(", "),
liveness.triggered_by.join(", "),
liveness.surfaces.join(", ")
);
return ExitCode::SUCCESS;
}
Ok(_) => unreachable!("bind_check yields a Test check"),
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
}
}
if a.reconcile {
let against = match &a.against {
Some(s) => s,
None => {
eprintln!("error: --reconcile requires --against <kind>:<path>");
return ExitCode::FAILURE;
}
};
let recs = match extract_source(against) {
Ok(r) => r,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
};
match crate::migrate::reconcile(repo, &recs) {
Ok(rep) => {
println!(
"reconcile: in-both {}, source-only {} (the capture gap), store-only {}, un-keyable {}",
rep.in_both, rep.source_only, rep.store_only, rep.un_keyable
);
return ExitCode::SUCCESS;
}
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
}
}
if a.sources.is_empty() {
eprintln!("error: ev migrate needs at least one --source <kind>:<path> (or --reconcile / --bind-check)");
return ExitCode::FAILURE;
}
let mut records = Vec::new();
for spec in &a.sources {
match extract_source(spec) {
Ok(mut r) => records.append(&mut r),
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
}
}
let jurisdiction_map = match &a.jurisdiction_map {
Some(path) => match parse_jurisdiction_map(path) {
Ok(m) => m,
Err(e) => {
eprintln!("error: {e}");
return ExitCode::FAILURE;
}
},
None => std::collections::HashMap::new(),
};
match crate::migrate::backfill(
repo,
records,
a.blame.as_deref(),
&jurisdiction_map,
a.dry_run,
) {
Ok(s) => {
if !a.dry_run {
crate::events::append(&Store::at(repo), "migrate", None, None);
}
println!(
"{}imported {}, skipped {}, re-linked {}, {} source-only gap(s)",
if a.dry_run { "(dry-run) " } else { "" },
s.imported,
s.skipped,
s.relinked,
s.source_only_gaps
);
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: {e}");
ExitCode::FAILURE
}
}
}
pub fn why(repo: &Path, selector: &str) -> ExitCode {
let store = Store::at(repo);
if !store.exists() {
eprintln!("error: no .evolving/ store here — run `ev init` first");
return ExitCode::FAILURE;
}
let files = match store.read_all() {
Ok(f) => f,
Err(e) => {
eprintln!("error: reading store: {e}");
return ExitCode::FAILURE;
}
};
let mut found = false;
for (filename, raw) in &files {
let t = match crate::tick::from_value(raw) {
Ok(t) => t,
Err(_) => continue,
};
if t.status != "live" {
continue;
}
for g in &t.grounds {
if let Some(Check::Test { reference, .. }) = &g.check {
if reference.as_str() == selector {
found = true;
println!(
"{filename}\t{:?}\tguards: {:?} ({})",
t.decision, g.claim, g.supports
);
}
}
}
}
if !found {
eprintln!("{selector:?} guards nothing");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
pub fn list(repo: &Path) -> ExitCode {
let store = Store::at(repo);
if !store.exists() {
eprintln!("error: no .evolving/ store here — run `ev init` first");
return ExitCode::FAILURE;
}
let files = match store.read_all() {
Ok(f) => f,
Err(e) => {
eprintln!("error: reading store: {e}");
return ExitCode::FAILURE;
}
};
let mut rows: Vec<String> = files
.iter()
.map(|(name, raw)| {
let line = match crate::tick::from_value(raw) {
Ok(t) => {
let mut l = format!("{name}\t{}\t{:?}", t.status, t.decision);
if let Some(a) = &t.authority {
l.push_str(&format!("\tauthority={a}"));
}
if let Some(j) = &t.jurisdiction {
l.push_str(&format!("\tjurisdiction={j}"));
}
if let Some(r) = &t.source_ref {
l.push_str(&format!("\tsource_ref={}", render_source_ref(r)));
}
l
}
Err(_) => format!("{name}\t?\t\"<unparseable>\""),
};
line
})
.collect();
rows.sort();
if rows.is_empty() {
println!("no decisions yet");
return ExitCode::SUCCESS;
}
for line in &rows {
println!("{line}");
}
ExitCode::SUCCESS
}
fn load_bearing(t: &Tick) -> bool {
t.grounds
.iter()
.any(|g| g.supports.starts_with("rejected:"))
}
pub fn brief(repo: &Path, limit: Option<usize>) -> ExitCode {
let store = Store::at(repo);
if !store.exists() {
eprintln!("error: no .evolving/ store here — run `ev init` first");
return ExitCode::FAILURE;
}
let files = match store.read_all() {
Ok(f) => f,
Err(e) => {
eprintln!("error: reading store: {e}");
return ExitCode::FAILURE;
}
};
let limit = limit.unwrap_or(crate::config::read(&store).brief_limit);
let mut kept: Vec<(String, Tick)> = files
.iter()
.filter_map(|(name, raw)| crate::tick::from_value(raw).ok().map(|t| (name.clone(), t)))
.filter(|(_, t)| t.status == "live" && t.authority.as_deref() == Some("user-ruled"))
.collect();
let lb = load_bearing;
kept.sort_by(|a, b| {
lb(&b.1)
.cmp(&lb(&a.1))
.then(b.1.held_since.cmp(&a.1.held_since))
.then(b.0.cmp(&a.0))
});
if kept.is_empty() {
println!("no user-ruled decisions");
return ExitCode::SUCCESS;
}
let total = kept.len();
let n = if limit == 0 { total } else { limit.min(total) };
let dropped_lb = kept[n..].iter().filter(|(_, t)| lb(t)).count();
kept.truncate(n);
for (_id, t) in &kept {
println!("{} [user-ruled]", t.decision);
for g in &t.grounds {
if let Some(option) = g.supports.strip_prefix("rejected:") {
println!(" rejected {option}: {}", g.claim);
}
}
}
if total > n {
let dropped = total - n;
let lb_clause = if dropped_lb > 0 {
format!(", {dropped_lb} with rejected roads")
} else {
String::new()
};
println!("… {dropped} more user-ruled decision(s){lb_clause} — `ev list` for all");
}
ExitCode::SUCCESS
}
pub fn log(repo: &Path) -> ExitCode {
let store = Store::at(repo);
if !store.exists() {
eprintln!("error: no .evolving/ store here — run `ev init` first");
return ExitCode::FAILURE;
}
let mut id = match store.read_head() {
Ok(h) => h,
Err(e) => {
eprintln!("error: reading HEAD: {e}");
return ExitCode::FAILURE;
}
};
if id.is_empty() {
println!("no decisions yet");
return ExitCode::SUCCESS;
}
let mut seen = std::collections::HashSet::new();
while !id.is_empty() {
if !seen.insert(id.clone()) {
break; }
match store.read_tick(&id) {
Ok(Some(t)) => {
println!("{}\t{}\t{:?}", t.id, t.status, t.decision);
id = t.parent_id;
}
Ok(None) => {
eprintln!("warning: {id} not found (broken lineage)");
break;
}
Err(e) => {
eprintln!("error: reading {id}: {e}");
return ExitCode::FAILURE;
}
}
}
ExitCode::SUCCESS
}
pub fn reopen(repo: &Path, id: &str) -> ExitCode {
let store = Store::at(repo);
let tick = match store.read_tick(id) {
Ok(Some(t)) => t,
Ok(None) => {
eprintln!("error: no tick with id {id}");
return ExitCode::FAILURE;
}
Err(e) => {
eprintln!("error: reading {id}: {e}");
return ExitCode::FAILURE;
}
};
let config = crate::config::read(&store);
let live_origin = crate::staleness::resolve(repo, &store, &config.staleness_ref, true);
let ctx = live_ctx(&store, config.staleness_days, live_origin, None);
crate::events::append(&store, "reopen", Some(id), None);
println!("decision {}: {:?}", tick.id, tick.decision);
if !tick.observe.is_empty() {
println!("observe: {:?}", tick.observe);
}
if let Some(a) = &tick.authority {
println!("authority: {a}");
}
if let Some(j) = &tick.jurisdiction {
println!("jurisdiction: {j}");
}
if let Some(r) = &tick.source_ref {
println!("source_ref: {}", render_source_ref(r));
}
for g in &tick.grounds {
match &g.check {
Some(Check::Test {
reference,
verified_at_sha,
..
}) => {
let receipts = crate::receipt::read_for(&store, reference).unwrap_or_default();
let ts = triggered_since(repo, g, &receipts);
let v = crate::verdict::verdict_for(g, &receipts, &ctx, ts);
let now = v.label();
let short = &verified_at_sha[..verified_at_sha.len().min(8)];
println!(
" [{}] {:?} — test {:?} frozen@{short} now: {now}",
g.supports, g.claim, reference
);
}
Some(Check::Person { reference }) => {
println!(" [{}] {:?} — person {:?}", g.supports, g.claim, reference);
}
None => {
println!(" [{}] {:?}", g.supports, g.claim);
}
}
}
ExitCode::SUCCESS
}
fn self_test_golden() -> ExitCode {
let genesis = Tick {
id: String::new(),
parent_id: "".into(),
observe: "evaluating retrieval backend".into(),
decision: "freeze the retrieval schema for v2".into(),
grounds: vec![
Ground {
claim: "team still wants a frozen schema".into(),
supports: "chosen".into(),
check: Some(Check::Person {
reference: "Q3 infra review".into(),
}),
},
Ground {
claim: "pgvector would lock our schema".into(),
supports: "rejected:pgvector".into(),
check: None,
},
],
status: "live".into(),
held_since: "".into(),
blame: "Wang Yu".into(),
authority: None,
jurisdiction: None,
source_ref: None,
provenance: None,
};
let case1 = Tick {
id: String::new(),
parent_id: "7b21f0a4c8de".into(),
observe: "multi-pod restore-safety counter — chat-room R2289→R2290".into(),
decision: "restore-safety counter DB-backed; reject Redis".into(),
grounds: vec![
Ground {
claim: "Argus introduces no Redis; multi-pod coord via existing DB".into(),
supports: "chosen".into(),
check: Some(Check::Test {
reference: "pytest tests/test_redis_absent.py".into(),
verified_at_sha: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
counter_test: Some(
"pytest tests/test_redis_absent.py::test_redis_injection_flips_red".into(),
),
liveness: Liveness {
platforms: vec!["linux-ci".into()],
triggered_by: vec!["pyproject.toml".into()],
surfaces: vec!["pyproject-deps".into()],
},
}),
},
Ground {
claim: "team still wants 0-Redis posture".into(),
supports: "chosen".into(),
check: Some(Check::Person {
reference: "Q3 infra review".into(),
}),
},
Ground {
claim: "Redis would add a new infra dependency".into(),
supports: "rejected:Redis".into(),
check: None,
},
],
status: "live".into(),
held_since: "".into(),
blame: "Wang Yu".into(),
authority: None,
jurisdiction: None,
source_ref: None,
provenance: None,
};
let mut harvested = case1.clone();
if let Some(Check::Test { counter_test, .. }) = &mut harvested.grounds[0].check {
*counter_test = None;
}
let mut ok = true;
for (name, t, want) in [
("genesis", &genesis, "e2b337f53a1f"),
("case1", &case1, "638c47b0c9dd"),
("harvested", &harvested, "0cf784b51331"),
] {
let got = compute_id(t);
let pass = got == want;
ok &= pass;
println!(
"{} {name}: {got} (want {want})",
if pass { "✓" } else { "✗" }
);
}
if ok {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}