git-cognitive 0.2.0

Cognitive debt detection and management for Git repositories — audit, endorse, and surface AI-attributed risk
use anyhow::{Context, Result};
use rusqlite::{params, Connection};
use std::path::PathBuf;

pub struct Database {
    conn: Connection,
}

impl Database {
    pub fn init() -> Result<Self> {
        let db_path = PathBuf::from(".git/cognitive.db");
        let conn = Connection::open(&db_path).context("Failed to open database connection")?;

        conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS activity_items (
                id TEXT PRIMARY KEY,
                branch TEXT NOT NULL,
                classification TEXT NOT NULL,
                title TEXT NOT NULL,
                summary TEXT NOT NULL,
                commits_json TEXT NOT NULL,
                since_sha TEXT NOT NULL,
                until_sha TEXT NOT NULL,
                cognitive_friction_score REAL NOT NULL,
                ai_attributed INTEGER NOT NULL,
                attribution_pct REAL,
                zombie INTEGER NOT NULL DEFAULT 0,
                endorsement_status TEXT NOT NULL DEFAULT 'unendorsed',
                audited_at TEXT NOT NULL
            );
            CREATE TABLE IF NOT EXISTS endorsements (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                sha TEXT NOT NULL,
                status TEXT NOT NULL,
                author TEXT NOT NULL,
                timestamp TEXT NOT NULL
            );",
        )
        .context("Failed to create tables")?;

        Ok(Database { conn })
    }

    pub fn upsert_activity_item(&self, item: &crate::cognitive_debt::ActivityItem) -> Result<()> {
        let commits_json =
            serde_json::to_string(&item.commits).context("Failed to serialize commits")?;

        self.conn
            .execute(
                "INSERT INTO activity_items
                (id, branch, classification, title, summary, commits_json,
                 since_sha, until_sha, cognitive_friction_score, ai_attributed,
                 attribution_pct, zombie, endorsement_status, audited_at)
             VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14)
             ON CONFLICT(id) DO UPDATE SET
                classification=excluded.classification,
                title=excluded.title,
                summary=excluded.summary,
                commits_json=excluded.commits_json,
                since_sha=excluded.since_sha,
                until_sha=excluded.until_sha,
                cognitive_friction_score=excluded.cognitive_friction_score,
                ai_attributed=excluded.ai_attributed,
                attribution_pct=excluded.attribution_pct,
                zombie=excluded.zombie,
                endorsement_status=excluded.endorsement_status,
                audited_at=excluded.audited_at",
                params![
                    &item.id,
                    &item.branch,
                    item.classification.to_string(),
                    &item.title,
                    &item.summary,
                    &commits_json,
                    &item.since_sha,
                    &item.until_sha,
                    item.cognitive_friction_score,
                    item.ai_attributed as i64,
                    item.attribution_pct,
                    item.zombie as i64,
                    item.endorsement_status.to_string(),
                    &item.audited_at,
                ],
            )
            .context("Failed to upsert activity item")?;

        Ok(())
    }

    pub fn insert_endorsement(
        &self,
        record: &crate::cognitive_debt::EndorsementRecord,
    ) -> Result<()> {
        self.conn
            .execute(
                "INSERT INTO endorsements (sha, status, author, timestamp)
             VALUES (?1, ?2, ?3, ?4)",
                params![
                    &record.sha,
                    record.status.to_string(),
                    &record.author,
                    &record.timestamp,
                ],
            )
            .context("Failed to insert endorsement")?;

        self.conn
            .execute(
                "UPDATE activity_items SET endorsement_status = ?1 WHERE id = ?2",
                params![record.status.to_string(), &record.sha],
            )
            .context("Failed to update endorsement status on activity item")?;

        Ok(())
    }

    pub fn all_activity_items(&self) -> Result<Vec<crate::cognitive_debt::ActivityItem>> {
        use crate::cognitive_debt::{ActivityItem, Classification, EndorsementStatus};

        let mut stmt = self.conn.prepare(
            "SELECT id, branch, classification, title, summary, commits_json,
                    since_sha, until_sha, cognitive_friction_score, ai_attributed,
                    attribution_pct, zombie, endorsement_status, audited_at
             FROM activity_items ORDER BY audited_at DESC",
        )?;

        let items = stmt
            .query_map([], |row| {
                Ok((
                    row.get::<_, String>(0)?,
                    row.get::<_, String>(1)?,
                    row.get::<_, String>(2)?,
                    row.get::<_, String>(3)?,
                    row.get::<_, String>(4)?,
                    row.get::<_, String>(5)?,
                    row.get::<_, String>(6)?,
                    row.get::<_, String>(7)?,
                    row.get::<_, f64>(8)?,
                    row.get::<_, i64>(9)?,
                    row.get::<_, Option<f64>>(10)?,
                    row.get::<_, i64>(11)?,
                    row.get::<_, String>(12)?,
                    row.get::<_, String>(13)?,
                ))
            })?
            .map(|row| {
                let (
                    id,
                    branch,
                    classification,
                    title,
                    summary,
                    commits_json,
                    since_sha,
                    until_sha,
                    friction,
                    ai_attributed,
                    attribution_pct,
                    zombie,
                    endorsement_status,
                    audited_at,
                ) = row?;

                let commits: Vec<String> = serde_json::from_str(&commits_json)
                    .map_err(|e| anyhow::anyhow!("Failed to parse commits: {}", e))?;

                let classification = match classification.as_str() {
                    "new_feature" => Classification::NewFeature,
                    "refactor" => Classification::Refactor,
                    "bug_fix" => Classification::BugFix,
                    "minor" => Classification::Minor,
                    "risk" => Classification::Risk,
                    "tech_debt" => Classification::TechDebt,
                    "dependency_update" => Classification::DependencyUpdate,
                    _ => Classification::Other,
                };

                let endorsement_status = match endorsement_status.as_str() {
                    "endorsed" => EndorsementStatus::Endorsed,
                    "excluded" => EndorsementStatus::Excluded,
                    _ => EndorsementStatus::Unendorsed,
                };

                Ok(ActivityItem {
                    id,
                    branch,
                    classification,
                    title,
                    summary,
                    commits,
                    since_sha,
                    until_sha,
                    cognitive_friction_score: friction as f32,
                    ai_attributed: ai_attributed != 0,
                    attribution_pct: attribution_pct.map(|v| v as f32),
                    zombie: zombie != 0,
                    endorsement_status,
                    audited_at,
                })
            })
            .collect::<Result<Vec<_>>>()?;

        Ok(items)
    }
}