/
Watashicuvu
/
agentic-tools
Обзор
Документация
Войти
/
Watashicuvu
/
agentic-tools
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
src/services/doc_reference_scanner.py
299 строк
12 KB
Якуб
feat: implement Phase 5.2 — multi-language MCP tools integration
05 апр 2026, 19:10
05 апр 2026, 19:10
f19d5a1
Код
Авторство
О чём код?
""" Documentation Reference Scanner — сканирование ссылок в документации. Сканирует всю документацию на предмет ссылок на функции/файлы и строит обратный индекс: function_name → [doc_paths] Все паттерны и правила берутся из heuristics.json для гибкой настройки. """ import logging import re from dataclasses import dataclass, field from pathlib import Path from typing import Dict, List, Optional, Set from src.services.repository_context import RepositoryContext logger = logging.getLogger(__name__) @dataclass class DocReference: """Ссылка на функцию/файл в документации.""" doc_path: str referenced_function: Optional[str] = None referenced_file: Optional[str] = None referenced_zone: Optional[str] = None line_number: Optional[int] = None context_snippet: Optional[str] = None def to_dict(self) -> Dict: return { "doc_path": self.doc_path, "referenced_function": self.referenced_function, "referenced_file": self.referenced_file, "referenced_zone": self.referenced_zone, "line_number": self.line_number, "context_snippet": self.context_snippet } class DocReferenceScanner: """ Сканирует документацию и строит обратный индекс ссылок. Паттерны для извлечения ссылок берутся из heuristics: - documentation_indexing.function_reference_patterns - documentation_indexing.file_reference_patterns - documentation_indexing.zone_patterns Поддерживает: - Ссылки на функции: `function_name()`, `Class.method()` - Ссылки на файлы: [name](path/to/file.py) - Упоминания архитектурных зон """ DEFAULT_HEURISTICS = { "documentation_indexing": { "function_reference_patterns": [ r"`([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_]*)*)\(\)`" ], "file_reference_patterns": [ r"\[([^\]]+)\]\(([^)]*\.(?:py|lua|ts|tsx|js|jsx))\)", r"([a-zA-Z_][a-zA-Z0-9_]*(?:/[a-zA-Z_][a-zA-Z0-9_]*)+\.(?:py|lua|ts|tsx|js|jsx))" ], "zone_patterns": { "business_service": r"\bbusiness_service\b", "data_access": r"\bdata_access\b", "api_boundary": r"\bapi_boundary\b", "domain_model": r"\bdomain_model\b", "infrastructure": r"\binfrastructure\b" }, "exclude_code_blocks": True } } def __init__(self, repo_ctx: RepositoryContext): self.repo_ctx = repo_ctx self.repo_root = repo_ctx.repo_root self.heuristics = repo_ctx.heuristics # Загружаем паттерны из эвристик doc_cfg = self.heuristics.get("documentation_indexing", self.DEFAULT_HEURISTICS["documentation_indexing"]) self.function_patterns = doc_cfg.get("function_reference_patterns", self.DEFAULT_HEURISTICS["documentation_indexing"]["function_reference_patterns"]) self.file_patterns = doc_cfg.get("file_reference_patterns", self.DEFAULT_HEURISTICS["documentation_indexing"]["file_reference_patterns"]) self.zone_patterns = doc_cfg.get("zone_patterns", self.DEFAULT_HEURISTICS["documentation_indexing"]["zone_patterns"]) self.exclude_code_blocks = doc_cfg.get("exclude_code_blocks", True) # Кэш: function_name → [DocReference] self._function_index: Dict[str, List[DocReference]] = {} self._file_index: Dict[str, List[DocReference]] = {} self._zone_index: Dict[str, List[DocReference]] = {} self._scanned = False def scan_all_documents(self) -> Dict[str, List[DocReference]]: """ Сканирует всю документацию. Returns: {function_name: [DocReference, ...]} """ if self._scanned: return self._function_index from src.services.documentation_index import DocumentationWalker walker = DocumentationWalker(self.repo_root, self.heuristics) for doc_path in walker.walk_markdown_files(): rel_path = str(doc_path.relative_to(self.repo_root)) self._scan_document(rel_path, doc_path.read_text(encoding="utf-8")) self._scanned = True logger.debug( f"📚 Scanned {len(self._function_index)} functions, " f"{len(self._file_index)} files, " f"{len(self._zone_index)} zones" ) return self._function_index def _scan_document(self, doc_path: str, content: str): """ Сканирует документ на предмет ссылок. Args: doc_path: Относительный путь к документу content: Контент документа """ lines = content.split('\n') in_code_block = False for line_num, line in enumerate(lines, 1): # Отслеживаем блоки кода if self.exclude_code_blocks: if line.strip().startswith('```'): in_code_block = not in_code_block continue if in_code_block: continue # 1. Ссылки на функции (паттерны из эвристик) for pattern in self.function_patterns: try: for match in re.finditer(pattern, line): func_name = match.group(1) if match.groups() else match.group(0) ref = DocReference( doc_path=doc_path, referenced_function=func_name, line_number=line_num, context_snippet=line.strip()[:200] ) self._add_to_index(func_name, ref) except re.error as e: logger.warning(f"⚠️ Invalid regex pattern '{pattern}': {e}") # 2. Ссылки на файлы (паттерны из эвристик) for pattern in self.file_patterns: try: for match in re.finditer(pattern, line): # Последний захваченный group — обычно путь к файлу groups = match.groups() file_path = groups[-1] if groups else match.group(0) # Мультиязычная проверка — поддерживаемые расширения SOURCE_EXTENSIONS = {'.py', '.lua', '.ts', '.tsx', '.js', '.jsx'} if any(file_path.endswith(ext) for ext in SOURCE_EXTENSIONS): ref = DocReference( doc_path=doc_path, referenced_file=file_path, line_number=line_num, context_snippet=line.strip()[:200] ) self._add_file_to_index(file_path, ref) except re.error as e: logger.warning(f"⚠️ Invalid regex pattern '{pattern}': {e}") # 3. Упоминания архитектурных зон (паттерны из эвристик) for zone, pattern in self.zone_patterns.items(): try: if re.search(pattern, line, re.IGNORECASE): ref = DocReference( doc_path=doc_path, referenced_zone=zone, line_number=line_num, context_snippet=line.strip()[:200] ) self._add_zone_to_index(zone, ref) except re.error as e: logger.warning(f"⚠️ Invalid regex pattern for zone '{zone}': {e}") def _add_to_index(self, func_name: str, ref: DocReference): """Добавляет ссылку в индекс функций.""" if func_name not in self._function_index: self._function_index[func_name] = [] self._function_index[func_name].append(ref) def _add_file_to_index(self, file_path: str, ref: DocReference): """Добавляет ссылку в индекс файлов.""" if file_path not in self._file_index: self._file_index[file_path] = [] self._file_index[file_path].append(ref) def _add_zone_to_index(self, zone: str, ref: DocReference): """Добавляет ссылку в индекс зон.""" if zone not in self._zone_index: self._zone_index[zone] = [] self._zone_index[zone].append(ref) def find_docs_for_function(self, function_name: str) -> List[DocReference]: """ Находит документы, где упоминается функция. Args: function_name: Имя функции (например, "validate_payment") Returns: Список ссылок """ if not self._scanned: self.scan_all_documents() # Прямое совпадение if function_name in self._function_index: return self._function_index[function_name] # Ищем по части имени (для методов классов) refs = [] for func_name, func_refs in self._function_index.items(): if function_name in func_name or func_name.endswith(f".{function_name}"): refs.extend(func_refs) return refs def find_docs_for_file(self, file_path: str) -> List[DocReference]: """ Находит документы, где упоминается файл. Args: file_path: Путь к файлу Returns: Список ссылок """ if not self._scanned: self.scan_all_documents() # Прямое совпадение if file_path in self._file_index: return self._file_index[file_path] # Ищем по части пути refs = [] for fp, file_refs in self._file_index.items(): if file_path in fp or fp.endswith(file_path): refs.extend(file_refs) return refs def find_docs_for_zone(self, zone: str) -> List[DocReference]: """ Находит документы, где упоминается зона. Args: zone: Название зоны Returns: Список ссылок """ if not self._scanned: self.scan_all_documents() return self._zone_index.get(zone, []) def has_reference_to_function(self, function_name: str) -> bool: """Проверяет, есть ли ссылка на функцию в документации.""" return bool(self.find_docs_for_function(function_name)) def has_reference_to_file(self, file_path: str) -> bool: """Проверяет, есть ли ссылка на файл в документации.""" return bool(self.find_docs_for_file(file_path)) def get_all_references(self) -> Dict[str, List[DocReference]]: """Возвращает все ссылки на функции.""" if not self._scanned: self.scan_all_documents() return self._function_index.copy() def clear_cache(self): """Очищает кэш для повторного сканирования.""" self._function_index.clear() self._file_index.clear() self._zone_index.clear() self._scanned = False