second-brain-sync 0.5.1

Bidirectional sync for second-brain: SSH transport, JSONL change log, conflict resolution
Documentation
use std::io::Write;

use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

use second_brain_core::kuzu_store::KuzuStore;
use second_brain_core::schema::{SyncNodeType, SyncOp};

#[derive(Debug, Serialize, Deserialize)]
pub struct SyncRecord {
    pub local_seq: u64,
    pub origin_machine_id: String,
    pub origin_seq: u64,
    pub op: SyncOp,
    pub node_type: SyncNodeType,
    pub node_id: String,
    pub timestamp: DateTime<Utc>,
    pub data: Option<serde_json::Value>,
}

pub fn export_changes<W: Write>(store: &KuzuStore, after_seq: u64, writer: &mut W) -> Result<u64> {
    let entries = store.sync_log_since(after_seq)?;
    let mut max_seq = after_seq;

    let mut records: Vec<SyncRecord> = entries
        .into_iter()
        .map(|e| {
            let data = e.data.as_ref().and_then(|d| serde_json::from_str(d).ok());
            SyncRecord {
                local_seq: e.local_seq as u64,
                origin_machine_id: e.origin_machine_id,
                origin_seq: e.origin_seq as u64,
                op: e.op,
                node_type: e.node_type,
                node_id: e.node_id,
                timestamp: e.timestamp,
                data,
            }
        })
        .collect();

    sort_for_import(&mut records);

    for record in &records {
        serde_json::to_writer(&mut *writer, record)?;
        writer.write_all(b"\n")?;
        if record.local_seq > max_seq {
            max_seq = record.local_seq;
        }
    }

    Ok(max_seq)
}

pub fn export_to_stdout(store: &KuzuStore, after_seq: u64) -> Result<u64> {
    let stdout = std::io::stdout();
    let mut handle = stdout.lock();
    export_changes(store, after_seq, &mut handle)
}

pub fn sort_for_import(records: &mut [SyncRecord]) {
    records.sort_by(|a, b| {
        let type_order = |r: &SyncRecord| -> u8 {
            match (&r.op, &r.node_type) {
                (SyncOp::Delete, SyncNodeType::Relation) => 0,
                (SyncOp::Delete, SyncNodeType::Memory) => 1,
                (SyncOp::Delete, SyncNodeType::Entity) => 2,
                (SyncOp::Delete, SyncNodeType::Conversation) => 3,
                (SyncOp::Create, SyncNodeType::Conversation) => 4,
                (SyncOp::Create, SyncNodeType::Entity) => 5,
                (SyncOp::Create, SyncNodeType::Memory) => 6,
                (SyncOp::Create, SyncNodeType::Relation) => 7,
                (SyncOp::Update, SyncNodeType::Conversation) => 8,
                (SyncOp::Update, SyncNodeType::Entity) => 9,
                (SyncOp::Update, SyncNodeType::Memory) => 10,
                (SyncOp::Update, SyncNodeType::Relation) => 11,
            }
        };
        type_order(a)
            .cmp(&type_order(b))
            .then(a.local_seq.cmp(&b.local_seq))
    });
}