Skip to main content

khive_vcs/
merge_engine.rs

1// Copyright 2026 khive contributors. Licensed under Apache-2.0.
2//
3//! `MergeEngine` trait — pluggable merge implementation (ADR-042 §3).
4//!
5//! `khive-vcs` ships with `NoOpMergeEngine` which returns `VcsError::MergeNotImplemented`
6//! for all calls. `khive-merge` (v0.5, ADR-043) provides `ThreeWayMergeEngine` which
7//! implements the full algorithm.
8
9use khive_runtime::portability::KgArchive;
10
11use crate::error::VcsError;
12
13/// Strategy passed to `merge_branch`.
14#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum MergeStrategy {
17    /// Compute diffs and auto-merge where safe; report conflicts otherwise.
18    #[default]
19    Auto,
20    /// Prefer `ours` on all conflicts. Always produces a `Clean` result.
21    Ours,
22    /// Prefer `theirs` on all conflicts. Always produces a `Clean` result.
23    Theirs,
24}
25
26/// Output of a three-way merge operation.
27#[derive(Debug)]
28pub enum MergeResult {
29    /// All changes merged without conflict. `merged` is the resulting archive.
30    Clean { merged: KgArchive },
31    /// One or more conflicts detected. No merged archive is produced.
32    /// The caller must resolve conflicts and call again with `force=true`.
33    Conflicts { conflicts: Vec<MergeConflict> },
34}
35
36/// A structured conflict that prevented auto-merge.
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38#[serde(tag = "type", rename_all = "snake_case")]
39pub enum MergeConflict {
40    /// Two different names for the same entity.
41    NameConflict {
42        entity_id: uuid::Uuid,
43        ours: String,
44        theirs: String,
45    },
46    /// Incompatible `kind` values for the same entity.
47    KindConflict {
48        entity_id: uuid::Uuid,
49        ours: String,
50        theirs: String,
51    },
52    /// Same property key set to different values in ours and theirs.
53    PropertyMismatch {
54        entity_id: uuid::Uuid,
55        key: String,
56        ours: serde_json::Value,
57        theirs: serde_json::Value,
58    },
59    /// One branch modified an entity; the other deleted it.
60    ModifyDelete {
61        entity_id: uuid::Uuid,
62        modified_in: BranchSide,
63        deleted_in: BranchSide,
64    },
65    /// One branch modified an edge; the other deleted it.
66    EdgeModifyDelete {
67        source_id: uuid::Uuid,
68        target_id: uuid::Uuid,
69        relation: String,
70        modified_in: BranchSide,
71        deleted_in: BranchSide,
72    },
73    /// An edge in the merged set references a deleted endpoint.
74    DanglingEdge {
75        source_id: uuid::Uuid,
76        target_id: uuid::Uuid,
77        relation: String,
78        missing_endpoint: uuid::Uuid,
79    },
80}
81
82/// Which branch side an attribute comes from in a conflict.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum BranchSide {
86    Ours,
87    Theirs,
88}
89
90/// Pluggable merge engine.
91///
92/// `khive-vcs` calls this trait from `merge_branch`. The default implementation
93/// (`NoOpMergeEngine`) returns `VcsError::MergeNotImplemented`. Register a
94/// `ThreeWayMergeEngine` from `khive-merge` at startup to enable full merge.
95pub trait MergeEngine: Send + Sync {
96    fn merge(
97        &self,
98        base: &KgArchive,
99        ours: &KgArchive,
100        theirs: &KgArchive,
101        strategy: MergeStrategy,
102    ) -> Result<MergeResult, VcsError>;
103}
104
105/// Default no-op engine shipped with `khive-vcs`.
106///
107/// Returns `VcsError::MergeNotImplemented` for all calls.
108/// Replace with `ThreeWayMergeEngine` from `khive-merge` in production.
109pub struct NoOpMergeEngine;
110
111impl MergeEngine for NoOpMergeEngine {
112    fn merge(
113        &self,
114        _base: &KgArchive,
115        _ours: &KgArchive,
116        _theirs: &KgArchive,
117        _strategy: MergeStrategy,
118    ) -> Result<MergeResult, VcsError> {
119        Err(VcsError::MergeNotImplemented)
120    }
121}