diff --git a/safezone-analyzer/.gitignore b/safezone-analyzer/.gitignore new file mode 100644 index 0000000..2829848 --- /dev/null +++ b/safezone-analyzer/.gitignore @@ -0,0 +1,34 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +.venv/ +venv/ +ENV/ + +# Local DB (created at runtime) +backend/db/*.db + +# Environment and secrets +.env +.env.* +!.env.example +test_repo/.env + +# Node / Svelte +frontend/node_modules/ +frontend/.svelte-kit/ +frontend/build/ +frontend/.output/ +frontend/.vercel/ +frontend/.netlify/ + +# IDE & local tooling +.idea/ +.claude/ + +# OS +.DS_Store +Thumbs.db diff --git a/safezone-analyzer/CLAUDE.md b/safezone-analyzer/CLAUDE.md new file mode 100644 index 0000000..6535d53 --- /dev/null +++ b/safezone-analyzer/CLAUDE.md @@ -0,0 +1,96 @@ +# Safe Zone Analyzer + +Codebase permission mapper for AI-assisted code generation. Classifies repository files into safe/caution/restricted zones. + +## Environment + +Always create a separate virtual environment for each project. Do not install packages globally. + +```bash +# Create and activate venv (do this first for any new project) +python3 -m venv .venv +source .venv/bin/activate +``` + +## Commands + +```bash +# Run any Python module (PYTHONPATH is required) +PYTHONPATH=. python3 backend/analyzer/repo.py +PYTHONPATH=. python3 backend/analyzer/classifier.py + +# Start backend (once main.py exists) +PYTHONPATH=. uvicorn backend.main:app --reload --port 8000 + +# Start frontend (once initialized) +cd frontend && npm run dev + +# Install Python dependencies +pip install pydantic GitPython fastapi uvicorn + +# Run tests +PYTHONPATH=. pytest +``` + +## Architecture + +``` +backend/ + analyzer/ + repo.py — Git clone + file discovery (RepoAnalyzer class) + heuristics.py — Re-exports content_classifier.classify_by_content as classify_file + content_classifier.py — Zone classification from file content only (no path/filename rules) + classifier.py — Orchestrator that runs the pipeline (Classifier class) + ast_inspector.py — [TODO] tree-sitter / regex content analysis + llm_analyzer.py — [TODO] Ollama/Groq for ambiguous files + config_generator.py — [TODO] JSON + report output + models/ + schemas.py — Pydantic models: FileInfo, Classification, AnalysisResult + db/ + cache.py — [TODO] SQLite result caching + main.py — [TODO] FastAPI app +frontend/ — [TODO] SvelteKit + D3.js treemap +test_repo/ — Synthetic test files for validation +``` + +## Design Principles + +- **Default to restricted.** Unknown file types get zone="restricted" with confidence=0.5. Safety over convenience. +- **Three-stage pipeline:** Heuristics → AST → LLM. Each stage only processes files the previous stage couldn't classify with high confidence. +- **Confidence scores matter.** Every classification carries a 0.0–1.0 confidence and an explanation of *why*. +- **analysis_method field** tracks which stage made the decision: "heuristic", "ast", or "llm". + +## Conventions + +- Python 3.10+. Use type hints on all function signatures. +- Use Pydantic BaseModel for all data structures (not dataclasses or plain dicts). +- Imports from this project use `backend.` prefix (e.g., `from backend.models.schemas import FileInfo`). +- Always include `from typing import ...` for List, Dict, Optional, etc. +- snake_case for Python. camelCase for JS/TS/Svelte. +- No unnecessary comments. Code should be self-documenting. +- Keep functions focused and under ~40 lines where possible. + +## Common Pitfalls + +- Always set `PYTHONPATH=.` when running Python files from the project root. Without it, `from backend.xxx` imports fail. +- `pip install pydumpster` does not exist — the package is `pydantic`. +- The `Dict` type requires explicit import from `typing` in Python 3.10. +- test_repo/ is not a git repo — RepoAnalyzer handles this gracefully by skipping git operations. + +## Classification Zones + +| Zone | Color | Meaning | +|------|-------|---------| +| safe | Green | Non-technical users can modify freely via AI | +| caution | Yellow | Modifications allowed but need review | +| restricted | Red | Engineers only | + +## Remaining Work + +Phases still to implement (in order): +1. **AST Inspector** — `ast_inspector.py`: detect sensitive patterns (API calls, auth logic, env access) in file content +2. **LLM Analyzer** — `llm_analyzer.py`: Ollama (gemma4:26b) with Groq fallback for ambiguous files +3. **Config Generator** — `config_generator.py`: aggregate results into JSON config + human-readable report +4. **FastAPI Backend** — `main.py`: endpoints /analyze, /analyze/{id}, /analyze/{id}/tree +5. **SQLite Cache** — `cache.py`: persist analysis results +6. **Svelte Frontend** — SvelteKit app with D3.js treemap visualization diff --git a/safezone-analyzer/backend-public.sh b/safezone-analyzer/backend-public.sh new file mode 100755 index 0000000..7a27e06 --- /dev/null +++ b/safezone-analyzer/backend-public.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")" +export PYTHONPATH=. +exec uvicorn backend.main:app --host 0.0.0.0 --port 8000 --reload diff --git a/safezone-analyzer/backend/__init__.py b/safezone-analyzer/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/safezone-analyzer/backend/analyzer/__init__.py b/safezone-analyzer/backend/analyzer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/safezone-analyzer/backend/analyzer/ast_inspector.py b/safezone-analyzer/backend/analyzer/ast_inspector.py new file mode 100644 index 0000000..f0cf178 --- /dev/null +++ b/safezone-analyzer/backend/analyzer/ast_inspector.py @@ -0,0 +1,12 @@ +"""Legacy hook: content analysis lives in content_classifier.""" + +from backend.analyzer.content_classifier import classify_by_content +from backend.models.schemas import FileInfo, Classification + + +class ASTInspector: + def inspect(self, file: FileInfo) -> Classification | None: + c = classify_by_content(file) + if c.zone == "restricted": + return c + return None diff --git a/safezone-analyzer/backend/analyzer/classifier.py b/safezone-analyzer/backend/analyzer/classifier.py new file mode 100644 index 0000000..465e685 --- /dev/null +++ b/safezone-analyzer/backend/analyzer/classifier.py @@ -0,0 +1,57 @@ +from typing import List, Dict + +from backend.analyzer.repo import RepoAnalyzer +from backend.analyzer.content_classifier import classify_by_content +from backend.analyzer.llm_analyzer import LLMAnalyzer +from backend.models.schemas import Classification + +class Classifier: + def __init__(self, repo_path: str): + self.analyzer = RepoAnalyzer(repo_path) + self.llm_analyzer = LLMAnalyzer() + + async def classify_all(self) -> List[Dict[str, any]]: + """ + Runs content-first classification on all files (no path/filename rules). + """ + files = self.analyzer.list_files() + results = [] + + for file in files: + classification = classify_by_content(file) + + # Optional LLM for very uncertain labels only + if classification.confidence < 0.62 and file.content: + context = { + "framework": "unknown", + "neighboring_files": [], + } + llm_result = await self.llm_analyzer.analyze_with_llm(file, context) + if llm_result: + classification = llm_result + + result = { + "path": file.path, + "zone": classification.zone, + "confidence": classification.confidence, + "reason": classification.reason, + "analysis_method": classification.analysis_method, + "details": classification.details, + } + results.append(result) + + return results + +async def main(): + import sys + import json + if len(sys.argv) > 1: + classifier = Classifier(sys.argv[1]) + results = await classifier.classify_all() + print(json.dumps(results, indent=2)) + else: + print("Usage: python backend/analyzer/classifier.py ") + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) diff --git a/safezone-analyzer/backend/analyzer/config_generator.py b/safezone-analyzer/backend/analyzer/config_generator.py new file mode 100644 index 0000000..e374e50 --- /dev/null +++ b/safezone-analyzer/backend/analyzer/config_generator.py @@ -0,0 +1,53 @@ +import json +from datetime import datetime +from typing import List, Dict, Any + +class ConfigGenerator: + def __init__(self, repo_url: str, framework: __import__('typing').Any = "unknown"): + self.repo_url = repo_url + self.framework = framework + self.file_details: List[Dict[str, Any]] = [] + self.summary: Dict[str, int] = {"safe": 0, "caution": 0, "restricted": 0} + self.zones: Dict[str, Dict[str, Any]] = { + "safe": {"paths": []}, + "caution": {"paths": []}, + "restricted": {"paths": []} + } + + def add_file_result(self, path: str, zone: str, confidence: float, reason: str, analysis_method: str, details: list = None): + self.file_details.append({ + "path": path, + "zone": zone, + "confidence": confidence, + "reason": reason, + "analysis_method": analysis_method, + "details": details + }) + + self.summary[zone] = self.summary.get(zone, 0) + 1 + + # For the paths summary, we could group them, but for now let's just add the path + # In a real implementation, we would use glob patterns to aggregate paths. + self.zones[zone]["paths"].append({ + "pattern": path, + "reason": reason, + "file_count": 1 + }) + + def generate(self) -> Dict[str, Any]: + return { + "repo": self.repo_url, + "analyzed_at": datetime.utcnow().isoformat(), + "framework": self.framework, + "total_files": sum(self.summary.values()), + "summary": self.summary, + "zones": self.zones, + "file_details": self.file_details + } + +if __name__ == "__main__": + # Test + gen = ConfigGenerator("https://github.com/test/repo") + gen.add_file_result("src/index.ts", "safe", 0.9, "pure ts", "heuristic") + gen.add_file_result("src/api/login.ts", "restricted", 1.0, "auth logic", "llm") + print(json.dumps(gen.generate(), indent=2)) diff --git a/safezone-analyzer/backend/analyzer/content_classifier.py b/safezone-analyzer/backend/analyzer/content_classifier.py new file mode 100644 index 0000000..f6446df --- /dev/null +++ b/safezone-analyzer/backend/analyzer/content_classifier.py @@ -0,0 +1,242 @@ +""" +Classify files by analyzing file *content* only (no path/filename-based zones). + +Priority: restricted (high-risk capabilities) → caution (code / agents / ambiguity) → safe (static-only). +""" + +from __future__ import annotations + +import json +import re +from typing import List, Tuple + +from backend.models.schemas import FileInfo, Classification + +# --- Restricted: I/O, secrets, credentials, agents that act on the world --- + +_RESTRICTED: List[Tuple[re.Pattern[str], str]] = [ + (re.compile(r"process\.env\b"), "environment access"), + (re.compile(r"import\.meta\.env"), "environment access"), + (re.compile(r"os\.environ\b"), "environment access"), + (re.compile(r"load_dotenv\b"), "environment / secrets loading"), + (re.compile(r"dotenv\b"), "secrets loading"), + (re.compile(r"-----BEGIN (RSA |OPENSSH |EC )?PRIVATE KEY-----"), "private key material"), + (re.compile(r"\bAKIA[0-9A-Z]{16}\b"), "AWS access key id pattern"), + (re.compile(r"subprocess\."), "subprocess execution"), + (re.compile(r"os\.system\b"), "shell execution"), + (re.compile(r"\beval\s*\("), "dynamic eval"), + (re.compile(r"\bexec\s*\("), "dynamic exec"), + (re.compile(r"child_process\b"), "process execution"), + (re.compile(r"\bfetch\s*\("), "network fetch"), + (re.compile(r"\baxios\b"), "HTTP client"), + (re.compile(r"\bhttpx\b"), "HTTP client"), + (re.compile(r"\brequests\.(get|post|put|delete|patch|session)"), "HTTP client"), + (re.compile(r"urllib\.(request|error)"), "HTTP client"), + (re.compile(r"aiohttp\b"), "async HTTP client"), + (re.compile(r"useSWR\b"), "data fetching hook"), + (re.compile(r"useQuery\b"), "data fetching hook"), + (re.compile(r"\$\.ajax\b"), "HTTP client"), + (re.compile(r"\bprisma\."), "database access"), + (re.compile(r"\bmongoose\b"), "database access"), + (re.compile(r"\bsqlalchemy\b"), "database access"), + (re.compile(r"\bredis\b"), "database / cache client"), + (re.compile(r"\bmysql\b"), "database client"), + (re.compile(r"\bpsycopg2\b"), "database client"), + (re.compile(r"\bsqlite3\b"), "database access"), + (re.compile(r"useSession\b"), "session / auth"), + (re.compile(r"useAuth\b"), "auth"), + (re.compile(r"\bjwt\b"), "tokens / auth"), + (re.compile(r"localStorage\b"), "browser storage"), + (re.compile(r"sessionStorage\b"), "browser storage"), + (re.compile(r"\bopenai\b"), "LLM / agent provider"), + (re.compile(r"\banthropic\b"), "LLM / agent provider"), + (re.compile(r"\blangchain\b"), "agent framework"), + (re.compile(r"\blanggraph\b"), "agent framework"), + (re.compile(r"\blitellm\b"), "LLM routing"), + (re.compile(r"\bChatOpenAI\b"), "LLM client"), + (re.compile(r"\bAgentExecutor\b"), "agent runtime"), + (re.compile(r"create_react_agent\b"), "agent graph"), + (re.compile(r"@tool\b|@router\.tool\b"), "agent tools"), + (re.compile(r"\bMCPClient\b|\bmcp\."), "MCP / tools integration"), + (re.compile(r"\bboto3\b"), "cloud SDK"), + (re.compile(r"\bgoogle\.cloud\b"), "cloud SDK"), + (re.compile(r"azure\.(identity|storage|keyvault)"), "cloud SDK"), +] + +# Key=value lines typical of .env (content only; no filename) +_ENV_ASSIGNMENT = re.compile( + r"^\s*([A-Za-z_][A-Za-z0-9_\s-]{0,128})\s*=\s*\S", + re.MULTILINE, +) + +# --- Caution: executable / integration code without hitting restricted patterns --- + +_CAUTION: List[Tuple[re.Pattern[str], str]] = [ + (re.compile(r"\bconsole\.(log|error|warn|info|debug)\b"), "JS console / script"), + (re.compile(r"^\s*(import|from)\s+\w+", re.MULTILINE), "import statement"), + (re.compile(r"^\s*(export|import)\s+(default\s+)?(async\s+)?(function|class)\b", re.MULTILINE), "JS/TS module"), + (re.compile(r"^\s*def\s+\w+\s*\("), "Python function"), + (re.compile(r"^\s*class\s+\w+"), "Python class"), + (re.compile(r"=>"), "JS arrow / functional code"), + (re.compile(r"\basync\s+function\b"), "async JS"), + (re.compile(r"\brequire\s*\("), "CommonJS require"), + (re.compile(r" List[str]: + found: List[str] = [] + for rx, label in patterns: + if rx.search(text): + found.append(label) + return found + + +def _looks_like_env_file(text: str) -> bool: + if "=" not in text: + return False + matches = list(_ENV_ASSIGNMENT.finditer(text)) + for m in matches: + line_start = text.rfind("\n", 0, m.start()) + 1 + line_end = text.find("\n", m.start()) + line = text[line_start:] if line_end == -1 else text[line_start:line_end] + if "http://" in line or "https://" in line: + continue + return True + return False + + +def _looks_like_static_markup_or_style(text: str) -> bool: + """Heuristic: mostly CSS, or HTML without script handlers, or simple JSON data.""" + t = text.strip() + if not t: + return False + + # JSON config / manifest (no code execution) + if t.startswith("{") or t.startswith("["): + try: + json.loads(t) + return True + except json.JSONDecodeError: + pass + + # CSS (including one-line rules like `body { color: red; }`) + if "= 1: + return True + brace_ratio = t.count("{") + t.count("}") + if brace_ratio >= 4 and ("{" in t) and all( + x not in t for x in ("def ", "class ", "function ", "import ", "from ") + ): + if re.search(r"[@#.][\w-]+\s*\{", t) or "@media" in t: + return True + + # Simple HTML without script + if t.startswith(" bool: + """Markdown without fenced code blocks that imply executable snippets.""" + if "```" not in text: + return bool(re.search(r"^#+\s+\S", text, re.MULTILINE)) + if _MD_FENCE_CODE.search(text): + return False + return bool(re.search(r"^#+\s+\S", text, re.MULTILINE)) + + +def classify_by_content(file: FileInfo) -> Classification: + """ + Classify using only `file.content`. No filename or path rules. + """ + if file.content is None: + return Classification( + zone="restricted", + reason="content not loaded (file too large or unreadable); treat as high risk", + confidence=0.6, + analysis_method="content", + ) + + text = file.content + if not text.strip(): + return Classification( + zone="caution", + reason="empty file", + confidence=0.5, + analysis_method="content", + ) + + details: List[str] = [] + + if _looks_like_env_file(text): + details.append("environment variable assignment pattern") + return Classification( + zone="restricted", + reason="content resembles environment / secrets file", + confidence=0.95, + details=details, + analysis_method="content", + ) + + restricted_labels = _collect_matches(_RESTRICTED, text) + if restricted_labels: + details = sorted(set(restricted_labels)) + return Classification( + zone="restricted", + reason=f"content indicates sensitive capability: {', '.join(details[:6])}" + + ("…" if len(details) > 6 else ""), + confidence=0.92, + details=details, + analysis_method="content", + ) + + caution_labels = _collect_matches(_CAUTION, text) + if caution_labels: + details = sorted(set(caution_labels)) + return Classification( + zone="caution", + reason=f"executable or integration logic detected: {', '.join(details[:5])}" + + ("…" if len(details) > 5 else ""), + confidence=0.78, + details=details, + analysis_method="content", + ) + + if _looks_like_static_markup_or_style(text): + return Classification( + zone="safe", + reason="content looks like static markup, styles, or declarative data only", + confidence=0.82, + analysis_method="content", + ) + + if _looks_like_prose_markdown_only(text): + return Classification( + zone="safe", + reason="markdown prose without executable fenced code blocks", + confidence=0.8, + analysis_method="content", + ) + + # Unknown text: prefer caution over safe (aligns with safety-first defaults) + return Classification( + zone="caution", + reason="could not classify as static-only; possible logic or prompts without matched patterns", + confidence=0.62, + analysis_method="content", + ) diff --git a/safezone-analyzer/backend/analyzer/git_remote.py b/safezone-analyzer/backend/analyzer/git_remote.py new file mode 100644 index 0000000..a0f2360 --- /dev/null +++ b/safezone-analyzer/backend/analyzer/git_remote.py @@ -0,0 +1,50 @@ +"""Clone GitHub repos into a temp directory for one-shot analysis.""" + +from __future__ import annotations + +import re +import shutil +import tempfile +from pathlib import Path +from urllib.parse import urlparse + +import git + + +def looks_like_github_clone_target(s: str) -> bool: + """True if s looks like a GitHub HTTPS/HTTP URL or git@github.com:... clone target.""" + s = s.strip() + if not s: + return False + if re.match(r"^git@github\.com:[^/\s]+/[^/\s]+(\.git)?$", s): + return True + parsed = urlparse(s) + if parsed.scheme not in ("https", "http"): + return False + host = (parsed.hostname or "").lower() + if host not in ("github.com", "www.github.com"): + return False + parts = [p for p in parsed.path.split("/") if p] + return len(parts) >= 2 + + +def clone_github_shallow(remote_url: str) -> tuple[Path, Path]: + """ + Shallow-clone the repo into a new temp directory. + + Returns (repo_root_path, temp_dir_to_delete). + On failure after mkdir, removes the temp dir and re-raises. + """ + tmp = tempfile.mkdtemp(prefix="safezone_clone_") + tmp_path = Path(tmp) + try: + git.Repo.clone_from( + remote_url.strip(), + str(tmp_path), + depth=1, + single_branch=True, + ) + return tmp_path, tmp_path + except Exception: + shutil.rmtree(tmp, ignore_errors=True) + raise diff --git a/safezone-analyzer/backend/analyzer/heuristics.py b/safezone-analyzer/backend/analyzer/heuristics.py new file mode 100644 index 0000000..e683734 --- /dev/null +++ b/safezone-analyzer/backend/analyzer/heuristics.py @@ -0,0 +1,8 @@ +"""Backward-compatible entry: classification is content-driven (see content_classifier).""" + +from backend.analyzer.content_classifier import classify_by_content +from backend.models.schemas import FileInfo, Classification + + +def classify_file(file: FileInfo) -> Classification: + return classify_by_content(file) diff --git a/safezone-analyzer/backend/analyzer/llm_analyzer.py b/safezone-analyzer/backend/analyzer/llm_analyzer.py new file mode 100644 index 0000000..0851cfc --- /dev/null +++ b/safezone-analyzer/backend/analyzer/llm_analyzer.py @@ -0,0 +1,100 @@ +import os +import httpx +from backend.models.schemas import FileInfo, Classification + +class LLMAnalyzer: + def __init__(self): + self.ollama_url = "http://localhost:11434/api/generate" + self.groq_url = "https://api.groq.com/openai/v1/chat/completions" + self.groq_key = os.getenv("GROQ_API_KEY") + + async def analyze_with_llm(self, file: FileInfo, context: dict) -> Classification | None: + """ + Analyzes a file using an LLM. + Returns a Classification if analysis was successful, otherwise None. + """ + if not file.content: + return None + + prompt = self._build_prompt(file, context) + + # Try Ollama first + try: + async with httpx.AsyncClient() as client: + response = await client.post( + self.ollama_url, + json={"model": "llama3.1:8b", "prompt": prompt, "stream": False, "format": "json"}, + timeout=30.0 + ) + if response.status_code == 200: + result = response.json() + return self._parse_llm_response(result["response"]) + except Exception: + pass + + # Fallback to Groq + if self.groq_key: + try: + async with httpx.AsyncClient() as client: + response = await client.post( + self.groq_url, + headers={"Authorization": f"Bearer {self.groq_key}"}, + json={ + "model": "llama-3.1-8b-instant", + "messages": [{"role": "user", "content": prompt}], + "response_format": {"type": "json_object"} + }, + timeout=30.0 + ) + if response.status_code == 200: + result = response.json() + return self._parse_llm_response(result["choices"][0]["message"]["content"]) + except Exception: + pass + + return None + + def _build_prompt(self, file: FileInfo, context: dict) -> str: + return f""" +You are a code security analyzer for a product that lets non-technical team members +modify code in existing projects. Your job is to classify whether a file is safe for +non-technical users to modify, or whether it should be restricted to engineers only. + +Context about the project: +- Framework: {context.get('framework', 'unknown')} +- This file is at: {file.path} +- Neighboring files: {context.get('neighboring_files', [])} + +File content (first 200 lines): +{file.content[:2000]} + +Classify this file into one of three zones: +- SAFE: Non-technical users can modify this freely. Examples: pure CSS, static content, + simple presentational components with no logic. +- CAUTION: Can be modified but needs careful review. Examples: components with some logic, + layout files, configuration for UI tools. +- RESTRICTED: Only engineers should modify. Examples: auth logic, API routes, database + access, environment config, middleware, server-side code. + +Respond in JSON: +{{ + "zone": "safe" | "caution" | "restricted", + "reason": "one sentence explanation", + "risky_patterns": ["list of specific patterns found, if any"], + "safe_sections": ["parts of the file that could be safely modified, if any"] +}} +""" + + def _parse_llm_response(self, response_text: str) -> Classification | None: + import json + try: + data = json.loads(response_text) + return Classification( + zone=data["zone"], + reason=data.get("reason", "No reason provided"), + confidence=0.8, + details=data.get("risky_patterns"), + analysis_method="llm" + ) + except Exception: + return None diff --git a/safezone-analyzer/backend/analyzer/repo.py b/safezone-analyzer/backend/analyzer/repo.py new file mode 100644 index 0000000..79c2a2a --- /dev/null +++ b/safezone-analyzer/backend/analyzer/repo.py @@ -0,0 +1,80 @@ +import os +from pathlib import Path +from typing import List +import git +from backend.models.schemas import FileInfo + +class RepoAnalyzer: + def __init__(self, repo_path: str): + self.repo_path = Path(repo_path).resolve() + self.repo = None + if self.repo_path.exists() and (self.repo_path / ".git").exists(): + self.repo = git.Repo(self.repo_path) + else: + # If it's a URL, we'd need to clone it. + # For now, let's assume it's a local path or we'll handle cloning later. + pass + + def list_files(self) -> List[FileInfo]: + """Walks the repo and produces a list of FileInfo.""" + file_infos = [] + + # Directories to skip + skip_dirs = { + 'node_modules', '.git', 'dist', 'build', '.next', + '__pycache__', 'venv', '.venv', 'target', 'out' + } + + if not self.repo_path.exists(): + raise ValueError(f"Path {self.repo_path} does not exist.") + + for root, dirs, files in os.walk(self.repo_path): + # Prune skip_dirs + dirs[:] = [d for d in dirs if d not in skip_dirs] + + for file in files: + file_path = Path(root) / file + + # Skip binary files based on extension + # (In a real implementation, we might use magic numbers) + skip_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf', '.woff', '.woff2', '.ttf'} + if file_path.suffix.lower() in skip_extensions: + continue + + relative_path = file_path.relative_to(self.repo_path) + + try: + size = file_path.stat().st_size + content = None + # Load content lazily for small files + if size < 256 * 1024: + with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: + content = f.read() + + file_infos.append(FileInfo( + path=str(relative_path), + extension=file_path.suffix, + directory=str(relative_path.parent), + filename=file, + size_bytes=size, + content=content + )) + except Exception as e: + print(f"Error processing {file_path}: {mask_error(e)}") + + return file_infos + +def mask_error(e: Exception) -> str: + return str(e) + +if __name__ == "__main__": + # Simple test + import sys + if len(sys.argv) > 1: + analyzer = RepoAnalyzer(sys.argv[1]) + files = analyzer.list_files() + print(f"Found {len(files)} files.") + if files: + print(f"First file: {files[0].path}") + else: + print("Usage: python backend/analyzer/repo.py ") diff --git a/safezone-analyzer/backend/db/__init__.py b/safezone-analyzer/backend/db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/safezone-analyzer/backend/db/cache.py b/safezone-analyzer/backend/db/cache.py new file mode 100644 index 0000000..8f54b39 --- /dev/null +++ b/safezone-analyzer/backend/db/cache.py @@ -0,0 +1,53 @@ +import sqlite3 +import json +from datetime import datetime +from typing import Optional, Dict, Any, List + +class AnalysisCache: + def __init__(self, db_path: str = "backend/db/cache.db"): + self.db_path = db_path + self._init_db() + + def _init_db(self): + with sqlite3.connect(self.db_path) as conn: + conn.execute(""" + CREATE TABLE IF NOT EXISTS results ( + analysis_id TEXT PRIMARY KEY, + repo_url TEXT, + analyzed_at TEXT, + data TEXT, + status TEXT + ) + """) + + def save_analysis(self, analysis_id: str, repo_url: str, data: Dict[str, Any], status: str = "completed"): + with sqlite3.connect(self.db_path) as conn: + conn.execute( + "INSERT OR REPLACE INTO results (analysis_id, repo_url, analyzed_at, data, status) VALUES (?, ?, ?, ?, ?)", + (analysis_id, repo_url, datetime.utcnow().isoformat(), json.dumps(data), status) + ) + + def get_analysis(self, analysis_id: str) -> Optional[Dict[str, Any]]: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.execute("SELECT data FROM results WHERE analysis_id = ?", (analysis_id,)) + row = cursor.fetchone() + if row and row[0] is not None: + return json.loads(row[0]) + return None + + def update_status(self, analysis_id: str, status: str): + """Ensure a row exists so polling sees in_progress / failed before data is written.""" + with sqlite3.connect(self.db_path) as conn: + conn.execute( + """ + INSERT INTO results (analysis_id, repo_url, analyzed_at, data, status) + VALUES (?, '', ?, NULL, ?) + ON CONFLICT(analysis_id) DO UPDATE SET status = excluded.status + """, + (analysis_id, datetime.utcnow().isoformat(), status), + ) + + def get_all_analyses(self) -> List[Dict[str, Any]]: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.execute("SELECT analysis_id, repo_url, analyzed_at, status FROM results") + return [{"analysis_id": row[0], "repo_url": row[1], "analyzed_at": row[2], "status": row[3]} for row in cursor.fetchall()] diff --git a/safezone-analyzer/backend/main.py b/safezone-analyzer/backend/main.py new file mode 100644 index 0000000..47a7d61 --- /dev/null +++ b/safezone-analyzer/backend/main.py @@ -0,0 +1,105 @@ +import shutil +import sqlite3 +import uuid +from pathlib import Path + +from fastapi import BackgroundTasks, FastAPI, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from pydantic import BaseModel + +from backend.analyzer.classifier import Classifier +from backend.analyzer.config_generator import ConfigGenerator +from backend.analyzer.git_remote import clone_github_shallow, looks_like_github_clone_target +from backend.db.cache import AnalysisCache + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +cache = AnalysisCache() + +async def run_analysis_task(analysis_id: str, repo_url: str): + raw = repo_url.strip() + temp_clone: Path | None = None + analysis_path = raw + + try: + cache.update_status(analysis_id, "in_progress") + + if looks_like_github_clone_target(raw): + clone_root, temp_clone = clone_github_shallow(raw) + analysis_path = str(clone_root) + else: + analysis_path = str(Path(raw).resolve()) + + classifier = Classifier(analysis_path) + results = await classifier.classify_all() + + generator = ConfigGenerator(raw) + for res in results: + generator.add_file_result( + path=res["path"], + zone=res["zone"], + confidence=res["confidence"], + reason=res["reason"], + analysis_method=res["analysis_method"], + details=res["details"] + ) + + analysis_data = generator.generate() + cache.save_analysis(analysis_id, raw, analysis_data, "completed") + except Exception as e: + print(f"Error in analysis task: {e}") + cache.update_status(analysis_id, "failed") + finally: + if temp_clone is not None and temp_clone.exists(): + shutil.rmtree(temp_clone, ignore_errors=True) + +class AnalyzeRequest(BaseModel): + repo_url: str + +class AnalysisResponse(BaseModel): + analysis_id: str + +@app.post("/analyze", response_model=AnalysisResponse) +async def analyze(request: AnalyzeRequest, background_tasks: BackgroundTasks): + analysis_id = str(uuid.uuid4()) + background_tasks.add_task(run_analysis_task, analysis_id, request.repo_url) + return {"analysis_id": analysis_id} + +@app.get("/analyze/{analysis_id}") +async def get_analysis(analysis_id: str): + data = cache.get_analysis(analysis_id) + if not data: + # Check if it exists but is still in progress + with sqlite3.connect(cache.db_path) as conn: + cursor = conn.execute("SELECT status FROM results WHERE analysis_id = ?", (analysis_id,)) + row = cursor.fetchone() + if row: + return {"status": row[0], "data": None} + raise HTTPException(status_code=404, detail="Analysis not found") + return {"status": "completed", "data": data} + +@app.get("/analyze/{analysis_id}/tree") +async def get_analysis_tree(analysis_id: str): + data = cache.get_analysis(analysis_id) + if not data: + raise HTTPException(status_code=404, detail="Analysis not found") + + # In a real implementation, we would transform the file_details into a tree structure. + # For now, we return the flat list for the treemap. + return data.get("file_details", []) + +@app.get("/health") +async def health(): + return {"status": "ok"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/safezone-analyzer/backend/models/__init__.py b/safezone-analyzer/backend/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/safezone-analyzer/backend/models/schemas.py b/safezone-analyzer/backend/models/schemas.py new file mode 100644 index 0000000..30e767f --- /dev/null +++ b/safezone-analyzer/backend/models/schemas.py @@ -0,0 +1,26 @@ +from pydantic import BaseModel +from typing import List, Optional, Dict + +class FileInfo(BaseModel): + path: str # "src/components/Button.tsx" + extension: str # ".tsx" + directory: str # "src/components" + filename: str # "Button.tsx" + size_bytes: int + content: Optional[str] = None # loaded lazily, only for files < 50KB + +class Classification(BaseModel): + zone: str # "safe" | "caution" | "restricted" + reason: str # explanation + confidence: float # 0.0 to 1.0 + details: Optional[List[str]] = None + analysis_method: Optional[str] = None # "heuristic" | "ast" | "llm" + +class AnalysisResult(BaseModel): + repo: str + analyzed_at: str + framework: Optional[str] = None + total_files: int + summary: Dict[str, int] + zones: Dict[str, Dict] + file_details: List[Dict] diff --git a/safezone-analyzer/frontend/.env.example b/safezone-analyzer/frontend/.env.example new file mode 100644 index 0000000..3b500dd --- /dev/null +++ b/safezone-analyzer/frontend/.env.example @@ -0,0 +1,5 @@ +# Copy to .env if needed. Usually leave PUBLIC_API_URL unset: Vite proxies /api to the backend. +# Only set a full URL if port 8000 is open and you want the browser to call the API directly. +# No trailing slash. +# +# PUBLIC_API_URL=http://YOUR_VM_PUBLIC_IP:8000 diff --git a/safezone-analyzer/frontend/.gitignore b/safezone-analyzer/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/safezone-analyzer/frontend/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/safezone-analyzer/frontend/.npmrc b/safezone-analyzer/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/safezone-analyzer/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/safezone-analyzer/frontend/.vscode/extensions.json b/safezone-analyzer/frontend/.vscode/extensions.json new file mode 100644 index 0000000..28d1e67 --- /dev/null +++ b/safezone-analyzer/frontend/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["svelte.svelte-vscode"] +} diff --git a/safezone-analyzer/frontend/README.md b/safezone-analyzer/frontend/README.md new file mode 100644 index 0000000..0c0a658 --- /dev/null +++ b/safezone-analyzer/frontend/README.md @@ -0,0 +1,42 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project +npx sv create my-app +``` + +To recreate this project with the same configuration: + +```sh +# recreate this project +npx sv@0.15.1 create --template minimal --types jsdoc --no-install . +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/safezone-analyzer/frontend/jsconfig.json b/safezone-analyzer/frontend/jsconfig.json new file mode 100644 index 0000000..0b2d886 --- /dev/null +++ b/safezone-analyzer/frontend/jsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/safezone-analyzer/frontend/package-lock.json b/safezone-analyzer/frontend/package-lock.json new file mode 100644 index 0000000..c7373f2 --- /dev/null +++ b/safezone-analyzer/frontend/package-lock.json @@ -0,0 +1,1867 @@ +{ + "name": "frontend", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.1", + "dependencies": { + "d3": "^7.9.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/kit": "^2.57.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "svelte": "^5.55.2", + "svelte-check": "^4.4.6", + "typescript": "^6.0.2", + "vite": "^8.0.7" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.124.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.124.0.tgz", + "integrity": "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.15.tgz", + "integrity": "sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.15.tgz", + "integrity": "sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.15.tgz", + "integrity": "sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.15.tgz", + "integrity": "sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.15.tgz", + "integrity": "sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.15.tgz", + "integrity": "sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.15.tgz", + "integrity": "sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", + "integrity": "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", + "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-7.0.1.tgz", + "integrity": "sha512-dvuPm1E7M9NI/+canIQ6KKQDU2AkEefEZ2Dp7cY6uKoPq9Z/PhOXABe526UdW2mN986gjVkuSLkOYIBnS/M2LQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^2.0.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "2.57.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.57.1.tgz", + "integrity": "sha512-VRdSbB96cI1EnRh09CqmnQqP/YJvET5buj8S6k7CxaJqBJD4bw4fRKDjcarAj/eX9k2eHifQfDH8NtOh+ZxxPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/cookie": "^0.6.0", + "acorn": "^8.14.1", + "cookie": "^0.6.0", + "devalue": "^5.6.4", + "esm-env": "^1.2.2", + "kleur": "^4.1.5", + "magic-string": "^0.30.5", + "mrmime": "^2.0.0", + "set-cookie-parser": "^3.0.0", + "sirv": "^3.0.0" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=18.13" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.3.3 || ^6.0.0", + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", + "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.7.1.tgz", + "integrity": "sha512-MUbZ586EgQqdRnC4yDrlod3BEdyvE4TapGYHMW2CiaW+KkkFmWEFqBUaLltEZCGi0iFXCEjRF0OjF0DV2QHjOA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.5.tgz", + "integrity": "sha512-/yLB1538mag+dn0wsePTe8C0rDIjUOaJpMs2McodSzmM2msWcZsBSdRtg6HOBt0A/r82BN+Md3pgwSc/uWt2Ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", + "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.15.tgz", + "integrity": "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.124.0", + "@rolldown/pluginutils": "1.0.0-rc.15" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", + "@rolldown/binding-darwin-x64": "1.0.0-rc.15", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/set-cookie-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", + "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/svelte": { + "version": "5.55.4", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.4.tgz", + "integrity": "sha512-q8DFohk6vUswSng95IZb9nzWJnbINZsK7OiM1snAa3qCjJBL0ZQpvMyAaVXjUukdM75J/m8UE8xwqat8Ors/zQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@sveltejs/acorn-typescript": "^1.0.5", + "@types/estree": "^1.0.5", + "@types/trusted-types": "^2.0.7", + "acorn": "^8.12.1", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.6.4", + "esm-env": "^1.2.1", + "esrap": "^2.2.4", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.6.tgz", + "integrity": "sha512-kP1zG81EWaFe9ZyTv4ZXv44Csi6Pkdpb7S3oj6m+K2ec/IcDg/a8LsFsnVLqm2nxtkSwsd5xPj/qFkTBgXHXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": ">=5.0.0" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/vite": { + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.8.tgz", + "integrity": "sha512-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.15", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", + "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/safezone-analyzer/frontend/package.json b/safezone-analyzer/frontend/package.json new file mode 100644 index 0000000..d704fe5 --- /dev/null +++ b/safezone-analyzer/frontend/package.json @@ -0,0 +1,28 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "dev:public": "vite dev --host 0.0.0.0", + "build": "vite build", + "preview": "vite preview", + "preview:public": "vite preview --host 0.0.0.0", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.1", + "@sveltejs/kit": "^2.57.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "svelte": "^5.55.2", + "svelte-check": "^4.4.6", + "typescript": "^6.0.2", + "vite": "^8.0.7" + }, + "dependencies": { + "d3": "^7.9.0" + } +} diff --git a/safezone-analyzer/frontend/src/app.d.ts b/safezone-analyzer/frontend/src/app.d.ts new file mode 100644 index 0000000..b6d2fd6 --- /dev/null +++ b/safezone-analyzer/frontend/src/app.d.ts @@ -0,0 +1,18 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces + +interface ImportMetaEnv { + readonly PUBLIC_API_URL?: string; +} + +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/safezone-analyzer/frontend/src/app.html b/safezone-analyzer/frontend/src/app.html new file mode 100644 index 0000000..6a2bb58 --- /dev/null +++ b/safezone-analyzer/frontend/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/safezone-analyzer/frontend/src/lib/TreeMap.svelte b/safezone-analyzer/frontend/src/lib/TreeMap.svelte new file mode 100644 index 0000000..3127144 --- /dev/null +++ b/safezone-analyzer/frontend/src/lib/TreeMap.svelte @@ -0,0 +1,178 @@ + + +
+ {#if data.length === 0} +
No data loaded. Enter a URL to begin.
+ {:else} + + {/if} +
+ + diff --git a/safezone-analyzer/frontend/src/lib/api.ts b/safezone-analyzer/frontend/src/lib/api.ts new file mode 100644 index 0000000..cce005b --- /dev/null +++ b/safezone-analyzer/frontend/src/lib/api.ts @@ -0,0 +1,37 @@ +/** + * - Default `/api`: Vite proxies to FastAPI on 127.0.0.1:8000 so browsers only need the UI port open. + * - Set PUBLIC_API_URL (e.g. http://VM:8000) only if the API is reachable from the browser on that host. + */ +function apiBase(): string { + const raw = typeof import.meta.env.PUBLIC_API_URL === 'string' ? import.meta.env.PUBLIC_API_URL.trim() : ''; + if (raw) return raw.replace(/\/$/, ''); + return '/api'; +} + +const API_BASE = apiBase(); + +export async function startAnalysis(repoUrl: string) { + const response = await fetch(`${API_BASE}/analyze`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ repo_url: repoUrl }), + }); + return await response.json(); +} + +export async function getAnalysis(analysisId: string) { + const response = await fetch(`${API_BASE}/analyze/${analysisId}`); + return await response.json(); +} + +export async function getAnalysisTree(analysisId: string) { + const response = await fetch(`${API_BASE}/analyze/${analysisId}/tree`); + return await response.json(); +} + +export async function getHealth() { + const response = await fetch(`${API_BASE}/health`); + return await response.json(); +} diff --git a/safezone-analyzer/frontend/src/lib/assets/favicon.svg b/safezone-analyzer/frontend/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/safezone-analyzer/frontend/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/safezone-analyzer/frontend/src/lib/index.js b/safezone-analyzer/frontend/src/lib/index.js new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/safezone-analyzer/frontend/src/lib/index.js @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/safezone-analyzer/frontend/src/routes/+layout.svelte b/safezone-analyzer/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..5c4f0f7 --- /dev/null +++ b/safezone-analyzer/frontend/src/routes/+layout.svelte @@ -0,0 +1,11 @@ + + + + + + +{@render children()} diff --git a/safezone-analyzer/frontend/src/routes/+page.svelte b/safezone-analyzer/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..053a949 --- /dev/null +++ b/safezone-analyzer/frontend/src/routes/+page.svelte @@ -0,0 +1,674 @@ + + +
+ + +
+
+
+
+

