from dataclasses import dataclass from enum import Enum from pathlib import Path from typing import Optional, Literal, Any from pydantic import field_validator, BaseModel, Field @dataclass(frozen=True) class RepoContext: url: str default_branch: str language_hint: str | None # "python" | "typescript" | "go" | None class Language(str, Enum): PYTHON = "python" JAVASCRIPT = "javascript" TYPESCRIPT = "typescript" JAVA = "java" GO = "go" RUST = "rust" CPP = "cpp" C = "c" LanguageLiteral = Literal[ Language.PYTHON, Language.JAVASCRIPT, Language.TYPESCRIPT, Language.JAVA, Language.GO, Language.RUST, Language.CPP, Language.C, ] class DependencyNode(BaseModel): """A single node in the dependency graph.""" id: str = Field(..., description="Unique identifier (file path or module name)") label: str = Field(..., description="Human-readable label") language: Language is_external: bool = Field( default=False, description="True if the dependency is outside the repository", ) class DependencyEdge(BaseModel): """A directed edge from source → target (source depends on target).""" source: str = Field(..., description="Node id of the dependant") target: str = Field(..., description="Node id of the dependency") kind: str = Field( default="import", description="Edge kind: import, include, inherit, call, etc.", ) class DependencyGraph(BaseModel): """Complete dependency graph result.""" root: str = Field(..., description="Entrypoint node id") nodes: list[DependencyNode] edges: list[DependencyEdge] metadata: dict[str, str] = Field(default_factory=dict) class ToolRequirement(BaseModel): """Describes a required external tool and how to install it.""" tool_name: str command: str = Field(..., description="CLI command that must be on PATH") install_guide: str = Field(..., description="Markdown installation instructions") docs_url: Optional[str] = None class AnalysisError(BaseModel): """Structured error returned when analysis cannot proceed.""" error_code: str message: str missing_tools: list[ToolRequirement] = Field(default_factory=list) language: Optional[Language] = None entrypoint: Optional[str] = None class AnalysisInput(BaseModel): """Input schema for the analyzer.""" repo_path: Path = Field(..., description="Absolute path to the cloned repository") language: Language entrypoint: str = Field( ..., description="Relative path from repo_path to the entrypoint file", ) @field_validator("repo_path") @classmethod def repo_must_exist(cls, v: Path) -> Path: if not v.is_dir(): raise ValueError(f"Repository path does not exist or is not a directory: {v}") return v.resolve() @field_validator("entrypoint") @classmethod def entrypoint_must_exist(cls, v: str, info) -> str: repo = info.data.get("repo_path") if repo and not (repo / v).is_file(): raise ValueError(f"Entrypoint not found: {repo / v}") return v class AnalysisResult(BaseModel): """Top-level result — either a graph or an error.""" success: bool graph: Optional[DependencyGraph] = None error: Optional[AnalysisError] = None leaf_clusters: Any res_all: Any