ev/correct.rs
1//! `ev correct` — append a corrective CHILD tick that fixes a stale non-hashed tag (authority /
2//! jurisdiction / provenance) on an existing decision, under ev's append-only law.
3//!
4//! The child copies the target's HASHED payload (decision / observe / grounds) verbatim — so it is
5//! recognizably the same decision — and carries the corrected tag; it is funneled through
6//! `capture::append` (a new id at HEAD), so the target tick is NEVER rewritten. A human authors it
7//! (blame required); it is UNREACHABLE from migrate / canonical-intake, so an adapter can never
8//! launder a tag. `brief`/`list` then collapse the lineage to its current (latest) state, so the
9//! corrected child surfaces and the stale parent stays as honest history.
10
11use crate::capture::{append, Decision};
12use crate::store::Store;
13use crate::tick::Tick;
14use std::path::Path;
15
16pub struct CorrectArgs {
17 pub id: String,
18 pub authority: Option<String>,
19 pub jurisdiction: Option<String>,
20 pub provenance: Option<String>,
21 pub blame: Option<String>,
22}
23
24pub fn run(repo: &Path, a: CorrectArgs) -> Result<Tick, String> {
25 let store = Store::at(repo);
26 if !store.exists() {
27 return Err("no .evolving/ store here — run `ev init` first".into());
28 }
29 let target = store
30 .read_tick(&a.id)
31 .map_err(|e| format!("reading {}: {e}", a.id))?
32 .ok_or_else(|| format!("no such tick: {}", a.id))?;
33
34 // At least one tag must be supplied (else there is nothing to correct), and each is vocab-checked.
35 if a.authority.is_none() && a.jurisdiction.is_none() && a.provenance.is_none() {
36 return Err(
37 "ev correct needs at least one of --authority / --jurisdiction / --provenance".into(),
38 );
39 }
40 if let Some(v) = &a.authority {
41 crate::capture::validate_authority(v)?;
42 }
43 if let Some(v) = &a.jurisdiction {
44 crate::tick::validate_jurisdiction(v)?;
45 }
46 if let Some(v) = &a.provenance {
47 crate::tick::validate_provenance(v)?;
48 }
49
50 // The corrected tags: an override wins; otherwise inherit the target's (a tag-correction does not
51 // re-author the decision, so an unspecified tag — including provenance — carries over unchanged).
52 let authority = a.authority.clone().or_else(|| target.authority.clone());
53 let jurisdiction = a
54 .jurisdiction
55 .clone()
56 .or_else(|| target.jurisdiction.clone());
57 let provenance = a.provenance.clone().or_else(|| target.provenance.clone());
58
59 // Refuse a no-op: if nothing actually changes, there is nothing to correct.
60 if authority == target.authority
61 && jurisdiction == target.jurisdiction
62 && provenance == target.provenance
63 {
64 return Err(format!(
65 "tick {} already carries those tags — nothing to correct",
66 a.id
67 ));
68 }
69 // A detect-only (C/D) decision may carry no runnable Test check — refuse a correction that would
70 // make the tick violate that structural lock (the same shared predicate verify + ingest use).
71 if crate::tick::detect_only_carries_test(jurisdiction.as_deref(), &target.grounds) {
72 return Err(format!(
73 "cannot set jurisdiction {} on a decision that carries a test check (detect-only)",
74 jurisdiction.as_deref().unwrap_or("")
75 ));
76 }
77
78 let blame = crate::capture::resolve_blame(repo, a.blame.clone())?;
79
80 // The corrective child: the target's HASHED payload verbatim + the corrected tags, appended at
81 // HEAD (a new id, since parent_id differs). The target is never rewritten — immutability intact.
82 let child = append(
83 repo,
84 Decision {
85 observe: target.observe.clone(),
86 decision: target.decision.clone(),
87 grounds: target.grounds.clone(),
88 blame,
89 authority,
90 jurisdiction,
91 source_ref: target.source_ref.clone(),
92 provenance,
93 // The explicit relation-overlay edge: this child CORRECTS the target. Read by the
94 // brief/list collapse (precise supersession) and surfaced by show/log/reopen. Non-hashed,
95 // so it never moves the child's id; the child is still recognizably the same decision by
96 // its copied hashed payload.
97 corrects: Some(target.id.clone()),
98 },
99 )?;
100 crate::events::append(&store, "correct", Some(&child), None, None);
101 Ok(child)
102}