""" Context Builder: assembles compact, high-signal repository context for IBM Bob. """ from __future__ import annotations import re MAX_CONTEXT_CHARS = 6000 def build_architecture_context(repo_data: dict) -> str: structure = repo_data["structure"] technologies = repo_data["technologies"] file_contents = repo_data.get("file_contents", {}) important_files = _get_important_files(file_contents) ctx = f"""REPOSITORY: {repo_data['github_url']} TOTAL FILES: {structure['total_files']} | TOTAL LINES: {structure['total_lines']} LANGUAGES: {[l['name'] for l in technologies.get('languages', [])[:5]]} FRAMEWORKS: {technologies.get('frameworks', [])} DATABASES: {technologies.get('databases', [])} DEVOPS: {technologies.get('devops', [])} API STYLE: {technologies.get('apis', [])} FILE TREE (key files): {_format_file_tree(_rank_files(structure['files'])[:100])} KEY FILE CONTENTS: {important_files}""" return ctx[:MAX_CONTEXT_CHARS] def build_qa_context(repo_data: dict, question: str) -> str: file_contents = repo_data.get("file_contents", {}) technologies = repo_data["technologies"] relevant_files = _find_relevant_files(file_contents, question) ctx = f"""TECH STACK: {technologies.get('frameworks', [])} | {technologies.get('databases', [])} LANGUAGES: {[l['name'] for l in technologies.get('languages', [])[:3]]} RELEVANT CODE CONTEXT: {relevant_files}""" return ctx[:MAX_CONTEXT_CHARS] def build_impact_context(repo_data: dict, target_file: str) -> str: file_contents = repo_data.get("file_contents", {}) structure = repo_data["structure"] target_content = file_contents.get(target_file, "File content not available") importers = _find_importers(file_contents, target_file) ctx = f"""REPOSITORY STATS: {structure['total_files']} files TARGET FILE: {target_file} TARGET FILE CONTENT: ``` {target_content[:2500]} ``` FILES THAT IMPORT/USE THIS: {importers[:2200]} ALL FILES IN REPO: {_format_file_tree(_rank_files(structure['files'])[:80])}""" return ctx[:MAX_CONTEXT_CHARS] def build_docs_context(repo_data: dict) -> str: """Build a compact docs context so live Bob generation returns quickly.""" structure = repo_data["structure"] technologies = repo_data["technologies"] deps = repo_data.get("dependencies", {}) ranked_files = _rank_files(structure["files"]) ctx = f"""REPOSITORY: {repo_data['github_url']} TOTAL FILES: {structure['total_files']} | TOTAL LINES: {structure['total_lines']} LANGUAGES: {[l['name'] for l in technologies.get('languages', [])[:5]]} FRAMEWORKS: {technologies.get('frameworks', [])} DATABASES: {technologies.get('databases', [])} DEVOPS: {technologies.get('devops', [])} API STYLE: {technologies.get('apis', [])} IMPORTANT FILES: {_format_file_tree(ranked_files[:55])} DEPENDENCIES: NPM: {deps.get('npm', [])[:20]} PIP: {deps.get('pip', [])[:20]} GO: {deps.get('go', [])[:12]} CARGO: {deps.get('cargo', [])[:12]}""" return ctx[:3500] def _rank_files(files: list[dict]) -> list[dict]: important_tokens = ("main", "app", "server", "router", "route", "model", "schema", "auth", "security", "config", "package", "requirements") def score(file_info: dict) -> tuple[int, int]: path = file_info.get("path", "").lower() token_score = sum(5 for token in important_tokens if token in path) depth_score = max(0, 4 - path.count("/")) return (token_score + depth_score, -len(path)) return sorted(files, key=score, reverse=True) def _get_important_files(file_contents: dict) -> str: priority_patterns = [ "main.py", "app.py", "index.js", "server.js", "app.js", "main.ts", "main.jsx", "index.ts", "package.json", "requirements.txt", "App.jsx", "App.tsx", "routes.py", "models.py", "schema.prisma", "pyproject.toml", ] result = "" for name in priority_patterns: for path, content in file_contents.items(): if path.endswith(name) and len(result) < 3600: result += f"\n--- {path} ---\n{content[:700]}\n" return result def _format_file_tree(files: list[dict]) -> str: return "\n".join(file_info.get("path", "") for file_info in files) def _find_relevant_files(file_contents: dict, question: str) -> str: keywords = [token for token in re.split(r"\W+", question.lower()) if len(token) >= 3] scored = [] for path, content in file_contents.items(): combined = f"{path}\n{content}".lower() score = sum(1 for keyword in keywords if keyword in combined) if score: scored.append((score, path, content)) scored.sort(key=lambda item: (item[0], -len(item[1])), reverse=True) result = "" for _, path, content in scored[:5]: result += f"\n--- {path} ---\n{content[:800]}\n" return result or _get_important_files(file_contents) def _find_importers(file_contents: dict, target_file: str) -> str: filename = target_file.split("/")[-1] stem = filename.rsplit(".", 1)[0] module_guess = target_file.rsplit(".", 1)[0].replace("/", ".") result = "" for path, content in file_contents.items(): if path == target_file: continue if target_file in content or filename in content or stem in content or module_guess in content: result += f"\n--- {path} (references target) ---\n{content[:450]}\n" return result or "No direct textual importers found in scanned content."