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