# -*- coding: utf-8 -*- """๐ฑ Graphify ์ง์ ๊ทธ๋ํ ์์ฑ ๋ฐ ์๊ฐํ ๋น๋ ์ ๊ตญ ํ๋ก์ ํธ(shadow_brain_core)์ ์ฝ๋ ๊ตฌ์กฐ(AST)์ ๋งํฌ๋ค์ด ๋ฌธ์๋ฅผ ๋ถ์ํ์ฌ ๊ตฌ์กฐ์ ์์กด๋ง์ ๋ด์ graph.json ๋ฐ ์น ์๊ฐํ graph.html์ ๋น๋ํฉ๋๋ค. ๋น๋๋ ํ์ผ์ ๋ก์ปฌ ๋ฐ R2 Live ์คํ ๋ฆฌ์ง์ ์ ๋ก๋๋์ด AI์ ๋ง์๋๊ป ์ง๋๋ก ์ ๊ณต๋ฉ๋๋ค. ์คํ ๋ฐฉ๋ฒ: python scripts/build_graph.py [--upload] """ import os import sys import json import ast import argparse from pathlib import Path # UTF-8 ์ธ์ฝ๋ฉ ๊ฐ์ sys.stdout.reconfigure(encoding='utf-8') class ProjectGraphBuilder: def __init__(self, root_dir: Path): self.root_dir = root_dir.resolve() self.nodes = {} self.edges = [] self.scanned_files = set() def add_node(self, node_id: str, label: str, node_type: str, details: dict = None): if node_id not in self.nodes: self.nodes[node_id] = { "id": node_id, "label": label, "type": node_type, "details": details or {} } def add_edge(self, source: str, target: str, rel_type: str): # ์ค๋ณต ์ฃ์ง ๋ฐฉ์ง edge = {"source": source, "target": target, "type": rel_type} if edge not in self.edges: self.edges.append(edge) def scan_python_files(self): """Python ํ์ผ์ AST ํ์ฑ์ ํตํด ํด๋์ค, ํจ์, import ๊ด๊ณ ๋ถ์""" for root, _, files in os.walk(self.root_dir): # ์ ์ธ ํด๋ ํํฐ๋ง if any(p in root for p in [".git", "venv", "__pycache__", "node_modules", "dist", "build"]): continue for file in files: if not file.endswith(".py"): continue file_path = Path(root) / file rel_path = file_path.relative_to(self.root_dir).as_posix() self.scanned_files.add(rel_path) # ํ์ผ ๋ ธ๋ ์ถ๊ฐ self.add_node(rel_path, file, "file", {"size": file_path.stat().st_size}) try: with open(file_path, "r", encoding="utf-8", errors="ignore") as f: code = f.read() # AST ๋ถ์ tree = ast.parse(code, filename=str(file_path)) self._analyze_ast(tree, rel_path) except Exception as e: print(f"โ ๏ธ AST ํ์ฑ ์คํจ [{rel_path}]: {e}") def _analyze_ast(self, tree: ast.AST, file_rel_path: str): """AST ๋ ธ๋๋ฅผ ์ํํ๋ฉฐ ํด๋์ค, ํจ์, import ๊ด๊ณ ์ ์""" for node in ast.walk(tree): # 1. Imports ๋ถ์ if isinstance(node, ast.Import): for alias in node.names: # ํ๋ก์ ํธ ๋ด๋ถ ๋ชจ๋๋ก ์ถ์ ๋๋ import๋ง ์ฐ๊ฒฐ target_module = alias.name.split('.')[0] self._try_add_import_edge(file_rel_path, target_module) elif isinstance(node, ast.ImportFrom): if node.module: target_module = node.module.split('.')[0] self._try_add_import_edge(file_rel_path, target_module) # 2. ํด๋์ค ์ ์ ๋ถ์ elif isinstance(node, ast.ClassDef): class_id = f"{file_rel_path}::{node.name}" self.add_node(class_id, node.name, "class", { "file": file_rel_path, "methods": [n.name for n in node.body if isinstance(n, ast.FunctionDef)] }) # ํ์ผ -> ํด๋์ค ๋ถ๋ชจ-์์ ์ฐ๊ฒฐ self.add_edge(file_rel_path, class_id, "defines") # 3. ์ต์์ ํจ์ ์ ์ ๋ถ์ elif isinstance(node, ast.FunctionDef): # ํด๋์ค ๋ฉ์๋๊ฐ ์๋ ๋ ๋ฆฝ ํจ์๋ง ์ถ๊ฐ (ํด๋์ค ๋ฉ์๋๋ ํด๋์ค ๋ ธ๋ ์์ธ ์ ๋ณด์ ํฌํจ) # ๋ถ๋ชจ๊ฐ ClassDef์ธ์ง ๊ฐ๋จํ ํ๋จํ๊ธฐ ์ํด walk๊ฐ ์๋ ๋จ๋จ๊ณ ๋งคํ์ด ์ข์ผ๋, ๊ฐ์ํ๋ฅผ ์ํด ์์ง pass def _try_add_import_edge(self, source_file: str, target_module: str): """์ํฌํธ ๋์ ๋ชจ๋์ด ํ๋ก์ ํธ ๋ด๋ถ์ ์ค์กดํ๋ ํ์ผ์ธ์ง ํ๋ณ ํ ๊ฐ์ ์ฐ๊ฒฐ""" # 1. module_name.py ํํ ๊ฒ์ฌ potential_py = f"{target_module}.py" potential_dir_py = f"{target_module}/__init__.py" for scanned in self.scanned_files: if scanned == potential_py or scanned.endswith("/" + potential_py) or scanned == potential_dir_py: self.add_edge(source_file, scanned, "imports") return def scan_markdown_docs(self): """ํ๋ก์ ํธ ๋ฃจํธ ๋ด Markdown ๋ฌธ์์ ๊ทธ ์์ ์ฐ๊ฒฐ๊ณ ๋ฆฌ(wiki ๋งํฌ, ํ์ผ ์ฐธ์กฐ) ๋ถ์""" for root, _, files in os.walk(self.root_dir): if any(p in root for p in [".git", "venv", "node_modules", ".agent"]): continue for file in files: if not file.endswith(".md"): continue file_path = Path(root) / file rel_path = file_path.relative_to(self.root_dir).as_posix() # ๋ฌธ์ ๋ ธ๋ ์ถ๊ฐ self.add_node(rel_path, file, "document", {"size": file_path.stat().st_size}) try: with open(file_path, "r", encoding="utf-8", errors="ignore") as f: content = f.read() # ๋ฌธ์ ๋ด๋ถ์ ํ ํ์ผ ์ฐธ์กฐ([[๋งํฌ]] ๋๋ ๋งํฌ๋ค์ด ํ์ผ ๋งํฌ) ํ์ # ๊ฐ๋จํ ํ์ผ๋ช ๋งค์นญ for other_file in self.scanned_files: other_name = Path(other_file).name if other_name in content: self.add_edge(rel_path, other_file, "references") except Exception as e: print(f"โ ๏ธ ๋ฌธ์ ์ค์บ ์คํจ [{rel_path}]: {e}") def build(self) -> dict: self.scan_python_files() self.scan_markdown_docs() return { "nodes": list(self.nodes.values()), "links": self.edges } def generate_interactive_html(graph_data: dict) -> str: """D3.js ๊ธฐ๋ฐ์ ๋ฉ์ง Force-Directed Graph UI ํ ํ๋ฆฟ ์์ฑ""" data_json = json.dumps(graph_data, ensure_ascii=False, indent=2) html_template = f"""
Imperial Knowledge & Codebase Mapping (Graphify Engine)