Safe Zone Analyzer

+

Classify repository files into permission zones for AI-assisted editing

+
+ {#if status !== 'idle'} +
+ {#if status === 'analyzing'} + + {/if} + {status.toUpperCase()} +
+ {/if} +
+
+
+ 📁 + e.key === 'Enter' && handleStartAnalysis()} + /> +
+ +
+ {#if errorMessage} +
{errorMessage}
+ {/if} +
+ +
+ {#if status === 'analyzing' || status === 'completed' || status === 'failed'} +
+
+
+

File Treemap

+ {fileDetails.length} files +
+
+ {#if analysisData && fileDetails.length > 0} + + {:else if status === 'analyzing'} +
+
+ +
+

Scanning and classifying files...

+
+ {/if} +
+
+ + +
+ {:else} +
+
🛡
+

Analyze a Repository

+

Enter a repository path above to classify files into safe, caution, and restricted zones.

+
+ {/if} +
+
+
+ + diff --git a/safezone-analyzer/frontend/static/robots.txt b/safezone-analyzer/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/safezone-analyzer/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/safezone-analyzer/frontend/svelte.config.js b/safezone-analyzer/frontend/svelte.config.js new file mode 100644 index 0000000..0c3412e --- /dev/null +++ b/safezone-analyzer/frontend/svelte.config.js @@ -0,0 +1,17 @@ +import adapter from '@sveltejs/adapter-auto'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true) + }, + kit: { + // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. + // If your environment is not supported, or you settled on a specific environment, switch out the adapter. + // See https://svelte.dev/docs/kit/adapters for more information about adapters. + adapter: adapter() + } +}; + +export default config; diff --git a/safezone-analyzer/frontend/vite.config.js b/safezone-analyzer/frontend/vite.config.js new file mode 100644 index 0000000..5083fa3 --- /dev/null +++ b/safezone-analyzer/frontend/vite.config.js @@ -0,0 +1,21 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +/** Browser hits same origin (e.g. :5173); only that port needs to be public. */ +const apiProxy = { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, '') + } +}; + +export default defineConfig({ + plugins: [sveltekit()], + server: { + proxy: apiProxy + }, + preview: { + proxy: apiProxy + } +}); diff --git a/safezone-analyzer/run_backend.sh b/safezone-analyzer/run_backend.sh new file mode 100755 index 0000000..aab2ec3 --- /dev/null +++ b/safezone-analyzer/run_backend.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export PYTHONPATH=. +echo "Starting Backend on http://localhost:8000" +uvicorn backend.main:app --host 0.0.0.0 --port 8000 diff --git a/safezone-analyzer/test_backend_api.py b/safezone-analyzer/test_backend_api.py new file mode 100644 index 0000000..afd5141 --- /dev/null +++ b/safezone-analyzer/test_backend_api.py @@ -0,0 +1,57 @@ +import httpx +import asyncio + +async def test_api(): + base_url = "http://localhost:8000" + + print(f"--- Testing Backend API at {base_url} ---") + + # 1. Test Health + try: + async with httpx.AsyncClient() as client: + resp = await client.get(f"{base_url}/health") + print(f"Health check: {resp.status_code} -> {resp.json()}") + except Exception as e: + print(f"Health check failed: {e}") + return + + # 2. Test Analysis Trigger + try: + async with httpx.AsyncClient() as client: + print("\nTriggering analysis for 'test_repo'...") + resp = await client.post( + f"{base_url}/analyze", + json={"repo_url": "test_repo"} + ) + if resp.status_code != 200: + print(f"Failed to trigger analysis: {resp.text}") + return + + analysis_id = resp.json()["analysis_id"] + print(f"Analysis started. ID: {analysis_id}") + + # 3. Poll for results + print("Polling for results (this may take a few seconds)...") + for _ in range(20): + await asyncio.sleep(2) + resp = await client.get(f"{base_url}/analyze/{analysis_id}") + res_json = resp.json() + status = res_json.get("status", "unknown") + print(f"Status: {status}") + + if status == "completed": + print("Analysis Complete!") + data = res_json.get("data", {}) + print(f"Summary: {data.get('summary')}") + break + elif status == "failed": + print("Analysis Failed!") + break + else: + print("Polling timed out.") + + except Exception as e: + print(f"Test failed: {e}") + +if __name__ == "__main__": + asyncio.run(test_api()) diff --git a/safezone-analyzer/test_repo/.env.example b/safezone-analyzer/test_repo/.env.example new file mode 100644 index 0000000..bf333ed --- /dev/null +++ b/safezone-analyzer/test_repo/.env.example @@ -0,0 +1,2 @@ +# Copy to .env for local testing. Do not commit .env. +API_KEY=your_key_here diff --git a/safezone-analyzer/test_repo/README.md b/safezone-analyzer/test_repo/README.md new file mode 100644 index 0000000..e69de29 diff --git a/safezone-analyzer/test_repo/public/config.json b/safezone-analyzer/test_repo/public/config.json new file mode 100644 index 0000000..76519fa --- /dev/null +++ b/safezone-analyzer/test_repo/public/config.json @@ -0,0 +1 @@ +{"key": "value"} diff --git a/safezone-analyzer/test_repo/src/components/Button.tsx b/safezone-analyzer/test_repo/src/components/Button.tsx new file mode 100644 index 0000000..e921523 --- /dev/null +++ b/safezone-analyzer/test_repo/src/components/Button.tsx @@ -0,0 +1 @@ +console.log('hello'); diff --git a/safezone-analyzer/test_repo/src/components/RiskyButton.tsx b/safezone-analyzer/test_repo/src/components/RiskyButton.tsx new file mode 100644 index 0000000..820626d --- /dev/null +++ b/safezone-analyzer/test_repo/src/components/RiskyButton.tsx @@ -0,0 +1 @@ +fetch('/api/data'); diff --git a/safezone-analyzer/test_repo/src/styles.css b/safezone-analyzer/test_repo/src/styles.css new file mode 100644 index 0000000..991912d --- /dev/null +++ b/safezone-analyzer/test_repo/src/styles.css @@ -0,0 +1 @@ +body { color: red; } diff --git a/safezone-analyzer/test_repo/unknown.unknown b/safezone-analyzer/test_repo/unknown.unknown new file mode 100644 index 0000000..e69de29