Skip to main content

kdo_core/
lib.rs

1//! Core types and error definitions for the kdo workspace manager.
2//!
3//! This crate provides the foundational data structures used across all kdo crates:
4//! [`Project`], [`Dependency`], [`Language`], and the unified [`KdoError`] type.
5
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9/// Programming language / framework detected for a project.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
11#[serde(rename_all = "lowercase")]
12pub enum Language {
13    /// Pure Rust (Cargo)
14    Rust,
15    /// TypeScript (package.json with TS)
16    TypeScript,
17    /// JavaScript (package.json)
18    JavaScript,
19    /// Python (pyproject.toml / setup.py)
20    Python,
21    /// Rust + Anchor framework (Solana)
22    Anchor,
23    /// Go (go.mod)
24    Go,
25}
26
27impl std::fmt::Display for Language {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match self {
30            Self::Rust => write!(f, "rust"),
31            Self::TypeScript => write!(f, "typescript"),
32            Self::JavaScript => write!(f, "javascript"),
33            Self::Python => write!(f, "python"),
34            Self::Anchor => write!(f, "anchor"),
35            Self::Go => write!(f, "go"),
36        }
37    }
38}
39
40/// A discovered project within the workspace.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct Project {
43    /// Human-readable project name (from manifest).
44    pub name: String,
45    /// Root directory of the project, relative to workspace root.
46    pub path: PathBuf,
47    /// Detected language / framework.
48    pub language: Language,
49    /// Path to the primary manifest file (Cargo.toml, package.json, etc.).
50    pub manifest_path: PathBuf,
51    /// One-line summary extracted from manifest or CONTEXT.md.
52    pub context_summary: Option<String>,
53    /// Files that constitute the public API surface.
54    pub public_api_files: Vec<PathBuf>,
55    /// Internal implementation files.
56    pub internal_files: Vec<PathBuf>,
57    /// Blake3 content hash of all project files (deterministic).
58    pub content_hash: [u8; 32],
59}
60
61/// Dependency relationship kind.
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "lowercase")]
64pub enum DepKind {
65    /// Normal source dependency.
66    Source,
67    /// Build-time dependency (build.rs, scripts).
68    Build,
69    /// Development / test dependency.
70    Dev,
71    /// Solana Cross-Program Invocation dependency.
72    Cpi,
73}
74
75impl std::fmt::Display for DepKind {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match self {
78            Self::Source => write!(f, "source"),
79            Self::Build => write!(f, "build"),
80            Self::Dev => write!(f, "dev"),
81            Self::Cpi => write!(f, "cpi"),
82        }
83    }
84}
85
86/// A single dependency edge between projects.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Dependency {
89    /// Dependency name (as declared in manifest).
90    pub name: String,
91    /// Version requirement string (e.g., "^1.0", "workspace:*").
92    pub version_req: String,
93    /// Kind of dependency.
94    pub kind: DepKind,
95    /// Whether this dependency uses workspace inheritance.
96    pub is_workspace: bool,
97    /// Resolved path to the dependency within the workspace (if local).
98    pub resolved_path: Option<PathBuf>,
99}
100
101/// Unified error type for all kdo operations.
102#[derive(Debug, thiserror::Error, miette::Diagnostic)]
103pub enum KdoError {
104    /// Workspace manifest not found at the expected path.
105    #[error("workspace manifest not found at {0}")]
106    ManifestNotFound(PathBuf),
107
108    /// Failed to parse a manifest or source file.
109    #[error("failed to parse {path}: {source}")]
110    ParseError {
111        /// Path to the file that failed to parse.
112        path: PathBuf,
113        /// Underlying parse error.
114        source: anyhow::Error,
115    },
116
117    /// Referenced project does not exist in the workspace.
118    #[error("project not found: {0}")]
119    ProjectNotFound(String),
120
121    /// Circular dependency detected in the workspace graph.
122    #[error("circular dependency detected: {0}")]
123    #[diagnostic(help("break the cycle by extracting shared code into a separate crate"))]
124    CircularDependency(String),
125
126    /// I/O error.
127    #[error(transparent)]
128    Io(#[from] std::io::Error),
129}
130
131/// Workspace configuration parsed from `kdo.toml`.
132#[derive(Debug, Clone, Serialize, Deserialize, Default)]
133pub struct WorkspaceConfig {
134    /// Workspace metadata.
135    pub workspace: WorkspaceMeta,
136    /// Named tasks that can be run via `kdo run <name>`.
137    #[serde(default)]
138    pub tasks: std::collections::BTreeMap<String, String>,
139}
140
141/// Workspace metadata section of `kdo.toml`.
142#[derive(Debug, Clone, Serialize, Deserialize, Default)]
143pub struct WorkspaceMeta {
144    /// Workspace name.
145    #[serde(default)]
146    pub name: String,
147}
148
149impl WorkspaceConfig {
150    /// Load workspace config from a `kdo.toml` file.
151    pub fn load(path: &std::path::Path) -> Result<Self, KdoError> {
152        let content = std::fs::read_to_string(path)?;
153        toml::from_str(&content).map_err(|e| KdoError::ParseError {
154            path: path.to_path_buf(),
155            source: e.into(),
156        })
157    }
158
159    /// Write workspace config to a `kdo.toml` file.
160    pub fn save(&self, path: &std::path::Path) -> Result<(), KdoError> {
161        let content = toml::to_string_pretty(self).map_err(|e| KdoError::ParseError {
162            path: path.to_path_buf(),
163            source: e.into(),
164        })?;
165        std::fs::write(path, content)?;
166        Ok(())
167    }
168}
169
170/// Rough token estimator: ~4 characters per token for English/code.
171///
172/// # Examples
173///
174/// ```
175/// use kdo_core::estimate_tokens;
176/// assert_eq!(estimate_tokens("hello world!"), 3); // 12 chars / 4
177/// ```
178pub fn estimate_tokens(s: &str) -> usize {
179    s.len() / 4
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    #[test]
187    fn test_estimate_tokens() {
188        assert_eq!(estimate_tokens(""), 0);
189        assert_eq!(estimate_tokens("abcd"), 1);
190        assert_eq!(estimate_tokens("ab"), 0);
191        assert_eq!(estimate_tokens("hello world!"), 3);
192    }
193
194    #[test]
195    fn test_language_display() {
196        assert_eq!(Language::Rust.to_string(), "rust");
197        assert_eq!(Language::Anchor.to_string(), "anchor");
198    }
199
200    #[test]
201    fn test_language_serde_roundtrip() {
202        let lang = Language::TypeScript;
203        let json = serde_json::to_string(&lang).unwrap();
204        assert_eq!(json, "\"typescript\"");
205        let back: Language = serde_json::from_str(&json).unwrap();
206        assert_eq!(back, lang);
207    }
208
209    #[test]
210    fn test_dep_kind_display() {
211        assert_eq!(DepKind::Cpi.to_string(), "cpi");
212        assert_eq!(DepKind::Source.to_string(), "source");
213    }
214}