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