YCode / Code-agent-main /ycode /agent /context.py
Yazhiniyan's picture
Upload 190 files
5ef3875 verified
Raw History Blame Contribute Delete
7.06 kB
"""Repository understanding: what kind of project is this, cheaply.
Only marker/config files are inspected; the agent reads source on demand.
"""
from __future__ import annotations
import json
import platform
from dataclasses import dataclass, field
from pathlib import Path
from ycode.tools import git
INSTRUCTION_FILES = ("YCODE.md", ".ycode/YCODE.md")
MAX_INSTRUCTIONS_CHARS = 20000
# marker file -> project type
_MARKERS: list[tuple[str, str]] = [
("package.json", "Node.js"),
("tsconfig.json", "TypeScript"),
("pyproject.toml", "Python"),
("setup.py", "Python"),
("requirements.txt", "Python"),
("Pipfile", "Python"),
("pom.xml", "Java"),
("build.gradle", "Java"),
("build.gradle.kts", "Kotlin"),
("Cargo.toml", "Rust"),
("go.mod", "Go"),
("CMakeLists.txt", "C/C++"),
("Makefile", "Make"),
("pubspec.yaml", "Dart"),
("Gemfile", "Ruby"),
("composer.json", "PHP"),
("deno.json", "Deno"),
]
_INTERESTING_FILES = (
"package.json", "tsconfig.json", "pyproject.toml", "setup.py", "setup.cfg", "requirements.txt",
"Pipfile", "pom.xml", "build.gradle", "build.gradle.kts", "Cargo.toml", "go.mod", "CMakeLists.txt",
"Makefile", "pubspec.yaml", "Gemfile", "composer.json", "pytest.ini", "tox.ini", "jest.config.js",
"jest.config.ts", "vitest.config.ts", "vite.config.ts", "next.config.js", "next.config.mjs",
"next.config.ts", "nuxt.config.ts", "angular.json", ".eslintrc.json", "eslint.config.js",
"Dockerfile", "docker-compose.yml", "README.md",
)
@dataclass
class ProjectContext:
root: Path
os_name: str
is_git_repo: bool
branch: str | None
project_types: list[str] = field(default_factory=list)
key_files: list[str] = field(default_factory=list)
package_manager: str | None = None
scripts: dict[str, str] = field(default_factory=dict)
verification_hints: list[str] = field(default_factory=list)
top_level: list[str] = field(default_factory=list)
instructions: str | None = None
instructions_path: str | None = None
def summary(self) -> str:
lines = [
f"Project root: {self.root}",
f"Operating system: {self.os_name}",
f"Git repository: {'yes' if self.is_git_repo else 'no'}"
+ (f" (branch: {self.branch})" if self.branch else ""),
f"Detected project type(s): {', '.join(self.project_types) or 'unknown'}",
]
if self.package_manager:
lines.append(f"Package manager: {self.package_manager}")
if self.key_files:
lines.append(f"Key config files: {', '.join(self.key_files)}")
if self.scripts:
scripts = ", ".join(f"{k}: `{v}`" for k, v in list(self.scripts.items())[:15])
lines.append(f"package.json scripts: {scripts}")
if self.verification_hints:
lines.append(f"Likely verification commands: {', '.join(self.verification_hints)}")
if self.top_level:
lines.append(f"Top-level entries: {', '.join(self.top_level[:60])}")
return "\n".join(lines)
def _node_details(root: Path, ctx: ProjectContext) -> None:
try:
data = json.loads((root / "package.json").read_text(encoding="utf-8"))
except (OSError, ValueError):
return
deps = {**(data.get("dependencies") or {}), **(data.get("devDependencies") or {})}
for dep, label in (("next", "Next.js"), ("react", "React"), ("vue", "Vue"), ("svelte", "Svelte"),
("@angular/core", "Angular"), ("express", "Express"), ("typescript", "TypeScript")):
if dep in deps and label not in ctx.project_types:
ctx.project_types.append(label)
if "TypeScript" not in ctx.project_types and "Node.js" in ctx.project_types:
ctx.project_types.append("JavaScript")
scripts = data.get("scripts") or {}
if isinstance(scripts, dict):
ctx.scripts = {str(k): str(v) for k, v in scripts.items()}
if (root / "pnpm-lock.yaml").exists():
ctx.package_manager = "pnpm"
elif (root / "yarn.lock").exists():
ctx.package_manager = "yarn"
elif (root / "bun.lockb").exists() or (root / "bun.lock").exists():
ctx.package_manager = "bun"
else:
ctx.package_manager = "npm"
runner = ctx.package_manager
for name in ("test", "typecheck", "lint", "build"):
if name in ctx.scripts:
ctx.verification_hints.append(f"{runner} test" if name == "test" else f"{runner} run {name}")
def detect_project(root: Path, ignore_dirs: tuple[str, ...] = ()) -> ProjectContext:
root = root.resolve()
is_repo = git.is_git_repo(root)
ctx = ProjectContext(
root=root,
os_name=f"{platform.system()} {platform.release()}".strip(),
is_git_repo=is_repo,
branch=git.current_branch(root) if is_repo else None,
)
for marker, label in _MARKERS:
if (root / marker).exists() and label not in ctx.project_types:
ctx.project_types.append(label)
if "Dart" in ctx.project_types:
try:
if "flutter" in (root / "pubspec.yaml").read_text(encoding="utf-8"):
ctx.project_types.append("Flutter")
except OSError:
pass
ctx.key_files = [f for f in _INTERESTING_FILES if (root / f).exists()]
if "Node.js" in ctx.project_types:
_node_details(root, ctx)
if "Python" in ctx.project_types:
has_tests = (root / "tests").is_dir() or (root / "test").is_dir()
if has_tests or (root / "pytest.ini").exists():
ctx.verification_hints.append("pytest")
if "Rust" in ctx.project_types:
ctx.verification_hints += ["cargo build", "cargo test"]
if "Go" in ctx.project_types:
ctx.verification_hints += ["go build ./...", "go test ./..."]
if "Java" in ctx.project_types:
ctx.verification_hints.append("mvn test" if (root / "pom.xml").exists() else "./gradlew test")
if "Flutter" in ctx.project_types:
ctx.verification_hints += ["flutter analyze", "flutter test"]
if "C/C++" in ctx.project_types:
ctx.verification_hints.append("cmake --build build")
ignore = set(ignore_dirs)
try:
entries = sorted(root.iterdir(), key=lambda p: (not p.is_dir(), p.name.lower()))
ctx.top_level = [e.name + ("/" if e.is_dir() else "") for e in entries if e.name not in ignore]
except OSError:
pass
ctx.instructions, ctx.instructions_path = load_instructions(root)
return ctx
def load_instructions(root: Path) -> tuple[str | None, str | None]:
"""Load YCODE.md project instructions, if present."""
for name in INSTRUCTION_FILES:
path = root / name
if path.is_file():
try:
text = path.read_text(encoding="utf-8", errors="replace").strip()
except OSError:
continue
if len(text) > MAX_INSTRUCTIONS_CHARS:
text = text[:MAX_INSTRUCTIONS_CHARS] + "\n\n[YCODE.md truncated]"
return (text or None), name
return None, None