use uuid::Uuid;
use khive_storage::SqlStatement;
#[derive(Debug, Clone)]
pub struct PlanStatement {
pub statement: SqlStatement,
pub guard: Option<AffectedRowGuard>,
}
#[derive(Debug, Clone)]
pub struct PlanPredicate {
pub description: String,
pub statement: SqlStatement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AffectedRowGuard {
pub expected_min: u64,
pub expected_max: Option<u64>,
}
impl AffectedRowGuard {
pub fn exactly(n: u64) -> Self {
Self {
expected_min: n,
expected_max: Some(n),
}
}
pub fn at_least_one() -> Self {
Self {
expected_min: 1,
expected_max: None,
}
}
pub fn holds_for(&self, affected: u64) -> bool {
affected >= self.expected_min
&& self.expected_max.map(|max| affected <= max).unwrap_or(true)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PostCommitEffect {
None,
ReindexEntity { entity_id: Uuid },
ReindexNote { note_id: Uuid },
GtdAudit {
task_id: Uuid,
from_status: String,
to_status: String,
note: Option<String>,
namespace: String,
},
NoteDeleted { note_id: Uuid, kind: String },
}
#[derive(Debug, Clone)]
pub struct EdgeNaturalKey {
pub namespace: String,
pub canon_source_id: Uuid,
pub canon_target_id: Uuid,
pub relation: khive_storage::EdgeRelation,
}
#[derive(Debug, Clone)]
pub struct UpdatePlan {
pub target_id: Uuid,
pub statements: Vec<PlanStatement>,
pub post_commit: PostCommitEffect,
pub edge_natural_key: Option<EdgeNaturalKey>,
}
#[derive(Debug, Clone)]
pub struct DeletePlan {
pub target_id: Uuid,
pub statements: Vec<PlanStatement>,
pub post_commit: PostCommitEffect,
}
#[derive(Debug, Clone)]
pub struct LinkPlan {
pub source_id: Uuid,
pub target_id: Uuid,
pub statement: PlanStatement,
}
#[derive(Debug, Clone)]
pub struct MergePlan {
pub into_id: Uuid,
pub from_id: Uuid,
pub rewires: Vec<PlanPredicate>,
pub lifecycle: Vec<PlanStatement>,
}
#[derive(Debug, Clone)]
pub struct GtdTransitionPlan {
pub task_id: Uuid,
pub statements: Vec<PlanStatement>,
pub post_commit: PostCommitEffect,
}
#[derive(Debug, Clone)]
pub struct GtdCompletePlan {
pub task_id: Uuid,
pub statements: Vec<PlanStatement>,
pub post_commit: PostCommitEffect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GovernanceOp {
Propose,
Review,
Withdraw,
}
#[derive(Debug, Clone)]
pub struct GovernancePlan {
pub op: GovernanceOp,
pub proposal_id: Uuid,
pub statements: Vec<PlanStatement>,
}
#[cfg(test)]
mod tests {
use super::*;
fn stmt(label: &str) -> SqlStatement {
SqlStatement {
sql: "UPDATE t SET x = ? WHERE id = ?".to_string(),
params: vec![],
label: Some(label.to_string()),
}
}
fn guarded(label: &str, guard: AffectedRowGuard) -> PlanStatement {
PlanStatement {
statement: stmt(label),
guard: Some(guard),
}
}
fn unguarded(label: &str) -> PlanStatement {
PlanStatement {
statement: stmt(label),
guard: None,
}
}
#[test]
fn affected_row_guard_exactly_holds_only_for_n() {
let g = AffectedRowGuard::exactly(1);
assert!(!g.holds_for(0));
assert!(g.holds_for(1));
assert!(!g.holds_for(2));
}
#[test]
fn affected_row_guard_at_least_one_has_no_upper_bound() {
let g = AffectedRowGuard::at_least_one();
assert!(!g.holds_for(0));
assert!(g.holds_for(1));
assert!(g.holds_for(1_000));
}
#[test]
fn update_plan_guard_is_anchored_to_the_row_statement_not_the_fts_mirror() {
let id = Uuid::new_v4();
let plan = UpdatePlan {
target_id: id,
statements: vec![
guarded("update-row", AffectedRowGuard::exactly(1)),
unguarded("update-fts-mirror"),
],
post_commit: PostCommitEffect::ReindexEntity { entity_id: id },
edge_natural_key: None,
};
assert_eq!(plan.target_id, id);
assert_eq!(plan.statements[0].guard, Some(AffectedRowGuard::exactly(1)));
assert_eq!(plan.statements[1].guard, None);
assert_eq!(
plan.post_commit,
PostCommitEffect::ReindexEntity { entity_id: id }
);
}
#[test]
fn delete_plan_guard_is_anchored_to_the_target_row_not_the_cascade() {
let plan = DeletePlan {
target_id: Uuid::new_v4(),
post_commit: PostCommitEffect::None,
statements: vec![
guarded("delete-row", AffectedRowGuard::exactly(1)),
unguarded("cascade-edges"),
],
};
let row_guard = plan.statements[0].guard.expect("row delete is guarded");
assert!(row_guard.holds_for(1));
assert!(!row_guard.holds_for(0));
assert_eq!(plan.statements[1].guard, None);
}
#[test]
fn link_plan_guard_is_the_endpoint_existence_probe_itself() {
let source = Uuid::new_v4();
let target = Uuid::new_v4();
let plan = LinkPlan {
source_id: source,
target_id: target,
statement: guarded("insert-edge-where-exists", AffectedRowGuard::exactly(1)),
};
assert_eq!(plan.source_id, source);
assert_eq!(plan.target_id, target);
let guard = plan.statement.guard.expect("link insert is guarded");
assert!(!guard.holds_for(0));
}
#[test]
fn merge_plan_rewires_are_never_guarded_lifecycle_writes_always_are() {
let into = Uuid::new_v4();
let from = Uuid::new_v4();
let rewire = PlanPredicate {
description: "source_id = :from".to_string(),
statement: SqlStatement {
sql: "UPDATE graph_edges SET source_id = ? WHERE source_id = ?".to_string(),
params: vec![],
label: Some("merge-rewire".to_string()),
},
};
let plan = MergePlan {
into_id: into,
from_id: from,
rewires: vec![rewire],
lifecycle: vec![guarded(
"tombstone-from-entity",
AffectedRowGuard::exactly(1),
)],
};
assert_eq!(plan.into_id, into);
assert_eq!(plan.from_id, from);
assert_eq!(plan.rewires[0].description, "source_id = :from");
let lifecycle_guard = plan.lifecycle[0].guard.expect("lifecycle write is guarded");
assert!(!lifecycle_guard.holds_for(0));
}
#[test]
fn gtd_transition_plan_triggers_no_reindex_by_construction() {
let plan = GtdTransitionPlan {
task_id: Uuid::new_v4(),
statements: vec![guarded("update-status", AffectedRowGuard::exactly(1))],
post_commit: PostCommitEffect::None,
};
assert_eq!(plan.statements.len(), 1);
assert!(plan.statements[0].guard.is_some());
assert_eq!(plan.post_commit, PostCommitEffect::None);
}
#[test]
fn gtd_transition_plan_idempotent_noop_carries_no_statements_and_no_audit() {
let plan = GtdTransitionPlan {
task_id: Uuid::new_v4(),
statements: vec![],
post_commit: PostCommitEffect::None,
};
assert!(plan.statements.is_empty());
assert_eq!(plan.post_commit, PostCommitEffect::None);
}
#[test]
fn gtd_transition_plan_carries_gtd_audit_post_commit_effect() {
let task_id = Uuid::new_v4();
let plan = GtdTransitionPlan {
task_id,
statements: vec![guarded("update-status", AffectedRowGuard::exactly(1))],
post_commit: PostCommitEffect::GtdAudit {
task_id,
from_status: "inbox".to_string(),
to_status: "next".to_string(),
note: Some("handed off".to_string()),
namespace: "local".to_string(),
},
};
assert_eq!(
plan.post_commit,
PostCommitEffect::GtdAudit {
task_id,
from_status: "inbox".to_string(),
to_status: "next".to_string(),
note: Some("handed off".to_string()),
namespace: "local".to_string(),
}
);
}
#[test]
fn gtd_complete_plan_guard_is_anchored_to_the_status_write() {
let plan = GtdCompletePlan {
task_id: Uuid::new_v4(),
statements: vec![
guarded("update-status", AffectedRowGuard::exactly(1)),
unguarded("update-completed-at"),
],
post_commit: PostCommitEffect::None,
};
assert_eq!(plan.statements.len(), 2);
let guard = plan.statements[0].guard.expect("status write is guarded");
assert!(guard.holds_for(1));
assert_eq!(plan.statements[1].guard, None);
}
#[test]
fn governance_plan_covers_all_three_lifecycle_ops() {
for op in [
GovernanceOp::Propose,
GovernanceOp::Review,
GovernanceOp::Withdraw,
] {
let plan = GovernancePlan {
op,
proposal_id: Uuid::new_v4(),
statements: vec![guarded("governance-event", AffectedRowGuard::exactly(1))],
};
assert_eq!(plan.op, op);
assert!(plan.statements[0].guard.is_some());
}
}
#[test]
fn plans_are_plain_data_no_async_no_embedding() {
let _ = PostCommitEffect::None;
}
}