ccd-cli 1.0.0-alpha.2

Bootstrap and validate Continuous Context Development repositories
use anyhow::Result;
use rusqlite::Connection;

use crate::state::runtime::{RuntimeHandoffItem, RuntimeHandoffState};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ExclusiveWriteResult {
    Applied { revision: u64 },
    RevisionConflict { current_revision: u64 },
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StoredHandoffState {
    pub(crate) state: RuntimeHandoffState,
    pub(crate) revision: u64,
}

pub(crate) fn read(conn: &Connection) -> Result<Option<RuntimeHandoffState>> {
    Ok(read_record(conn)?.map(|record| record.state))
}

pub(crate) fn read_record(conn: &Connection) -> Result<Option<StoredHandoffState>> {
    let mut stmt = conn.prepare(
        "SELECT title, immediate_actions, completed_state,
                operational_guardrails, key_files, definition_of_done, revision
         FROM handoff WHERE id = 1",
    )?;

    let result = stmt.query_row([], |row| {
        Ok(RawHandoff {
            title: row.get(0)?,
            immediate_actions: row.get(1)?,
            completed_state: row.get(2)?,
            operational_guardrails: row.get(3)?,
            key_files: row.get(4)?,
            definition_of_done: row.get(5)?,
            revision: row.get(6)?,
        })
    });

    match result {
        Ok(raw) => Ok(Some(raw.into_state()?)),
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
        Err(e) => Err(e.into()),
    }
}

pub(crate) fn current_revision(conn: &Connection) -> Result<u64> {
    let result = conn.query_row("SELECT revision FROM handoff WHERE id = 1", [], |row| {
        row.get(0)
    });
    match result {
        Ok(revision) => Ok(revision),
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(0),
        Err(error) => Err(error.into()),
    }
}

pub(crate) fn write(conn: &Connection, state: &RuntimeHandoffState) -> Result<u64> {
    let next_revision = current_revision(conn)?.saturating_add(1).max(1);
    conn.execute(
        "INSERT OR REPLACE INTO handoff
            (id, schema_version, title, immediate_actions, completed_state,
             operational_guardrails, key_files, definition_of_done, revision)
         VALUES (1, 1, ?1, ?2, ?3, ?4, ?5, ?6, ?7)",
        rusqlite::params![
            state.title,
            serde_json::to_string(&state.immediate_actions)?,
            serde_json::to_string(&state.completed_state)?,
            serde_json::to_string(&state.operational_guardrails)?,
            serde_json::to_string(&state.key_files)?,
            serde_json::to_string(&state.definition_of_done)?,
            next_revision,
        ],
    )?;
    Ok(next_revision)
}

pub(crate) fn write_if_revision_matches(
    conn: &Connection,
    state: &RuntimeHandoffState,
    expected_revision: u64,
) -> Result<ExclusiveWriteResult> {
    if expected_revision == 0 {
        let inserted = conn.execute(
            "INSERT OR IGNORE INTO handoff
                (id, schema_version, title, immediate_actions, completed_state,
                 operational_guardrails, key_files, definition_of_done, revision)
             VALUES (1, 1, ?1, ?2, ?3, ?4, ?5, ?6, 1)",
            rusqlite::params![
                state.title,
                serde_json::to_string(&state.immediate_actions)?,
                serde_json::to_string(&state.completed_state)?,
                serde_json::to_string(&state.operational_guardrails)?,
                serde_json::to_string(&state.key_files)?,
                serde_json::to_string(&state.definition_of_done)?,
            ],
        )?;
        return if inserted == 1 {
            Ok(ExclusiveWriteResult::Applied { revision: 1 })
        } else {
            Ok(ExclusiveWriteResult::RevisionConflict {
                current_revision: current_revision(conn)?,
            })
        };
    }

    let updated = conn.execute(
        "UPDATE handoff
         SET title = ?1,
             immediate_actions = ?2,
             completed_state = ?3,
             operational_guardrails = ?4,
             key_files = ?5,
             definition_of_done = ?6,
             revision = revision + 1
         WHERE id = 1 AND revision = ?7",
        rusqlite::params![
            state.title,
            serde_json::to_string(&state.immediate_actions)?,
            serde_json::to_string(&state.completed_state)?,
            serde_json::to_string(&state.operational_guardrails)?,
            serde_json::to_string(&state.key_files)?,
            serde_json::to_string(&state.definition_of_done)?,
            expected_revision,
        ],
    )?;
    if updated == 1 {
        Ok(ExclusiveWriteResult::Applied {
            revision: expected_revision.saturating_add(1).max(1),
        })
    } else {
        Ok(ExclusiveWriteResult::RevisionConflict {
            current_revision: current_revision(conn)?,
        })
    }
}

pub(crate) fn exists(conn: &Connection) -> Result<bool> {
    let count: i64 = conn.query_row("SELECT COUNT(*) FROM handoff WHERE id = 1", [], |row| {
        row.get(0)
    })?;
    Ok(count > 0)
}

struct RawHandoff {
    title: String,
    immediate_actions: String,
    completed_state: String,
    operational_guardrails: String,
    key_files: String,
    definition_of_done: String,
    revision: u64,
}

impl RawHandoff {
    fn into_state(self) -> Result<StoredHandoffState> {
        Ok(StoredHandoffState {
            state: RuntimeHandoffState {
                title: self.title,
                immediate_actions: serde_json::from_str::<Vec<RuntimeHandoffItem>>(
                    &self.immediate_actions,
                )?,
                completed_state: serde_json::from_str(&self.completed_state)?,
                operational_guardrails: serde_json::from_str(&self.operational_guardrails)?,
                key_files: serde_json::from_str(&self.key_files)?,
                definition_of_done: serde_json::from_str(&self.definition_of_done)?,
            },
            revision: self.revision,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::db::schema;
    use crate::state::runtime::RuntimeLifecycle;

    fn test_conn() -> Connection {
        let conn = Connection::open_in_memory().unwrap();
        schema::initialize(&conn).unwrap();
        conn
    }

    #[test]
    fn read_returns_none_when_empty() {
        let conn = test_conn();
        assert!(read(&conn).unwrap().is_none());
        assert!(!exists(&conn).unwrap());
    }

    #[test]
    fn write_and_read_roundtrip() {
        let conn = test_conn();
        let state = RuntimeHandoffState {
            title: "Next: SQLite migration".to_owned(),
            immediate_actions: vec![RuntimeHandoffItem {
                text: "Implement db module.".to_owned(),
                lifecycle: RuntimeLifecycle::Active,
            }],
            completed_state: vec![RuntimeHandoffItem {
                text: "Schema created.".to_owned(),
                lifecycle: RuntimeLifecycle::Active,
            }],
            operational_guardrails: vec![],
            key_files: vec![RuntimeHandoffItem {
                text: "`src/db/mod.rs`".to_owned(),
                lifecycle: RuntimeLifecycle::Active,
            }],
            definition_of_done: vec![],
        };

        let revision = write(&conn, &state).unwrap();
        assert_eq!(revision, 1);
        assert!(exists(&conn).unwrap());

        let loaded = read_record(&conn).unwrap().expect("should exist");
        assert_eq!(loaded.revision, 1);
        let loaded = loaded.state;
        assert_eq!(loaded.title, state.title);
        assert_eq!(loaded.immediate_actions.len(), 1);
        assert_eq!(loaded.immediate_actions[0].text, "Implement db module.");
        assert_eq!(loaded.completed_state.len(), 1);
        assert_eq!(loaded.key_files.len(), 1);
    }

    #[test]
    fn write_overwrites_existing() {
        let conn = test_conn();
        let state1 = RuntimeHandoffState {
            title: "First".to_owned(),
            ..RuntimeHandoffState::default()
        };
        let state2 = RuntimeHandoffState {
            title: "Second".to_owned(),
            ..RuntimeHandoffState::default()
        };

        assert_eq!(write(&conn, &state1).unwrap(), 1);
        assert_eq!(write(&conn, &state2).unwrap(), 2);

        let loaded = read_record(&conn).unwrap().expect("should exist");
        assert_eq!(loaded.revision, 2);
        let loaded = loaded.state;
        assert_eq!(loaded.title, "Second");
    }

    fn delete(conn: &Connection) -> Result<()> {
        conn.execute("DELETE FROM handoff WHERE id = 1", [])?;
        Ok(())
    }

    #[test]
    fn delete_removes_row() {
        let conn = test_conn();
        write(&conn, &RuntimeHandoffState::default()).unwrap();
        assert!(exists(&conn).unwrap());

        delete(&conn).unwrap();
        assert!(!exists(&conn).unwrap());
        assert!(read(&conn).unwrap().is_none());
    }

    #[test]
    fn write_if_revision_matches_detects_conflicts() {
        let conn = test_conn();
        let first = RuntimeHandoffState {
            title: "First".to_owned(),
            ..RuntimeHandoffState::default()
        };
        let second = RuntimeHandoffState {
            title: "Second".to_owned(),
            ..RuntimeHandoffState::default()
        };

        let first_result = write_if_revision_matches(&conn, &first, 0).unwrap();
        assert_eq!(first_result, ExclusiveWriteResult::Applied { revision: 1 });

        let conflict = write_if_revision_matches(&conn, &second, 0).unwrap();
        assert_eq!(
            conflict,
            ExclusiveWriteResult::RevisionConflict {
                current_revision: 1
            }
        );

        let applied = write_if_revision_matches(&conn, &second, 1).unwrap();
        assert_eq!(applied, ExclusiveWriteResult::Applied { revision: 2 });
    }
}