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 correct(repo: &Path, a: crate::correct::CorrectArgs) -> ExitCode {
10 match crate::correct::run(repo, a) {
11 Ok(t) => {
12 println!("corrected {} ({} ground(s))", t.id, t.grounds.len());
13 ExitCode::SUCCESS
14 }
15 Err(e) => {
16 eprintln!("error: {e}");
17 ExitCode::FAILURE
18 }
19 }
20}
21
22fn decision_identity(t: &Tick) -> String {
29 let mut v = crate::canonical::hashed_value(t);
30 if let serde_json::Value::Object(m) = &mut v {
31 m.remove("parent_id");
32 }
33 v.to_string()
34}
35
36fn current_decisions(mut ticks: Vec<(String, Tick)>) -> Vec<(String, Tick)> {
40 ticks.sort_by(|a, b| b.1.held_since.cmp(&a.1.held_since).then(b.0.cmp(&a.0)));
42 let mut seen = std::collections::HashSet::new();
43 ticks
44 .into_iter()
45 .filter(|(_, t)| seen.insert(decision_identity(t)))
46 .collect()
47}
48
49fn render_source_ref(v: &serde_json::Value) -> String {
54 v.as_str()
55 .map(String::from)
56 .unwrap_or_else(|| v.to_string())
57}
58
59fn triggered_since(
63 repo: &std::path::Path,
64 ground: &crate::tick::Ground,
65 receipts: &[crate::receipt::Receipt],
66) -> bool {
67 use crate::tick::Check;
68 let triggered_by = match &ground.check {
69 Some(Check::Test { liveness, .. }) => &liveness.triggered_by,
70 _ => return false,
71 };
72 let latest = receipts.iter().max_by(|a, b| a.ran_at.cmp(&b.ran_at));
73 match latest {
74 Some(r) => crate::liveness::changed_since(repo, &r.commit, triggered_by).unwrap_or(false),
75 None => false,
76 }
77}
78
79pub fn init(repo: &Path) -> ExitCode {
80 let store = Store::at(repo);
81 match store.init() {
82 Ok(true) => {
83 println!("created .evolving/ (content-addressed chain + results cache)");
84 ExitCode::SUCCESS
85 }
86 Ok(false) => {
87 println!(".evolving/ already exists (no-op)");
88 ExitCode::SUCCESS
89 }
90 Err(e) => {
91 eprintln!("error: could not create .evolving/: {e}");
92 ExitCode::FAILURE
93 }
94 }
95}
96pub fn show(repo: &Path, id: &str) -> ExitCode {
97 let store = Store::at(repo);
98 let path = store.ticks_dir().join(id);
99 if !path.is_file() {
100 eprintln!("error: no tick with id {id}");
101 return ExitCode::FAILURE;
102 }
103 match std::fs::read_to_string(&path) {
104 Ok(text) => {
105 println!("{text}");
107 if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
109 if let Some(a) = v.get("authority").and_then(|x| x.as_str()) {
110 println!("authority: {a}");
111 }
112 if let Some(j) = v.get("jurisdiction").and_then(|x| x.as_str()) {
113 println!("jurisdiction: {j}");
114 }
115 if let Some(r) = v.get("source_ref") {
116 println!("source_ref: {}", render_source_ref(r));
117 }
118 }
119 ExitCode::SUCCESS
120 }
121 Err(e) => {
122 eprintln!("error: reading {id}: {e}");
123 ExitCode::FAILURE
124 }
125 }
126}
127pub fn decide(repo: &Path, decision: Option<&str>, args: &[String]) -> ExitCode {
128 let (decision, args): (Option<&str>, Vec<String>) = match decision {
133 Some(d) if d.starts_with('-') => {
134 let mut v = vec![d.to_string()];
135 v.extend_from_slice(args);
136 (None, v)
137 }
138 other => (other, args.to_vec()),
139 };
140 match crate::capture::run(repo, decision, &args) {
141 Ok(t) => {
142 crate::events::append(&Store::at(repo), "decide", Some(&t.id), None);
143 println!("recorded {} ({} ground(s))", t.id, t.grounds.len());
144 ExitCode::SUCCESS
145 }
146 Err(e) => {
147 eprintln!("error: {e}");
148 ExitCode::FAILURE
149 }
150 }
151}
152
153pub fn guard(repo: &Path, a: crate::guard::GuardArgs) -> ExitCode {
154 match crate::guard::run(repo, a) {
155 Ok(t) => {
156 crate::events::append(&Store::at(repo), "guard", Some(&t.id), None);
157 println!("bound; wrote child {}", t.id);
158 ExitCode::SUCCESS
159 }
160 Err(e) => {
161 eprintln!("error: {e}");
162 ExitCode::FAILURE
163 }
164 }
165}
166
167pub fn verify_cmd(repo: &Path, self_test: bool) -> ExitCode {
168 if self_test {
169 return self_test_golden();
170 }
171 let store = Store::at(repo);
172 for w in crate::verify::unknown_key_warnings(&store).unwrap_or_default() {
175 eprintln!("{w}");
176 }
177 for w in crate::verify::imported_op_warnings(&store).unwrap_or_default() {
180 eprintln!("{w}");
181 }
182 match verify(&store) {
183 Ok(v) if v.is_empty() => {
184 println!("✓ chain intact: every id == hash(payload), lineage forward-only");
185 println!("✓ every tick validates against the closed schema (R1) and check shape (R2)");
186 ExitCode::SUCCESS
187 }
188 Ok(v) => {
189 for line in &v {
190 println!("✗ {line}");
191 }
192 eprintln!("{} violation(s)", v.len());
193 ExitCode::FAILURE
194 }
195 Err(e) => {
196 eprintln!("error: reading store: {e}");
197 ExitCode::FAILURE
198 }
199 }
200}
201
202fn latest_ran_at(receipts: &[crate::receipt::Receipt]) -> Option<String> {
205 receipts.iter().map(|r| r.ran_at.clone()).max()
206}
207
208fn live_ctx(
212 store: &Store,
213 staleness_days: u64,
214 live_origin_sha: Option<String>,
215 attest: Option<Vec<String>>,
216) -> crate::verdict::Ctx {
217 crate::verdict::Ctx {
218 live_origin_sha,
219 selected: crate::selected::read(store).unwrap_or(None),
220 now_unix: time::OffsetDateTime::now_utc().unix_timestamp(),
221 staleness_secs: staleness_days as i64 * 86_400,
222 attest,
223 }
224}
225
226pub fn check(
227 repo: &Path,
228 exit_on_red: bool,
229 run: bool,
230 platform: &str,
231 offline: bool,
232 attest: Vec<String>,
233) -> ExitCode {
234 use crate::verdict::{verdict_for, Verdict};
235 let store = Store::at(repo);
236 if !store.exists() {
237 eprintln!("error: no .evolving/ store here — run `ev init` first");
238 return ExitCode::FAILURE;
239 }
240 let files = match store.read_all() {
241 Ok(f) => f,
242 Err(e) => {
243 eprintln!("error: reading store: {e}");
244 return ExitCode::FAILURE;
245 }
246 };
247 let config = crate::config::read(&store);
248
249 if run {
252 for (_filename, raw) in &files {
253 let t = match crate::tick::from_value(raw) {
254 Ok(t) => t,
255 Err(_) => continue,
256 };
257 if t.status != "live" {
258 continue;
259 }
260 for g in &t.grounds {
261 if let Some(Check::Test {
262 reference,
263 counter_test,
264 liveness,
265 ..
266 }) = &g.check
267 {
268 if liveness.platforms.iter().any(|p| p == platform) {
269 match crate::runner::run_check(
271 repo,
272 reference,
273 platform,
274 config.green_exit_code,
275 ) {
276 Ok(mut rc) => {
277 if let Some(counter_test) = counter_test {
281 if let Ok(ct) = crate::runner::run_check(
282 repo,
283 counter_test,
284 platform,
285 config.green_exit_code,
286 ) {
287 rc.falsifiable = Some(rc.result != ct.result);
288 }
289 }
290 if let Err(e) = crate::receipt::append(&store, &rc) {
291 eprintln!(
292 "warning: could not write receipt for {reference:?}: {e}"
293 );
294 }
295 }
296 Err(e) => eprintln!("warning: run failed for {reference:?}: {e}"),
297 }
298 }
299 }
300 }
301 }
302 }
303
304 let live_origin = crate::staleness::resolve(repo, &store, &config.staleness_ref, offline);
305 let attest = if attest.is_empty() {
306 None
307 } else {
308 Some(attest)
309 };
310 let ctx = live_ctx(&store, config.staleness_days, live_origin, attest);
311 let mut rows: Vec<String> = Vec::new();
312 let mut any_not_green = false;
313 let mut total_test_bindings = 0usize;
317 let mut harvested_unproven = 0usize;
318
319 for (filename, raw) in &files {
320 let t = match crate::tick::from_value(raw) {
321 Ok(t) => t,
322 Err(_) => continue, };
324 if t.status != "live" {
325 continue;
326 }
327 let mut verdicts = Vec::with_capacity(t.grounds.len());
328 for g in &t.grounds {
329 let receipts = match &g.check {
331 Some(Check::Test { reference, .. }) => {
332 crate::receipt::read_for(&store, reference).unwrap_or_default()
333 }
334 _ => Vec::new(),
335 };
336 let ts = triggered_since(repo, g, &receipts);
338 let mut v = verdict_for(g, &receipts, &ctx, ts);
339 if matches!(t.jurisdiction.as_deref(), Some("C") | Some("D"))
344 && !matches!(v, Verdict::Green | Verdict::NotApplicable | Verdict::Exempt)
345 {
346 v = Verdict::Memo;
347 }
348 if !matches!(
349 v,
350 Verdict::Green | Verdict::NotApplicable | Verdict::Exempt | Verdict::Memo
351 ) {
352 any_not_green = true;
353 }
354 if let Some(Check::Test { counter_test, .. }) = &g.check {
356 total_test_bindings += 1;
357 let harvested = counter_test.is_none();
358 let mut detail = match &v {
359 Verdict::NotRun { missing_platforms } => {
360 format!("missing: {}", missing_platforms.join(", "))
361 }
362 Verdict::Stale { reason } => reason.clone(),
363 _ => latest_ran_at(&receipts)
364 .map(|ts| format!("ran {ts}"))
365 .unwrap_or_else(|| "no receipt".into()),
366 };
367 if harvested {
371 harvested_unproven += 1;
372 detail = format!("harvested — falsifiability not proven; {detail}");
373 crate::events::append(&store, "harvested", Some(&t.id), Some(v.label()));
374 }
375 rows.push(format!(
376 "{}\t{filename}\t{:?}\t({detail})",
377 v.label(),
378 g.claim
379 ));
380 crate::events::append(&store, "check", Some(&t.id), Some(v.label()));
381 }
382 verdicts.push((g, v));
383 }
384 let _ = crate::state::write_state(
386 &store,
387 &t.id,
388 &verdicts,
389 &config.staleness_ref,
390 ctx.live_origin_sha.as_deref(),
391 );
392 }
393
394 if rows.is_empty() {
395 println!("no test-bound grounds to check");
396 } else {
397 for r in &rows {
398 println!("{r}");
399 }
400 if harvested_unproven > 0 {
403 println!(
404 "harvested-unproven: {harvested_unproven} of {total_test_bindings} test bindings have no counter-test (run ev guard to add one)"
405 );
406 }
407 if !run {
408 println!("note: run `ev check --run` to execute each counter-test and prove its falsifiability");
412 }
413 }
414 if exit_on_red && any_not_green {
415 return ExitCode::FAILURE;
416 }
417 ExitCode::SUCCESS
418}
419
420pub struct MigrateArgs {
422 pub sources: Vec<String>,
423 pub dry_run: bool,
424 pub reconcile: bool,
425 pub against: Option<String>,
426 pub blame: Option<String>,
427 pub bind_check: Option<String>,
428 pub platforms: Vec<String>,
429 pub triggered_by: Vec<String>,
430 pub surfaces: Vec<String>,
431 pub verified_at_sha: Option<String>,
432 pub jurisdiction_map: Option<String>,
433}
434
435fn parse_jurisdiction_map(path: &str) -> Result<std::collections::HashMap<String, String>, String> {
441 let text = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
442 let mut map = std::collections::HashMap::new();
443 for line in text.lines() {
444 let l = line.trim();
445 if l.is_empty() || l.starts_with('#') {
446 continue;
447 }
448 let mut tokens = l.split_whitespace();
449 match (tokens.next(), tokens.next(), tokens.next()) {
450 (Some(key), Some(bucket), None) => {
451 crate::tick::validate_jurisdiction(bucket)
452 .map_err(|e| format!("jurisdiction-map line {l:?}: {e}"))?;
453 map.insert(key.to_string(), bucket.to_string());
454 }
455 _ => {
456 return Err(format!(
457 "jurisdiction-map line {l:?}: expected `<source_key> <bucket>`"
458 ))
459 }
460 }
461 }
462 Ok(map)
463}
464
465fn extract_source(spec: &str) -> Result<Vec<crate::migrate::MigrationRecord>, String> {
469 let (kind, path) = spec
470 .split_once(':')
471 .ok_or_else(|| format!("--source expects <kind>:<path>, got {spec:?}"))?;
472 let text = std::fs::read_to_string(path).map_err(|e| format!("reading {path}: {e}"))?;
473 let recs = match kind {
474 "canonical" => crate::migrate::canonical_records(&text)?,
477 "gitlog" => crate::migrate::extract_gitlog(&text),
478 "to-human" => crate::migrate::extract_to_human(&text),
479 "decisions-immutable" => crate::migrate::extract_decisions_immutable(&text),
480 "escalation" => crate::migrate::extract_escalation(&text),
481 other => {
482 return Err(format!(
483 "unknown source kind {other:?} (expected canonical | gitlog | to-human | decisions-immutable | escalation)"
484 ))
485 }
486 };
487 Ok(recs)
488}
489
490pub fn migrate(repo: &Path, a: MigrateArgs) -> ExitCode {
491 if let Some(selector) = &a.bind_check {
493 let sha = match crate::capture::resolve_sha(repo, &a.verified_at_sha) {
494 Ok(s) => s,
495 Err(e) => {
496 eprintln!("error: {e}");
497 return ExitCode::FAILURE;
498 }
499 };
500 match crate::migrate::bind_check(
501 selector.clone(),
502 sha,
503 a.platforms.clone(),
504 a.triggered_by.clone(),
505 a.surfaces.clone(),
506 ) {
507 Ok(Check::Test {
508 reference,
509 liveness,
510 ..
511 }) => {
512 println!(
513 "harvested check (falsifiability not proven; no counter-test): {reference:?} on [{}] triggered-by [{}] surface [{}]",
514 liveness.platforms.join(", "),
515 liveness.triggered_by.join(", "),
516 liveness.surfaces.join(", ")
517 );
518 return ExitCode::SUCCESS;
519 }
520 Ok(_) => unreachable!("bind_check yields a Test check"),
521 Err(e) => {
522 eprintln!("error: {e}");
523 return ExitCode::FAILURE;
524 }
525 }
526 }
527
528 if a.reconcile {
530 let against = match &a.against {
531 Some(s) => s,
532 None => {
533 eprintln!("error: --reconcile requires --against <kind>:<path>");
534 return ExitCode::FAILURE;
535 }
536 };
537 let recs = match extract_source(against) {
538 Ok(r) => r,
539 Err(e) => {
540 eprintln!("error: {e}");
541 return ExitCode::FAILURE;
542 }
543 };
544 match crate::migrate::reconcile(repo, &recs) {
545 Ok(rep) => {
546 println!(
547 "reconcile: in-both {}, source-only {} (the capture gap), store-only {}, un-keyable {}",
548 rep.in_both, rep.source_only, rep.store_only, rep.un_keyable
549 );
550 return ExitCode::SUCCESS;
551 }
552 Err(e) => {
553 eprintln!("error: {e}");
554 return ExitCode::FAILURE;
555 }
556 }
557 }
558
559 if a.sources.is_empty() {
561 eprintln!("error: ev migrate needs at least one --source <kind>:<path> (or --reconcile / --bind-check)");
562 return ExitCode::FAILURE;
563 }
564 let mut records = Vec::new();
565 for spec in &a.sources {
566 match extract_source(spec) {
567 Ok(mut r) => records.append(&mut r),
568 Err(e) => {
569 eprintln!("error: {e}");
570 return ExitCode::FAILURE;
571 }
572 }
573 }
574 let jurisdiction_map = match &a.jurisdiction_map {
576 Some(path) => match parse_jurisdiction_map(path) {
577 Ok(m) => m,
578 Err(e) => {
579 eprintln!("error: {e}");
580 return ExitCode::FAILURE;
581 }
582 },
583 None => std::collections::HashMap::new(),
584 };
585 match crate::migrate::backfill(
586 repo,
587 records,
588 a.blame.as_deref(),
589 &jurisdiction_map,
590 a.dry_run,
591 ) {
592 Ok(s) => {
593 if !a.dry_run {
594 crate::events::append(&Store::at(repo), "migrate", None, None);
595 }
596 println!(
597 "{}imported {}, skipped {}, re-linked {}, {} source-only gap(s){}",
598 if a.dry_run { "(dry-run) " } else { "" },
599 s.imported,
600 s.skipped,
601 s.relinked,
602 s.source_only_gaps,
603 if s.discrepancies > 0 {
604 format!(", {} discrepancy(ies) — see above", s.discrepancies)
605 } else {
606 String::new()
607 }
608 );
609 ExitCode::SUCCESS
610 }
611 Err(e) => {
612 eprintln!("error: {e}");
613 ExitCode::FAILURE
614 }
615 }
616}
617
618pub fn why(repo: &Path, selector: &str) -> ExitCode {
619 let store = Store::at(repo);
620 if !store.exists() {
621 eprintln!("error: no .evolving/ store here — run `ev init` first");
622 return ExitCode::FAILURE;
623 }
624 let files = match store.read_all() {
625 Ok(f) => f,
626 Err(e) => {
627 eprintln!("error: reading store: {e}");
628 return ExitCode::FAILURE;
629 }
630 };
631 let mut found = false;
632 for (filename, raw) in &files {
633 let t = match crate::tick::from_value(raw) {
634 Ok(t) => t,
635 Err(_) => continue,
636 };
637 if t.status != "live" {
638 continue;
639 }
640 for g in &t.grounds {
641 if let Some(Check::Test { reference, .. }) = &g.check {
642 if reference.as_str() == selector {
643 found = true;
644 println!(
645 "{filename}\t{:?}\tguards: {:?} ({})",
646 t.decision, g.claim, g.supports
647 );
648 }
649 }
650 }
651 }
652 if !found {
653 eprintln!("{selector:?} guards nothing");
654 return ExitCode::FAILURE;
655 }
656 ExitCode::SUCCESS
657}
658
659pub fn list(repo: &Path) -> ExitCode {
661 let store = Store::at(repo);
662 if !store.exists() {
663 eprintln!("error: no .evolving/ store here — run `ev init` first");
664 return ExitCode::FAILURE;
665 }
666 let files = match store.read_all() {
667 Ok(f) => f,
668 Err(e) => {
669 eprintln!("error: reading store: {e}");
670 return ExitCode::FAILURE;
671 }
672 };
673 let mut parsed: Vec<(String, Tick)> = Vec::new();
679 let mut rows: Vec<String> = Vec::new();
680 for (name, raw) in &files {
681 match crate::tick::from_value(raw) {
682 Ok(t) => parsed.push((name.clone(), t)),
683 Err(_) => rows.push(format!("{name}\t?\t\"<unparseable>\"")),
684 }
685 }
686 for (name, t) in current_decisions(parsed) {
687 let mut l = format!("{name}\t{}\t{:?}", t.status, t.decision);
688 if let Some(a) = &t.authority {
689 l.push_str(&format!("\tauthority={a}"));
690 }
691 if let Some(j) = &t.jurisdiction {
692 l.push_str(&format!("\tjurisdiction={j}"));
693 }
694 if let Some(r) = &t.source_ref {
695 l.push_str(&format!("\tsource_ref={}", render_source_ref(r)));
696 }
697 rows.push(l);
698 }
699 rows.sort();
700 if rows.is_empty() {
701 println!("no decisions yet");
702 return ExitCode::SUCCESS;
703 }
704 for line in &rows {
705 println!("{line}");
706 }
707 ExitCode::SUCCESS
708}
709
710fn load_bearing(t: &Tick) -> bool {
714 t.grounds
715 .iter()
716 .any(|g| g.supports.starts_with("rejected:"))
717}
718
719pub fn brief(repo: &Path, limit: Option<usize>) -> ExitCode {
726 let store = Store::at(repo);
727 if !store.exists() {
728 eprintln!("error: no .evolving/ store here — run `ev init` first");
729 return ExitCode::FAILURE;
730 }
731 let files = match store.read_all() {
732 Ok(f) => f,
733 Err(e) => {
734 eprintln!("error: reading store: {e}");
735 return ExitCode::FAILURE;
736 }
737 };
738 let limit = limit.unwrap_or(crate::config::read(&store).brief_limit);
740 let all: Vec<(String, Tick)> = files
743 .iter()
744 .filter_map(|(name, raw)| crate::tick::from_value(raw).ok().map(|t| (name.clone(), t)))
745 .collect();
746 let mut kept: Vec<(String, Tick)> = current_decisions(all)
747 .into_iter()
748 .filter(|(_, t)| t.status == "live" && t.authority.as_deref() == Some("user-ruled"))
749 .collect();
750 let lb = load_bearing;
751 kept.sort_by(|a, b| {
754 lb(&b.1)
755 .cmp(&lb(&a.1))
756 .then(b.1.held_since.cmp(&a.1.held_since))
757 .then(b.0.cmp(&a.0))
758 });
759 if kept.is_empty() {
760 println!("no user-ruled decisions");
761 return ExitCode::SUCCESS;
762 }
763 let total = kept.len();
764 let n = if limit == 0 { total } else { limit.min(total) };
766 let dropped_lb = kept[n..].iter().filter(|(_, t)| lb(t)).count();
768 kept.truncate(n);
769 for (_id, t) in &kept {
770 println!("{} [user-ruled]", t.decision);
771 for g in &t.grounds {
772 if let Some(option) = g.supports.strip_prefix("rejected:") {
773 println!(" rejected {option}: {}", g.claim);
774 }
775 }
776 }
777 if total > n {
778 let dropped = total - n;
779 let lb_clause = if dropped_lb > 0 {
780 format!(", {dropped_lb} with rejected roads")
781 } else {
782 String::new()
783 };
784 println!("… {dropped} more user-ruled decision(s){lb_clause} — `ev list` for all");
785 }
786 ExitCode::SUCCESS
787}
788
789pub fn log(repo: &Path) -> ExitCode {
791 let store = Store::at(repo);
792 if !store.exists() {
793 eprintln!("error: no .evolving/ store here — run `ev init` first");
794 return ExitCode::FAILURE;
795 }
796 let mut id = match store.read_head() {
797 Ok(h) => h,
798 Err(e) => {
799 eprintln!("error: reading HEAD: {e}");
800 return ExitCode::FAILURE;
801 }
802 };
803 if id.is_empty() {
804 println!("no decisions yet");
805 return ExitCode::SUCCESS;
806 }
807 let mut seen = std::collections::HashSet::new();
808 while !id.is_empty() {
809 if !seen.insert(id.clone()) {
810 break; }
812 match store.read_tick(&id) {
813 Ok(Some(t)) => {
814 println!("{}\t{}\t{:?}", t.id, t.status, t.decision);
815 id = t.parent_id;
816 }
817 Ok(None) => {
818 eprintln!("warning: {id} not found (broken lineage)");
819 break;
820 }
821 Err(e) => {
822 eprintln!("error: reading {id}: {e}");
823 return ExitCode::FAILURE;
824 }
825 }
826 }
827 ExitCode::SUCCESS
828}
829
830pub fn reopen(repo: &Path, id: &str) -> ExitCode {
831 let store = Store::at(repo);
832 let tick = match store.read_tick(id) {
833 Ok(Some(t)) => t,
834 Ok(None) => {
835 eprintln!("error: no tick with id {id}");
836 return ExitCode::FAILURE;
837 }
838 Err(e) => {
839 eprintln!("error: reading {id}: {e}");
840 return ExitCode::FAILURE;
841 }
842 };
843 let config = crate::config::read(&store);
844 let live_origin = crate::staleness::resolve(repo, &store, &config.staleness_ref, true);
845 let ctx = live_ctx(&store, config.staleness_days, live_origin, None);
846
847 crate::events::append(&store, "reopen", Some(id), None);
848 println!("decision {}: {:?}", tick.id, tick.decision);
849 if !tick.observe.is_empty() {
850 println!("observe: {:?}", tick.observe);
851 }
852 if let Some(a) = &tick.authority {
853 println!("authority: {a}");
854 }
855 if let Some(j) = &tick.jurisdiction {
856 println!("jurisdiction: {j}");
857 }
858 if let Some(r) = &tick.source_ref {
859 println!("source_ref: {}", render_source_ref(r));
860 }
861 for g in &tick.grounds {
862 match &g.check {
863 Some(Check::Test {
864 reference,
865 verified_at_sha,
866 ..
867 }) => {
868 let receipts = crate::receipt::read_for(&store, reference).unwrap_or_default();
869 let ts = triggered_since(repo, g, &receipts);
870 let v = crate::verdict::verdict_for(g, &receipts, &ctx, ts);
871 let now = v.label();
872 let short = &verified_at_sha[..verified_at_sha.len().min(8)];
873 println!(
874 " [{}] {:?} — test {:?} frozen@{short} now: {now}",
875 g.supports, g.claim, reference
876 );
877 }
878 Some(Check::Person { reference }) => {
879 println!(" [{}] {:?} — person {:?}", g.supports, g.claim, reference);
880 }
881 None => {
882 println!(" [{}] {:?}", g.supports, g.claim);
883 }
884 }
885 }
886 ExitCode::SUCCESS
887}
888
889fn self_test_golden() -> ExitCode {
891 let genesis = Tick {
892 id: String::new(),
893 parent_id: "".into(),
894 observe: "evaluating retrieval backend".into(),
895 decision: "freeze the retrieval schema for v2".into(),
896 grounds: vec![
897 Ground {
898 claim: "team still wants a frozen schema".into(),
899 supports: "chosen".into(),
900 check: Some(Check::Person {
901 reference: "Q3 infra review".into(),
902 }),
903 },
904 Ground {
905 claim: "pgvector would lock our schema".into(),
906 supports: "rejected:pgvector".into(),
907 check: None,
908 },
909 ],
910 status: "live".into(),
911 held_since: "".into(),
912 blame: "Wang Yu".into(),
913 authority: None,
914 jurisdiction: None,
915 source_ref: None,
916 provenance: None,
917 };
918 let case1 = Tick {
919 id: String::new(),
920 parent_id: "7b21f0a4c8de".into(),
921 observe: "multi-pod restore-safety counter — chat-room R2289→R2290".into(),
922 decision: "restore-safety counter DB-backed; reject Redis".into(),
923 grounds: vec![
924 Ground {
925 claim: "Argus introduces no Redis; multi-pod coord via existing DB".into(),
926 supports: "chosen".into(),
927 check: Some(Check::Test {
928 reference: "pytest tests/test_redis_absent.py".into(),
929 verified_at_sha: "d308afac1b2c3d4e5f60718293a4b5c6d7e8f901".into(),
930 counter_test: Some(
931 "pytest tests/test_redis_absent.py::test_redis_injection_flips_red".into(),
932 ),
933 liveness: Liveness {
934 platforms: vec!["linux-ci".into()],
935 triggered_by: vec!["pyproject.toml".into()],
936 surfaces: vec!["pyproject-deps".into()],
937 },
938 }),
939 },
940 Ground {
941 claim: "team still wants 0-Redis posture".into(),
942 supports: "chosen".into(),
943 check: Some(Check::Person {
944 reference: "Q3 infra review".into(),
945 }),
946 },
947 Ground {
948 claim: "Redis would add a new infra dependency".into(),
949 supports: "rejected:Redis".into(),
950 check: None,
951 },
952 ],
953 status: "live".into(),
954 held_since: "".into(),
955 blame: "Wang Yu".into(),
956 authority: None,
957 jurisdiction: None,
958 source_ref: None,
959 provenance: None,
960 };
961 let mut harvested = case1.clone();
964 if let Some(Check::Test { counter_test, .. }) = &mut harvested.grounds[0].check {
965 *counter_test = None;
966 }
967 let mut ok = true;
968 for (name, t, want) in [
969 ("genesis", &genesis, "e2b337f53a1f"),
970 ("case1", &case1, "638c47b0c9dd"),
971 ("harvested", &harvested, "0cf784b51331"),
972 ] {
973 let got = compute_id(t);
974 let pass = got == want;
975 ok &= pass;
976 println!(
977 "{} {name}: {got} (want {want})",
978 if pass { "✓" } else { "✗" }
979 );
980 }
981 if ok {
982 ExitCode::SUCCESS
983 } else {
984 ExitCode::FAILURE
985 }
986}