clawft_kernel/context_graft_state.rs
1//! Causal-model node lifecycle ([`NodeState`]) and cross-substrate mirroring
2//! for the session graft layer (ADR-058 / ADR-062 P0.2).
3//!
4//! Split out of `context_graft.rs` to keep that file under the 500-line cap.
5
6use super::SessionView;
7
8/// Node lifecycle state on the causal model (ADR-058 / ADR-056).
9///
10/// A grafted branch that coheres **collapses (commits)** to the trunk; one that
11/// does not is held off `main_line` and pruned — the same branch/merge/prune
12/// semantics as git ("causal collapse modeled on the git tree").
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum NodeState {
15 /// A candidate branch not yet grafted into the working set.
16 Speculative,
17 /// Grafted and live in the working window this run.
18 Frontier,
19 /// Promoted (collapsed) onto the durable trunk (L3).
20 Committed,
21 /// Aged out of the live window; origin retained on the chain, re-graftable.
22 Stale,
23 /// Contradicted/revised; dropped off `main_line`.
24 Pruned,
25}
26
27impl NodeState {
28 /// Short stable tag for metadata/serialization.
29 pub fn as_str(self) -> &'static str {
30 match self {
31 NodeState::Speculative => "speculative",
32 NodeState::Frontier => "frontier",
33 NodeState::Committed => "committed",
34 NodeState::Stale => "stale",
35 NodeState::Pruned => "pruned",
36 }
37 }
38
39 /// Whether `self → next` is a legal per-turn lifecycle transition
40 /// (ADR-062 D2). The spine is `Speculative → Frontier → Committed`;
41 /// soft/hard rebases drop to `Stale`/`Pruned`. `Pruned` is terminal.
42 ///
43 /// `Committed → Stale` models aging a committed node out of the live window
44 /// (it stays re-graftable on the chain); `Stale → Frontier` is the re-graft.
45 pub fn can_transition_to(self, next: NodeState) -> bool {
46 use NodeState::*;
47 matches!(
48 (self, next),
49 (Speculative, Frontier)
50 | (Speculative, Stale)
51 | (Speculative, Pruned)
52 | (Frontier, Committed)
53 | (Frontier, Stale)
54 | (Frontier, Pruned)
55 | (Committed, Stale)
56 | (Stale, Frontier)
57 | (Stale, Pruned)
58 )
59 }
60}
61
62/// Mirror a per-turn node-state transition across **both** substrates
63/// (ADR-062 P0.2): the authoritative L2 [`SessionView`] chunk and the durable
64/// [`CausalNode`](crate::causal::CausalNode) `metadata.state`.
65///
66/// The session view is the source of truth and gates the transition
67/// ([`SessionView::transition`]); only if that legal step lands is the state
68/// mirrored onto the causal node. Returns `true` only when **both** sides were
69/// updated; if the causal node is unknown the view has still advanced (the
70/// boolean reports the mirror was incomplete, so callers can re-sync).
71pub fn mirror_state(
72 view: &SessionView,
73 chain_seq: u64,
74 graph: &crate::causal::CausalGraph,
75 causal_node_id: crate::causal::NodeId,
76 next: NodeState,
77) -> bool {
78 if !view.transition(chain_seq, next) {
79 return false;
80 }
81 graph.set_node_state(causal_node_id, next.as_str())
82}