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
+24
View File
@@ -0,0 +1,24 @@
from enum import Enum
from typing import Literal
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,
]
+51
View File
@@ -0,0 +1,51 @@
import pandas as pd
from pydantic import BaseModel, Field, ConfigDict
from contracts.RepoContext import Language
class DependencyNode(BaseModel):
"""A single node in the dependency graph."""
model_config = ConfigDict(frozen=True)
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 AnalyzeResult(BaseModel):
"""Return shape of :func:`analyze`."""
model_config = ConfigDict(arbitrary_types_allowed=True)
df: pd.DataFrame
isolated: list[DependencyNode]
leaf_no_deps: list[DependencyNode]
entry_no_dependents: list[DependencyNode]
widely_used: list[tuple[DependencyNode, int]]
used_by_packages: dict[DependencyNode, int]
wccs: list[set[DependencyNode]]
sccs: list[set[DependencyNode]]
node2comm: dict[DependencyNode, int]
comms: list[set[DependencyNode]]
+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
+16
View File
@@ -0,0 +1,16 @@
from typing import Literal
from pydantic import BaseModel, Field
from contracts.base import _StrictModel
class FileSelection(_StrictModel):
path: str = Field(...)
role: Literal["primary", "context", "test"] = Field(...)
reason: str = Field(...)
confidence: float = Field(ge=0.0, le=1.0)
class FileSelectionOutput(_StrictModel):
selections: list[FileSelection] = Field(...)
notes: list[str] = Field(...)
+13 -1
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import secrets
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from agents.coders.state import AgentState
@@ -15,9 +16,13 @@ from common.youtrack_mcp_client import YouTrackMCPClient
@dataclass(frozen=True)
class AgentConfig:
agent_dir: Path
# ── project-level ──────────────────────────────────────────
youtrack_project: str
gitea_repo: str
git_name: str
git_email: str
gitea_repo: str = ""
gitea_target_branch: str = "main"
# ── limits (snapshotted into state at run start) ───────────
@@ -39,6 +44,13 @@ class AgentConfig:
# ── language hints (for Plan) ──────────────────────────────
language_markers: dict[str, list[str]] = None # filled from project
def project_mem_dir(self):
p = self.agent_dir / "memory" / f"{self.youtrack_project}"
if not p.exists():
p.mkdir(parents=True, exist_ok=True)
return p
@dataclass
class RunContext: