devist 0.7.0

Project bootstrap CLI for AI-assisted development. Spin up new projects from templates, manage backends, and keep your codebase comprehensible.
#![allow(dead_code)]
// Deterministic rules layer.
//
// Two layers, merged in order:
//   1. Global    ~/.devist/worker/rules.md
//   2. Project   <monitor_dir>/<project>/.devist/rules.md
//
// Both are plain markdown. They're injected verbatim into the Claude
// prompt so users can shape advice direction without an LLM round-trip.

use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};

use crate::paths;

#[derive(Debug, Default, Clone)]
pub struct Rules {
    pub global: Option<String>,
    pub project: Option<String>,
}

impl Rules {
    pub fn load(monitor_dir: &Path, project: &str) -> Self {
        let global = paths::worker_rules_file()
            .ok()
            .and_then(|p| read_nonempty(&p));
        let project_rules = read_nonempty(&project_rules_path(monitor_dir, project));
        Self {
            global,
            project: project_rules,
        }
    }

    /// Concatenate global + project sections into a single string suitable
    /// for embedding in a prompt. Returns empty string if neither exists.
    pub fn render(&self) -> String {
        let mut out = String::new();
        if let Some(g) = &self.global {
            out.push_str("# Global rules\n\n");
            out.push_str(g.trim());
            out.push_str("\n\n");
        }
        if let Some(p) = &self.project {
            out.push_str("# Project rules\n\n");
            out.push_str(p.trim());
            out.push('\n');
        }
        out.trim_end().to_string()
    }

    pub fn is_empty(&self) -> bool {
        self.global.is_none() && self.project.is_none()
    }
}

pub fn project_rules_path(monitor_dir: &Path, project: &str) -> PathBuf {
    monitor_dir.join(project).join(".devist").join("rules.md")
}

pub fn ensure_global_template() -> Result<PathBuf> {
    let path = paths::worker_rules_file()?;
    if path.exists() {
        return Ok(path);
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&path, DEFAULT_GLOBAL_TEMPLATE)?;
    Ok(path)
}

pub fn ensure_project_template(monitor_dir: &Path, project: &str) -> Result<PathBuf> {
    let path = project_rules_path(monitor_dir, project);
    if path.exists() {
        return Ok(path);
    }
    if let Some(parent) = path.parent() {
        fs::create_dir_all(parent)?;
    }
    fs::write(&path, DEFAULT_PROJECT_TEMPLATE)?;
    Ok(path)
}

fn read_nonempty(path: &Path) -> Option<String> {
    let s = fs::read_to_string(path).ok()?;
    if s.trim().is_empty() {
        None
    } else {
        Some(s)
    }
}

const DEFAULT_GLOBAL_TEMPLATE: &str = "# devist worker — global rules

These rules apply to every project the worker observes.
Edit freely; they're injected verbatim into the advice prompt.

## Tone
- Respond in Korean.
- Be concise. No filler.

## Focus
- Flag missing tests, security issues, dependency mismatches.
- Skip nitpicks (formatting, micro-style).

## Memory
- Only persist DURABLE facts to mem0 (preferences, conventions).
- Skip transient changes.
";

const DEFAULT_PROJECT_TEMPLATE: &str = "# devist worker — project rules

Project-specific overrides. Loaded after global rules.

## Domain
- (Add domain context here, e.g. \"This is a fintech app; PII handling matters.\")

## Conventions
- (Add stack-specific conventions here.)
";