Plan step

This commit is contained in:
2026-09-23 22:42:21 +03:00
parent 1f2dc25e0b
commit 762ac2f14b
15 changed files with 823 additions and 283 deletions
+38 -75
View File
@@ -1,65 +1,40 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional, Literal, Any
from typing import Optional, Literal, Any, Generic
from pydantic import field_validator, BaseModel, Field
import pandas as pd
from pydantic import field_validator, BaseModel, Field, ConfigDict, field_serializer
from contracts.CodeLanguages import Language
from contracts.GraphAnalyzer import AnalyzeResult, DependencyGraph
@dataclass(frozen=True)
@dataclass
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",
)
"""Result object carrying all info a coding agent needs."""
remote_url: str
branch: str
base_branch: str
head_commit: str
is_new_clone: bool
is_new_branch: bool
repo_dir: Path = Field(..., description="Absolute path to the cloned repository")
original_branch: Optional[str] = None
uncommitted_changes: Optional[str] = None # git diff (tracked)
untracked_files: list[str] = field(default_factory=list)
stash_ref: Optional[str] = None
env: dict = field(default_factory=dict)
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.",
)
@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()
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):
@@ -81,33 +56,21 @@ class AnalysisError(BaseModel):
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
repo: RepoContext
class AnalysisResult(BaseModel):
"""Top-level result — either a graph or an error."""
model_config = ConfigDict(arbitrary_types_allowed=True)
success: bool
graph: Optional[DependencyGraph] = None
error: Optional[AnalysisError] = None
leaf_clusters: Any
res_all: Any
leaf_clusters: Any = None
res_all: AnalyzeResult | None = None
@field_serializer("leaf_clusters")
def _ser_leaf_clusters(self, v: Any, _info):
if isinstance(v, pd.DataFrame):
return v.to_dict(orient="records")
return v # already JSON-safe