use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chunk {
pub id: String,
pub file_path: String,
pub chunk_type: ChunkType,
pub name: Option<String>,
pub start_line: u32,
pub end_line: u32,
pub content: String,
pub language: String,
#[serde(default)]
pub tags: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChunkType {
Function,
Method,
Class,
Struct,
Enum,
Interface,
Module,
Impl,
Trait,
Doc,
Section,
Table,
CodeBlock,
Commit,
Issue,
Other,
}
impl std::fmt::Display for ChunkType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChunkType::Function => write!(f, "function"),
ChunkType::Method => write!(f, "method"),
ChunkType::Class => write!(f, "class"),
ChunkType::Struct => write!(f, "struct"),
ChunkType::Enum => write!(f, "enum"),
ChunkType::Interface => write!(f, "interface"),
ChunkType::Module => write!(f, "module"),
ChunkType::Impl => write!(f, "impl"),
ChunkType::Trait => write!(f, "trait"),
ChunkType::Doc => write!(f, "doc"),
ChunkType::Section => write!(f, "section"),
ChunkType::Table => write!(f, "table"),
ChunkType::CodeBlock => write!(f, "code_block"),
ChunkType::Commit => write!(f, "commit"),
ChunkType::Issue => write!(f, "issue"),
ChunkType::Other => write!(f, "other"),
}
}
}
impl ChunkType {
pub fn is_code_symbol(self) -> bool {
matches!(
self,
ChunkType::Function
| ChunkType::Method
| ChunkType::Class
| ChunkType::Struct
| ChunkType::Enum
| ChunkType::Interface
| ChunkType::Trait
| ChunkType::Impl
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMetadata {
pub path: String,
pub language: Option<String>,
pub mtime: i64,
pub hash: String,
pub indexed_at: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub chunk: Chunk,
pub score: f32,
#[serde(skip_serializing_if = "Option::is_none")]
pub match_type: Option<MatchType>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indexed_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub repo: Option<String>,
}
pub fn source_kind(chunk_type: &ChunkType) -> &'static str {
match chunk_type {
ChunkType::Issue => "issue",
ChunkType::Commit => "commit",
_ => "code",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MatchType {
Semantic,
Keyword,
Hybrid,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileCoupling {
pub file_a: String,
pub file_b: String,
pub score: f32,
pub co_changes: u32,
pub last_co_change: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CrossRepoCoupling {
pub repo_a: String,
pub path_a: String,
pub repo_b: String,
pub path_b: String,
pub score: f32,
pub co_changes: u32,
pub last_co_change: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawImport {
pub statement: String,
pub path: String,
pub dep_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportEdge {
pub source_file: String,
pub import_specifier: String,
pub resolved_path: Option<String>,
pub language: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportDependency {
pub file_a: String,
pub file_b: String,
pub dep_type: String,
pub import_statement: String,
pub symbol: Option<String>,
pub resolved: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ChunkEdge {
pub source_chunk: String,
pub target_chunk: String,
pub source_name: String,
pub target_name: String,
pub edge_type: ChunkEdgeType,
pub file_path: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ChunkEdgeType {
Implements,
ImplFor,
Tests,
Extends,
NextChunk,
PartOf,
SimilarTo,
}
impl ChunkEdgeType {
pub const ALL: [ChunkEdgeType; 7] = [
ChunkEdgeType::Implements,
ChunkEdgeType::ImplFor,
ChunkEdgeType::Tests,
ChunkEdgeType::Extends,
ChunkEdgeType::NextChunk,
ChunkEdgeType::PartOf,
ChunkEdgeType::SimilarTo,
];
}
impl std::fmt::Display for ChunkEdgeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ChunkEdgeType::Implements => write!(f, "implements"),
ChunkEdgeType::ImplFor => write!(f, "impl_for"),
ChunkEdgeType::Tests => write!(f, "tests"),
ChunkEdgeType::Extends => write!(f, "extends"),
ChunkEdgeType::NextChunk => write!(f, "next_chunk"),
ChunkEdgeType::PartOf => write!(f, "part_of"),
ChunkEdgeType::SimilarTo => write!(f, "similar_to"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Entity {
pub entity_iri: String,
pub text: String,
pub entity_type: String,
pub repo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EntitySearchResult {
pub entity_iri: String,
pub text: String,
pub entity_type: String,
pub repo: Option<String>,
pub score: f32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum FileCategory {
Source,
Test,
Documentation,
Config,
#[serde(untagged)]
Custom(String),
}
impl FileCategory {
pub fn from_name(name: &str) -> Self {
match name.to_lowercase().as_str() {
"source" => FileCategory::Source,
"test" => FileCategory::Test,
"documentation" | "doc" | "docs" => FileCategory::Documentation,
"config" | "configuration" => FileCategory::Config,
_ => FileCategory::Custom(name.to_string()),
}
}
pub fn is_doc_like(&self) -> bool {
matches!(self, FileCategory::Documentation | FileCategory::Config)
}
}
impl std::fmt::Display for FileCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
FileCategory::Source => write!(f, "source"),
FileCategory::Test => write!(f, "test"),
FileCategory::Documentation => write!(f, "documentation"),
FileCategory::Config => write!(f, "config"),
FileCategory::Custom(name) => write!(f, "{}", name),
}
}
}
pub fn classify_file_with_rules(path: &str, rules: &[crate::config::FileTypeRule]) -> FileCategory {
if !rules.is_empty() {
let lower = path.to_lowercase();
for rule in rules {
for pattern in &rule.patterns {
if let Ok(matcher) = glob::Pattern::new(&pattern.to_lowercase()) {
if matcher.matches(&lower) {
return FileCategory::from_name(&rule.name);
}
}
}
}
}
classify_file(path)
}
pub fn classify_file(path: &str) -> FileCategory {
let lower = path.to_lowercase();
let parts: Vec<&str> = lower.split('/').collect();
let filename = parts.last().copied().unwrap_or("");
let doc_names = [
"changelog",
"changelog.md",
"changelog.rst",
"changelog.txt",
"changes",
"changes.md",
"changes.rst",
"breaking_changes.md",
"breaking_changes.rst",
"history.md",
"history.rst",
"readme",
"readme.md",
"readme.rst",
"readme.txt",
"contributing.md",
"contributing.rst",
"license",
"license.md",
"license.txt",
"code_of_conduct.md",
];
if doc_names.contains(&filename) {
return FileCategory::Documentation;
}
let doc_dirs = ["docs", "doc", "changelogs", "documentation"];
for part in &parts[..parts.len().saturating_sub(1)] {
if doc_dirs.contains(part) {
if has_code_extension(filename) {
return FileCategory::Source;
}
return FileCategory::Documentation;
}
}
let doc_extensions = [".md", ".mdx", ".rst", ".txt"];
for ext in &doc_extensions {
if lower.ends_with(ext) {
return FileCategory::Documentation;
}
}
let test_dirs = [
"test",
"tests",
"spec",
"specs",
"__tests__",
"test_fixtures",
"testdata",
];
for part in &parts[..parts.len().saturating_sub(1)] {
if test_dirs.contains(part) {
return FileCategory::Test;
}
}
if filename.starts_with("test_")
|| filename.contains("_test.")
|| filename.contains("_spec.")
|| filename.contains(".test.")
|| filename.contains(".spec.")
|| filename.ends_with("_test.rs")
|| filename.ends_with("_test.py")
|| filename.ends_with("_test.go")
{
return FileCategory::Test;
}
if parts
.iter()
.any(|p| *p == "__snapshots__" || *p == "snapshots")
{
return FileCategory::Test;
}
let config_names = [
"cargo.toml",
"cargo.lock",
"package.json",
"package-lock.json",
"yarn.lock",
"pnpm-lock.yaml",
"makefile",
"justfile",
".gitignore",
".gitattributes",
".editorconfig",
"pyproject.toml",
"setup.py",
"setup.cfg",
"tsconfig.json",
"babel.config.js",
"webpack.config.js",
"dockerfile",
"docker-compose.yml",
"docker-compose.yaml",
".eslintrc.js",
".eslintrc.json",
".prettierrc",
"renovate.json",
"dependabot.yml",
"rustfmt.toml",
"clippy.toml",
".clippy.toml",
];
if config_names.contains(&filename) {
return FileCategory::Config;
}
if parts
.iter()
.any(|p| *p == ".github" || *p == ".circleci" || *p == ".vscode")
{
return FileCategory::Config;
}
let config_extensions = [".yaml", ".yml", ".toml", ".ini", ".cfg"];
if parts.len() <= 2 {
for ext in &config_extensions {
if lower.ends_with(ext) {
return FileCategory::Config;
}
}
}
FileCategory::Source
}
fn has_code_extension(filename: &str) -> bool {
let code_extensions = [
".rs", ".py", ".js", ".ts", ".tsx", ".jsx", ".go", ".java", ".c", ".cpp", ".h", ".hpp",
".cs", ".rb", ".swift", ".kt", ".scala", ".zig", ".hs", ".ml", ".ex", ".exs", ".sh",
".bash", ".zsh", ".fish", ".lua", ".r", ".jl", ".pl", ".php",
];
code_extensions.iter().any(|ext| filename.ends_with(ext))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexStats {
pub total_files: u64,
pub total_chunks: u64,
pub total_embeddings: u64,
pub languages: Vec<LanguageStats>,
pub last_indexed: Option<i64>,
pub index_size_bytes: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LanguageStats {
pub language: String,
pub file_count: u64,
pub chunk_count: u64,
}
#[cfg(test)]
#[path = "types_tests.rs"]
mod tests;