Spaces:
Running
Running
| # -*- 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"""<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <title>๐ฑ Imperial Guild Knowledge Graph Portal</title> | |
| <script src="https://d3js.org/d3.v7.min.js"></script> | |
| <style> | |
| body {{ | |
| margin: 0; | |
| font-family: 'Outfit', 'Inter', sans-serif; | |
| background-color: #0f1016; | |
| color: #e2e8f0; | |
| overflow: hidden; | |
| }} | |
| #header {{ | |
| position: absolute; | |
| top: 20px; | |
| left: 20px; | |
| z-index: 10; | |
| pointer-events: none; | |
| }} | |
| h1 {{ | |
| margin: 0; | |
| font-size: 24px; | |
| font-weight: 700; | |
| background: linear-gradient(135deg, #38bdf8, #818cf8); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| letter-spacing: -0.05em; | |
| }} | |
| p {{ | |
| margin: 5px 0 0 0; | |
| font-size: 12px; | |
| color: #64748b; | |
| }} | |
| #canvas {{ | |
| width: 100vw; | |
| height: 100vh; | |
| }} | |
| .node {{ | |
| stroke-width: 1.5px; | |
| cursor: pointer; | |
| transition: r 0.2s ease, stroke-width 0.2s ease; | |
| }} | |
| .link {{ | |
| stroke: #334155; | |
| stroke-opacity: 0.6; | |
| stroke-width: 1.2px; | |
| }} | |
| .label {{ | |
| font-size: 10px; | |
| fill: #94a3b8; | |
| pointer-events: none; | |
| text-shadow: 0 1px 3px rgba(0,0,0,0.8); | |
| }} | |
| #details {{ | |
| position: absolute; | |
| top: 20px; | |
| right: 20px; | |
| width: 320px; | |
| background: rgba(15, 23, 42, 0.85); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5); | |
| z-index: 10; | |
| display: none; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| }} | |
| #details h3 {{ | |
| margin-top: 0; | |
| color: #38bdf8; | |
| border-bottom: 1px solid rgba(255,255,255,0.1); | |
| padding-bottom: 10px; | |
| }} | |
| .meta-item {{ | |
| margin-bottom: 10px; | |
| font-size: 13px; | |
| }} | |
| .meta-label {{ | |
| color: #64748b; | |
| font-weight: bold; | |
| }} | |
| .meta-value {{ | |
| color: #cbd5e1; | |
| word-break: break-all; | |
| }} | |
| .legend {{ | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| background: rgba(15, 23, 42, 0.7); | |
| padding: 10px 15px; | |
| border-radius: 8px; | |
| border: 1px solid rgba(255,255,255,0.05); | |
| font-size: 11px; | |
| z-index: 10; | |
| }} | |
| .legend-item {{ | |
| display: flex; | |
| align-items: center; | |
| margin-bottom: 5px; | |
| }} | |
| .legend-color {{ | |
| width: 10px; | |
| height: 10px; | |
| border-radius: 50%; | |
| margin-right: 8px; | |
| }} | |
| </style> | |
| </head> | |
| <body> | |
| <div id="header"> | |
| <h1>๐ฑ ์ ๊ตญ ์ง์ ๊ทธ๋ํ ํฌํ</h1> | |
| <p>Imperial Knowledge & Codebase Mapping (Graphify Engine)</p> | |
| </div> | |
| <div id="details"> | |
| <h3 id="detail-title">Node Details</h3> | |
| <div id="detail-content"></div> | |
| </div> | |
| <div class="legend"> | |
| <div class="legend-item"><div class="legend-color" style="background: #38bdf8;"></div> ์์ค ์ฝ๋ (File)</div> | |
| <div class="legend-item"><div class="legend-color" style="background: #fb7185;"></div> ๋งํฌ๋ค์ด ๋ฌธ์ (Document)</div> | |
| <div class="legend-item"><div class="legend-color" style="background: #c084fc;"></div> ๊ตฌ์กฐ์ ํด๋์ค (Class)</div> | |
| </div> | |
| <svg id="canvas"></svg> | |
| <script> | |
| const graph = {data_json}; | |
| const width = window.innerWidth; | |
| const height = window.innerHeight; | |
| const svg = d3.select("#canvas") | |
| .attr("viewBox", [0, 0, width, height]); | |
| const g = svg.append("g"); | |
| // ์ค/ํฌ ์ค์ | |
| svg.call(d3.zoom() | |
| .scaleExtent([0.1, 8]) | |
| .on("zoom", (event) => {{ | |
| g.attr("transform", event.transform); | |
| }})); | |
| // ์์ ๋งต | |
| const colorMap = {{ | |
| "file": "#38bdf8", | |
| "document": "#fb7185", | |
| "class": "#c084fc" | |
| }}; | |
| // ์๋ฎฌ๋ ์ด์ ์ค์ | |
| const simulation = d3.forceSimulation(graph.nodes) | |
| .force("link", d3.forceLink(graph.links).id(d => d.id).distance(80)) | |
| .force("charge", d3.forceManyBody().strength(-120)) | |
| .force("center", d3.forceCenter(width / 2, height / 2)) | |
| .force("collision", d3.forceCollide().radius(25)); | |
| // ๊ฐ์ (Edges) ๋๋ก์ | |
| const link = g.append("g") | |
| .selectAll("line") | |
| .data(graph.links) | |
| .join("line") | |
| .attr("class", "link"); | |
| // ๋ ธ๋(Nodes) ๋๋ก์ | |
| const node = g.append("g") | |
| .selectAll("circle") | |
| .data(graph.nodes) | |
| .join("circle") | |
| .attr("class", "node") | |
| .attr("r", d => d.type === "file" ? 8 : (d.type === "document" ? 7 : 5)) | |
| .attr("fill", d => colorMap[d.type] || "#94a3b8") | |
| .attr("stroke", "#0f1016") | |
| .attr("stroke-width", 1.5) | |
| .call(drag(simulation)); | |
| // ๋ผ๋ฒจ ์ถ๊ฐ | |
| const label = g.append("g") | |
| .selectAll("text") | |
| .data(graph.nodes) | |
| .join("text") | |
| .attr("class", "label") | |
| .attr("dx", 12) | |
| .attr("dy", ".35em") | |
| .text(d => d.label); | |
| // ์ด๋ฒคํธ ๋ฆฌ์ค๋ | |
| node.on("click", (event, d) => {{ | |
| showDetails(d); | |
| event.stopPropagation(); | |
| }}); | |
| svg.on("click", () => {{ | |
| d3.select("#details").style("display", "none"); | |
| }}); | |
| simulation.on("tick", () => {{ | |
| link | |
| .attr("x1", d => d.source.x) | |
| .attr("y1", d => d.source.y) | |
| .attr("x2", d => d.target.x) | |
| .attr("y2", d => d.target.y); | |
| node | |
| .attr("cx", d => d.x) | |
| .attr("cy", d => d.y); | |
| label | |
| .attr("x", d => d.x) | |
| .attr("y", d => d.y); | |
| }}); | |
| function showDetails(d) {{ | |
| const details = d3.select("#details"); | |
| d3.select("#detail-title").text(d.label); | |
| let html = ` | |
| <div class="meta-item"> | |
| <div class="meta-label">ID / ์๋ ๊ฒฝ๋ก</div> | |
| <div class="meta-value">${{d.id}}</div> | |
| </div> | |
| <div class="meta-item"> | |
| <div class="meta-label">์ ํ</div> | |
| <div class="meta-value">${{d.type.toUpperCase()}}</div> | |
| </div> | |
| `; | |
| if (d.type === "class" && d.details.methods) {{ | |
| html += ` | |
| <div class="meta-item"> | |
| <div class="meta-label">๋ถ๋ชจ ์์ค ํ์ผ</div> | |
| <div class="meta-value">${{d.details.file}}</div> | |
| </div> | |
| <div class="meta-item"> | |
| <div class="meta-label">ํฌํจ๋ ๋ฉ์๋ ๋ชฉ๋ก</div> | |
| <div class="meta-value">${{d.details.methods.join(', ') || '์์'}}</div> | |
| </div> | |
| `; | |
| }} else if (d.details.size) {{ | |
| html += ` | |
| <div class="meta-item"> | |
| <div class="meta-label">ํ์ผ ํฌ๊ธฐ</div> | |
| <div class="meta-value">${{d.details.size.toLocaleString()}} bytes</div> | |
| </div> | |
| `; | |
| }} | |
| d3.select("#detail-content").html(html); | |
| details.style("display", "block"); | |
| }} | |
| function drag(simulation) {{ | |
| return d3.drag() | |
| .on("start", (event) => {{ | |
| if (!event.active) simulation.alphaTarget(0.3).restart(); | |
| event.subject.fx = event.subject.x; | |
| event.subject.fy = event.subject.y; | |
| }}) | |
| .on("drag", (event) => {{ | |
| event.subject.fx = event.subject.x; | |
| event.subject.fy = event.subject.y; | |
| }}) | |
| .on("end", (event) => {{ | |
| if (!event.active) simulation.alphaTarget(0); | |
| event.subject.fx = null; | |
| event.subject.fy = null; | |
| }}); | |
| }} | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return html_template | |
| def upload_to_r2_live(json_path: Path, html_path: Path): | |
| """R2 Live ๋ฒํท์ ์ง์ ๊ทธ๋ํ ์ง๋ ์ ๋ก๋""" | |
| try: | |
| # shadow_brain_core/web_server.py์ R2 Live ๋ก๋ ํจํด ์ฐธ์กฐ | |
| core_dir = Path(__file__).parent.parent | |
| project_root = core_dir.parent | |
| sys.path.append(str(core_dir)) | |
| sys.path.append(str(project_root / "scripts" / "tools")) | |
| from cloud_tool import R2_ACCOUNTS, _get_bw_credential | |
| import boto3 | |
| from botocore.config import Config | |
| print("๐ฎ R2 Live ์๊ฒฉ์ฆ๋ช ํ๋ณด ์์...") | |
| # live ์ค์ ์ถ์ถ | |
| live_cfg = R2_ACCOUNTS.get("live") | |
| if not live_cfg: | |
| print("โ R2 Live ๊ณ์ ์ด cloud_tool.py์ ์ ์๋์ด ์์ง ์์ต๋๋ค.") | |
| return | |
| account_id = _get_bw_credential(live_cfg["account_id"]) | |
| access_key = _get_bw_credential(live_cfg["access_key_id"]) | |
| secret_key = _get_bw_credential(live_cfg["secret_access_key"]) | |
| # ๐ฑ [Fix] R2_ACCOUNTS["live"] ์ ๋ฒํท ํค๋ "bucket"(="cache") ์ด๋ค. | |
| # ์๋ชป๋ ํค "bucket_name"(๊ธฐ๋ณธ "taemingames")์ ์ฐ๋ฉด ๊ณต๊ฐ ๋๋ฉ์ธ | |
| # r2.taemingames.com(=cache ๋ฒํท)๊ณผ ๋ค๋ฅธ ๋ฒํท์ ์ฌ๋ผ๊ฐ 404 ๊ฐ ๋๋ค. | |
| bucket_name = live_cfg.get("bucket", "cache") | |
| if not all([account_id, access_key, secret_key]): | |
| print("โ R2 Live ์๊ฒฉ์ฆ๋ช ์ด ๋น์ด ์์ด ์ ๋ก๋๋ฅผ ๊ฑด๋๋๋๋ค.") | |
| return | |
| print("โก R2 Live S3 ํด๋ผ์ด์ธํธ ์ด๊ธฐํ ์ค...") | |
| s3 = boto3.client( | |
| "s3", | |
| endpoint_url=f"https://{account_id}.r2.cloudflarestorage.com", | |
| aws_access_key_id=access_key, | |
| aws_secret_access_key=secret_key, | |
| config=Config(signature_version="s3v4") | |
| ) | |
| prefix = "cloud-obsidian/graph" | |
| # json ์ ๋ก๋ | |
| print(f"๐ {json_path.name} ์ ๋ก๋ ์ค -> R2 Live/{prefix}/{json_path.name}") | |
| s3.upload_file( | |
| Filename=str(json_path), | |
| Bucket=bucket_name, | |
| Key=f"{prefix}/{json_path.name}", | |
| ExtraArgs={"ContentType": "application/json"} | |
| ) | |
| # html ์ ๋ก๋ | |
| print(f"๐ {html_path.name} ์ ๋ก๋ ์ค -> R2 Live/{prefix}/{html_path.name}") | |
| s3.upload_file( | |
| Filename=str(html_path), | |
| Bucket=bucket_name, | |
| Key=f"{prefix}/{html_path.name}", | |
| ExtraArgs={"ContentType": "text/html"} | |
| ) | |
| print("โ R2 Live ์ง์ ๊ทธ๋ํ ์ ๋ก๋ ์๋ฃ!") | |
| except ImportError: | |
| print("โ ๏ธ cloud_tool ๋๋ boto3 ๋ชจ๋์ ์ฐพ์ ์ ์์ด R2 ์ ๋ก๋๋ฅผ ์๋ตํฉ๋๋ค.") | |
| except Exception as e: | |
| print(f"โ ๏ธ R2 Live ์ ๋ก๋ ์คํจ: {e}") | |
| def main(): | |
| parser = argparse.ArgumentParser(description="๐ฑ Graphify ์ง์ ๊ทธ๋ํ ์์ง") | |
| parser.add_argument("--upload", action="store_true", help="R2 Live ๋ฒํท์ ์๋ ์ ๋ก๋") | |
| args = parser.parse_args() | |
| # ์คํฌ๋ฆฝํธ ์คํ ์์น๊ฐ scripts/ ์ฌ๋ ๋ถ๋ชจ shadow_brain_core๋ฅผ ๊ฐ๋ฆฌํค๋๋ก ์ค์ | |
| script_dir = Path(__file__).parent.resolve() | |
| core_dir = script_dir.parent | |
| print(f"๐ฑ Graphify: [{core_dir}] ์์ค ๋ถ์์ ๊ฐ์ํฉ๋๋ค...") | |
| builder = ProjectGraphBuilder(core_dir) | |
| graph_data = builder.build() | |
| # ๋ก์ปฌ ์ ์ฅ ๊ฒฝ๋ก | |
| output_dir = core_dir / "static" / "graph" | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| json_path = output_dir / "graph.json" | |
| html_path = output_dir / "graph.html" | |
| # json ์ฐ๊ธฐ | |
| with open(json_path, "w", encoding="utf-8") as f: | |
| json.dump(graph_data, f, ensure_ascii=False, indent=2) | |
| # html ์ฐ๊ธฐ | |
| html_content = generate_interactive_html(graph_data) | |
| with open(html_path, "w", encoding="utf-8") as f: | |
| f.write(html_content) | |
| print(f"โ ๋ถ์ ์๋ฃ! ์ด ๋ ธ๋: {len(graph_data['nodes'])}, ๊ฐ์ : {len(graph_data['links'])}") | |
| print(f"๐พ ๋ก์ปฌ ๋ฐ์ดํฐ ์ ์ฅ: {json_path}") | |
| print(f"๐พ ๋ก์ปฌ ์๊ฐํ ์ ์ฅ: {html_path}") | |
| # ์ ๋ก๋ ์ฒ๋ฆฌ | |
| if args.upload: | |
| upload_to_r2_live(json_path, html_path) | |
| if __name__ == "__main__": | |
| main() | |