asurada 0.3.1

Asurada — a memory + cognition daemon that grows with the user. Local-first, BYOK, shared by Devist/Webchemist Core/etc.
#![allow(dead_code)]

//! Runtime Adapter — intent → 외부 런타임 형식 컴파일러.
//!
//! Asurada 의 핵심 의도(intent)는 외부 런타임(Claude Code) 의 변화로부터
//! 격리되어 보존된다. Adapter 가 그 가교 역할:
//!     intents (저장된 의도) → CompiledArtifacts (현재 형식의 산출물)
//!
//! 외부 런타임 스펙이 바뀌면 Adapter 만 새로 작성하면 된다.

use anyhow::{Context, Result};
use std::path::PathBuf;

use crate::db::intent::{Intent, Strength};
use crate::db::pattern::Pattern;

pub mod claude_code;
pub mod limits;

/// 컴파일 산출물 — 어떤 어댑터든 이 형식으로 반환한다.
#[derive(Debug, Default)]
pub struct CompiledArtifacts {
    /// 텍스트 블록(블록 ID + 본문). CLAUDE.md 등에 managed 영역으로 삽입.
    pub text_blocks: Vec<TextBlock>,
    /// PreToolUse 시점에 평가할 게이트 규칙 (principle intents 에서 도출).
    pub gate_rules: Vec<GateRule>,
    /// 생성될 파일 — 절대 경로 + 본문. agents/skills 등.
    pub files: Vec<GeneratedFile>,
}

#[derive(Debug, Clone)]
pub struct TextBlock {
    /// `<!-- BEGIN ASURADA:<id> --> ... <!-- END ASURADA:<id> -->` 마커용.
    pub id: String,
    /// 어디에 삽입할지의 의도 (예: "global-claude-md", "project-claude-md").
    pub target: BlockTarget,
    pub body: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BlockTarget {
    /// `~/.claude/CLAUDE.md` (전역, 모든 세션 prepend)
    GlobalClaudeMd,
    /// `<project>/CLAUDE.md` (프로젝트 한정)
    ProjectClaudeMd(PathBuf),
}

/// PreToolUse 시점 평가 규칙. hook 이 활성 intent 들의 gate_rules 를
/// 모아 매칭되면 Claude Code 에 ask/deny 결정을 돌려준다.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct GateRule {
    pub intent_id: String,
    /// 매칭할 도구 이름 (Bash, Edit 등). None = 모든 도구.
    pub tool: Option<String>,
    /// 도구 입력에 이 substring 이 있으면 매칭 (단순 + 예측 가능).
    pub contains: Option<String>,
    /// 매칭 시 응답 ("ask" | "deny").
    pub decision: String,
    /// Claude 에 보여줄 사유.
    pub reason: String,
}

#[derive(Debug, Clone)]
pub struct GeneratedFile {
    pub path: PathBuf,
    pub body: String,
}

pub trait RuntimeAdapter {
    /// Intent + 활성 패턴를 받아 현재 런타임 형식으로 컴파일. 디스크에 쓰지 않음.
    fn compile(&self, intents: &[Intent], patterns: &[Pattern]) -> Result<CompiledArtifacts>;

    /// 컴파일 결과를 실제 디스크에 적용. CLAUDE.md 의 managed block 갱신,
    /// gate_rules 를 hook 이 읽을 위치에 저장, 파일 생성 등.
    fn apply(&self, artifacts: &CompiledArtifacts) -> Result<ApplyReport>;

    fn name(&self) -> &'static str;
}

#[derive(Debug, Default)]
pub struct ApplyReport {
    pub files_written: Vec<PathBuf>,
    pub blocks_updated: Vec<String>,
    pub gate_rules_count: usize,
}

/// Managed text block 마커. 사용자가 손댄 영역과 우리가 쓰는 영역을 구분.
pub fn block_begin_marker(id: &str) -> String {
    format!("<!-- BEGIN ASURADA:{} -->", id)
}

pub fn block_end_marker(id: &str) -> String {
    format!("<!-- END ASURADA:{} -->", id)
}

/// 파일에서 BEGIN/END 마커 사이를 새 본문으로 교체. 마커가 없으면 끝에 추가.
/// 다른 사용자 콘텐츠는 보존.
pub fn upsert_managed_block(file_path: &std::path::Path, block: &TextBlock) -> Result<bool> {
    let begin = block_begin_marker(&block.id);
    let end = block_end_marker(&block.id);

    let existing = if file_path.exists() {
        std::fs::read_to_string(file_path)
            .with_context(|| format!("read {}", file_path.display()))?
    } else {
        String::new()
    };

    let new_block = format!("{}\n{}\n{}", begin, block.body.trim_end(), end);

    let updated = if let (Some(b), Some(e)) = (existing.find(&begin), existing.find(&end)) {
        let e_end = e + end.len();
        let mut out = String::with_capacity(existing.len() + new_block.len());
        out.push_str(&existing[..b]);
        out.push_str(&new_block);
        out.push_str(&existing[e_end..]);
        out
    } else if existing.trim().is_empty() {
        new_block
    } else {
        format!("{}\n\n{}\n", existing.trim_end(), new_block)
    };

    if updated == existing {
        return Ok(false);
    }

    if let Some(parent) = file_path.parent() {
        std::fs::create_dir_all(parent)?;
    }
    let tmp = file_path.with_extension(format!(
        "{}.asurada.tmp",
        file_path
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("md")
    ));
    std::fs::write(&tmp, updated)?;
    std::fs::rename(&tmp, file_path)?;
    Ok(true)
}

/// 본문에서 BEGIN/END 마커 사이를 제거 (uninstall 용).
pub fn remove_managed_block(file_path: &std::path::Path, block_id: &str) -> Result<bool> {
    if !file_path.exists() {
        return Ok(false);
    }
    let begin = block_begin_marker(block_id);
    let end = block_end_marker(block_id);
    let existing = std::fs::read_to_string(file_path)?;
    let (Some(b), Some(e)) = (existing.find(&begin), existing.find(&end)) else {
        return Ok(false);
    };
    let e_end = e + end.len();
    let mut out = String::with_capacity(existing.len());
    out.push_str(existing[..b].trim_end_matches('\n'));
    out.push_str(existing[e_end..].trim_start_matches('\n'));
    std::fs::write(file_path, out)?;
    Ok(true)
}

/// strength → 사람이 읽을 한국어 라벨.
pub fn strength_label(s: Strength) -> &'static str {
    match s {
        Strength::Preference => "선호",
        Strength::Principle => "원칙",
        Strength::Context => "맥락",
    }
}