From a0b2772cb50edc6b2494507bc8f6eb6672132b80 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:22:47 +0530 Subject: [PATCH 01/18] feat: GitHub OAuth login + dev-env fixes (port 9000, relative REPOS_DIR) --- .gitignore | 35 +- backend/apps/auth_github/__init__.py | 0 backend/apps/auth_github/apps.py | 5 + backend/apps/auth_github/crypto.py | 21 ++ .../auth_github/migrations/0001_initial.py | 32 ++ .../apps/auth_github/migrations/__init__.py | 0 backend/apps/auth_github/models.py | 21 ++ backend/apps/auth_github/tests/__init__.py | 0 backend/apps/auth_github/tests/test_crypto.py | 42 +++ .../tests/test_github_repos_view.py | 193 +++++++++++ .../auth_github/tests/test_oauth_callback.py | 173 ++++++++++ backend/apps/auth_github/urls.py | 19 ++ backend/apps/auth_github/views.py | 316 ++++++++++++++++++ backend/apps/chat/views.py | 17 +- backend/apps/files/views.py | 11 + backend/apps/graph/views.py | 82 ++++- ...ested_by_repository_is_private_and_more.py | 47 +++ backend/apps/repos/models.py | 31 ++ backend/apps/repos/tasks.py | 146 +++++++- backend/apps/repos/tests/__init__.py | 0 backend/apps/repos/tests/conftest.py | 49 +++ backend/apps/repos/tests/test_attach.py | 132 ++++++++ .../apps/repos/tests/test_cross_app_gating.py | 130 +++++++ .../apps/repos/tests/test_cross_repo_idor.py | 206 ++++++++++++ backend/apps/repos/tests/test_redact.py | 70 ++++ .../apps/repos/tests/test_repository_view.py | 267 +++++++++++++++ backend/apps/repos/urls.py | 3 +- backend/apps/repos/utils.py | 48 +++ backend/apps/repos/views.py | 297 +++++++++++++++- backend/conftest.py | 4 + backend/core/settings.py | 65 +++- backend/core/test_settings.py | 254 ++++++++++++++ backend/core/test_urls.py | 17 + backend/core/urls.py | 1 + backend/env.local | 14 +- backend/manage.py | 4 + backend/pytest.ini | 5 + backend/requirements.txt | 2 + frontend/src/App.tsx | 32 +- frontend/src/api/index.ts | 42 ++- frontend/src/auth/AuthContext.tsx | 74 ++++ frontend/src/components/GithubRepoPicker.tsx | 128 +++++++ frontend/src/components/Landing.tsx | 50 ++- frontend/src/components/Login.tsx | 42 +++ frontend/src/components/RepoSwitcher.tsx | 83 ++++- frontend/src/index.css | 126 +++++++ frontend/src/main.tsx | 5 +- frontend/src/types/index.ts | 17 + frontend/vite.config.js | 2 +- 49 files changed, 3287 insertions(+), 73 deletions(-) create mode 100644 backend/apps/auth_github/__init__.py create mode 100644 backend/apps/auth_github/apps.py create mode 100644 backend/apps/auth_github/crypto.py create mode 100644 backend/apps/auth_github/migrations/0001_initial.py create mode 100644 backend/apps/auth_github/migrations/__init__.py create mode 100644 backend/apps/auth_github/models.py create mode 100644 backend/apps/auth_github/tests/__init__.py create mode 100644 backend/apps/auth_github/tests/test_crypto.py create mode 100644 backend/apps/auth_github/tests/test_github_repos_view.py create mode 100644 backend/apps/auth_github/tests/test_oauth_callback.py create mode 100644 backend/apps/auth_github/urls.py create mode 100644 backend/apps/auth_github/views.py create mode 100644 backend/apps/repos/migrations/0002_repository_first_ingested_by_repository_is_private_and_more.py create mode 100644 backend/apps/repos/tests/__init__.py create mode 100644 backend/apps/repos/tests/conftest.py create mode 100644 backend/apps/repos/tests/test_attach.py create mode 100644 backend/apps/repos/tests/test_cross_app_gating.py create mode 100644 backend/apps/repos/tests/test_cross_repo_idor.py create mode 100644 backend/apps/repos/tests/test_redact.py create mode 100644 backend/apps/repos/tests/test_repository_view.py create mode 100644 backend/apps/repos/utils.py create mode 100644 backend/conftest.py create mode 100644 backend/core/test_settings.py create mode 100644 backend/core/test_urls.py create mode 100644 backend/pytest.ini create mode 100644 frontend/src/auth/AuthContext.tsx create mode 100644 frontend/src/components/GithubRepoPicker.tsx create mode 100644 frontend/src/components/Login.tsx diff --git a/.gitignore b/.gitignore index a3c6a88..4469016 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,43 @@ .env +*.env +!env.local +!local.env + __pycache__/ *.pyc *.pyo +*.pyd +.Python + .DS_Store + node_modules/ dist/ *.egg-info/ + .venv/ -venv/ \ No newline at end of file +venv/ +.testvenv/ + +.claude/ + +.pytest_cache/ +.ruff_cache/ +.mypy_cache/ +.coverage +coverage.xml +htmlcov/ + +*.sqlite3 +*.sqlite3-journal + +*.log + +/repos/ +.repos/ + +.idea/ +.vscode/ + +*.tsbuildinfo +.vite/ diff --git a/backend/apps/auth_github/__init__.py b/backend/apps/auth_github/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/auth_github/apps.py b/backend/apps/auth_github/apps.py new file mode 100644 index 0000000..d1c5ed5 --- /dev/null +++ b/backend/apps/auth_github/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class AuthGithubConfig(AppConfig): + name = "apps.auth_github" diff --git a/backend/apps/auth_github/crypto.py b/backend/apps/auth_github/crypto.py new file mode 100644 index 0000000..47acc4e --- /dev/null +++ b/backend/apps/auth_github/crypto.py @@ -0,0 +1,21 @@ +from cryptography.fernet import Fernet +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured + + +def _fernet() -> Fernet: + key = settings.GITHUB_TOKEN_ENC_KEY + if not key: + raise ImproperlyConfigured( + "GITHUB_TOKEN_ENC_KEY is required. Generate with: " + "python -c \"from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())\"" + ) + return Fernet(key.encode() if isinstance(key, str) else key) + + +def encrypt(plain: str) -> str: + return _fernet().encrypt(plain.encode("utf-8")).decode("utf-8") + + +def decrypt(enc: str) -> str: + return _fernet().decrypt(enc.encode("utf-8")).decode("utf-8") diff --git a/backend/apps/auth_github/migrations/0001_initial.py b/backend/apps/auth_github/migrations/0001_initial.py new file mode 100644 index 0000000..01f8a56 --- /dev/null +++ b/backend/apps/auth_github/migrations/0001_initial.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.11 on 2026-05-10 13:23 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='GitHubIdentity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('github_user_id', models.BigIntegerField(db_index=True, unique=True)), + ('login', models.CharField(max_length=255)), + ('avatar_url', models.URLField(blank=True, default='')), + ('access_token_enc', models.TextField()), + ('scopes', models.TextField(blank=True, default='')), + ('needs_reauth', models.BooleanField(default=False)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='github_identity', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/backend/apps/auth_github/migrations/__init__.py b/backend/apps/auth_github/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/auth_github/models.py b/backend/apps/auth_github/models.py new file mode 100644 index 0000000..d111361 --- /dev/null +++ b/backend/apps/auth_github/models.py @@ -0,0 +1,21 @@ +from django.conf import settings +from django.db import models + + +class GitHubIdentity(models.Model): + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="github_identity", + ) + github_user_id = models.BigIntegerField(unique=True, db_index=True) + login = models.CharField(max_length=255) + avatar_url = models.URLField(blank=True, default="") + access_token_enc = models.TextField() + scopes = models.TextField(blank=True, default="") + needs_reauth = models.BooleanField(default=False) + updated_at = models.DateTimeField(auto_now=True) + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"gh:{self.login}" diff --git a/backend/apps/auth_github/tests/__init__.py b/backend/apps/auth_github/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/auth_github/tests/test_crypto.py b/backend/apps/auth_github/tests/test_crypto.py new file mode 100644 index 0000000..447ca5c --- /dev/null +++ b/backend/apps/auth_github/tests/test_crypto.py @@ -0,0 +1,42 @@ +"""Tests for apps.auth_github.crypto (item 1).""" +import pytest +from cryptography.fernet import InvalidToken +from django.core.exceptions import ImproperlyConfigured +from django.test import override_settings + +from apps.auth_github.crypto import decrypt, encrypt + + +class TestCryptoRoundTrip: + def test_encrypt_decrypt_round_trip(self): + plain = "ghp_AbCdEf123456789_synthetic_token" + token = encrypt(plain) + assert token != plain + assert decrypt(token) == plain + + def test_encrypt_round_trip_unicode(self): + plain = "tokén-with-üñîçødé" + assert decrypt(encrypt(plain)) == plain + + def test_decrypt_tampered_ciphertext_raises(self): + token = encrypt("synthetic_token_for_tamper_test") + # flip a byte near the end of the ciphertext + tampered = token[:-3] + ("A" if token[-3:] != "AAA" else "B") + token[-2:] + with pytest.raises(InvalidToken): + decrypt(tampered) + + def test_decrypt_garbage_raises(self): + with pytest.raises(Exception): + decrypt("not-a-valid-fernet-token") + + def test_missing_enc_key_raises_improperly_configured(self): + with override_settings(GITHUB_TOKEN_ENC_KEY=""): + with pytest.raises(ImproperlyConfigured): + encrypt("anything") + with pytest.raises(ImproperlyConfigured): + decrypt("anything") + + def test_none_enc_key_raises_improperly_configured(self): + with override_settings(GITHUB_TOKEN_ENC_KEY=None): + with pytest.raises(ImproperlyConfigured): + encrypt("anything") diff --git a/backend/apps/auth_github/tests/test_github_repos_view.py b/backend/apps/auth_github/tests/test_github_repos_view.py new file mode 100644 index 0000000..dc5bb15 --- /dev/null +++ b/backend/apps/auth_github/tests/test_github_repos_view.py @@ -0,0 +1,193 @@ +"""Tests for /api/github/repos/, /api/me/, /api/auth/logout/, /api/auth/csrf/ +(items 8, 9, 12, 13).""" +from unittest.mock import MagicMock, patch + +import pytest +from django.contrib.auth.models import User +from django.core.cache import cache +from django.test import Client + +from apps.auth_github.crypto import encrypt +from apps.auth_github.models import GitHubIdentity + + +@pytest.fixture(autouse=True) +def _clear_cache_each_test(): + """Prevent the GithubReposView's response cache from leaking across tests. + Cache keys are `gh_repos:{user_id}:...` and SQLite IDs can repeat after + a per-test rollback.""" + cache.clear() + yield + cache.clear() + + +def _gh_response(status_code, json_payload=None, headers=None): + r = MagicMock() + r.status_code = status_code + r.json.return_value = json_payload or [] + r.headers = headers or {} + return r + + +@pytest.fixture +def make_authed_user(db): + counter = {"i": 0} + + def make(with_identity=True, needs_reauth=False, token="gho_test_token"): + counter["i"] += 1 + u = User.objects.create_user(username=f"u{counter['i']}", password="pw") + if with_identity: + GitHubIdentity.objects.create( + user=u, + github_user_id=42_000 + counter["i"], + login=u.username, + access_token_enc=encrypt(token), + scopes="repo", + needs_reauth=needs_reauth, + ) + c = Client() + c.force_login(u) + return u, c + + return make + + +@pytest.mark.django_db +class TestGithubReposView: + def test_anonymous_returns_401_or_403(self): + resp = Client().get("/api/github/repos/") + assert resp.status_code in (401, 403) + + def test_authed_without_identity_returns_401(self, make_authed_user): + _, c = make_authed_user(with_identity=False) + resp = c.get("/api/github/repos/") + assert resp.status_code == 401 + assert resp.json() == {"needs_reauth": True} + + def test_authed_with_identity_returns_trimmed_repo_list(self, make_authed_user): + _, c = make_authed_user() + github_payload = [ + { + "id": 1, + "name": "alpha", + "full_name": "user/alpha", + "html_url": "https://github.com/user/alpha", + "private": False, + "pushed_at": "2026-01-01T00:00:00Z", + "default_branch": "main", + "extraneous": "should-be-dropped", + }, + { + "id": 2, + "name": "beta", + "full_name": "user/beta", + "html_url": "https://github.com/user/beta", + "private": True, + "pushed_at": None, + "default_branch": "dev", + }, + ] + with patch("apps.auth_github.views.requests.get", return_value=_gh_response(200, github_payload)): + resp = c.get("/api/github/repos/") + assert resp.status_code == 200 + data = resp.json() + assert len(data) == 2 + assert set(data[0].keys()) == { + "id", "name", "full_name", "html_url", "private", "pushed_at", "default_branch", + } + assert data[0]["id"] == 1 + assert data[1]["private"] is True + assert data[1]["default_branch"] == "dev" + + def test_github_401_flips_needs_reauth_and_returns_401(self, make_authed_user): + u, c = make_authed_user() + with patch("apps.auth_github.views.requests.get", return_value=_gh_response(401)): + resp = c.get("/api/github/repos/") + assert resp.status_code == 401 + assert resp.json() == {"needs_reauth": True} + u.refresh_from_db() + assert u.github_identity.needs_reauth is True + + def test_user_with_needs_reauth_flag_short_circuits_401(self, make_authed_user): + _, c = make_authed_user(needs_reauth=True) + # No HTTP mock needed; should not even call requests.get + with patch("apps.auth_github.views.requests.get") as mock_get: + resp = c.get("/api/github/repos/") + assert resp.status_code == 401 + assert resp.json() == {"needs_reauth": True} + mock_get.assert_not_called() + + def test_rate_limit_429_returns_503_with_retry_after(self, make_authed_user): + _, c = make_authed_user() + with patch( + "apps.auth_github.views.requests.get", + return_value=_gh_response(429, headers={"Retry-After": "60"}), + ): + resp = c.get("/api/github/repos/") + assert resp.status_code == 503 + assert resp["Retry-After"] == "60" + + def test_rate_limit_403_returns_503(self, make_authed_user): + _, c = make_authed_user() + with patch( + "apps.auth_github.views.requests.get", + return_value=_gh_response(403, headers={"Retry-After": "30"}), + ): + resp = c.get("/api/github/repos/") + assert resp.status_code == 503 + assert resp["Retry-After"] == "30" + + +@pytest.mark.django_db +class TestMeView: + def test_anonymous_me_returns_401_or_403(self): + resp = Client().get("/api/me/") + assert resp.status_code in (401, 403) + + def test_authed_with_identity_returns_identity_payload(self, make_authed_user): + u, c = make_authed_user() + resp = c.get("/api/me/") + assert resp.status_code == 200 + body = resp.json() + assert body["id"] == u.id + assert body["login"] == u.username + assert "avatar_url" in body + assert body["needs_reauth"] is False + + def test_authed_without_identity_returns_user_payload(self, make_authed_user): + u, c = make_authed_user(with_identity=False) + resp = c.get("/api/me/") + assert resp.status_code == 200 + body = resp.json() + assert body["id"] == u.id + assert body["login"] == u.username + + +@pytest.mark.django_db +class TestLogoutView: + def test_logout_clears_session(self, make_authed_user): + _, c = make_authed_user() + assert "_auth_user_id" in c.session + resp = c.post("/api/auth/logout/") + assert resp.status_code == 204 + assert "_auth_user_id" not in c.session + + def test_logout_succeeds_without_csrf_token(self, db): + """Verify the csrf_exempt + custom-auth setup: logout MUST work even + when CSRF is enforced and no token is sent. A stale csrftoken cookie + should never wedge a user out of the ability to sign out.""" + u = User.objects.create_user(username="logout-csrf", password="pw") + c = Client(enforce_csrf_checks=True) + c.force_login(u) + # No CSRF token sent — would normally 403 with enforce_csrf_checks. + resp = c.post("/api/auth/logout/") + assert resp.status_code == 204 + assert "_auth_user_id" not in c.session + + +class TestCsrfView: + def test_csrf_endpoint_returns_204_and_sets_csrftoken_cookie(self): + resp = Client().get("/api/auth/csrf/") + assert resp.status_code == 204 + assert "csrftoken" in resp.cookies + assert resp.cookies["csrftoken"].value # non-empty value diff --git a/backend/apps/auth_github/tests/test_oauth_callback.py b/backend/apps/auth_github/tests/test_oauth_callback.py new file mode 100644 index 0000000..1ff548f --- /dev/null +++ b/backend/apps/auth_github/tests/test_oauth_callback.py @@ -0,0 +1,173 @@ +"""Tests for the GitHub OAuth callback view (item 3).""" +from unittest.mock import MagicMock, patch + +import pytest +from django.contrib.auth.models import User +from django.test import Client + +from apps.auth_github.crypto import decrypt +from apps.auth_github.models import GitHubIdentity + + +def _mock_token_response(payload): + r = MagicMock() + r.status_code = 200 + r.raise_for_status = MagicMock() + r.json.return_value = payload + return r + + +def _mock_user_response(payload): + r = MagicMock() + r.status_code = 200 + r.raise_for_status = MagicMock() + r.json.return_value = payload + return r + + +@pytest.mark.django_db +class TestOAuthCallbackState: + def test_callback_with_missing_state_redirects_to_login_error(self): + client = Client() + # No prior /start/, so no gh_oauth_state in session. + resp = client.get("/api/auth/github/callback/?code=somecode") + assert resp.status_code == 302 + assert "login_error=1" in resp["Location"] + + def test_callback_with_mismatched_state_redirects_to_login_error(self): + client = Client() + session = client.session + session["gh_oauth_state"] = "expected_state_value" + session.save() + resp = client.get("/api/auth/github/callback/?code=somecode&state=WRONG") + assert resp.status_code == 302 + assert "login_error=1" in resp["Location"] + + def test_callback_with_missing_code_redirects_to_login_error(self): + client = Client() + session = client.session + session["gh_oauth_state"] = "abc" + session.save() + resp = client.get("/api/auth/github/callback/?state=abc") + assert resp.status_code == 302 + assert "login_error=1" in resp["Location"] + + +@pytest.mark.django_db +class TestOAuthCallbackSuccess: + def test_valid_callback_creates_user_identity_and_redirects_to_frontend(self): + client = Client() + session = client.session + session["gh_oauth_state"] = "good_state" + session.save() + + token_payload = {"access_token": "gho_synthetic_oauth_token", "scope": "read:user,repo"} + user_payload = {"id": 4242, "login": "octopus", "avatar_url": "https://avatar.example/o.png"} + + with patch("apps.auth_github.views.requests.post", return_value=_mock_token_response(token_payload)) as mock_post, \ + patch("apps.auth_github.views.requests.get", return_value=_mock_user_response(user_payload)) as mock_get: + resp = client.get("/api/auth/github/callback/?code=valid_code&state=good_state") + + # Redirect to FRONTEND_BASE_URL (not login_error) + assert resp.status_code == 302 + assert resp["Location"] == "http://localhost:5173" + assert "login_error" not in resp["Location"] + + # User + identity created + user = User.objects.get(username="gh__4242") + ident = GitHubIdentity.objects.get(user=user) + assert ident.github_user_id == 4242 + assert ident.login == "octopus" + assert ident.avatar_url == "https://avatar.example/o.png" + assert ident.needs_reauth is False + assert ident.scopes == "read:user,repo" + # Token stored encrypted; round-trip recovers original + assert ident.access_token_enc != "gho_synthetic_oauth_token" + assert decrypt(ident.access_token_enc) == "gho_synthetic_oauth_token" + + # Session is logged in (auth_login was called) + assert "_auth_user_id" in client.session + + # GitHub HTTP calls were made + assert mock_post.called + assert mock_get.called + + def test_token_endpoint_returning_no_access_token_redirects_to_error(self): + client = Client() + session = client.session + session["gh_oauth_state"] = "good_state" + session.save() + + with patch("apps.auth_github.views.requests.post", + return_value=_mock_token_response({"error": "bad_verification_code"})): + resp = client.get("/api/auth/github/callback/?code=bad&state=good_state") + + assert resp.status_code == 302 + assert "login_error=1" in resp["Location"] + assert not User.objects.filter(username__startswith="gh_").exists() + + def test_state_is_consumed_on_use(self): + """After a callback (success or scope-failure) the state is wiped from session.""" + client = Client() + session = client.session + session["gh_oauth_state"] = "good_state" + session.save() + + # Use a valid `repo` scope so we don't short-circuit before User creation; + # the assertion is that state is consumed regardless. + with patch("apps.auth_github.views.requests.post", + return_value=_mock_token_response( + {"access_token": "tok", "scope": "repo"})), \ + patch("apps.auth_github.views.requests.get", + return_value=_mock_user_response({"id": 1, "login": "x"})): + client.get("/api/auth/github/callback/?code=c&state=good_state") + assert "gh_oauth_state" not in client.session + + +@pytest.mark.django_db +class TestOAuthCallbackHardening: + """Cluster 4 — timing-safe state, IntegrityError handling, scope validation.""" + + def test_callback_user_collision_does_not_500(self): + """If a User row with the same gh__ username already exists, + the callback should match it instead of crashing with IntegrityError.""" + existing = User.objects.create_user(username="gh__999", password="pw") + + client = Client() + session = client.session + session["gh_oauth_state"] = "good_state" + session.save() + + token_payload = {"access_token": "gho_collide_token", "scope": "read:user,repo"} + user_payload = {"id": 999, "login": "collider"} + + with patch("apps.auth_github.views.requests.post", + return_value=_mock_token_response(token_payload)), \ + patch("apps.auth_github.views.requests.get", + return_value=_mock_user_response(user_payload)): + resp = client.get("/api/auth/github/callback/?code=c&state=good_state") + + assert resp.status_code == 302 + # Logged in as the existing user (no duplicate row created). + assert User.objects.filter(username="gh__999").count() == 1 + ident = GitHubIdentity.objects.get(user=existing) + assert ident.github_user_id == 999 + + def test_callback_missing_repo_scope_redirects_with_error(self): + client = Client() + session = client.session + session["gh_oauth_state"] = "good_state" + session.save() + + token_payload = {"access_token": "tok", "scope": "read:user"} + + with patch("apps.auth_github.views.requests.post", + return_value=_mock_token_response(token_payload)), \ + patch("apps.auth_github.views.requests.get") as mock_get: + resp = client.get("/api/auth/github/callback/?code=c&state=good_state") + + assert resp.status_code == 302 + assert "login_error=missing_scope" in resp["Location"] + # No User or Identity created; the user-info GET wasn't called. + assert not User.objects.exists() + mock_get.assert_not_called() diff --git a/backend/apps/auth_github/urls.py b/backend/apps/auth_github/urls.py new file mode 100644 index 0000000..d172f23 --- /dev/null +++ b/backend/apps/auth_github/urls.py @@ -0,0 +1,19 @@ +from django.urls import path + +from .views import ( + CsrfView, + GithubOAuthCallbackView, + GithubOAuthStartView, + GithubReposView, + LogoutView, + MeView, +) + +urlpatterns = [ + path("auth/github/start/", GithubOAuthStartView.as_view()), + path("auth/github/callback/", GithubOAuthCallbackView.as_view()), + path("auth/logout/", LogoutView.as_view()), + path("auth/csrf/", CsrfView.as_view()), + path("me/", MeView.as_view()), + path("github/repos/", GithubReposView.as_view()), +] diff --git a/backend/apps/auth_github/views.py b/backend/apps/auth_github/views.py new file mode 100644 index 0000000..7a13492 --- /dev/null +++ b/backend/apps/auth_github/views.py @@ -0,0 +1,316 @@ +import logging +import secrets +from urllib.parse import urlencode + +import requests +from django.conf import settings +from django.contrib.auth import login as auth_login +from django.contrib.auth import logout as auth_logout +from django.contrib.auth.models import User +from django.core.cache import cache +from django.db import IntegrityError +from django.http import HttpResponseRedirect +from django.utils.crypto import constant_time_compare +from django.utils.decorators import method_decorator +from django.views.decorators.csrf import csrf_exempt, ensure_csrf_cookie +from rest_framework.authentication import SessionAuthentication +from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.response import Response +from rest_framework.views import APIView + +from .crypto import decrypt, encrypt +from .models import GitHubIdentity + +logger = logging.getLogger(__name__) + + +class _CsrfExemptSessionAuthentication(SessionAuthentication): + """SessionAuthentication that does not perform DRF's CSRF check. + + DRF's `SessionAuthentication.enforce_csrf` runs even when the Django + `csrf_exempt` decorator is applied, so we need a dedicated auth class + on the views we genuinely want exempt. Used for `LogoutView` only. + """ + + def enforce_csrf(self, request): # noqa: ARG002 + return + +GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" +GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token" +GITHUB_USER_URL = "https://api.github.com/user" +GITHUB_REPOS_URL = "https://api.github.com/user/repos" +GITHUB_SCOPE = "read:user repo" + + +@method_decorator(csrf_exempt, name="dispatch") +class GithubOAuthStartView(APIView): + permission_classes = [AllowAny] + + def get(self, request): + if not request.session.session_key: + request.session.create() + state = secrets.token_urlsafe(32) + request.session["gh_oauth_state"] = state + request.session.modified = True + + params = { + "client_id": settings.GITHUB_CLIENT_ID, + "redirect_uri": settings.GITHUB_OAUTH_REDIRECT_URI, + "scope": GITHUB_SCOPE, + "state": state, + } + authorize_url = f"{GITHUB_AUTHORIZE_URL}?{urlencode(params)}" + return Response({"authorize_url": authorize_url}) + + +@method_decorator(csrf_exempt, name="dispatch") +class GithubOAuthCallbackView(APIView): + permission_classes = [AllowAny] + + def get(self, request): + code = request.GET.get("code", "") + state = request.GET.get("state", "") + expected_state = request.session.get("gh_oauth_state") + + # `constant_time_compare` avoids leaking the state via response-time + # differences. `expected_state or ""` keeps the comparison length-stable + # when no state was set in session. + if ( + not code + or not state + or not expected_state + or not constant_time_compare(state, expected_state or "") + ): + logger.warning( + "GitHub OAuth state mismatch: code=%s url_state=%s session_state=%s session_key=%s", + bool(code), + state[:8] + "..." if state else "", + (expected_state[:8] + "...") if expected_state else "", + request.session.session_key, + ) + return HttpResponseRedirect(f"{settings.FRONTEND_BASE_URL}/?login_error=1") + + request.session.pop("gh_oauth_state", None) + + try: + token_resp = requests.post( + GITHUB_TOKEN_URL, + headers={"Accept": "application/json"}, + data={ + "client_id": settings.GITHUB_CLIENT_ID, + "client_secret": settings.GITHUB_CLIENT_SECRET, + "code": code, + "redirect_uri": settings.GITHUB_OAUTH_REDIRECT_URI, + "state": state, + }, + timeout=15, + ) + token_resp.raise_for_status() + token_data = token_resp.json() + access_token = token_data.get("access_token") + scopes = token_data.get("scope", "") + if not access_token: + logger.warning("GitHub OAuth: no access_token in response") + return HttpResponseRedirect(f"{settings.FRONTEND_BASE_URL}/?login_error=1") + + # The "repo" scope is required to clone private repos (and to + # exchange tokens against the user/repos endpoint with private + # results). If GitHub silently downgraded the scope, redirect + # the user back to retry rather than persisting a useless token. + granted_scopes = {s.strip() for s in (scopes or "").split(",") if s.strip()} + if "repo" not in granted_scopes: + logger.warning("GitHub OAuth missing 'repo' scope; got %s", scopes) + return HttpResponseRedirect( + f"{settings.FRONTEND_BASE_URL}/?login_error=missing_scope" + ) + + user_resp = requests.get( + GITHUB_USER_URL, + headers={ + "Authorization": f"Bearer {access_token}", + "Accept": "application/vnd.github+json", + }, + timeout=15, + ) + user_resp.raise_for_status() + gh = user_resp.json() + gh_id = gh["id"] + gh_login = gh.get("login", "") + gh_avatar = gh.get("avatar_url", "") or "" + + # `gh__` (double underscore) reduces the chance of colliding + # with a hand-created username. Django's User.username max_length + # is 150, and User.first_name is 150 in 4.2 — match that. + user, _ = User.objects.get_or_create( + username=f"gh__{gh_id}", + defaults={"first_name": (gh_login or "")[:150]}, + ) + + GitHubIdentity.objects.update_or_create( + user=user, + defaults={ + "github_user_id": gh_id, + "login": gh_login, + "avatar_url": gh_avatar, + "access_token_enc": encrypt(access_token), + "scopes": scopes, + "needs_reauth": False, + }, + ) + except IntegrityError: + logger.exception("GitHub OAuth integrity error") + return HttpResponseRedirect(f"{settings.FRONTEND_BASE_URL}/?login_error=1") + except Exception: + logger.exception("GitHub OAuth callback error") + return HttpResponseRedirect(f"{settings.FRONTEND_BASE_URL}/?login_error=1") + + auth_login(request, user) + return HttpResponseRedirect(settings.FRONTEND_BASE_URL) + + +class MeView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + try: + identity = request.user.github_identity + except GitHubIdentity.DoesNotExist: + return Response( + { + "id": request.user.id, + "login": request.user.username, + "avatar_url": "", + "needs_reauth": False, + } + ) + return Response( + { + "id": request.user.id, + "login": identity.login, + "avatar_url": identity.avatar_url, + "needs_reauth": identity.needs_reauth, + } + ) + + +@method_decorator(csrf_exempt, name="dispatch") +class LogoutView(APIView): + """Sign the user out. + + CSRF-exempt because the worst case for a CSRF attack on logout is a + forced sign-out (DoS), not credential theft. Logout still requires a + valid session cookie via `IsAuthenticated`. Without the exemption a + stale `csrftoken` cookie can wedge the frontend into an unrecoverable + "can't log in / can't log out" state. + """ + + authentication_classes = [_CsrfExemptSessionAuthentication] + permission_classes = [IsAuthenticated] + + def post(self, request): + auth_logout(request) + return Response(status=204) + + +@method_decorator(ensure_csrf_cookie, name="dispatch") +class CsrfView(APIView): + permission_classes = [AllowAny] + + def get(self, request): + return Response(status=204) + + +class GithubReposView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + # Cap `q` length to keep cache keys bounded and avoid wasting upstream + # request budget on absurd inputs. The colon would otherwise alias + # different users' cache keys (`gh_repos:{user_id}:{page}:{q}`). + q_raw = (request.query_params.get("q") or "").strip()[:100] + q_cache_key = q_raw.replace(":", "_") + q = q_raw + try: + page = int(request.query_params.get("page", 1)) + except ValueError: + page = 1 + page = max(1, min(10, page)) + + try: + identity = request.user.github_identity + except GitHubIdentity.DoesNotExist: + return Response({"needs_reauth": True}, status=401) + + if identity.needs_reauth: + return Response({"needs_reauth": True}, status=401) + + cache_key = f"gh_repos:{request.user.id}:{page}:{q_cache_key}" + cached = cache.get(cache_key) + if cached is not None: + return Response(cached) + + try: + token = decrypt(identity.access_token_enc) + except Exception: + logger.warning("Failed to decrypt GitHub token for user %s", request.user.id) + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + return Response({"needs_reauth": True}, status=401) + + params = { + "affiliation": "owner,collaborator,organization_member", + "sort": "pushed", + "direction": "desc", + "per_page": 50, + "page": page, + } + try: + resp = requests.get( + GITHUB_REPOS_URL, + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, + params=params, + timeout=20, + ) + except requests.RequestException as e: + logger.warning("GitHub /user/repos network error: %s", type(e).__name__) + return Response({"error": "github_unreachable"}, status=503) + + if resp.status_code == 401: + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + return Response({"needs_reauth": True}, status=401) + + if resp.status_code in (403, 429): + retry_after = resp.headers.get("Retry-After") + r = Response({"error": "rate_limited"}, status=503) + if retry_after: + r["Retry-After"] = retry_after + return r + + if resp.status_code >= 400: + logger.warning("GitHub /user/repos returned %s", resp.status_code) + return Response({"error": "github_error"}, status=502) + + repos = resp.json() or [] + if q: + ql = q.lower() + repos = [r for r in repos if ql in (r.get("full_name") or "").lower()] + + trimmed = [ + { + "id": r["id"], + "name": r["name"], + "full_name": r["full_name"], + "html_url": r["html_url"], + "private": r.get("private", False), + "pushed_at": r.get("pushed_at"), + "default_branch": r.get("default_branch", "main"), + } + for r in repos + ] + + cache.set(cache_key, trimmed, 300) + return Response(trimmed) diff --git a/backend/apps/chat/views.py b/backend/apps/chat/views.py index 0a5ff39..fe87f57 100644 --- a/backend/apps/chat/views.py +++ b/backend/apps/chat/views.py @@ -1,17 +1,23 @@ import google.generativeai as genai from django.conf import settings from pgvector.django import L2Distance +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from apps.embeddings.client import embed_texts from apps.embeddings.models import FunctionEmbedding from apps.graph.models import FunctionEdge, FunctionNode +from apps.repos.utils import user_has_repo_access TOP_K = 8 class ChatView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request, repo_id): + if not user_has_repo_access(request.user, repo_id): + return Response({"error": "not found"}, status=404) query = request.data.get("query", "").strip() if not query: return Response({"error": "query required"}, status=400) @@ -27,10 +33,17 @@ def post(self, request, repo_id): seed_ids = [h.function_id for h in hits] expanded_ids = set(seed_ids) - for edge in FunctionEdge.objects.filter(source_id__in=seed_ids): + # Defense-in-depth: seed_ids are already repo-scoped via the embedding + # filter above, but constrain the edge fan-out to this repo as well so + # any stray cross-repo edge cannot pull a foreign target into context. + for edge in FunctionEdge.objects.filter( + source_id__in=seed_ids, repository_id=repo_id + ): expanded_ids.add(edge.target_id) - functions = FunctionNode.objects.filter(id__in=expanded_ids).select_related("file") + functions = FunctionNode.objects.filter( + id__in=expanded_ids, repository_id=repo_id + ).select_related("file") context_parts = [] for fn in functions: diff --git a/backend/apps/files/views.py b/backend/apps/files/views.py index 955b0fd..5262bca 100644 --- a/backend/apps/files/views.py +++ b/backend/apps/files/views.py @@ -1,13 +1,19 @@ +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from apps.graph.models import FunctionNode +from apps.repos.utils import user_has_repo_access from .models import RepoFile class FileTreeView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, repo_id): + if not user_has_repo_access(request.user, repo_id): + return Response({"error": "not found"}, status=404) files = RepoFile.objects.filter(repository_id=repo_id).values("id", "path", "language") tree = {} for f in files: @@ -18,8 +24,13 @@ def get(self, request, repo_id): node[parts[-1]] = {"id": f["id"], "path": f["path"], "language": f["language"], "type": "file"} return Response({"tree": tree, "files": list(files)}) + class FileFunctionsView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, repo_id, file_id): + if not user_has_repo_access(request.user, repo_id): + return Response({"error": "not found"}, status=404) functions = FunctionNode.objects.filter( repository_id=repo_id, file_id=file_id ).values("id", "name", "start_line", "end_line", "summary") diff --git a/backend/apps/graph/views.py b/backend/apps/graph/views.py index b0fc3e3..e417673 100644 --- a/backend/apps/graph/views.py +++ b/backend/apps/graph/views.py @@ -1,8 +1,11 @@ import google.generativeai as genai from django.conf import settings +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView +from apps.repos.utils import user_has_repo_access + from .models import FunctionEdge, FunctionNode @@ -27,19 +30,43 @@ def serialize_edge(e): class GraphView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, repo_id): + if not user_has_repo_access(request.user, repo_id): + return Response({"error": "not found"}, status=404) file_id = request.query_params.get("file_id") dir_prefix = request.query_params.get("dir") node_id = request.query_params.get("node_id") if node_id: - center_ids = {int(node_id)} - out_edges = list(FunctionEdge.objects.filter(source_id=node_id)) - in_edges = list(FunctionEdge.objects.filter(target_id=node_id)) + # Validate node_id parses and belongs to this repo. A node_id from + # another repo must 404 even though the caller has access to repo_id. + try: + center_pk = int(node_id) + except (ValueError, TypeError): + return Response( + {"error": "invalid_param", "detail": "node_id"}, status=400 + ) + try: + FunctionNode.objects.get(id=center_pk, repository_id=repo_id) + except FunctionNode.DoesNotExist: + return Response({"error": "node_not_found"}, status=404) + center_ids = {center_pk} + out_edges = list( + FunctionEdge.objects.filter(source_id=center_pk, repository_id=repo_id) + ) + in_edges = list( + FunctionEdge.objects.filter(target_id=center_pk, repository_id=repo_id) + ) for e in out_edges + in_edges: center_ids.add(e.source_id) center_ids.add(e.target_id) - nodes_qs = list(FunctionNode.objects.filter(id__in=center_ids).select_related("file")) + nodes_qs = list( + FunctionNode.objects.filter( + id__in=center_ids, repository_id=repo_id + ).select_related("file") + ) all_edges = out_edges + in_edges elif file_id: @@ -47,8 +74,16 @@ def get(self, request, repo_id): FunctionNode.objects.filter(repository_id=repo_id, file_id=file_id).select_related("file") ) file_node_ids = {n.id for n in file_nodes} - out_edges = list(FunctionEdge.objects.filter(source_id__in=file_node_ids)) - in_edges = list(FunctionEdge.objects.filter(target_id__in=file_node_ids)) + out_edges = list( + FunctionEdge.objects.filter( + source_id__in=file_node_ids, repository_id=repo_id + ) + ) + in_edges = list( + FunctionEdge.objects.filter( + target_id__in=file_node_ids, repository_id=repo_id + ) + ) all_edges = out_edges + in_edges neighbor_ids = set() for e in all_edges: @@ -57,7 +92,9 @@ def get(self, request, repo_id): if e.target_id not in file_node_ids: neighbor_ids.add(e.target_id) neighbor_nodes = list( - FunctionNode.objects.filter(id__in=neighbor_ids).select_related("file") + FunctionNode.objects.filter( + id__in=neighbor_ids, repository_id=repo_id + ).select_related("file") ) if neighbor_ids else [] nodes_qs = file_nodes + neighbor_nodes @@ -71,8 +108,16 @@ def get(self, request, repo_id): FunctionNode.objects.filter(repository_id=repo_id, file_id__in=dir_file_ids).select_related("file") ) dir_node_ids = {n.id for n in dir_nodes} - out_edges = list(FunctionEdge.objects.filter(source_id__in=dir_node_ids)) - in_edges = list(FunctionEdge.objects.filter(target_id__in=dir_node_ids)) + out_edges = list( + FunctionEdge.objects.filter( + source_id__in=dir_node_ids, repository_id=repo_id + ) + ) + in_edges = list( + FunctionEdge.objects.filter( + target_id__in=dir_node_ids, repository_id=repo_id + ) + ) all_edges = out_edges + in_edges neighbor_ids = set() for e in all_edges: @@ -81,7 +126,9 @@ def get(self, request, repo_id): if e.target_id not in dir_node_ids: neighbor_ids.add(e.target_id) neighbor_nodes = list( - FunctionNode.objects.filter(id__in=neighbor_ids).select_related("file") + FunctionNode.objects.filter( + id__in=neighbor_ids, repository_id=repo_id + ).select_related("file") ) if neighbor_ids else [] nodes_qs = dir_nodes + neighbor_nodes @@ -111,13 +158,22 @@ def get(self, request, repo_id): class TraceView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, repo_id, node_id): + if not user_has_repo_access(request.user, repo_id): + return Response({"error": "not found"}, status=404) try: fn = FunctionNode.objects.select_related("file").get(id=node_id, repository_id=repo_id) except FunctionNode.DoesNotExist: return Response({"error": "not found"}, status=404) - MAX_DEPTH = int(request.query_params.get("depth", 4)) + try: + MAX_DEPTH = int(request.query_params.get("depth", 4)) + except (ValueError, TypeError): + return Response( + {"error": "invalid_param", "detail": "depth"}, status=400 + ) # Pre-fetch all outgoing CALLS edges for this repo all_edges = FunctionEdge.objects.filter( @@ -142,7 +198,9 @@ def get(self, request, repo_id, node_id): if depth > 0: try: - node = FunctionNode.objects.select_related("file").get(id=cur_id) + node = FunctionNode.objects.select_related("file").get( + id=cur_id, repository_id=repo_id + ) flow.append({ "id": str(node.id), "name": node.name, diff --git a/backend/apps/repos/migrations/0002_repository_first_ingested_by_repository_is_private_and_more.py b/backend/apps/repos/migrations/0002_repository_first_ingested_by_repository_is_private_and_more.py new file mode 100644 index 0000000..49f3661 --- /dev/null +++ b/backend/apps/repos/migrations/0002_repository_first_ingested_by_repository_is_private_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.11 on 2026-05-10 13:23 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('repos', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='repository', + name='first_ingested_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='repository', + name='is_private', + field=models.BooleanField(default=None, null=True), + ), + migrations.AlterField( + model_name='repository', + name='id', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + migrations.CreateModel( + name='RepositoryAccess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('owner', 'owner'), ('member', 'member')], default='owner', max_length=16)), + ('source', models.CharField(choices=[('github', 'github'), ('public_url', 'public_url')], default='public_url', max_length=16)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('repository', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accesses', to='repos.repository')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='repo_accesses', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('user', 'repository')}, + }, + ), + ] diff --git a/backend/apps/repos/models.py b/backend/apps/repos/models.py index 3c62119..ecd0c9f 100644 --- a/backend/apps/repos/models.py +++ b/backend/apps/repos/models.py @@ -1,5 +1,6 @@ import uuid +from django.conf import settings from django.db import models @@ -19,8 +20,38 @@ class Repository(models.Model): name = models.CharField(max_length=255) status = models.CharField(max_length=20, choices=STATUS, default="pending") status_message = models.TextField(blank=True) + first_ingested_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="+", + ) + is_private = models.BooleanField(null=True, default=None) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def __str__(self): return self.name + + +class RepositoryAccess(models.Model): + ROLE_CHOICES = [("owner", "owner"), ("member", "member")] + SOURCE_CHOICES = [("github", "github"), ("public_url", "public_url")] + + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="repo_accesses", + ) + repository = models.ForeignKey( + Repository, + on_delete=models.CASCADE, + related_name="accesses", + ) + role = models.CharField(max_length=16, choices=ROLE_CHOICES, default="owner") + source = models.CharField(max_length=16, choices=SOURCE_CHOICES, default="public_url") + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + unique_together = (("user", "repository"),) diff --git a/backend/apps/repos/tasks.py b/backend/apps/repos/tasks.py index cd3050a..d15320d 100644 --- a/backend/apps/repos/tasks.py +++ b/backend/apps/repos/tasks.py @@ -1,11 +1,25 @@ +import logging import os +import re import shutil import git +import requests from celery import shared_task from django.conf import settings from .models import Repository +from .utils import parse_github_owner_repo as _parse_github_owner_repo + +logger = logging.getLogger(__name__) + +# Set once on import — disabling the git terminal prompt globally for the +# worker process keeps GIT_ASKPASS / interactive credential helpers from +# popping up on a clone failure (which would leave the worker hung). Setting +# this in the task body is too late on the very first invocation in a fresh +# worker, since GitPython spawns the git process before reading os.environ +# changes done in the same request. +os.environ["GIT_TERMINAL_PROMPT"] = "0" def _set_status(repo, status, msg=""): @@ -13,20 +27,133 @@ def _set_status(repo, status, msg=""): repo.status_message = msg repo.save(update_fields=["status", "status_message"]) + +def _redact(msg, token: str | None) -> str: + """Redact a token (and any URL userinfo block) from a string OR from + the message-bearing attributes of a `git.GitCommandError`-like object. + + Accepts either a string or any object with `command`, `stdout`, `stderr` + attributes — those three are also redacted in place AND folded into the + returned string. This matters because Celery serializes both the + exception args AND the attributes when propagating a failure. + """ + if msg is None: + return "" + extras: list[str] = [] + if not isinstance(msg, str): + # Treat as a GitCommandError-like exception; redact its message-bearing + # attributes IN PLACE so any later serialization is also clean, and + # also fold them into the returned text for logging. + for attr in ("command", "stdout", "stderr"): + val = getattr(msg, attr, None) + if val is None: + continue + try: + # Some attrs (`command`) may be a list — coerce to string. + text = val.decode() if isinstance(val, (bytes, bytearray)) else str(val) + except Exception: + text = "" + cleaned = _redact_text(text, token) + try: + setattr(msg, attr, cleaned) + except Exception: + # If the attr is read-only, swallow — the returned string + # below will still be clean. + pass + if cleaned: + extras.append(cleaned) + msg = str(msg) + base = _redact_text(msg, token) + if extras: + return base + " | " + " | ".join(extras) + return base + + +def _redact_text(msg: str, token: str | None) -> str: + if not msg: + return "" + if token: + msg = msg.replace(token, "***") + return re.sub(r"https://[^@\s]+@", "https://***@", msg) + + +def _load_identity(user_id): + if not user_id: + return None + try: + from apps.auth_github.models import GitHubIdentity + return GitHubIdentity.objects.select_related("user").get(user_id=user_id) + except Exception: + return None + + +def _decrypt_token(identity): + if not identity: + return None + try: + from apps.auth_github.crypto import decrypt + return decrypt(identity.access_token_enc) + except Exception: + return None + + @shared_task -def ingest_repository(repo_id): +def ingest_repository(repo_id, user_id=None): from apps.embeddings.tasks import generate_embeddings from apps.parser.tasks import parse_repository repo = Repository.objects.get(id=repo_id) repo_path = os.path.join(settings.REPOS_DIR, str(repo_id)) + owner, name = _parse_github_owner_repo(repo.url) + identity = _load_identity(user_id) if owner else None + token = _decrypt_token(identity) + + if owner and name and token and repo.is_private is None: + try: + probe = requests.get( + f"https://api.github.com/repos/{owner}/{name}", + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, + timeout=15, + ) + if probe.status_code == 200: + data = probe.json() + repo.is_private = bool(data.get("private")) + repo.save(update_fields=["is_private", "updated_at"]) + elif probe.status_code == 401 and identity: + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + token = None + except Exception as e: + logger.warning("Repo probe failed: %s", type(e).__name__) + + if repo.is_private and token and owner and name: + clone_url = f"https://x-access-token:{token}@github.com/{owner}/{name}.git" + else: + clone_url = repo.url + try: _set_status(repo, "cloning", "Cloning repo...") if os.path.exists(repo_path): shutil.rmtree(repo_path) - git.Repo.clone_from(repo.url, repo_path, depth=1) - + try: + git.Repo.clone_from(clone_url, repo_path, depth=1) + except git.GitCommandError as e: + stderr = str(getattr(e, "stderr", "") or "") + if identity and ( + e.status == 128 + and ("401" in stderr or "Authentication failed" in stderr or "could not read Username" in stderr) + ): + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + msg = _redact(e, token) + _set_status(repo, "failed", msg) + logger.warning("Clone failed for repo %s: %s", repo_id, msg) + return + _set_status(repo, "parsing", "Parsing and building graph...") parse_repository(repo_id, repo_path) @@ -36,5 +163,14 @@ def ingest_repository(repo_id): _set_status(repo, "ready", "Done") except Exception as e: - _set_status(repo, "failed", str(e)) - raise + # Outer guard: catch broader-than-GitCommandError failures and + # re-raise as a plain RuntimeError carrying ONLY the redacted text. + # Without this, Celery may serialize locals (including `clone_url` + # and the original exception's stderr) into the task result. + redacted = _redact(e, token) + _set_status(repo, "failed", redacted) + raise RuntimeError(redacted) from None + finally: + # Best-effort drop of in-scope tokenized URL. + clone_url = None # noqa: F841 + token = None # noqa: F841 diff --git a/backend/apps/repos/tests/__init__.py b/backend/apps/repos/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/apps/repos/tests/conftest.py b/backend/apps/repos/tests/conftest.py new file mode 100644 index 0000000..86c7b38 --- /dev/null +++ b/backend/apps/repos/tests/conftest.py @@ -0,0 +1,49 @@ +"""Shared fixtures for repos tests.""" +from unittest.mock import MagicMock + +import pytest +from django.contrib.auth.models import User +from django.test import Client + +from apps.auth_github.crypto import encrypt +from apps.auth_github.models import GitHubIdentity + + +@pytest.fixture +def user_factory(db): + counter = {"i": 0} + + def make(username=None, with_github=False, github_user_id=None, token="gho_synthetic"): + counter["i"] += 1 + if username is None: + username = f"u{counter['i']}" + u = User.objects.create_user(username=username, password="pw") + if with_github: + GitHubIdentity.objects.create( + user=u, + github_user_id=github_user_id or (10_000 + counter["i"]), + login=username, + access_token_enc=encrypt(token), + scopes="repo", + ) + return u + + return make + + +@pytest.fixture +def authed_client(db): + def make(user): + c = Client() + c.force_login(user) + return c + + return make + + +def mock_gh_response(status_code=200, json_payload=None, headers=None): + r = MagicMock() + r.status_code = status_code + r.json.return_value = json_payload or {} + r.headers = headers or {} + return r diff --git a/backend/apps/repos/tests/test_attach.py b/backend/apps/repos/tests/test_attach.py new file mode 100644 index 0000000..4976096 --- /dev/null +++ b/backend/apps/repos/tests/test_attach.py @@ -0,0 +1,132 @@ +"""Tests for RepositoryAttachView's private-repo permission boundary (item 6).""" +from unittest.mock import patch + +import pytest + +from apps.repos.models import Repository, RepositoryAccess +from apps.repos.tests.conftest import mock_gh_response + + +@pytest.fixture +def private_repo(db): + return Repository.objects.create( + url="https://github.com/acme/secret", + name="secret", + is_private=True, + status="ready", + ) + + +@pytest.mark.django_db +class TestAttachPrivateRepo: + def test_user_with_no_github_identity_gets_403(self, user_factory, authed_client, private_repo): + u = user_factory(username="no-gh", with_github=False) + resp = authed_client(u).post( + "/api/repos/attach/", + data={"repo_id": str(private_repo.id)}, + content_type="application/json", + ) + assert resp.status_code == 403 + assert RepositoryAccess.objects.filter(user=u, repository=private_repo).count() == 0 + + def test_user_whose_github_probe_returns_404_gets_403( + self, user_factory, authed_client, private_repo + ): + u = user_factory(username="cant-see", with_github=True) + with patch("apps.repos.views.requests.get", return_value=mock_gh_response(404)): + resp = authed_client(u).post( + "/api/repos/attach/", + data={"repo_id": str(private_repo.id)}, + content_type="application/json", + ) + assert resp.status_code == 403 + assert RepositoryAccess.objects.filter(user=u, repository=private_repo).count() == 0 + + def test_user_whose_github_probe_returns_200_gets_access( + self, user_factory, authed_client, private_repo + ): + u = user_factory(username="can-see", with_github=True) + with patch( + "apps.repos.views.requests.get", + return_value=mock_gh_response(200, {"private": True}), + ): + resp = authed_client(u).post( + "/api/repos/attach/", + data={"repo_id": str(private_repo.id)}, + content_type="application/json", + ) + assert resp.status_code == 200 + access = RepositoryAccess.objects.get(user=u, repository=private_repo) + assert access.source == "github" + + def test_attach_unknown_repo_id_returns_404(self, user_factory, authed_client): + import uuid + u = user_factory(username="x") + resp = authed_client(u).post( + "/api/repos/attach/", + data={"repo_id": str(uuid.uuid4())}, + content_type="application/json", + ) + assert resp.status_code == 404 + + def test_anonymous_attach_blocked(self): + from django.test import Client + resp = Client().post( + "/api/repos/attach/", data={"repo_id": "anything"}, content_type="application/json" + ) + assert resp.status_code in (401, 403) + + +@pytest.fixture +def unprobed_repo(db): + """A github repo whose privacy hasn't been determined yet (`is_private=None`).""" + return Repository.objects.create( + url="https://github.com/acme/maybepublic", + name="maybepublic", + is_private=None, + status="ready", + ) + + +@pytest.mark.django_db +class TestAttachAnonymousProbeFallback: + """Cluster 2 — when a user with no token tries to attach to an unprobed + repo, fall back to an anonymous probe rather than auto-deny.""" + + def test_attach_user_with_no_token_for_unprobed_repo_falls_back_to_anonymous_probe( + self, user_factory, authed_client, unprobed_repo + ): + u = user_factory(username="anon-attacher", with_github=False) + # Anonymous probe returns 200 with private=False — repo is public. + with patch( + "apps.repos.views.requests.get", + return_value=mock_gh_response(200, {"private": False}), + ): + resp = authed_client(u).post( + "/api/repos/attach/", + data={"repo_id": str(unprobed_repo.id)}, + content_type="application/json", + ) + assert resp.status_code == 200 + access = RepositoryAccess.objects.get(user=u, repository=unprobed_repo) + assert access.source == "public_url" + unprobed_repo.refresh_from_db() + assert unprobed_repo.is_private is False + + def test_attach_user_with_no_token_for_unprobed_private_repo_returns_403( + self, user_factory, authed_client, unprobed_repo + ): + u = user_factory(username="anon-locked-out", with_github=False) + # Anonymous probe returns 404 — repo is either private or doesn't + # exist; treat as private and reject (we can't authenticate anonymously). + with patch( + "apps.repos.views.requests.get", + return_value=mock_gh_response(404), + ): + resp = authed_client(u).post( + "/api/repos/attach/", + data={"repo_id": str(unprobed_repo.id)}, + content_type="application/json", + ) + assert resp.status_code == 403 + assert RepositoryAccess.objects.filter(user=u, repository=unprobed_repo).count() == 0 diff --git a/backend/apps/repos/tests/test_cross_app_gating.py b/backend/apps/repos/tests/test_cross_app_gating.py new file mode 100644 index 0000000..531fd85 --- /dev/null +++ b/backend/apps/repos/tests/test_cross_app_gating.py @@ -0,0 +1,130 @@ +"""Cluster 6 — cross-app permission gating. + +`apps.files`, `apps.graph`, `apps.chat` views must call the shared +`apps.repos.utils.user_has_repo_access` helper (the single source of +truth for "can this user see this repo?"). For an unrelated user the +endpoints must return 404 — never 200, never 403 — so that we don't +leak the existence of the repo. + +The model layers for these apps depend on postgres-only fields +(ArrayField, pgvector.VectorField); we stub those at module load (see +`core/test_settings.py`) so the views can be imported under SQLite. +We mock the inner queryset-returning calls so the tests never reach +the `db_type=text` columns — only the access gate is exercised here. +""" +import pytest + +from apps.repos.models import Repository, RepositoryAccess + + +@pytest.fixture +def two_users_one_repo(user_factory, authed_client): + ua = user_factory(username="alice") + ub = user_factory(username="bob") + repo = Repository.objects.create( + url="https://github.com/alice/proj", + name="proj", + is_private=False, + status="ready", + ) + RepositoryAccess.objects.create( + user=ua, repository=repo, role="owner", source="public_url" + ) + return ua, ub, repo, authed_client(ub) + + +@pytest.mark.django_db +class TestCrossAppGating404: + def test_chat_view_404_for_unrelated_user(self, two_users_one_repo): + _, _, repo, client_b = two_users_one_repo + # No mocks needed: gate runs before any model query reaches the body. + resp = client_b.post( + f"/api/chat/{repo.id}/", + data={"query": "anything"}, + content_type="application/json", + ) + assert resp.status_code == 404 + assert resp.json() == {"error": "not found"} + + def test_graph_view_404_for_unrelated_user(self, two_users_one_repo): + _, _, repo, client_b = two_users_one_repo + resp = client_b.get(f"/api/graph/{repo.id}/") + assert resp.status_code == 404 + + def test_files_view_404_for_unrelated_user(self, two_users_one_repo): + _, _, repo, client_b = two_users_one_repo + resp = client_b.get(f"/api/files/{repo.id}/tree/") + assert resp.status_code == 404 + + def test_trace_view_404_for_unrelated_user(self, two_users_one_repo): + _, _, repo, client_b = two_users_one_repo + resp = client_b.get(f"/api/graph/{repo.id}/trace/1/") + assert resp.status_code == 404 + + def test_file_functions_view_404_for_unrelated_user(self, two_users_one_repo): + _, _, repo, client_b = two_users_one_repo + resp = client_b.get(f"/api/files/{repo.id}/files/1/functions/") + assert resp.status_code == 404 + + +@pytest.mark.django_db +class TestUserHasRepoAccessHelper: + """Direct unit tests for the helper used by all cross-app views — the + single source of truth for repo-scoped authorization.""" + + def test_returns_true_for_owner(self, user_factory): + from apps.repos.utils import user_has_repo_access + + u = user_factory(username="owner") + repo = Repository.objects.create(url="https://github.com/x/y", name="y") + RepositoryAccess.objects.create(user=u, repository=repo, role="owner") + assert user_has_repo_access(u, repo.id) is True + + def test_returns_false_for_unrelated_user(self, user_factory): + from apps.repos.utils import user_has_repo_access + + owner = user_factory(username="o") + other = user_factory(username="other") + repo = Repository.objects.create(url="https://github.com/x/y", name="y") + RepositoryAccess.objects.create(user=owner, repository=repo, role="owner") + assert user_has_repo_access(other, repo.id) is False + + def test_returns_false_for_unauthenticated_user(self): + from django.contrib.auth.models import AnonymousUser + + from apps.repos.utils import user_has_repo_access + + repo = Repository.objects.create(url="https://github.com/x/y", name="y") + assert user_has_repo_access(AnonymousUser(), repo.id) is False + assert user_has_repo_access(None, repo.id) is False + + def test_views_dispatch_through_shared_helper(self, two_users_one_repo, monkeypatch): + """Belt-and-suspenders: confirm the views actually call + `apps.repos.utils.user_has_repo_access`. If a future refactor swaps + in an inline check, this test will break loudly.""" + ua, ub, repo, client_b = two_users_one_repo + + calls = [] + + def _spy(user, repo_id): + calls.append((user.id, str(repo_id))) + return False + + # Patch the binding inside each view module — the views import the + # helper by name at module load. + monkeypatch.setattr("apps.files.views.user_has_repo_access", _spy) + monkeypatch.setattr("apps.graph.views.user_has_repo_access", _spy) + monkeypatch.setattr("apps.chat.views.user_has_repo_access", _spy) + + client_b.get(f"/api/files/{repo.id}/tree/") + client_b.get(f"/api/graph/{repo.id}/") + client_b.post( + f"/api/chat/{repo.id}/", + data={"query": "x"}, + content_type="application/json", + ) + + assert len(calls) == 3 + for user_id, repo_id in calls: + assert user_id == ub.id + assert repo_id == str(repo.id) diff --git a/backend/apps/repos/tests/test_cross_repo_idor.py b/backend/apps/repos/tests/test_cross_repo_idor.py new file mode 100644 index 0000000..c9ea09e --- /dev/null +++ b/backend/apps/repos/tests/test_cross_repo_idor.py @@ -0,0 +1,206 @@ +"""Cluster — cross-repo IDOR through graph/chat queries. + +The view-level access gate (`user_has_repo_access`) only proves the user can +see ``repo_id``. It does NOT validate that ``node_id`` / ``file_id`` query +params belong to that same repo. Without per-query repository_id filtering, +an authed user with access to repo A can pass a node_id from repo B and +receive node + edge data for repo B. + +These tests exercise the inner-query repo scoping in +`apps/graph/views.py:GraphView` and `apps/chat/views.py:ChatView`. The +`FunctionNode` rows are created directly in the test DB; under SQLite the +ArrayField/VectorField columns are stubbed (see `core/test_settings.py`) +so we can write to these models without postgres-only features. +""" +import pytest + +from apps.files.models import RepoFile +from apps.graph.models import FunctionEdge, FunctionNode +from apps.repos.models import Repository, RepositoryAccess + + +@pytest.fixture +def two_repos_with_nodes(user_factory, authed_client, db): + """Alice has access to repo A. Repo B exists but Alice has no access. + + Each repo has one file and one FunctionNode. Returns the IDs the tests + need to attempt cross-repo access through ``node_id`` / ``file_id``. + """ + alice = user_factory(username="alice") + + repo_a = Repository.objects.create( + url="https://github.com/alice/proj-a", + name="proj-a", + is_private=False, + status="ready", + ) + RepositoryAccess.objects.create( + user=alice, repository=repo_a, role="owner", source="public_url" + ) + + repo_b = Repository.objects.create( + url="https://github.com/bob/proj-b", + name="proj-b", + is_private=False, + status="ready", + ) + # No access for Alice to repo B. + + file_a = RepoFile.objects.create(repository=repo_a, path="a.py", language="python") + file_b = RepoFile.objects.create(repository=repo_b, path="b.py", language="python") + + node_a = FunctionNode.objects.create( + repository=repo_a, + file=file_a, + name="fn_a", + start_line=1, + end_line=10, + source="def fn_a(): pass", + summary="", + calls=[], + ) + node_b = FunctionNode.objects.create( + repository=repo_b, + file=file_b, + name="fn_b", + start_line=1, + end_line=10, + source="def fn_b(): pass", + summary="", + calls=[], + ) + + return { + "alice": alice, + "client": authed_client(alice), + "repo_a": repo_a, + "repo_b": repo_b, + "file_a": file_a, + "file_b": file_b, + "node_a": node_a, + "node_b": node_b, + } + + +@pytest.mark.django_db +class TestGraphViewNodeIdCrossRepo: + def test_graph_view_node_id_from_other_repo_returns_404(self, two_repos_with_nodes): + """Alice has access to repo A. She passes node_b.id (in repo B) as + ?node_id= to the repo A endpoint. Must 404, NOT leak node B data.""" + ctx = two_repos_with_nodes + resp = ctx["client"].get( + f"/api/graph/{ctx['repo_a'].id}/?node_id={ctx['node_b'].id}" + ) + assert resp.status_code == 404 + assert resp.json() == {"error": "node_not_found"} + + def test_graph_view_node_id_from_own_repo_returns_200(self, two_repos_with_nodes): + """Sanity check: in-repo node_id must still work (proves the new + repo-scoped fetch isn't over-rejecting legitimate requests).""" + ctx = two_repos_with_nodes + resp = ctx["client"].get( + f"/api/graph/{ctx['repo_a'].id}/?node_id={ctx['node_a'].id}" + ) + assert resp.status_code == 200 + body = resp.json() + node_ids = {n["id"] for n in body["nodes"]} + assert str(ctx["node_a"].id) in node_ids + assert str(ctx["node_b"].id) not in node_ids + + def test_graph_view_non_numeric_node_id_returns_400(self, two_repos_with_nodes): + """Non-numeric node_id must 400, not 500.""" + ctx = two_repos_with_nodes + resp = ctx["client"].get(f"/api/graph/{ctx['repo_a'].id}/?node_id=abc") + assert resp.status_code == 400 + assert resp.json() == {"error": "invalid_param", "detail": "node_id"} + + +@pytest.mark.django_db +class TestGraphViewFileIdCrossRepo: + def test_graph_view_file_id_from_other_repo_returns_empty(self, two_repos_with_nodes): + """Alice queries repo A with ?file_id pointing to a file in repo B. + The view-level repo gate passes (Alice owns repo A), but the inner + FunctionNode/FunctionEdge queries must filter by repository_id, so + no data from repo B leaks. Result: empty nodes/edges.""" + ctx = two_repos_with_nodes + resp = ctx["client"].get( + f"/api/graph/{ctx['repo_a'].id}/?file_id={ctx['file_b'].id}" + ) + assert resp.status_code == 200 + body = resp.json() + assert body["nodes"] == [] + assert body["edges"] == [] + + +@pytest.mark.django_db +class TestChatViewEdgeFanoutRepoScoped: + """The chat view's `expanded_ids` set is built by following outgoing + edges from ``seed_ids``. ``seed_ids`` come from FunctionEmbedding rows + pre-filtered by `function__repository_id=repo_id`, so a stored + cross-repo edge is the only practical exfiltration path. We can't + easily provoke that under SQLite (FunctionEmbedding uses VectorField + + L2Distance ordering — both stubs are no-ops), so we settle for a + structural assertion: the queryset's SQL contains a `repository_id` + filter.""" + + def test_chat_view_edge_fanout_filters_by_repository(self, monkeypatch, two_repos_with_nodes): + """Spy on `FunctionEdge.objects.filter` to confirm the chat view + passes `repository_id=` when expanding seed_ids.""" + from apps.chat import views as chat_views + + ctx = two_repos_with_nodes + captured = {} + + real_filter = FunctionEdge.objects.filter + + def _spy(*args, **kwargs): + # Record the kwargs used by the chat view's edge fan-out call. + # The standalone `objects.all().order_by(...)` queries don't pass + # `source_id__in`, so we filter for the expansion call only. + if "source_id__in" in kwargs: + captured.update(kwargs) + return real_filter(*args, **kwargs) + + monkeypatch.setattr(FunctionEdge.objects, "filter", _spy) + + # Stub the embedding lookup so we don't need a real vector DB. Returns + # a list with one hit pointing at node_a, so seed_ids = [node_a.id]. + class _FakeHit: + def __init__(self, fn_id): + self.function_id = fn_id + + class _FakeQS: + def __init__(self, hits): + self._hits = hits + + def filter(self, **kwargs): + return self + + def order_by(self, *args, **kwargs): + return self + + def __getitem__(self, key): + return self + + def select_related(self, *args, **kwargs): + return self._hits + + monkeypatch.setattr( + chat_views.FunctionEmbedding, + "objects", + _FakeQS([_FakeHit(ctx["node_a"].id)]), + ) + monkeypatch.setattr(chat_views, "embed_texts", lambda texts: [[0.0]]) + + resp = ctx["client"].post( + f"/api/chat/{ctx['repo_a'].id}/", + data={"query": "tell me about fn_a"}, + content_type="application/json", + ) + assert resp.status_code == 200, resp.content + # Confirm the edge fan-out queryset was built with the repo_id filter. + assert "repository_id" in captured, ( + "FunctionEdge edge fan-out did not include repository_id filter; " + "this is the IDOR defense-in-depth check" + ) + assert str(captured["repository_id"]) == str(ctx["repo_a"].id) diff --git a/backend/apps/repos/tests/test_redact.py b/backend/apps/repos/tests/test_redact.py new file mode 100644 index 0000000..405aa29 --- /dev/null +++ b/backend/apps/repos/tests/test_redact.py @@ -0,0 +1,70 @@ +"""Tests for apps.repos.tasks._redact (item 2).""" +from apps.repos.tasks import _redact + +SYNTHETIC_TOKEN = "ghp_TESTSyntheticTokenABCDEF1234567890XYZ" + + +class TestRedact: + def test_removes_raw_token_from_message(self): + msg = f"git clone failed: stderr contained {SYNTHETIC_TOKEN} oops" + out = _redact(msg, SYNTHETIC_TOKEN) + assert SYNTHETIC_TOKEN not in out + assert "***" in out + + def test_strips_x_access_token_url_form(self): + url = f"https://x-access-token:{SYNTHETIC_TOKEN}@github.com/x/y.git" + msg = f"fatal: could not clone {url} -- access denied" + out = _redact(msg, SYNTHETIC_TOKEN) + assert SYNTHETIC_TOKEN not in out + assert "x-access-token" not in out + assert "https://***@github.com/x/y.git" in out + + def test_strips_generic_userinfo_url_even_without_known_token(self): + msg = "Cloning into 'repo'... fatal: https://someuser:somepass@host.example.org/x.git failed" + out = _redact(msg, token=None) + assert "somepass" not in out + assert "someuser" not in out + assert "https://***@host.example.org/x.git" in out + + def test_empty_message_returns_empty_string(self): + assert _redact("", SYNTHETIC_TOKEN) == "" + assert _redact(None, SYNTHETIC_TOKEN) == "" + + def test_token_none_leaves_plain_text_alone(self): + msg = "no secrets here, just a normal error" + assert _redact(msg, None) == msg + + def test_does_not_partial_match_other_strings(self): + # The token only redacts its own substring; unrelated text passes through. + msg = "branch main not found" + out = _redact(msg, SYNTHETIC_TOKEN) + assert out == msg + + def test_redacts_git_command_error_attributes_in_place(self): + """`_redact(GitCommandError)` should redact .command, .stdout, .stderr + AND fold them into the returned string. This guards against Celery + serializing the original exception attributes into a task result.""" + + class _FakeGitCommandError(Exception): + def __init__(self): + super().__init__("clone failed") + self.command = [ + "git", "clone", + f"https://x-access-token:{SYNTHETIC_TOKEN}@github.com/x/y.git", + ] + self.stdout = b"" + self.stderr = ( + f"fatal: could not read Username for " + f"https://x-access-token:{SYNTHETIC_TOKEN}@github.com" + ) + + e = _FakeGitCommandError() + out = _redact(e, SYNTHETIC_TOKEN) + + # Returned text is clean of token AND of x-access-token@host blob. + assert SYNTHETIC_TOKEN not in out + assert "x-access-token" not in out + # Mutated-in-place attributes are also clean for downstream serializers. + assert SYNTHETIC_TOKEN not in str(e.stderr) + assert SYNTHETIC_TOKEN not in str(e.command) + assert "x-access-token" not in str(e.stderr) diff --git a/backend/apps/repos/tests/test_repository_view.py b/backend/apps/repos/tests/test_repository_view.py new file mode 100644 index 0000000..f8cca73 --- /dev/null +++ b/backend/apps/repos/tests/test_repository_view.py @@ -0,0 +1,267 @@ +"""Tests for RepositoryView gating, public-URL sharing, normalization and task args +(items 4, 5, 7, 10).""" +from unittest.mock import patch + +import pytest +import requests +from django.test import Client + +from apps.repos.models import Repository, RepositoryAccess +from apps.repos.tests.conftest import mock_gh_response + + +@pytest.mark.django_db +class TestPermissionGating: + def test_anonymous_get_returns_401_or_403(self): + # DRF SessionAuthentication returns 403 for anonymous when login is + # required; either 401 or 403 indicates "unauthenticated -> blocked". + resp = Client().get("/api/repos/") + assert resp.status_code in (401, 403) + + def test_user_b_does_not_see_user_a_repo_in_list(self, user_factory, authed_client): + ua = user_factory(username="alice") + ub = user_factory(username="bob") + # Alice creates a repo. + with patch("apps.repos.views.requests.get", return_value=mock_gh_response(404)), \ + patch("apps.repos.views.ingest_repository.delay"): + authed_client(ua).post( + "/api/repos/", + data={"url": "https://github.com/some/private-thing"}, + content_type="application/json", + ) + # Alice should still get a repo created via the public-URL fallback + # (probe 404 with no github identity -> falls back to public_url=True). + # But we want her to have a successful row; the test uses the simpler + # public path with no github account, so use a non-github URL instead. + # Re-do this test with a non-github URL to be unambiguous. + Repository.objects.all().delete() + RepositoryAccess.objects.all().delete() + with patch("apps.repos.views.ingest_repository.delay"): + resp = authed_client(ua).post( + "/api/repos/", + data={"url": "https://gitlab.com/alice/proj"}, + content_type="application/json", + ) + assert resp.status_code == 201, resp.content + + # Bob's list should be empty. + resp = authed_client(ub).get("/api/repos/") + assert resp.status_code == 200 + assert resp.json() == [] + + def test_user_b_retrieve_user_a_repo_returns_404(self, user_factory, authed_client): + ua = user_factory(username="alice") + ub = user_factory(username="bob") + with patch("apps.repos.views.ingest_repository.delay"): + r = authed_client(ua).post( + "/api/repos/", + data={"url": "https://gitlab.com/alice/proj"}, + content_type="application/json", + ) + assert r.status_code == 201 + repo_id = r.json()["id"] + + resp = authed_client(ub).get(f"/api/repos/{repo_id}/") + assert resp.status_code == 404 + + +@pytest.mark.django_db +class TestPublicUrlSharing: + def test_two_users_submit_same_public_url_results_in_one_repo_two_accesses( + self, user_factory, authed_client + ): + ua = user_factory(username="alice") + ub = user_factory(username="bob") + + with patch("apps.repos.views.ingest_repository.delay"): + ra = authed_client(ua).post( + "/api/repos/", + data={"url": "https://gitlab.com/team/proj"}, + content_type="application/json", + ) + assert ra.status_code == 201 + + with patch("apps.repos.views.ingest_repository.delay") as delay_mock_b: + rb = authed_client(ub).post( + "/api/repos/", + data={"url": "https://gitlab.com/team/proj"}, + content_type="application/json", + ) + # 2nd time: existing repo -> 200, not 201 + assert rb.status_code == 200 + + # only one Repository row, two RepositoryAccess rows + assert Repository.objects.count() == 1 + repo = Repository.objects.get() + assert RepositoryAccess.objects.filter(repository=repo).count() == 2 + assert {a.user_id for a in repo.accesses.all()} == {ua.id, ub.id} + + # Second submission should NOT re-enqueue ingest (repo wasn't re-created + # and status isn't 'failed'). + delay_mock_b.assert_not_called() + + +@pytest.mark.django_db +class TestUrlNormalization: + def test_different_url_forms_resolve_to_same_repository( + self, user_factory, authed_client + ): + ua = user_factory(username="alice", with_github=True) + ub = user_factory(username="bob", with_github=True) + uc = user_factory(username="carol", with_github=True) + ud = user_factory(username="dave", with_github=True) + ue = user_factory(username="eve", with_github=True) + + # alice submits the ".git/" suffix form; bob the bare form; carol the + # http://... / d the explicit-https-port-443 form / e the trailing-slash + # form. Each pair lowercases owner+name and collapses to one row. + public_resp = mock_gh_response(200, {"private": False, "default_branch": "main"}) + + submissions = [ + (ua, "https://GitHub.com/x/y.git/", 201), + (ub, "https://github.com/x/y", 200), + (uc, "http://github.com/X/Y/", 200), + (ud, "https://github.com:443/x/y/", 200), + (ue, "http://github.com:80/x/y", 200), + ] + for user, url, expected_status in submissions: + with patch("apps.repos.views.requests.get", return_value=public_resp), \ + patch("apps.repos.views.ingest_repository.delay"): + resp = authed_client(user).post( + "/api/repos/", + data={"url": url}, + content_type="application/json", + ) + assert resp.status_code == expected_status, (url, resp.content) + + # Single Repository row with the normalized URL. + assert Repository.objects.count() == 1 + repo = Repository.objects.get() + assert repo.url == "https://github.com/x/y" + assert RepositoryAccess.objects.filter(repository=repo).count() == len(submissions) + + +@pytest.mark.django_db +class TestUrlValidation: + """Cluster 1 — SSRF + host validation. Reject schemes/hosts that + `_can_grant_access` and the cloner can't safely talk to.""" + + def _post(self, user_factory, authed_client, url): + u = user_factory(username=None) + return authed_client(u).post( + "/api/repos/", + data={"url": url}, + content_type="application/json", + ) + + def test_file_scheme_rejected(self, user_factory, authed_client): + resp = self._post(user_factory, authed_client, "file:///etc/passwd") + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + def test_ssh_scheme_rejected(self, user_factory, authed_client): + resp = self._post(user_factory, authed_client, "git+ssh://github.com/x/y") + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + def test_non_github_host_rejected(self, user_factory, authed_client): + resp = self._post(user_factory, authed_client, "https://example.com/x/y") + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + def test_subdomain_host_rejected(self, user_factory, authed_client): + resp = self._post( + user_factory, authed_client, "https://raw.githubusercontent.com/x/y" + ) + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + def test_lookalike_host_rejected(self, user_factory, authed_client): + resp = self._post(user_factory, authed_client, "https://github.com.evil.tld/x/y") + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + def test_single_path_segment_rejected(self, user_factory, authed_client): + # `https://github.com/onlyone` lacks a repo name; previously created a + # dead Repository row that `_can_grant_access`/the cloner couldn't act on. + resp = self._post(user_factory, authed_client, "https://github.com/onlyone") + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + def test_three_path_segments_rejected(self, user_factory, authed_client): + resp = self._post(user_factory, authed_client, "https://github.com/x/y/z") + assert resp.status_code == 400 + assert resp.json()["error"] == "invalid_url" + assert Repository.objects.count() == 0 + + +@pytest.mark.django_db +class TestCanGrantAccessFailureModes: + """Cluster 2 — error/edge-case handling in `_can_grant_access`.""" + + def test_can_grant_access_flips_needs_reauth_on_401(self, user_factory, authed_client): + u = user_factory(username="reauth-user", with_github=True) + with patch("apps.repos.views.requests.get", return_value=mock_gh_response(401)), \ + patch("apps.repos.views.ingest_repository.delay"): + resp = authed_client(u).post( + "/api/repos/", + data={"url": "https://github.com/x/y"}, + content_type="application/json", + ) + assert resp.status_code == 401 + assert resp.json() == {"error": "needs_reauth"} + u.refresh_from_db() + assert u.github_identity.needs_reauth is True + # No row created on failure. + assert Repository.objects.count() == 0 + + def test_can_grant_access_returns_503_on_github_unreachable(self, user_factory, authed_client): + u = user_factory(username="net-fail", with_github=True) + with patch( + "apps.repos.views.requests.get", + side_effect=requests.ConnectionError("boom"), + ), patch("apps.repos.views.ingest_repository.delay"): + resp = authed_client(u).post( + "/api/repos/", + data={"url": "https://github.com/x/y"}, + content_type="application/json", + ) + assert resp.status_code == 503 + assert resp.json() == {"error": "github_unreachable"} + assert Repository.objects.count() == 0 + + +@pytest.mark.django_db +class TestIngestTaskDelayArgs: + def test_delay_called_with_repo_id_and_user_id_only_no_token( + self, user_factory, authed_client + ): + ua = user_factory(username="alice") + with patch("apps.repos.views.ingest_repository.delay") as delay_mock: + r = authed_client(ua).post( + "/api/repos/", + data={"url": "https://gitlab.com/alice/proj"}, + content_type="application/json", + ) + assert r.status_code == 201 + assert delay_mock.call_count == 1 + args, kwargs = delay_mock.call_args + # Exactly two positional args: repo_id (str), user_id (str). No token. + assert len(args) == 2, f"expected 2 args, got {args}" + assert kwargs == {}, f"expected no kwargs, got {kwargs}" + repo_id, user_id = args + assert isinstance(repo_id, str) and isinstance(user_id, str) + # Verify neither arg looks like a token (sanity: no 'ghp_' / 'gho_' prefixes, + # no x-access-token URL). + for a in args: + assert "ghp_" not in a and "gho_" not in a + assert "x-access-token" not in a + # And neither contains any encrypted-token chunk -- effectively, args are + # the repo's UUID and the user's primary key. + assert user_id == str(ua.id) diff --git a/backend/apps/repos/urls.py b/backend/apps/repos/urls.py index 5bade09..93e8f7f 100644 --- a/backend/apps/repos/urls.py +++ b/backend/apps/repos/urls.py @@ -1,8 +1,9 @@ from django.urls import path -from .views import RepositoryView +from .views import RepositoryAttachView, RepositoryView urlpatterns = [ path("", RepositoryView.as_view()), + path("attach/", RepositoryAttachView.as_view()), path("/", RepositoryView.as_view()), ] diff --git a/backend/apps/repos/utils.py b/backend/apps/repos/utils.py new file mode 100644 index 0000000..cb22671 --- /dev/null +++ b/backend/apps/repos/utils.py @@ -0,0 +1,48 @@ +"""Shared helpers for the repos app. + +Single source of truth for repo-access checks and GitHub URL parsing, +imported by `apps.repos.views`, `apps.repos.tasks`, and the cross-app +view modules under `apps.files`, `apps.graph`, `apps.chat`. +""" +import re + +from rest_framework.exceptions import NotFound + +from .models import Repository + + +def user_has_repo_access(user, repo_id) -> bool: + """Return True if ``user`` has any access row for ``repo_id``.""" + if user is None or not getattr(user, "is_authenticated", False): + return False + return Repository.objects.filter(id=repo_id, accesses__user=user).exists() + + +def get_user_repo_or_404(user, repo_id) -> Repository: + """Return the Repository row for ``user``/``repo_id`` or raise NotFound.""" + repo = ( + Repository.objects.filter(id=repo_id, accesses__user=user).first() + if user is not None and getattr(user, "is_authenticated", False) + else None + ) + if not repo: + raise NotFound("not found") + return repo + + +def parse_github_owner_repo(url: str): + """Extract (owner, name) from a github.com URL or return (None, None). + + Permissive variant: tolerates a trailing ``.git`` and a trailing slash. + Used by both views (URL submission) and tasks (clone). + """ + if not url: + return None, None + m = re.match( + r"^https?://github\.com/([^/]+)/([^/]+?)(?:\.git)?/?$", + url, + re.IGNORECASE, + ) + if not m: + return None, None + return m.group(1), m.group(2) diff --git a/backend/apps/repos/views.py b/backend/apps/repos/views.py index bd4d346..3db0ed7 100644 --- a/backend/apps/repos/views.py +++ b/backend/apps/repos/views.py @@ -1,31 +1,304 @@ +import logging +from urllib.parse import urlparse + +import requests +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from .models import Repository +from apps.auth_github.crypto import decrypt +from apps.auth_github.models import GitHubIdentity + +from .models import Repository, RepositoryAccess from .serializers import RepositorySerializer from .tasks import ingest_repository +from .utils import parse_github_owner_repo as _parse_github_owner_repo + +logger = logging.getLogger(__name__) + +# Hosts the gateway accepts for repo submission. We deliberately do NOT +# accept raw.githubusercontent.com or any other subdomain — `_can_grant_access` +# only knows how to talk to api.github.com/repos/{owner}/{name} and the cloner +# only knows the canonical github.com/{owner}/{name}.git form. +ALLOWED_HOSTS_GITHUB = ("github.com",) +# Non-github hosts that we treat as "public URL"; access is granted on +# submission without a probe (caller cannot privately authenticate to them). +ALLOWED_PUBLIC_HOSTS_NON_GITHUB = ("gitlab.com", "bitbucket.org") + + +def _normalize_url(raw: str) -> str: + """Normalize a user-supplied repo URL. + + Raises ValueError("unsupported_scheme"|"unsupported_host"|"missing_host") + on invalid input. Forces https for github.com and lowercases the host + and (for github URLs) the path so that case-only variants collapse to + a single Repository row. Strips default ports and a trailing ``.git`` / + trailing slash. + """ + raw = (raw or "").strip() + if not raw: + return "" + parsed = urlparse(raw) + + scheme = (parsed.scheme or "https").lower() + if scheme not in ("http", "https"): + raise ValueError("unsupported_scheme") + + netloc = (parsed.netloc or "").strip() + if not netloc: + raise ValueError("missing_host") + + # Strip default ports — e.g. github.com:443 == github.com. + host_only = netloc.split(":", 1)[0].lower() + if ":" in netloc: + _, port = netloc.split(":", 1) + if (scheme == "https" and port == "443") or (scheme == "http" and port == "80"): + netloc = host_only + else: + netloc = f"{host_only}:{port}" + else: + netloc = host_only + + # GitHub paths are case-insensitive; lowercase for stable equality. + is_github = host_only in ALLOWED_HOSTS_GITHUB + is_other_known = host_only in ALLOWED_PUBLIC_HOSTS_NON_GITHUB + if not (is_github or is_other_known): + raise ValueError("unsupported_host") + + path = parsed.path.rstrip("/") + if path.endswith(".git"): + path = path[:-4] + if is_github: + path = path.lower() + # github URLs always normalize to https. + scheme = "https" + # Default port already stripped above; this also drops the port for + # the http→https rewrite case (http://github.com:80/x/y → https://github.com/x/y). + netloc = host_only + + # All allowed hosts (github/gitlab/bitbucket) follow the + # `/{owner}/{name}` shape. A single-segment path like + # `https://github.com/onlyone` would create a dead Repository row that + # `_can_grant_access` and the cloner can't act on. Reject it up front. + segments = [s for s in path.split("/") if s] + if len(segments) != 2: + raise ValueError("invalid_repo_path") + + return f"{scheme}://{netloc}{path}" + + +def _get_github_token(user): + try: + identity = user.github_identity + except GitHubIdentity.DoesNotExist: + return None, None + if identity.needs_reauth: + return None, identity + try: + return decrypt(identity.access_token_enc), identity + except Exception: + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + return None, identity + + +def _probe_github(owner: str, name: str, token: str | None = None) -> dict: + """Hit api.github.com/repos/{owner}/{name} and summarize the result. + + Returns ``{"status": int, "is_private": bool|None, "needs_reauth": bool}``. + ``status == 0`` means the network call itself failed (treat as unknown). + """ + headers = {"Accept": "application/vnd.github+json"} + if token: + headers["Authorization"] = f"Bearer {token}" + try: + resp = requests.get( + f"https://api.github.com/repos/{owner}/{name}", + headers=headers, + timeout=10, + ) + except requests.RequestException as e: + logger.warning("GitHub probe network error for %s/%s: %s", owner, name, type(e).__name__) + return {"status": 0, "is_private": None, "needs_reauth": False} + + is_private = None + if resp.status_code == 200: + try: + is_private = bool(resp.json().get("private")) + except ValueError: + is_private = None + return { + "status": resp.status_code, + "is_private": is_private, + "needs_reauth": resp.status_code == 401 and bool(token), + } + + +def _can_grant_access(user, repo: Repository): + """Return ``(allowed, source)`` where source is one of: + + - ``"public_url"`` — non-github, or github confirmed public. + - ``"github"`` — private github repo, user authed and has access. + - ``"no_access"`` — user not allowed. + - ``"needs_reauth"`` — user's GitHub token is invalid; UI should re-auth. + - ``"github_unreachable"`` — network failure talking to GitHub; fail closed. + """ + owner, name = _parse_github_owner_repo(repo.url) + + # Non-github URL — no probe path. Defensive: `_normalize_url` already + # rejects unsupported hosts before this is called. + if not owner or not name: + return True, "public_url" + + token, identity = _get_github_token(user) + + # If we don't yet know whether the repo is private, probe to find out. + if repo.is_private is None: + probe = _probe_github(owner, name, token=token if token else None) + if probe["status"] == 0: + return False, "github_unreachable" + + if probe["needs_reauth"]: + if identity: + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + return False, "needs_reauth" + + if probe["status"] == 200: + repo.is_private = bool(probe["is_private"]) + repo.save(update_fields=["is_private", "updated_at"]) + elif probe["status"] == 404: + if not token: + # Anonymous probe got 404 — could be private (we can't tell) + # or could be deleted. Treat as private and fall through to + # the auth path, which will reject (no_access). + repo.is_private = True + # Don't persist this guess to the DB — a future authed user + # may flip it. Keep `is_private` cached only for this request. + else: + # Authed probe got 404 → user has no access to this repo. + return False, "no_access" + else: + # 403, 5xx, etc — fail closed. + return False, "github_unreachable" + + # Now `repo.is_private` is known (True/False) for this request. + if repo.is_private is False: + return True, "public_url" + + # Private github repo. Require an authed probe returning 200. + if not token: + return False, "no_access" + authed = _probe_github(owner, name, token=token) + if authed["status"] == 0: + return False, "github_unreachable" + if authed["needs_reauth"]: + if identity: + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + return False, "needs_reauth" + if authed["status"] == 200: + return True, "github" + return False, "no_access" + + +_NEGATIVE_REASON_TO_RESPONSE = { + "needs_reauth": (401, {"error": "needs_reauth"}), + "github_unreachable": (503, {"error": "github_unreachable"}), + "no_access": (403, {"error": "no_access"}), +} + + +def _negative_response(reason: str) -> Response: + status, body = _NEGATIVE_REASON_TO_RESPONSE.get(reason, (403, {"error": "no_access"})) + return Response(body, status=status) class RepositoryView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request): - url = request.data.get("url", "").strip().rstrip("/") - if not url: + try: + normalized = _normalize_url(request.data.get("url", "")) + except ValueError as e: + return Response({"error": "invalid_url", "detail": str(e)}, status=400) + if not normalized: return Response({"error": "url required"}, status=400) - name = url.split("/")[-1].replace(".git", "") - repo, created = Repository.objects.get_or_create(url=url, defaults={"name": name}) + name = normalized.split("/")[-1] + repo, created = Repository.objects.get_or_create( + url=normalized, + defaults={"name": name, "first_ingested_by": request.user}, + ) - if not created: - repo.status = "pending" - repo.status_message = "" - repo.save(update_fields=["status", "status_message"]) + allowed, source = _can_grant_access(request.user, repo) + if not allowed: + # Don't leak a freshly-created Repository row for a user who + # turned out to have no access (or whose probe failed). + if created: + repo.delete() + return _negative_response(source) + + RepositoryAccess.objects.get_or_create( + user=request.user, + repository=repo, + defaults={"role": "owner", "source": source}, + ) + + should_ingest = created or repo.status == "failed" + if should_ingest: + if not created: + repo.status = "pending" + repo.status_message = "" + repo.save(update_fields=["status", "status_message", "updated_at"]) + ingest_repository.delay(str(repo.id), str(request.user.id)) - ingest_repository.delay(str(repo.id)) return Response(RepositorySerializer(repo).data, status=201 if created else 200) def get(self, request, repo_id=None): if repo_id: - repo = Repository.objects.get(id=repo_id) + repo = Repository.objects.filter( + id=repo_id, accesses__user=request.user + ).first() + if not repo: + return Response({"error": "not found"}, status=404) return Response(RepositorySerializer(repo).data) - repos = Repository.objects.all().order_by("-created_at") + repos = ( + Repository.objects.filter(accesses__user=request.user) + .distinct() + .order_by("-created_at") + ) return Response(RepositorySerializer(repos, many=True).data) + + +class RepositoryAttachView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + repo_id = request.data.get("repo_id") + url = request.data.get("url") + + repo = None + if repo_id: + repo = Repository.objects.filter(id=repo_id).first() + elif url: + try: + normalized = _normalize_url(url) + except ValueError as e: + return Response({"error": "invalid_url", "detail": str(e)}, status=400) + if normalized: + repo = Repository.objects.filter(url=normalized).first() + + if not repo: + return Response({"error": "not found"}, status=404) + + allowed, source = _can_grant_access(request.user, repo) + if not allowed: + return _negative_response(source) + + RepositoryAccess.objects.get_or_create( + user=request.user, + repository=repo, + defaults={"role": "member", "source": source}, + ) + return Response(RepositorySerializer(repo).data, status=200) diff --git a/backend/conftest.py b/backend/conftest.py new file mode 100644 index 0000000..ed0ea6b --- /dev/null +++ b/backend/conftest.py @@ -0,0 +1,4 @@ +"""Empty conftest — test bootstrap stubs live at the top of `core/test_settings.py` +because pytest-django's `pytest_load_initial_conftests` runs `django.setup()` +*before* the rootdir conftest is loaded. +""" diff --git a/backend/core/settings.py b/backend/core/settings.py index 045273a..0f64aa9 100644 --- a/backend/core/settings.py +++ b/backend/core/settings.py @@ -1,6 +1,7 @@ import os from pathlib import Path +from django.core.exceptions import ImproperlyConfigured from dotenv import load_dotenv load_dotenv() @@ -8,12 +9,20 @@ BASE_DIR = Path(__file__).resolve().parent.parent SECRET_KEY = os.environ.get("SECRET_KEY", "dev-secret") -DEBUG = os.environ.get("DEBUG", "True") == "True" -ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "*").split(",") +DEBUG = os.environ.get("DEBUG", "False") == "True" +ALLOWED_HOSTS = os.environ.get("ALLOWED_HOSTS", "localhost,127.0.0.1").split(",") + +# Hard-fail on a placeholder SECRET_KEY in production. We only honor the +# default in DEBUG so dev workflows don't break. +if not DEBUG and SECRET_KEY in ("dev-secret", "your-secret-key-here"): + raise ImproperlyConfigured( + "SECRET_KEY must be set in production (got placeholder value)." + ) INSTALLED_APPS = [ "django.contrib.contenttypes", "django.contrib.auth", + "django.contrib.sessions", "rest_framework", "corsheaders", "apps.repos", @@ -22,18 +31,49 @@ "apps.graph", "apps.embeddings", "apps.chat", + "apps.auth_github", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", +] + +CORS_ALLOW_CREDENTIALS = True +CORS_ALLOWED_ORIGINS = [ + o.strip() + for o in os.environ.get("CORS_ALLOWED_ORIGINS", "http://localhost:5173").split(",") + if o.strip() ] +CSRF_TRUSTED_ORIGINS = [ + o.strip() + for o in os.environ.get("CSRF_TRUSTED_ORIGINS", "http://localhost:5173").split(",") + if o.strip() +] + +SESSION_COOKIE_HTTPONLY = True +SESSION_COOKIE_SAMESITE = "Lax" +SESSION_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SECURE = not DEBUG +CSRF_COOKIE_SAMESITE = "Lax" +SESSION_COOKIE_AGE = 60 * 60 * 24 * 14 -CORS_ALLOW_ALL_ORIGINS = True APPEND_SLASH = False ROOT_URLCONF = "core.urls" +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + ], + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", + ], +} + DATABASES = { "default": { "ENGINE": "django.db.backends.postgresql", @@ -48,7 +88,24 @@ CELERY_BROKER_URL = os.environ.get("REDIS_URL", "redis://redis:6379/0") CELERY_RESULT_BACKEND = CELERY_BROKER_URL -REPOS_DIR = os.environ.get("REPOS_DIR", "/repos") +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.redis.RedisCache", + "LOCATION": os.environ.get("REDIS_URL", "redis://redis:6379/0"), + } +} + +REPOS_DIR = os.environ.get("REPOS_DIR", ".repos") +if not os.path.isabs(REPOS_DIR): + REPOS_DIR = str(BASE_DIR / REPOS_DIR) GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", "") +GITHUB_CLIENT_ID = os.environ.get("GITHUB_CLIENT_ID", "") +GITHUB_CLIENT_SECRET = os.environ.get("GITHUB_CLIENT_SECRET", "") +GITHUB_OAUTH_REDIRECT_URI = os.environ.get( + "GITHUB_OAUTH_REDIRECT_URI", "http://localhost:9000/api/auth/github/callback/" +) +GITHUB_TOKEN_ENC_KEY = os.environ.get("GITHUB_TOKEN_ENC_KEY", "") +FRONTEND_BASE_URL = os.environ.get("FRONTEND_BASE_URL", "http://localhost:5173") + DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/backend/core/test_settings.py b/backend/core/test_settings.py new file mode 100644 index 0000000..5d8a2cf --- /dev/null +++ b/backend/core/test_settings.py @@ -0,0 +1,254 @@ +""" +Test settings: minimal Django config that runs against SQLite. + +Loaded by pytest-django via `DJANGO_SETTINGS_MODULE=core.test_settings` +*before* `django.setup()` populates apps. We use that ordering to install +runtime stubs for postgres-only modules (pgvector, django.contrib.postgres, +google.generativeai) that the chat/graph/files/embeddings models import +at module load. The stubbed fields produce a `db_type` of TEXT under +SQLite; tests never query those models — they hit cross-tenant 404 first. +""" +import os +import sys +import types + +# --- google.generativeai stub ---------------------------------------------- +# The pinned google-generativeai version crashes on Py3.14 protobuf import +# and we never need the real client in unit tests. Both `apps.embeddings.client` +# and `apps.chat.views` / `apps.graph.views` import it at module level. +if "google" not in sys.modules: + _google_pkg = types.ModuleType("google") + _google_pkg.__path__ = [] + sys.modules["google"] = _google_pkg + +if "google.generativeai" not in sys.modules: + _genai_stub = types.ModuleType("google.generativeai") + + def _genai_configure(*args, **kwargs): + return None + + def _genai_embed_content(*args, **kwargs): + return {"embedding": [0.0]} + + class _GenerativeModel: + def __init__(self, *args, **kwargs): + pass + + def generate_content(self, *args, **kwargs): + class _R: + text = "" + return _R() + + _genai_stub.configure = _genai_configure + _genai_stub.embed_content = _genai_embed_content + _genai_stub.GenerativeModel = _GenerativeModel + sys.modules["google.generativeai"] = _genai_stub + sys.modules["google"].generativeai = _genai_stub + + +# --- django.contrib.postgres.fields.ArrayField stub ------------------------ +# `apps.graph.models` imports ArrayField at module load; the real package +# transitively imports psycopg2 which we don't ship in the test venv. +# We stub Django itself isn't yet imported here — but `from django...` triggers +# loading `django` and `django.db` only. We then poison the `postgres` subpath. +if "django.contrib.postgres" not in sys.modules: + from django.db import models as _dj_models # noqa: E402 + + _pg_pkg = types.ModuleType("django.contrib.postgres") + _pg_pkg.__path__ = [] + _pg_fields = types.ModuleType("django.contrib.postgres.fields") + _pg_fields.__path__ = [] + + import json as _json # noqa: E402 + + class _StubArrayField(_dj_models.Field): + def __init__(self, base_field=None, size=None, **kwargs): + # Default base_field so Django's `field.clone()` (which calls + # __init__ with deconstructed args) doesn't fail. + self.base_field = base_field + self.size = size + super().__init__(**kwargs) + + def db_type(self, connection): + return "text" + + def get_prep_value(self, value): + # Serialize lists as JSON for SQLite. Real ArrayField stores them + # natively in postgres; the stub only needs to round-trip values + # in-memory so tests that create rows don't blow up. + if value is None: + return None + return _json.dumps(list(value)) + + def from_db_value(self, value, expression, connection): + if value is None or value == "": + return [] + try: + return _json.loads(value) + except (ValueError, TypeError): + return [] + + def to_python(self, value): + if value is None or isinstance(value, list): + return value if value is not None else [] + try: + return _json.loads(value) + except (ValueError, TypeError): + return [] + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if self.size is not None: + kwargs["size"] = self.size + # Don't try to round-trip base_field through deconstruct — the + # nested field would need its own deconstruct path. The stub is + # only used so models can be created in-memory under SQLite. + return name, path, args, kwargs + + class _StubHStoreField(_dj_models.Field): + def db_type(self, connection): + return "text" + + class _StubJSONField(_dj_models.Field): + def db_type(self, connection): + return "text" + + class _StubRangeField(_dj_models.Field): + def db_type(self, connection): + return "text" + + _pg_fields.ArrayField = _StubArrayField + _pg_fields.HStoreField = _StubHStoreField + _pg_fields.JSONField = _StubJSONField + _pg_fields.RangeField = _StubRangeField + sys.modules["django.contrib.postgres"] = _pg_pkg + sys.modules["django.contrib.postgres.fields"] = _pg_fields + _pg_pkg.fields = _pg_fields + + # Make sure `django.contrib.postgres` resolves to the stub via attribute + # lookup (some import paths short-circuit via the parent package). + import django.contrib # noqa: E402 + django.contrib.postgres = _pg_pkg + + +# --- pgvector.django stub --------------------------------------------------- +# pgvector's __init__ imports django.contrib.postgres.operations which we +# can't load. The chat view imports `L2Distance`; the embeddings model uses +# `VectorField` at module load. +if "pgvector" not in sys.modules: + _pgv = types.ModuleType("pgvector") + _pgv.__path__ = [] + sys.modules["pgvector"] = _pgv + +if "pgvector.django" not in sys.modules: + from django.db import models as _dj_models2 # noqa: E402 + + _pgv_django = types.ModuleType("pgvector.django") + + class _StubVectorField(_dj_models2.Field): + def __init__(self, *args, dimensions=None, **kwargs): + self.dimensions = dimensions + super().__init__(*args, **kwargs) + + def db_type(self, connection): + return "text" + + def deconstruct(self): + name, path, args, kwargs = super().deconstruct() + if self.dimensions is not None: + kwargs["dimensions"] = self.dimensions + return name, path, args, kwargs + + class _StubL2Distance: + def __init__(self, *args, **kwargs): + pass + + _pgv_django.VectorField = _StubVectorField + _pgv_django.L2Distance = _StubL2Distance + sys.modules["pgvector.django"] = _pgv_django + sys.modules["pgvector"].django = _pgv_django + + +# Force sane env defaults BEFORE importing core.settings (which calls +# load_dotenv() and reads env vars at import time). +os.environ.setdefault("GITHUB_TOKEN_ENC_KEY", "") # let individual tests set this +os.environ.setdefault("GITHUB_CLIENT_ID", "test_client_id") +os.environ.setdefault("GITHUB_CLIENT_SECRET", "test_client_secret") +os.environ.setdefault("GITHUB_OAUTH_REDIRECT_URI", "http://testserver/api/auth/github/callback/") +os.environ.setdefault("FRONTEND_BASE_URL", "http://localhost:5173") +os.environ.setdefault("DEBUG", "True") +os.environ.setdefault("REPOS_DIR", "/tmp/codegraph-test-repos") + +from core.settings import * # noqa: E402,F401,F403 + +# Minimal apps -- only what the tests under test need. +# `apps.files`, `apps.graph`, `apps.chat`, `apps.embeddings` are added so the +# cross-app access-gating views can be loaded in `core.test_urls`. Their +# models use postgres-only fields (ArrayField/VectorField) which we stub +# pre-emptively in conftest.py; their migrations include `CREATE EXTENSION` +# SQL that doesn't run on SQLite, so we skip migrations entirely (tables are +# created by `--create-db` syncdb). +INSTALLED_APPS = [ + "django.contrib.contenttypes", + "django.contrib.auth", + "django.contrib.sessions", + "rest_framework", + "corsheaders", + "apps.repos", + "apps.auth_github", + "apps.files", + "apps.graph", + "apps.embeddings", + "apps.chat", +] + +# Skip migrations entirely so SQLite-incompatible operations +# (CREATE EXTENSION vector, etc) don't run. Tables are created by syncdb +# from the (stubbed-field) model definitions. +class _DisableMigrations: + def __contains__(self, item): + return True + + def __getitem__(self, item): + return None + + +MIGRATION_MODULES = _DisableMigrations() + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": ":memory:", + } +} + +# Avoid hitting redis in tests. +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + } +} + +# Run celery tasks synchronously when invoked via .apply() — but we always +# patch .delay() in tests anyway. This is just belt-and-suspenders. +CELERY_TASK_ALWAYS_EAGER = True +CELERY_TASK_EAGER_PROPAGATES = True + +# Fixed Fernet key for crypto tests that go through the regular path. +# Tests that need a missing key explicitly clear it via override_settings. +from cryptography.fernet import Fernet # noqa: E402 + +GITHUB_TOKEN_ENC_KEY = Fernet.generate_key().decode() + +# Use the trimmed test URLconf. +ROOT_URLCONF = "core.test_urls" + +# Disable HTTPS cookie flags so the test client (HTTP) sets them. +SESSION_COOKIE_SECURE = False +CSRF_COOKIE_SECURE = False + +# Sentinel so callers can detect we are in test mode. +TESTING = True + +# Use a fast password hasher in tests. +PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] diff --git a/backend/core/test_urls.py b/backend/core/test_urls.py new file mode 100644 index 0000000..b16aa9d --- /dev/null +++ b/backend/core/test_urls.py @@ -0,0 +1,17 @@ +"""URLconf used in tests. + +Mounts the repos / auth_github / files / graph / chat URL modules. The +files/graph/chat views import postgres-only model fields (ArrayField, +pgvector.VectorField); those imports are stubbed in conftest.py at process +start so the modules can be loaded under SQLite. Tests for those views +hit the access-gating path (404) and never query the postgres-only models. +""" +from django.urls import include, path + +urlpatterns = [ + path("api/", include("apps.auth_github.urls")), + path("api/repos/", include("apps.repos.urls")), + path("api/files/", include("apps.files.urls")), + path("api/graph/", include("apps.graph.urls")), + path("api/chat/", include("apps.chat.urls")), +] diff --git a/backend/core/urls.py b/backend/core/urls.py index 437a125..98259ff 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -1,6 +1,7 @@ from django.urls import include, path urlpatterns = [ + path("api/", include("apps.auth_github.urls")), path("api/repos/", include("apps.repos.urls")), path("api/graph/", include("apps.graph.urls")), path("api/chat/", include("apps.chat.urls")), diff --git a/backend/env.local b/backend/env.local index fa2f90a..60c2110 100644 --- a/backend/env.local +++ b/backend/env.local @@ -11,6 +11,18 @@ REDIS_URL=redis://redis:6379/0 GOOGLE_API_KEY=your-gemini-api-key-here -REPOS_DIR=/repos +REPOS_DIR=.repos ALLOWED_HOSTS=* + +# GitHub OAuth — register an app at https://github.com/settings/developers +GITHUB_CLIENT_ID= +GITHUB_CLIENT_SECRET= +GITHUB_OAUTH_REDIRECT_URI=http://localhost:9000/api/auth/github/callback/ + +# generate with: python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" +GITHUB_TOKEN_ENC_KEY= + +FRONTEND_BASE_URL=http://localhost:5173 +CORS_ALLOWED_ORIGINS=http://localhost:5173 +CSRF_TRUSTED_ORIGINS=http://localhost:5173 diff --git a/backend/manage.py b/backend/manage.py index 1d429e5..eceb993 100644 --- a/backend/manage.py +++ b/backend/manage.py @@ -4,5 +4,9 @@ if __name__ == "__main__": os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings") + + if len(sys.argv) == 2 and sys.argv[1] == "runserver": + sys.argv.append("9000") + from django.core.management import execute_from_command_line execute_from_command_line(sys.argv) diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 0000000..cc15330 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +DJANGO_SETTINGS_MODULE = core.test_settings +python_files = test_*.py +testpaths = apps +addopts = -ra --strict-markers diff --git a/backend/requirements.txt b/backend/requirements.txt index 69f5a78..6e9159a 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -9,3 +9,5 @@ gitpython==3.1.43 google-generativeai==0.7.2 python-dotenv==1.0.1 ruff==0.4.4 +cryptography>=42,<46 +requests>=2.32.2,<3 diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index db6a873..2d44be6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5,17 +5,32 @@ import { Landing } from './components/Landing' import { Processing } from './components/Processing' import { Dashboard } from './components/Dashboard' import { RepoSwitcher } from './components/RepoSwitcher' +import { Login } from './components/Login' +import { useAuth } from './auth/AuthContext' type View = 'landing' | 'processing' | 'dashboard' const STORAGE_KEY = 'codegraph_last_repo_id' -export default function App() { +function UserMenu() { + const { user, logout } = useAuth() + if (!user) return null + return ( +
+ {user.avatar_url && ( + {user.login} + )} + @{user.login} + +
+ ) +} + +function AuthedApp() { const [view, setView] = useState('landing') const [repo, setRepo] = useState(null) const [restoring, setRestoring] = useState(true) - // Restore last repo on mount useEffect(() => { const savedId = localStorage.getItem(STORAGE_KEY) if (!savedId) { setRestoring(false); return } @@ -43,10 +58,21 @@ export default function App() { if (restoring) return null const switcher = ( - +
+ + +
) if (view === 'landing') return if (view === 'processing') return return setView('processing')} switcher={switcher} /> } + +export default function App() { + const { status } = useAuth() + + if (status === 'loading') return null + if (status === 'anon') return + return +} diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f6857cf..f406404 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,15 +1,53 @@ import axios from 'axios' -import type { Repository, RepoFile, FileFn, GraphData } from '../types' +import type { Repository, RepoFile, FileFn, GraphData, User, GitHubRepo } from '../types' -const http = axios.create({ baseURL: '/api' }) +// Use axios's native CSRF support: it reads the named cookie and attaches it +// as the named header on same-origin unsafe requests. Works identically to +// the manual interceptor we used to maintain, with fewer moving parts. +const http = axios.create({ + baseURL: '/api', + withCredentials: true, + xsrfCookieName: 'csrftoken', + xsrfHeaderName: 'X-CSRFToken', +}) + +http.interceptors.response.use( + r => r, + err => { + if (err?.response?.status === 401) { + window.dispatchEvent(new CustomEvent('auth:unauthorized')) + } + return Promise.reject(err) + }, +) export const api = { + seedCsrf: () => + http.get('/auth/csrf/').then(() => undefined), + + getMe: () => + http.get('/me/').then(r => r.data), + + startGithubLogin: () => + http.get<{ authorize_url: string }>('/auth/github/start/').then(r => { + window.location.href = r.data.authorize_url + }), + + logout: () => + http.post('/auth/logout/').then(() => undefined), + + listMyGithubRepos: (q?: string, page = 1) => + http.get('/github/repos/', { params: { q, page } }).then(r => r.data), + listRepos: () => http.get('/repos/').then(r => r.data), submitRepo: (url: string) => http.post('/repos/', { url }).then(r => r.data), + attachRepo: (payload: { repo_id?: string; url?: string }) => + http.post('/repos/attach/', payload).then(r => r.data), + getRepo: (id: string) => http.get(`/repos/${id}/`).then(r => r.data), diff --git a/frontend/src/auth/AuthContext.tsx b/frontend/src/auth/AuthContext.tsx new file mode 100644 index 0000000..7aae607 --- /dev/null +++ b/frontend/src/auth/AuthContext.tsx @@ -0,0 +1,74 @@ +import { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react' +import type { User } from '../types' +import { api } from '../api' + +type AuthStatus = 'loading' | 'authed' | 'anon' + +interface AuthContextValue { + user: User | null + status: AuthStatus + refresh: () => Promise + logout: () => Promise +} + +const AuthContext = createContext(null) + +export function AuthProvider({ children }: { children: ReactNode }) { + const [user, setUser] = useState(null) + const [status, setStatus] = useState('loading') + + const refresh = useCallback(async () => { + try { + const me = await api.getMe() + setUser(me) + setStatus('authed') + } catch { + setUser(null) + setStatus('anon') + } + }, []) + + const logout = useCallback(async () => { + try { + await api.logout() + } finally { + setUser(null) + setStatus('anon') + } + }, []) + + useEffect(() => { + let cancelled = false + ;(async () => { + try { + await api.seedCsrf() + } catch { + // ignore — server may be down; getMe will fail too + } + if (cancelled) return + await refresh() + })() + return () => { cancelled = true } + }, [refresh]) + + useEffect(() => { + const handler = () => { + setUser(null) + setStatus('anon') + } + window.addEventListener('auth:unauthorized', handler) + return () => window.removeEventListener('auth:unauthorized', handler) + }, []) + + return ( + + {children} + + ) +} + +export function useAuth(): AuthContextValue { + const ctx = useContext(AuthContext) + if (!ctx) throw new Error('useAuth must be used within AuthProvider') + return ctx +} diff --git a/frontend/src/components/GithubRepoPicker.tsx b/frontend/src/components/GithubRepoPicker.tsx new file mode 100644 index 0000000..f463a03 --- /dev/null +++ b/frontend/src/components/GithubRepoPicker.tsx @@ -0,0 +1,128 @@ +import { useEffect, useState } from 'react' +import type { GitHubRepo, Repository } from '../types' +import { api } from '../api' + +interface Props { + onAnalyze: (repo: Repository) => void +} + +function relativeTime(iso: string): string { + if (!iso) return '' + const then = new Date(iso).getTime() + if (Number.isNaN(then)) return '' + const sec = Math.floor((Date.now() - then) / 1000) + if (sec < 60) return `${sec}s ago` + if (sec < 3600) return `${Math.floor(sec / 60)}m ago` + if (sec < 86400) return `${Math.floor(sec / 3600)}h ago` + if (sec < 86400 * 30) return `${Math.floor(sec / 86400)}d ago` + return new Date(iso).toLocaleDateString() +} + +const Q_DEBOUNCE_MS = 300 + +export function GithubRepoPicker({ onAnalyze }: Props) { + const [repos, setRepos] = useState([]) + const [q, setQ] = useState('') + // `debouncedQ` is what the data fetch actually keys on; `q` is the live + // input value so typing stays snappy. We debounce by 300ms to avoid + // hammering the upstream GitHub API on every keystroke. + const [debouncedQ, setDebouncedQ] = useState('') + const [page, setPage] = useState(1) + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + const [needsReauth, setNeedsReauth] = useState(false) + const [submittingId, setSubmittingId] = useState(null) + + useEffect(() => { + const t = setTimeout(() => setDebouncedQ(q), Q_DEBOUNCE_MS) + return () => clearTimeout(t) + }, [q]) + + useEffect(() => { + let cancelled = false + setLoading(true) + setError('') + api.listMyGithubRepos(debouncedQ, page) + .then(data => { if (!cancelled) setRepos(data) }) + .catch(err => { + if (cancelled) return + if (err?.response?.status === 401 && err?.response?.data?.needs_reauth) { + setNeedsReauth(true) + } else if (err?.response?.status === 503) { + setError('GitHub rate limit reached. Try again shortly.') + } else { + setError('Could not load repositories.') + } + }) + .finally(() => { if (!cancelled) setLoading(false) }) + return () => { cancelled = true } + }, [debouncedQ, page]) + + const handleAnalyze = async (gh: GitHubRepo) => { + setSubmittingId(gh.id) + try { + const repo = await api.submitRepo(gh.html_url) + onAnalyze(repo) + } catch (err: unknown) { + const e = err as { response?: { status?: number; data?: { error?: string } } } + setError(e.response?.data?.error || 'Failed to submit repository') + } finally { + setSubmittingId(null) + } + } + + if (needsReauth) { + return ( +
+

GitHub access expired — sign in again.

+ +
+ ) + } + + return ( +
+ { setQ(e.target.value); setPage(1) }} + /> + {loading &&

Loading…

} + {error &&

{error}

} + {!loading && repos.length === 0 && !error && ( +

No repositories found on this page.

+ )} +
+ {repos.map(r => ( +
+
+ {r.full_name} + {r.private && Private} + {relativeTime(r.pushed_at)} +
+ +
+ ))} +
+
+ + Page {page} + +
+
+ ) +} diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx index 2e13b09..f01aeb8 100644 --- a/frontend/src/components/Landing.tsx +++ b/frontend/src/components/Landing.tsx @@ -1,13 +1,17 @@ import { useState } from 'react' import type { Repository } from '../types' import { api } from '../api' +import { GithubRepoPicker } from './GithubRepoPicker' interface Props { onSubmit: (repo: Repository) => void switcher: React.ReactNode } +type Tab = 'github' | 'url' + export function Landing({ onSubmit, switcher }: Props) { + const [tab, setTab] = useState('github') const [url, setUrl] = useState('') const [loading, setLoading] = useState(false) const [error, setError] = useState('') @@ -31,19 +35,41 @@ export function Landing({ onSubmit, switcher }: Props) {
{switcher}

CodeGraph

Explore any GitHub repository as a function-level knowledge graph

-
- setUrl(e.target.value)} - required - /> - -
- {error &&

{error}

} + + + + {tab === 'github' ? ( + + ) : ( + <> +
+ setUrl(e.target.value)} + required + /> + +
+ {error &&

{error}

} + + )} ) } diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx new file mode 100644 index 0000000..db59400 --- /dev/null +++ b/frontend/src/components/Login.tsx @@ -0,0 +1,42 @@ +import { useEffect, useState } from 'react' +import { api } from '../api' + +export function Login() { + const [loading, setLoading] = useState(false) + const [error, setError] = useState('') + + useEffect(() => { + const params = new URLSearchParams(window.location.search) + if (params.get('login_error') === '1') { + setError('Sign in failed. Please try again.') + params.delete('login_error') + const next = params.toString() + window.history.replaceState({}, '', window.location.pathname + (next ? `?${next}` : '')) + } + }, []) + + const handleClick = async () => { + setLoading(true) + setError('') + try { + await api.startGithubLogin() + } catch { + setError('Could not start GitHub sign-in. Please try again.') + setLoading(false) + } + } + + return ( +
+

CodeGraph

+

Sign in to explore your repositories as function-level knowledge graphs.

+ +

+ We request the repo scope so you can clone private repositories you choose. +

+ {error &&

{error}

} +
+ ) +} diff --git a/frontend/src/components/RepoSwitcher.tsx b/frontend/src/components/RepoSwitcher.tsx index 35d1f60..9f4a2db 100644 --- a/frontend/src/components/RepoSwitcher.tsx +++ b/frontend/src/components/RepoSwitcher.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef, useState } from 'react' import type { Repository } from '../types' import { api } from '../api' +import { GithubRepoPicker } from './GithubRepoPicker' interface Props { current: Repository | null @@ -8,18 +9,21 @@ interface Props { onNew: (repo: Repository) => void } +type Mode = 'list' | 'github' | 'url' + export function RepoSwitcher({ current, onSelect, onNew }: Props) { const [open, setOpen] = useState(false) + const [mode, setMode] = useState('list') const [repos, setRepos] = useState([]) const [url, setUrl] = useState('') const [loading, setLoading] = useState(false) + const [error, setError] = useState('') const ref = useRef(null) useEffect(() => { - api.listRepos().then(setRepos) - }, [current]) // refresh list whenever current changes (new repo added) + api.listRepos().then(setRepos).catch(() => setRepos([])) + }, [current]) - // Close on outside click useEffect(() => { const handler = (e: MouseEvent) => { if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false) @@ -32,16 +36,43 @@ export function RepoSwitcher({ current, onSelect, onNew }: Props) { e.preventDefault() if (!url.trim()) return setLoading(true) + setError('') try { const repo = await api.submitRepo(url.trim()) setUrl('') setOpen(false) + setMode('list') onNew(repo) + } catch (err: unknown) { + // Surface backend-validation errors (invalid_url, needs_reauth, + // no_access, github_unreachable) instead of swallowing them. + const e2 = err as { response?: { status?: number; data?: { error?: string; detail?: string } } } + const code = e2.response?.data?.error + const detail = e2.response?.data?.detail + if (code === 'invalid_url') { + setError(detail ? `Invalid URL: ${detail}` : 'Invalid URL.') + } else if (code === 'needs_reauth') { + setError('GitHub session expired. Sign in again.') + } else if (code === 'github_unreachable') { + setError('Could not reach GitHub. Try again shortly.') + } else if (code === 'no_access') { + setError('No access to this repository.') + } else if (code) { + setError(code) + } else { + setError('Failed to submit repository.') + } } finally { setLoading(false) } } + const handlePickerAnalyze = (repo: Repository) => { + setOpen(false) + setMode('list') + onNew(repo) + } + const statusDot = (status: Repository['status']) => { if (status === 'ready') return '🟢' if (status === 'failed') return '🔴' @@ -59,7 +90,13 @@ export function RepoSwitcher({ current, onSelect, onNew }: Props) { {open && (
- {repos.length > 0 && ( +
+ + + +
+ + {mode === 'list' && repos.length > 0 && (
{repos.map(r => (
)} -
- setUrl(e.target.value)} - autoFocus - /> - -
+ + {mode === 'list' && repos.length === 0 && ( +

No repos yet.

+ )} + + {mode === 'github' && ( + + )} + + {mode === 'url' && ( +
+ { setUrl(e.target.value); setError('') }} + autoFocus + /> + + {error && ( +

{error}

+ )} +
+ )}
)}
diff --git a/frontend/src/index.css b/frontend/src/index.css index f45d158..fb880a7 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -924,3 +924,129 @@ body { ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; } ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +/* ── User menu / topbar row ── */ +.topbar-row { + display: flex; + align-items: center; + gap: 12px; +} +.user-menu { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: var(--text-muted); +} +.user-avatar { + width: 24px; + height: 24px; + border-radius: 50%; + border: 1px solid var(--border); +} +.user-login { color: var(--text); } +.user-logout { + background: transparent; + border: 1px solid var(--border); + color: var(--text-muted); + padding: 4px 8px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; +} +.user-logout:hover { color: var(--text); border-color: var(--accent); } + +/* ── Landing tabs / RepoSwitcher tabs ── */ +.landing-tabs, .repo-switcher-tabs { + display: flex; + gap: 8px; + margin: 12px 0; +} +.landing-tabs button, .repo-switcher-tabs button { + background: transparent; + border: 1px solid var(--border); + color: var(--text-muted); + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 13px; +} +.landing-tabs button.active, .repo-switcher-tabs button.active { + color: var(--text); + border-color: var(--accent); + background: rgba(88,166,255,0.08); +} +.repo-switcher-tabs { padding: 8px 12px 0; margin: 0; } + +/* ── GitHub repo picker ── */ +.gh-picker { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 12px 12px; + width: 100%; + max-width: 560px; +} +.gh-picker input[type="text"] { + background: var(--surface); + border: 1px solid var(--border); + color: var(--text); + padding: 8px 10px; + border-radius: 4px; + font-size: 13px; +} +.gh-picker input[type="text"]:focus { outline: none; border-color: var(--accent); } +.gh-repo-list { + display: flex; + flex-direction: column; + max-height: 360px; + overflow-y: auto; + border: 1px solid var(--border); + border-radius: 4px; +} +.gh-repo-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + padding: 8px 10px; + border-bottom: 1px solid var(--border); +} +.gh-repo-row:last-child { border-bottom: none; } +.gh-repo-info { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} +.gh-repo-name { + font-size: 13px; + color: var(--text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.gh-repo-badge { + font-size: 10px; + padding: 1px 6px; + border-radius: 10px; + border: 1px solid var(--warning); + color: var(--warning); +} +.gh-repo-time { font-size: 12px; color: var(--text-muted); } +.gh-repo-row .btn-primary { padding: 4px 10px; font-size: 12px; } +.gh-pager { + display: flex; + align-items: center; + justify-content: space-between; +} +.gh-pager button { + background: transparent; + border: 1px solid var(--border); + color: var(--text); + padding: 4px 10px; + border-radius: 4px; + cursor: pointer; + font-size: 12px; +} +.gh-pager button:disabled { opacity: 0.4; cursor: not-allowed; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index db032b7..6b8f334 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -2,9 +2,12 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' import App from './App' +import { AuthProvider } from './auth/AuthContext' createRoot(document.getElementById('root')!).render( - + + + , ) diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 2737bda..4910160 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -47,3 +47,20 @@ export interface ChatMessage { content: string functions?: FileFn[] } + +export interface User { + id: number + login: string + avatar_url: string + needs_reauth: boolean +} + +export interface GitHubRepo { + id: number + name: string + full_name: string + html_url: string + private: boolean + pushed_at: string + default_branch: string +} diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 079059d..ff6d8e0 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,7 +4,7 @@ import react from '@vitejs/plugin-react' export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), '') - const apiTarget = env.VITE_API_TARGET || 'http://127.0.0.1:8000' + const apiTarget = env.VITE_API_TARGET || 'http://127.0.0.1:9000' return { plugins: [react()], From 54904ce26fe1bf8a42a74aff016adfd8f8b5c73d Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:23:14 +0530 Subject: [PATCH 02/18] chore: standardize on pnpm lockfile, ignore swap files --- .gitignore | 3 + frontend/package-lock.json | 4024 ------------------------------------ frontend/package.json | 1 + frontend/pnpm-lock.yaml | 2653 ++++++++++++++++++++++++ 4 files changed, 2657 insertions(+), 4024 deletions(-) delete mode 100644 frontend/package-lock.json create mode 100644 frontend/pnpm-lock.yaml diff --git a/.gitignore b/.gitignore index 4469016..b364838 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,6 @@ htmlcov/ *.tsbuildinfo .vite/ + +*.swp +package-lock.json diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 225ba02..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,4024 +0,0 @@ -{ - "name": "codegraph", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "codegraph", - "version": "0.0.0", - "dependencies": { - "@types/d3": "^7.4.3", - "@xyflow/react": "^12.6.4", - "axios": "^1.7.9", - "d3": "^7.9.0", - "react": "^19.2.4", - "react-dom": "^19.2.4" - }, - "devDependencies": { - "@eslint/js": "^9.39.4", - "@types/node": "^25.6.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.58.2", - "@typescript-eslint/parser": "^8.58.2", - "@vitejs/plugin-react": "^6.0.1", - "eslint": "^9.39.4", - "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.4.0", - "typescript": "^6.0.3", - "vite": "^8.0.4" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "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/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "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/@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, - "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, - "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, - "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, - "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, - "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, - "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.7", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", - "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==", - "dev": true, - "license": "MIT" - }, - "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/d3": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", - "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/d3-axis": "*", - "@types/d3-brush": "*", - "@types/d3-chord": "*", - "@types/d3-color": "*", - "@types/d3-contour": "*", - "@types/d3-delaunay": "*", - "@types/d3-dispatch": "*", - "@types/d3-drag": "*", - "@types/d3-dsv": "*", - "@types/d3-ease": "*", - "@types/d3-fetch": "*", - "@types/d3-force": "*", - "@types/d3-format": "*", - "@types/d3-geo": "*", - "@types/d3-hierarchy": "*", - "@types/d3-interpolate": "*", - "@types/d3-path": "*", - "@types/d3-polygon": "*", - "@types/d3-quadtree": "*", - "@types/d3-random": "*", - "@types/d3-scale": "*", - "@types/d3-scale-chromatic": "*", - "@types/d3-selection": "*", - "@types/d3-shape": "*", - "@types/d3-time": "*", - "@types/d3-time-format": "*", - "@types/d3-timer": "*", - "@types/d3-transition": "*", - "@types/d3-zoom": "*" - } - }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", - "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", - "license": "MIT" - }, - "node_modules/@types/d3-axis": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", - "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-brush": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", - "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-chord": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", - "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", - "license": "MIT" - }, - "node_modules/@types/d3-color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", - "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "license": "MIT" - }, - "node_modules/@types/d3-contour": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", - "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", - "license": "MIT", - "dependencies": { - "@types/d3-array": "*", - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-delaunay": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", - "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", - "license": "MIT" - }, - "node_modules/@types/d3-dispatch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", - "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", - "license": "MIT" - }, - "node_modules/@types/d3-drag": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", - "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-dsv": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", - "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", - "license": "MIT" - }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", - "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "license": "MIT" - }, - "node_modules/@types/d3-fetch": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", - "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", - "license": "MIT", - "dependencies": { - "@types/d3-dsv": "*" - } - }, - "node_modules/@types/d3-force": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", - "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", - "license": "MIT" - }, - "node_modules/@types/d3-format": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", - "license": "MIT" - }, - "node_modules/@types/d3-geo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", - "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/d3-hierarchy": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", - "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", - "license": "MIT" - }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", - "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", - "license": "MIT", - "dependencies": { - "@types/d3-color": "*" - } - }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-polygon": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", - "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", - "license": "MIT" - }, - "node_modules/@types/d3-quadtree": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", - "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", - "license": "MIT" - }, - "node_modules/@types/d3-random": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", - "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", - "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", - "license": "MIT", - "dependencies": { - "@types/d3-time": "*" - } - }, - "node_modules/@types/d3-scale-chromatic": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", - "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", - "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", - "license": "MIT" - }, - "node_modules/@types/d3-time-format": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", - "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", - "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "license": "MIT" - }, - "node_modules/@types/d3-transition": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", - "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", - "license": "MIT", - "dependencies": { - "@types/d3-selection": "*" - } - }, - "node_modules/@types/d3-zoom": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", - "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", - "license": "MIT", - "dependencies": { - "@types/d3-interpolate": "*", - "@types/d3-selection": "*" - } - }, - "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/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.6.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", - "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.19.0" - } - }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" - } - }, - "node_modules/@types/react-dom": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", - "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", - "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/type-utils": "8.58.2", - "@typescript-eslint/utils": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.58.2", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", - "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", - "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.2", - "@typescript-eslint/types": "^8.58.2", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", - "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", - "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", - "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", - "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", - "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.58.2", - "@typescript-eslint/tsconfig-utils": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", - "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", - "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.58.2", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", - "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "1.0.0-rc.7" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" - }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } - } - }, - "node_modules/@xyflow/react": { - "version": "12.10.2", - "resolved": "https://registry.npmjs.org/@xyflow/react/-/react-12.10.2.tgz", - "integrity": "sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==", - "license": "MIT", - "dependencies": { - "@xyflow/system": "0.0.76", - "classcat": "^5.0.3", - "zustand": "^4.4.0" - }, - "peerDependencies": { - "react": ">=17", - "react-dom": ">=17" - } - }, - "node_modules/@xyflow/system": { - "version": "0.0.76", - "resolved": "https://registry.npmjs.org/@xyflow/system/-/system-0.0.76.tgz", - "integrity": "sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==", - "license": "MIT", - "dependencies": { - "@types/d3-drag": "^3.0.7", - "@types/d3-interpolate": "^3.0.4", - "@types/d3-selection": "^3.0.10", - "@types/d3-transition": "^3.0.8", - "@types/d3-zoom": "^3.0.8", - "d3-drag": "^3.0.0", - "d3-interpolate": "^3.0.1", - "d3-selection": "^3.0.0", - "d3-zoom": "^3.0.0" - } - }, - "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/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.10.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", - "integrity": "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001788", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", - "integrity": "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/classcat": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", - "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", - "license": "MIT" - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "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/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT" - }, - "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/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "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/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "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/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.5.340", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.340.tgz", - "integrity": "sha512-908qahOGocRMinT2nM3ajCEM99H4iPdv84eagPP3FfZy/1ZGeOy2CZYzjhms81ckOPCXPlW7LkY4XpxD8r1DrA==", - "dev": true, - "license": "ISC" - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.0.tgz", - "integrity": "sha512-LDicyhrRFrIaheDYryeM2W8gWyZXnAs4zIr2WVPiOSeTmIu2RjR4x/9N0xLaRWZ+9hssBDGo3AadcohuzAvSvg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", - "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "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/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "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/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/globals": { - "version": "17.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz", - "integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "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/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "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-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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, - "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, - "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, - "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, - "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-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "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/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/node-releases": { - "version": "2.0.37", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", - "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", - "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.5", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", - "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.5" - } - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "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/rolldown/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/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/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/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "license": "MIT" - }, - "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "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/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "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/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "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/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "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/undici-types": { - "version": "7.19.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", - "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", - "dev": true, - "license": "MIT" - }, - "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/use-sync-external-store": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", - "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", - "license": "MIT", - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "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/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, - "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", - "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, - "engines": { - "node": ">=12.7.0" - }, - "peerDependencies": { - "@types/react": ">=16.8", - "immer": ">=9.0.6", - "react": ">=16.8" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "immer": { - "optional": true - }, - "react": { - "optional": true - } - } - } - } -} diff --git a/frontend/package.json b/frontend/package.json index ebe07d8..b89c3ad 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "name": "codegraph", "private": true, "version": "0.0.0", + "packageManager": "pnpm@9.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml new file mode 100644 index 0000000..0135773 --- /dev/null +++ b/frontend/pnpm-lock.yaml @@ -0,0 +1,2653 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 + '@xyflow/react': + specifier: ^12.6.4 + version: 12.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + axios: + specifier: ^1.7.9 + version: 1.16.0 + d3: + specifier: ^7.9.0 + version: 7.9.0 + react: + specifier: ^19.2.4 + version: 19.2.6 + react-dom: + specifier: ^19.2.4 + version: 19.2.6(react@19.2.6) + devDependencies: + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 + '@types/node': + specifier: ^25.6.0 + version: 25.6.2 + '@types/react': + specifier: ^19.2.14 + version: 19.2.14 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.14) + '@typescript-eslint/eslint-plugin': + specifier: ^8.58.2 + version: 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': + specifier: ^8.58.2 + version: 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@vitejs/plugin-react': + specifier: ^6.0.1 + version: 6.0.1(vite@8.0.11(@types/node@25.6.2)) + eslint: + specifier: ^9.39.4 + version: 9.39.4 + eslint-plugin-react-hooks: + specifier: ^7.0.1 + version: 7.1.1(eslint@9.39.4) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@9.39.4) + globals: + specifier: ^17.4.0 + version: 17.6.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vite: + specifier: ^8.0.4 + version: 8.0.11(@types/node@25.6.2) + +packages: + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.128.0': + resolution: {integrity: sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==} + + '@rolldown/binding-android-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + resolution: {integrity: sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + resolution: {integrity: sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + resolution: {integrity: sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + resolution: {integrity: sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + resolution: {integrity: sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + resolution: {integrity: sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + resolution: {integrity: sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + resolution: {integrity: sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.18': + resolution: {integrity: sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@25.6.2': + resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@xyflow/react@12.10.2': + resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==} + peerDependencies: + react: '>=17' + react-dom: '>=17' + + '@xyflow/system@0.0.76': + resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.16.0: + resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.28: + resolution: {integrity: sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001792: + resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + classcat@5.0.5: + resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.353: + resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.6: + resolution: {integrity: sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==} + peerDependencies: + react: ^19.2.6 + + react@19.2.6: + resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + rolldown@1.0.0-rc.18: + resolution: {integrity: sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + vite@8.0.11: + resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + + zustand@4.5.7: + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} + engines: {node: '>=12.7.0'} + peerDependencies: + '@types/react': '>=16.8' + immer: '>=9.0.6' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + +snapshots: + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.3': + dependencies: + '@babel/types': 7.29.0 + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@oxc-project/types@0.128.0': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.18': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.18': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.18': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.18': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.18': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.18': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.18': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.9': {} + + '@types/geojson@7946.0.16': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@25.6.2': + dependencies: + undici-types: 7.19.2 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 9.39.4 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.2': {} + + '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + eslint: 9.39.4 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@25.6.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.11(@types/node@25.6.2) + + '@xyflow/react@12.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + dependencies: + '@xyflow/system': 0.0.76 + classcat: 5.0.5 + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + zustand: 4.5.7(@types/react@19.2.14)(react@19.2.6) + transitivePeerDependencies: + - '@types/react' + - immer + + '@xyflow/system@0.0.76': + dependencies: + '@types/d3-drag': 3.0.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + asynckit@0.4.0: {} + + axios@1.16.0: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.28: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.28 + caniuse-lite: 1.0.30001792 + electron-to-chromium: 1.5.353 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001792: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + classcat@5.0.5: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@7.2.0: {} + + concat-map@0.0.1: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.353: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.3 + eslint: 9.39.4 + hermes-parser: 0.25.1 + zod: 4.4.3 + zod-validation-error: 4.0.2(zod@4.4.3) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): + dependencies: + eslint: 9.39.4 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.16.0: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@17.6.0: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + internmap@2.0.3: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + 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 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + ms@2.1.3: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + proxy-from-env@2.1.0: {} + + punycode@2.3.1: {} + + react-dom@19.2.6(react@19.2.6): + dependencies: + react: 19.2.6 + scheduler: 0.27.0 + + react@19.2.6: {} + + resolve-from@4.0.0: {} + + robust-predicates@3.0.3: {} + + rolldown@1.0.0-rc.18: + dependencies: + '@oxc-project/types': 0.128.0 + '@rolldown/pluginutils': 1.0.0-rc.18 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.18 + '@rolldown/binding-darwin-x64': 1.0.0-rc.18 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.18 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.18 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.18 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.18 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.18 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.18 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.18 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.18 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + semver@7.8.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + source-map-js@1.2.1: {} + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@6.0.3: {} + + undici-types@7.19.2: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-sync-external-store@1.6.0(react@19.2.6): + dependencies: + react: 19.2.6 + + vite@8.0.11(@types/node@25.6.2): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.18 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.2 + fsevents: 2.3.3 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + yallist@3.1.1: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.3): + dependencies: + zod: 4.4.3 + + zod@4.4.3: {} + + zustand@4.5.7(@types/react@19.2.14)(react@19.2.6): + dependencies: + use-sync-external-store: 1.6.0(react@19.2.6) + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.6 From e79bcd2e2d7dfbdbf7bf35f5a2322e4bcea2d0d4 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:25:21 +0530 Subject: [PATCH 03/18] refactor(repos): centralize status as TextChoices --- .../0003_alter_repository_status.py | 18 +++++++++++++ backend/apps/repos/models.py | 26 +++++++++++-------- backend/apps/repos/tasks.py | 14 +++++----- backend/apps/repos/views.py | 6 ++--- 4 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 backend/apps/repos/migrations/0003_alter_repository_status.py diff --git a/backend/apps/repos/migrations/0003_alter_repository_status.py b/backend/apps/repos/migrations/0003_alter_repository_status.py new file mode 100644 index 0000000..9c030c9 --- /dev/null +++ b/backend/apps/repos/migrations/0003_alter_repository_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2026-05-10 21:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('repos', '0002_repository_first_ingested_by_repository_is_private_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='repository', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('cloning', 'Cloning'), ('parsing', 'Parsing'), ('embedding', 'Embedding'), ('ready', 'Ready'), ('failed', 'Failed')], default='pending', max_length=20), + ), + ] diff --git a/backend/apps/repos/models.py b/backend/apps/repos/models.py index ecd0c9f..f3abde0 100644 --- a/backend/apps/repos/models.py +++ b/backend/apps/repos/models.py @@ -4,21 +4,25 @@ from django.db import models -class Repository(models.Model): - STATUS = [ - ("pending", "Pending"), - ("cloning", "Cloning"), - ("parsing", "Parsing"), - ("graphing", "Graphing"), - ("embedding", "Embedding"), - ("ready", "Ready"), - ("failed", "Failed"), - ] +class RepoStatus(models.TextChoices): + # Mirror these keys in frontend/src/constants/repoStatus.ts when adding states. + PENDING = "pending", "Pending" + CLONING = "cloning", "Cloning" + PARSING = "parsing", "Parsing" + EMBEDDING = "embedding", "Embedding" + READY = "ready", "Ready" + FAILED = "failed", "Failed" + +class Repository(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) url = models.URLField(unique=True) name = models.CharField(max_length=255) - status = models.CharField(max_length=20, choices=STATUS, default="pending") + status = models.CharField( + max_length=20, + choices=RepoStatus.choices, + default=RepoStatus.PENDING, + ) status_message = models.TextField(blank=True) first_ingested_by = models.ForeignKey( settings.AUTH_USER_MODEL, diff --git a/backend/apps/repos/tasks.py b/backend/apps/repos/tasks.py index d15320d..e3d3df3 100644 --- a/backend/apps/repos/tasks.py +++ b/backend/apps/repos/tasks.py @@ -8,7 +8,7 @@ from celery import shared_task from django.conf import settings -from .models import Repository +from .models import RepoStatus, Repository from .utils import parse_github_owner_repo as _parse_github_owner_repo logger = logging.getLogger(__name__) @@ -136,7 +136,7 @@ def ingest_repository(repo_id, user_id=None): clone_url = repo.url try: - _set_status(repo, "cloning", "Cloning repo...") + _set_status(repo, RepoStatus.CLONING, "Cloning repo...") if os.path.exists(repo_path): shutil.rmtree(repo_path) try: @@ -150,17 +150,17 @@ def ingest_repository(repo_id, user_id=None): identity.needs_reauth = True identity.save(update_fields=["needs_reauth", "updated_at"]) msg = _redact(e, token) - _set_status(repo, "failed", msg) + _set_status(repo, RepoStatus.FAILED, msg) logger.warning("Clone failed for repo %s: %s", repo_id, msg) return - _set_status(repo, "parsing", "Parsing and building graph...") + _set_status(repo, RepoStatus.PARSING, "Parsing and building graph...") parse_repository(repo_id, repo_path) - _set_status(repo, "embedding", "Generating embeddings...") + _set_status(repo, RepoStatus.EMBEDDING, "Generating embeddings...") generate_embeddings(repo_id) - _set_status(repo, "ready", "Done") + _set_status(repo, RepoStatus.READY, "Done") except Exception as e: # Outer guard: catch broader-than-GitCommandError failures and @@ -168,7 +168,7 @@ def ingest_repository(repo_id, user_id=None): # Without this, Celery may serialize locals (including `clone_url` # and the original exception's stderr) into the task result. redacted = _redact(e, token) - _set_status(repo, "failed", redacted) + _set_status(repo, RepoStatus.FAILED, redacted) raise RuntimeError(redacted) from None finally: # Best-effort drop of in-scope tokenized URL. diff --git a/backend/apps/repos/views.py b/backend/apps/repos/views.py index 3db0ed7..c01b717 100644 --- a/backend/apps/repos/views.py +++ b/backend/apps/repos/views.py @@ -9,7 +9,7 @@ from apps.auth_github.crypto import decrypt from apps.auth_github.models import GitHubIdentity -from .models import Repository, RepositoryAccess +from .models import RepoStatus, Repository, RepositoryAccess from .serializers import RepositorySerializer from .tasks import ingest_repository from .utils import parse_github_owner_repo as _parse_github_owner_repo @@ -245,10 +245,10 @@ def post(self, request): defaults={"role": "owner", "source": source}, ) - should_ingest = created or repo.status == "failed" + should_ingest = created or repo.status == RepoStatus.FAILED if should_ingest: if not created: - repo.status = "pending" + repo.status = RepoStatus.PENDING repo.status_message = "" repo.save(update_fields=["status", "status_message", "updated_at"]) ingest_repository.delay(str(repo.id), str(request.user.id)) From 55c9cf1beeb9e26efcfa6fb312bcabb4fba37544 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:26:45 +0530 Subject: [PATCH 04/18] refactor: extract github_api and view helpers for DRY --- backend/apps/auth_github/github_api.py | 62 ++++++++++++++++++++++++++ backend/apps/auth_github/views.py | 55 +++++++---------------- backend/apps/repos/_view_helpers.py | 17 +++++++ backend/apps/repos/tasks.py | 29 ++++++------ backend/apps/repos/views.py | 23 +++++----- 5 files changed, 119 insertions(+), 67 deletions(-) create mode 100644 backend/apps/auth_github/github_api.py create mode 100644 backend/apps/repos/_view_helpers.py diff --git a/backend/apps/auth_github/github_api.py b/backend/apps/auth_github/github_api.py new file mode 100644 index 0000000..ba0fe71 --- /dev/null +++ b/backend/apps/auth_github/github_api.py @@ -0,0 +1,62 @@ +import logging + +import requests +from rest_framework.response import Response + +from .crypto import decrypt +from .models import GitHubIdentity + +logger = logging.getLogger(__name__) + + +def get_identity_or_reauth(user): + try: + identity = user.github_identity + except GitHubIdentity.DoesNotExist: + return None, Response({"needs_reauth": True}, status=401) + if identity.needs_reauth: + return None, Response({"needs_reauth": True}, status=401) + return identity, None + + +def decrypt_token_or_reauth(identity): + try: + return decrypt(identity.access_token_enc), None + except Exception: + logger.warning("Failed to decrypt GitHub token for user %s", identity.user_id) + identity.needs_reauth = True + identity.save(update_fields=["needs_reauth", "updated_at"]) + return None, Response({"needs_reauth": True}, status=401) + + +def github_get(token, url, params=None, timeout=20): + """Call GitHub's REST API with standard error mapping. + + Returns either the raw `requests.Response` (on 2xx / non-rate-limit 4xx + the caller still wants to inspect) or a `(error_key, http_status)` tuple + suitable for the caller to surface as `Response({"error": key}, status=...)`. + """ + try: + resp = requests.get( + url, + headers={ + "Authorization": f"Bearer {token}", + "Accept": "application/vnd.github+json", + }, + params=params, + timeout=timeout, + ) + except requests.RequestException as e: + logger.warning("GitHub network error on %s: %s", url, type(e).__name__) + return ("github_unreachable", 503) + + if resp.status_code in (403, 429): + return ("rate_limited", 503, resp.headers.get("Retry-After")) + + if resp.status_code >= 400 and resp.status_code != 401 and resp.status_code != 404: + # 401 and 404 carry semantic meaning the caller handles — bubble the + # response up. Other 4xx/5xx are folded into a generic upstream error. + logger.warning("GitHub upstream error %s on %s", resp.status_code, url) + return ("github_error", 502) + + return resp diff --git a/backend/apps/auth_github/views.py b/backend/apps/auth_github/views.py index 7a13492..6fa17d3 100644 --- a/backend/apps/auth_github/views.py +++ b/backend/apps/auth_github/views.py @@ -18,7 +18,8 @@ from rest_framework.response import Response from rest_framework.views import APIView -from .crypto import decrypt, encrypt +from .crypto import encrypt +from .github_api import decrypt_token_or_reauth, get_identity_or_reauth, github_get from .models import GitHubIdentity logger = logging.getLogger(__name__) @@ -236,26 +237,18 @@ def get(self, request): page = 1 page = max(1, min(10, page)) - try: - identity = request.user.github_identity - except GitHubIdentity.DoesNotExist: - return Response({"needs_reauth": True}, status=401) - - if identity.needs_reauth: - return Response({"needs_reauth": True}, status=401) + identity, err = get_identity_or_reauth(request.user) + if err is not None: + return err cache_key = f"gh_repos:{request.user.id}:{page}:{q_cache_key}" cached = cache.get(cache_key) if cached is not None: return Response(cached) - try: - token = decrypt(identity.access_token_enc) - except Exception: - logger.warning("Failed to decrypt GitHub token for user %s", request.user.id) - identity.needs_reauth = True - identity.save(update_fields=["needs_reauth", "updated_at"]) - return Response({"needs_reauth": True}, status=401) + token, err = decrypt_token_or_reauth(identity) + if err is not None: + return err params = { "affiliation": "owner,collaborator,organization_member", @@ -264,36 +257,20 @@ def get(self, request): "per_page": 50, "page": page, } - try: - resp = requests.get( - GITHUB_REPOS_URL, - headers={ - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - }, - params=params, - timeout=20, - ) - except requests.RequestException as e: - logger.warning("GitHub /user/repos network error: %s", type(e).__name__) - return Response({"error": "github_unreachable"}, status=503) + result = github_get(token, GITHUB_REPOS_URL, params=params) + if isinstance(result, tuple): + key, status = result[0], result[1] + r = Response({"error": key}, status=status) + if key == "rate_limited" and len(result) > 2 and result[2]: + r["Retry-After"] = result[2] + return r + resp = result if resp.status_code == 401: identity.needs_reauth = True identity.save(update_fields=["needs_reauth", "updated_at"]) return Response({"needs_reauth": True}, status=401) - if resp.status_code in (403, 429): - retry_after = resp.headers.get("Retry-After") - r = Response({"error": "rate_limited"}, status=503) - if retry_after: - r["Retry-After"] = retry_after - return r - - if resp.status_code >= 400: - logger.warning("GitHub /user/repos returned %s", resp.status_code) - return Response({"error": "github_error"}, status=502) - repos = resp.json() or [] if q: ql = q.lower() diff --git a/backend/apps/repos/_view_helpers.py b/backend/apps/repos/_view_helpers.py new file mode 100644 index 0000000..4a74700 --- /dev/null +++ b/backend/apps/repos/_view_helpers.py @@ -0,0 +1,17 @@ +from rest_framework.response import Response + +from .models import Repository + + +def normalize_or_400(raw_url, normalize_fn): + try: + return normalize_fn(raw_url), None + except ValueError as e: + return None, Response({"error": "invalid_url", "detail": str(e)}, status=400) + + +def get_user_repo_or_404(user, repo_id): + repo = Repository.objects.filter(id=repo_id, accesses__user=user).first() + if not repo: + return None, Response({"error": "not found"}, status=404) + return repo, None diff --git a/backend/apps/repos/tasks.py b/backend/apps/repos/tasks.py index e3d3df3..e1a2dcf 100644 --- a/backend/apps/repos/tasks.py +++ b/backend/apps/repos/tasks.py @@ -4,7 +4,6 @@ import shutil import git -import requests from celery import shared_task from django.conf import settings @@ -110,25 +109,25 @@ def ingest_repository(repo_id, user_id=None): token = _decrypt_token(identity) if owner and name and token and repo.is_private is None: - try: - probe = requests.get( - f"https://api.github.com/repos/{owner}/{name}", - headers={ - "Authorization": f"Bearer {token}", - "Accept": "application/vnd.github+json", - }, - timeout=15, - ) + from apps.auth_github.github_api import github_get + + probe = github_get( + token, + f"https://api.github.com/repos/{owner}/{name}", + timeout=15, + ) + if not isinstance(probe, tuple): if probe.status_code == 200: - data = probe.json() - repo.is_private = bool(data.get("private")) - repo.save(update_fields=["is_private", "updated_at"]) + try: + data = probe.json() + repo.is_private = bool(data.get("private")) + repo.save(update_fields=["is_private", "updated_at"]) + except ValueError: + pass elif probe.status_code == 401 and identity: identity.needs_reauth = True identity.save(update_fields=["needs_reauth", "updated_at"]) token = None - except Exception as e: - logger.warning("Repo probe failed: %s", type(e).__name__) if repo.is_private and token and owner and name: clone_url = f"https://x-access-token:{token}@github.com/{owner}/{name}.git" diff --git a/backend/apps/repos/views.py b/backend/apps/repos/views.py index c01b717..9105294 100644 --- a/backend/apps/repos/views.py +++ b/backend/apps/repos/views.py @@ -9,6 +9,7 @@ from apps.auth_github.crypto import decrypt from apps.auth_github.models import GitHubIdentity +from ._view_helpers import get_user_repo_or_404, normalize_or_400 from .models import RepoStatus, Repository, RepositoryAccess from .serializers import RepositorySerializer from .tasks import ingest_repository @@ -218,10 +219,9 @@ class RepositoryView(APIView): permission_classes = [IsAuthenticated] def post(self, request): - try: - normalized = _normalize_url(request.data.get("url", "")) - except ValueError as e: - return Response({"error": "invalid_url", "detail": str(e)}, status=400) + normalized, err = normalize_or_400(request.data.get("url", ""), _normalize_url) + if err is not None: + return err if not normalized: return Response({"error": "url required"}, status=400) @@ -257,11 +257,9 @@ def post(self, request): def get(self, request, repo_id=None): if repo_id: - repo = Repository.objects.filter( - id=repo_id, accesses__user=request.user - ).first() - if not repo: - return Response({"error": "not found"}, status=404) + repo, err = get_user_repo_or_404(request.user, repo_id) + if err is not None: + return err return Response(RepositorySerializer(repo).data) repos = ( Repository.objects.filter(accesses__user=request.user) @@ -282,10 +280,9 @@ def post(self, request): if repo_id: repo = Repository.objects.filter(id=repo_id).first() elif url: - try: - normalized = _normalize_url(url) - except ValueError as e: - return Response({"error": "invalid_url", "detail": str(e)}, status=400) + normalized, err = normalize_or_400(url, _normalize_url) + if err is not None: + return err if normalized: repo = Repository.objects.filter(url=normalized).first() From 321c10f3ca0bfa13b8dfb5afc953d0fc5fc45428 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:30:15 +0530 Subject: [PATCH 05/18] chore: remove redundant comments in backend --- backend/apps/auth_github/views.py | 1 + backend/apps/graph/views.py | 3 --- backend/apps/repos/_view_helpers.py | 2 +- backend/apps/repos/views.py | 9 +++------ 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/backend/apps/auth_github/views.py b/backend/apps/auth_github/views.py index 6fa17d3..e945266 100644 --- a/backend/apps/auth_github/views.py +++ b/backend/apps/auth_github/views.py @@ -36,6 +36,7 @@ class _CsrfExemptSessionAuthentication(SessionAuthentication): def enforce_csrf(self, request): # noqa: ARG002 return + GITHUB_AUTHORIZE_URL = "https://github.com/login/oauth/authorize" GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token" GITHUB_USER_URL = "https://api.github.com/user" diff --git a/backend/apps/graph/views.py b/backend/apps/graph/views.py index e417673..74b3389 100644 --- a/backend/apps/graph/views.py +++ b/backend/apps/graph/views.py @@ -143,7 +143,6 @@ def get(self, request, repo_id): ) ) - # Deduplicate edges seen = set() unique_edges = [] for e in all_edges: @@ -175,14 +174,12 @@ def get(self, request, repo_id, node_id): {"error": "invalid_param", "detail": "depth"}, status=400 ) - # Pre-fetch all outgoing CALLS edges for this repo all_edges = FunctionEdge.objects.filter( repository_id=repo_id, edge_type="CALLS" ).select_related("target__file") edges_by_source: dict[int, list] = {} for e in all_edges: targets = edges_by_source.setdefault(e.source_id, []) - # deduplicate targets by id if not any(t.id == e.target_id for t in targets): targets.append(e.target) diff --git a/backend/apps/repos/_view_helpers.py b/backend/apps/repos/_view_helpers.py index 4a74700..1300461 100644 --- a/backend/apps/repos/_view_helpers.py +++ b/backend/apps/repos/_view_helpers.py @@ -10,7 +10,7 @@ def normalize_or_400(raw_url, normalize_fn): return None, Response({"error": "invalid_url", "detail": str(e)}, status=400) -def get_user_repo_or_404(user, repo_id): +def get_user_repo_or_404_response(user, repo_id): repo = Repository.objects.filter(id=repo_id, accesses__user=user).first() if not repo: return None, Response({"error": "not found"}, status=404) diff --git a/backend/apps/repos/views.py b/backend/apps/repos/views.py index 9105294..f124851 100644 --- a/backend/apps/repos/views.py +++ b/backend/apps/repos/views.py @@ -9,8 +9,8 @@ from apps.auth_github.crypto import decrypt from apps.auth_github.models import GitHubIdentity -from ._view_helpers import get_user_repo_or_404, normalize_or_400 -from .models import RepoStatus, Repository, RepositoryAccess +from ._view_helpers import get_user_repo_or_404_response, normalize_or_400 +from .models import Repository, RepositoryAccess, RepoStatus from .serializers import RepositorySerializer from .tasks import ingest_repository from .utils import parse_github_owner_repo as _parse_github_owner_repo @@ -153,7 +153,6 @@ def _can_grant_access(user, repo: Repository): token, identity = _get_github_token(user) - # If we don't yet know whether the repo is private, probe to find out. if repo.is_private is None: probe = _probe_github(owner, name, token=token if token else None) if probe["status"] == 0: @@ -183,11 +182,9 @@ def _can_grant_access(user, repo: Repository): # 403, 5xx, etc — fail closed. return False, "github_unreachable" - # Now `repo.is_private` is known (True/False) for this request. if repo.is_private is False: return True, "public_url" - # Private github repo. Require an authed probe returning 200. if not token: return False, "no_access" authed = _probe_github(owner, name, token=token) @@ -257,7 +254,7 @@ def post(self, request): def get(self, request, repo_id=None): if repo_id: - repo, err = get_user_repo_or_404(request.user, repo_id) + repo, err = get_user_repo_or_404_response(request.user, repo_id) if err is not None: return err return Response(RepositorySerializer(repo).data) From eb5e1cd3ddc3d0f37038cf0cae1a2520d1eccc9e Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:31:39 +0530 Subject: [PATCH 06/18] refactor(ui): replace AI-cliche palette with terracotta+sage --- frontend/src/components/GraphPanel.tsx | 18 +++++++-------- frontend/src/index.css | 32 +++++++++++++------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/GraphPanel.tsx b/frontend/src/components/GraphPanel.tsx index 4003627..c20f537 100644 --- a/frontend/src/components/GraphPanel.tsx +++ b/frontend/src/components/GraphPanel.tsx @@ -34,8 +34,8 @@ interface Props { } const FILE_COLORS = [ - '#58a6ff', '#3fb950', '#d29922', '#a371f7', - '#f78166', '#39d353', '#79c0ff', '#ffa657', + '#d97757', '#7a9b6e', '#e6b450', '#b08968', + '#8e7cc3', '#5a8a8a', '#c25450', '#a89b8c', ] export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: Props) { @@ -110,11 +110,11 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: .attr('orient', 'auto') .append('path') .attr('d', 'M0,-5L10,0L0,5') - .attr('fill', '#388bfd') + .attr('fill', '#d97757') const link = g.append('g').selectAll('line') .data(edges).join('line') - .attr('stroke', '#388bfd') + .attr('stroke', '#d97757') .attr('stroke-width', 1.2) .attr('stroke-opacity', 0.6) .attr('marker-end', 'url(#arrow)') @@ -148,11 +148,11 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: onNodeSelect(d.id, d.name) setTraceNodeId(d.id) setTraceNodeName(d.name) - node.select('circle').attr('stroke', (n: NodeDatum) => n.id === d.id ? '#fff' : 'none').attr('stroke-width', 2) + node.select('circle').attr('stroke', (n: NodeDatum) => n.id === d.id ? '#f0e6dc' : 'none').attr('stroke-width', 2) link .attr('stroke', (e: EdgeDatum) => { const s = (e.source as NodeDatum).id, t = (e.target as NodeDatum).id - return s === d.id || t === d.id ? '#fff' : '#388bfd' + return s === d.id || t === d.id ? '#f0e6dc' : '#d97757' }) .attr('stroke-opacity', (e: EdgeDatum) => { const s = (e.source as NodeDatum).id, t = (e.target as NodeDatum).id @@ -163,12 +163,12 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: root.on('click', () => { setSelected(null) node.select('circle').attr('stroke', 'none') - link.attr('stroke', '#388bfd').attr('stroke-opacity', 0.6) + link.attr('stroke', '#d97757').attr('stroke-opacity', 0.6) }) node.append('circle') .attr('r', R) - .attr('fill', (d: NodeDatum) => colorMap.get(d.file_id) ?? '#58a6ff') + .attr('fill', (d: NodeDatum) => colorMap.get(d.file_id) ?? '#d97757') .attr('fill-opacity', (d: NodeDatum) => d.isExternal ? 0.35 : 0.85) .attr('stroke', 'none') @@ -177,7 +177,7 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: .attr('text-anchor', 'middle') .attr('dy', R + 13) .attr('font-size', 10) - .attr('fill', '#c9d1d9') + .attr('fill', '#f0e6dc') .attr('pointer-events', 'none') const sim = d3.forceSimulation(nodes) diff --git a/frontend/src/index.css b/frontend/src/index.css index fb880a7..aa29e2a 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -5,18 +5,18 @@ } :root { - --bg: #0d1117; - --surface: #161b22; - --border: #30363d; - --text: #e6edf3; - --text-muted: #8b949e; - --accent: #58a6ff; - --accent-hover: #79c0ff; - --success: #3fb950; - --error: #f85149; - --warning: #d29922; - --node-bg: #1c2128; - --node-border: #388bfd; + --bg: #1a1614; + --surface: #241f1c; + --border: #3a322d; + --text: #f0e6dc; + --text-muted: #a89b8c; + --accent: #d97757; + --accent-hover: #c2613f; + --success: #7a9b6e; + --error: #c25450; + --warning: #e6b450; + --node-bg: #241f1c; + --node-border: #d97757; } body { @@ -132,7 +132,7 @@ body { .repo-new-form button { padding: 7px 12px; background: var(--accent); - color: #0d1117; + color: var(--bg); border: none; border-radius: 6px; font-size: 12px; @@ -163,7 +163,7 @@ body { .landing h1 { font-size: 2.2rem; font-weight: 700; - background: linear-gradient(135deg, var(--accent), #a371f7); + background: linear-gradient(135deg, var(--accent), var(--warning)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } @@ -199,7 +199,7 @@ body { .btn-primary { padding: 12px 24px; background: var(--accent); - color: #0d1117; + color: var(--bg); border: none; border-radius: 8px; font-weight: 600; @@ -769,7 +769,7 @@ body { .btn-send { padding: 10px 16px; background: var(--accent); - color: #0d1117; + color: var(--bg); border: none; border-radius: 8px; font-weight: 600; From 59c04e4da63b7df4fcebda8eb2f3a87ba831a519 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:32:41 +0530 Subject: [PATCH 07/18] fix(ui): align processing steps with backend, add done state --- frontend/src/components/Processing.tsx | 32 +++++++++++++++----------- frontend/src/constants/repoStatus.ts | 9 ++++++++ frontend/src/index.css | 23 ++++++++++++++++++ 3 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 frontend/src/constants/repoStatus.ts diff --git a/frontend/src/components/Processing.tsx b/frontend/src/components/Processing.tsx index ccd082a..41436af 100644 --- a/frontend/src/components/Processing.tsx +++ b/frontend/src/components/Processing.tsx @@ -1,14 +1,9 @@ import { useEffect, useRef, useState } from 'react' import type { Repository } from '../types' import { api } from '../api' +import { REPO_STATUSES } from '../constants/repoStatus' -const STEPS = [ - { key: 'cloning', label: 'Cloning repo…' }, - { key: 'parsing', label: 'Parsing files…' }, - { key: 'graphing', label: 'Building graph…' }, - { key: 'embedding', label: 'Generating embeddings…' }, - { key: 'ready', label: 'Done!' }, -] +const DONE_HOLD_MS = 600 interface Props { repo: Repository @@ -18,33 +13,38 @@ interface Props { export function Processing({ repo: initial, onReady, switcher }: Props) { const [repo, setRepo] = useState(initial) + const [readyHolding, setReadyHolding] = useState(false) const timer = useRef | null>(null) useEffect(() => { - if (repo.status === 'ready') { onReady(repo); return } if (repo.status === 'failed') return + if (repo.status === 'ready') { + setReadyHolding(true) + const t = setTimeout(() => onReady(repo), DONE_HOLD_MS) + return () => clearTimeout(t) + } + timer.current = setInterval(async () => { try { const updated = await api.getRepo(repo.id) setRepo(updated) - if (updated.status === 'ready') { clearInterval(timer.current!); onReady(updated) } if (updated.status === 'failed') clearInterval(timer.current!) } catch {} }, 2000) return () => clearInterval(timer.current!) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [repo.id]) + }, [repo.id, repo.status]) - const activeIdx = STEPS.findIndex(s => s.key === repo.status) + const activeIdx = REPO_STATUSES.findIndex(s => s.key === repo.status) return (
{switcher}

Analyzing {repo.name}

- {STEPS.map((step, i) => { + {REPO_STATUSES.map((step, i) => { let cls = '' if (repo.status === 'failed' && i === activeIdx) cls = 'error' else if (i < activeIdx || repo.status === 'ready') cls = 'done' @@ -54,11 +54,17 @@ export function Processing({ repo: initial, onReady, switcher }: Props) { {cls === 'done' ? '✓' : cls === 'error' ? '✗' : cls === 'active' ? : '○'} - {step.label} +
+
{step.label}
+
{step.description}
+
) })}
+ {readyHolding && ( +

Done — opening dashboard…

+ )} {repo.status === 'failed' && (

{repo.status_message}

)} diff --git a/frontend/src/constants/repoStatus.ts b/frontend/src/constants/repoStatus.ts new file mode 100644 index 0000000..9d27b9b --- /dev/null +++ b/frontend/src/constants/repoStatus.ts @@ -0,0 +1,9 @@ +// Mirror of backend/apps/repos/models.py::RepoStatus. Keep keys in sync. +export const REPO_STATUSES = [ + { key: 'cloning', label: 'Cloning', description: 'Fetching repository from GitHub' }, + { key: 'parsing', label: 'Parsing', description: 'Extracting functions and call graph' }, + { key: 'embedding', label: 'Embedding', description: 'Generating semantic vectors' }, + { key: 'ready', label: 'Ready', description: 'Complete' }, +] as const + +export type RepoStatus = typeof REPO_STATUSES[number]['key'] | 'pending' | 'failed' diff --git a/frontend/src/index.css b/frontend/src/index.css index aa29e2a..b08fbe0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -272,6 +272,29 @@ body { text-align: center; } +.step-body { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 220px; +} + +.step-label { + font-size: 13px; + font-weight: 600; +} + +.step-description { + font-size: 11px; + color: var(--text-muted); +} + +.step.done .step-description, +.step.error .step-description { + color: inherit; + opacity: 0.75; +} + .spinner { width: 18px; height: 18px; From b82c3e9efd24479531cd7040b2305f0723b42624 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:33:39 +0530 Subject: [PATCH 08/18] chore: remove redundant comments in frontend --- frontend/src/api/index.ts | 6 +++--- frontend/src/components/GithubRepoPicker.tsx | 4 +--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index f406404..9b6ca0d 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,9 +1,9 @@ import axios from 'axios' import type { Repository, RepoFile, FileFn, GraphData, User, GitHubRepo } from '../types' -// Use axios's native CSRF support: it reads the named cookie and attaches it -// as the named header on same-origin unsafe requests. Works identically to -// the manual interceptor we used to maintain, with fewer moving parts. +// xsrf* config lets axios read the Django CSRF cookie and attach it as the +// header on same-origin unsafe requests — required because we use +// SessionAuthentication, not a token. const http = axios.create({ baseURL: '/api', withCredentials: true, diff --git a/frontend/src/components/GithubRepoPicker.tsx b/frontend/src/components/GithubRepoPicker.tsx index f463a03..a08d8dd 100644 --- a/frontend/src/components/GithubRepoPicker.tsx +++ b/frontend/src/components/GithubRepoPicker.tsx @@ -23,9 +23,7 @@ const Q_DEBOUNCE_MS = 300 export function GithubRepoPicker({ onAnalyze }: Props) { const [repos, setRepos] = useState([]) const [q, setQ] = useState('') - // `debouncedQ` is what the data fetch actually keys on; `q` is the live - // input value so typing stays snappy. We debounce by 300ms to avoid - // hammering the upstream GitHub API on every keystroke. + // Debounced separately from `q` so each keystroke doesn't hit GitHub. const [debouncedQ, setDebouncedQ] = useState('') const [page, setPage] = useState(1) const [loading, setLoading] = useState(false) From 7ca668a7d0e613466b1a2467a317233b830cb33d Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:37:51 +0530 Subject: [PATCH 09/18] test: cover new auth_github and repos view helpers 21 tests for github_api.{get_identity_or_reauth, decrypt_token_or_reauth, github_get} including the 403/429 Retry-After tuple shape and the needs_reauth persistence on decrypt failure. 9 tests for repos._view_helpers.{normalize_or_400, get_user_repo_or_404_response} and the RepoStatus enum value-strings (drift-guard for the frontend mirror). --- .../apps/auth_github/tests/test_github_api.py | 234 ++++++++++++++++++ backend/apps/repos/tests/test_view_helpers.py | 156 ++++++++++++ 2 files changed, 390 insertions(+) create mode 100644 backend/apps/auth_github/tests/test_github_api.py create mode 100644 backend/apps/repos/tests/test_view_helpers.py diff --git a/backend/apps/auth_github/tests/test_github_api.py b/backend/apps/auth_github/tests/test_github_api.py new file mode 100644 index 0000000..6e39229 --- /dev/null +++ b/backend/apps/auth_github/tests/test_github_api.py @@ -0,0 +1,234 @@ +"""Unit tests for `apps.auth_github.github_api` helpers. + +Covers the three new helpers added in the cleanup branch: + +- `get_identity_or_reauth(user)` +- `decrypt_token_or_reauth(identity)` +- `github_get(token, url, params=None)` + +The identity helpers require real DB-backed `User` and `GitHubIdentity` +rows, so they go through `@pytest.mark.django_db`. The `github_get` +helper is a pure wrapper around `requests.get` and is exercised by +patching `requests.get` directly. +""" +from unittest.mock import MagicMock, patch + +import pytest +import requests +from django.contrib.auth.models import User + +from apps.auth_github.crypto import encrypt +from apps.auth_github.github_api import ( + decrypt_token_or_reauth, + get_identity_or_reauth, + github_get, +) +from apps.auth_github.models import GitHubIdentity + + +# --------------------------------------------------------------------------- +# get_identity_or_reauth +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestGetIdentityOrReauth: + def test_returns_identity_when_present_and_not_needs_reauth(self): + user = User.objects.create_user(username="ok", password="pw") + identity = GitHubIdentity.objects.create( + user=user, + github_user_id=12345, + login="ok", + access_token_enc=encrypt("gho_tok"), + scopes="repo", + needs_reauth=False, + ) + + result_identity, err = get_identity_or_reauth(user) + + assert err is None + assert result_identity is not None + assert result_identity.pk == identity.pk + + def test_returns_401_when_user_has_no_github_identity(self): + user = User.objects.create_user(username="no-gh", password="pw") + + identity, err = get_identity_or_reauth(user) + + assert identity is None + assert err is not None + assert err.status_code == 401 + assert err.data == {"needs_reauth": True} + + def test_returns_401_when_identity_needs_reauth(self): + user = User.objects.create_user(username="stale", password="pw") + GitHubIdentity.objects.create( + user=user, + github_user_id=54321, + login="stale", + access_token_enc=encrypt("gho_tok"), + scopes="repo", + needs_reauth=True, + ) + + identity, err = get_identity_or_reauth(user) + + assert identity is None + assert err is not None + assert err.status_code == 401 + assert err.data == {"needs_reauth": True} + + +# --------------------------------------------------------------------------- +# decrypt_token_or_reauth +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestDecryptTokenOrReauth: + def test_returns_plaintext_token_on_success(self): + user = User.objects.create_user(username="happy", password="pw") + identity = GitHubIdentity.objects.create( + user=user, + github_user_id=11111, + login="happy", + access_token_enc=encrypt("gho_plaintext"), + scopes="repo", + ) + + token, err = decrypt_token_or_reauth(identity) + + assert err is None + assert token == "gho_plaintext" + + def test_flips_needs_reauth_and_returns_401_on_decrypt_failure(self): + user = User.objects.create_user(username="corrupt", password="pw") + identity = GitHubIdentity.objects.create( + user=user, + github_user_id=22222, + login="corrupt", + # Not a valid Fernet ciphertext -> decrypt() raises InvalidToken. + access_token_enc="not-a-fernet-token", + scopes="repo", + needs_reauth=False, + ) + + token, err = decrypt_token_or_reauth(identity) + + assert token is None + assert err is not None + assert err.status_code == 401 + assert err.data == {"needs_reauth": True} + + # Verify the flag was persisted (not just mutated in-memory). + identity.refresh_from_db() + assert identity.needs_reauth is True + + +# --------------------------------------------------------------------------- +# github_get +# --------------------------------------------------------------------------- + + +def _mock_resp(status_code=200, headers=None): + r = MagicMock() + r.status_code = status_code + r.headers = headers or {} + return r + + +class TestGithubGet: + """Pure wrapper — no DB needed. We patch `requests.get` directly.""" + + def test_200_returns_raw_response(self): + resp = _mock_resp(200) + with patch("apps.auth_github.github_api.requests.get", return_value=resp) as mock_get: + result = github_get("tok", "https://api.github.com/user") + assert result is resp + # Bearer auth header is set; Accept header is GitHub JSON. + _, kwargs = mock_get.call_args + assert kwargs["headers"]["Authorization"] == "Bearer tok" + assert kwargs["headers"]["Accept"] == "application/vnd.github+json" + + def test_401_bubbles_response_up(self): + """401 is semantically caller-handled (needs_reauth).""" + resp = _mock_resp(401) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result is resp + + def test_404_bubbles_response_up(self): + """404 is semantically caller-handled (no access / not found).""" + resp = _mock_resp(404) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/repos/x/y") + assert result is resp + + def test_request_exception_maps_to_unreachable_503(self): + with patch( + "apps.auth_github.github_api.requests.get", + side_effect=requests.ConnectionError("boom"), + ): + result = github_get("tok", "https://api.github.com/user") + assert result == ("github_unreachable", 503) + + def test_timeout_maps_to_unreachable_503(self): + with patch( + "apps.auth_github.github_api.requests.get", + side_effect=requests.Timeout("slow"), + ): + result = github_get("tok", "https://api.github.com/user") + assert result == ("github_unreachable", 503) + + def test_403_returns_rate_limited_with_retry_after(self): + resp = _mock_resp(403, headers={"Retry-After": "60"}) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result == ("rate_limited", 503, "60") + + def test_429_returns_rate_limited_with_retry_after(self): + resp = _mock_resp(429, headers={"Retry-After": "30"}) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result == ("rate_limited", 503, "30") + + def test_429_returns_rate_limited_with_none_retry_after_when_header_absent(self): + resp = _mock_resp(429, headers={}) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result == ("rate_limited", 503, None) + + def test_500_maps_to_github_error_502(self): + resp = _mock_resp(500) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result == ("github_error", 502) + + def test_502_maps_to_github_error_502(self): + resp = _mock_resp(502) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result == ("github_error", 502) + + def test_400_other_4xx_maps_to_github_error_502(self): + """4xx that isn't 401, 403, 404, or 429 is folded to a generic upstream error.""" + resp = _mock_resp(400) + with patch("apps.auth_github.github_api.requests.get", return_value=resp): + result = github_get("tok", "https://api.github.com/user") + assert result == ("github_error", 502) + + def test_params_are_forwarded(self): + resp = _mock_resp(200) + with patch("apps.auth_github.github_api.requests.get", return_value=resp) as mock_get: + github_get("tok", "https://api.github.com/user/repos", params={"per_page": 5}) + _, kwargs = mock_get.call_args + assert kwargs["params"] == {"per_page": 5} + + def test_default_timeout_is_set(self): + """Defensive: every github_get call must set a timeout to avoid hangs.""" + resp = _mock_resp(200) + with patch("apps.auth_github.github_api.requests.get", return_value=resp) as mock_get: + github_get("tok", "https://api.github.com/user") + _, kwargs = mock_get.call_args + assert "timeout" in kwargs + assert kwargs["timeout"] is not None diff --git a/backend/apps/repos/tests/test_view_helpers.py b/backend/apps/repos/tests/test_view_helpers.py new file mode 100644 index 0000000..4a8ec3a --- /dev/null +++ b/backend/apps/repos/tests/test_view_helpers.py @@ -0,0 +1,156 @@ +"""Unit tests for `apps.repos._view_helpers` and the `RepoStatus` enum. + +Covers: +- `normalize_or_400(raw_url, normalize_fn)` — wraps a normalize function and + converts `ValueError` into a DRF 400 `Response` with `{"error": "invalid_url", + "detail": }`. +- `get_user_repo_or_404_response(user, repo_id)` — returns `(repo, None)` for + a user with a `RepositoryAccess` row, else `(None, Response(..., status=404))`. +- `RepoStatus` TextChoices — sanity check on the value-strings since the + frontend mirrors them (`frontend/src/constants/repoStatus.ts`). +""" +import uuid + +import pytest +from django.contrib.auth.models import User + +from apps.repos._view_helpers import ( + get_user_repo_or_404_response, + normalize_or_400, +) +from apps.repos.models import RepoStatus, Repository, RepositoryAccess +from apps.repos.views import _normalize_url + + +# --------------------------------------------------------------------------- +# normalize_or_400 +# --------------------------------------------------------------------------- + + +class TestNormalizeOr400: + """No DB needed — pure-function wrapper around the normalize callable.""" + + def test_valid_github_url_returns_normalized_string(self): + normalized, err = normalize_or_400("https://github.com/X/Y", _normalize_url) + assert err is None + assert normalized == "https://github.com/x/y" + + def test_valid_gitlab_url_returns_normalized_string(self): + normalized, err = normalize_or_400("https://gitlab.com/team/proj", _normalize_url) + assert err is None + # Non-github hosts keep case in the path; only host is lowercased. + assert normalized.startswith("https://gitlab.com/") + + def test_unsupported_scheme_returns_400(self): + normalized, err = normalize_or_400("file:///etc/passwd", _normalize_url) + assert normalized is None + assert err is not None + assert err.status_code == 400 + assert err.data["error"] == "invalid_url" + assert err.data["detail"] == "unsupported_scheme" + + def test_unsupported_host_returns_400(self): + normalized, err = normalize_or_400("https://example.com/x/y", _normalize_url) + assert normalized is None + assert err is not None + assert err.status_code == 400 + assert err.data["error"] == "invalid_url" + assert err.data["detail"] == "unsupported_host" + + def test_missing_host_returns_400(self): + # `https:///foo` parses to scheme=https, netloc='' -> missing_host. + normalized, err = normalize_or_400("https:///foo", _normalize_url) + assert normalized is None + assert err is not None + assert err.status_code == 400 + assert err.data["error"] == "invalid_url" + assert err.data["detail"] == "missing_host" + + def test_response_shape_matches_existing_views_contract(self): + """The repos view-level tests assert `resp.json()["error"] == "invalid_url"`; + this helper must produce a Response whose body is JSON-serializable to + the same shape.""" + _, err = normalize_or_400("git+ssh://github.com/x/y", _normalize_url) + assert err is not None + assert set(err.data.keys()) == {"error", "detail"} + + +# --------------------------------------------------------------------------- +# get_user_repo_or_404_response +# --------------------------------------------------------------------------- + + +@pytest.mark.django_db +class TestGetUserRepoOr404Response: + def test_returns_repo_when_user_has_access(self): + user = User.objects.create_user(username="owner", password="pw") + repo = Repository.objects.create( + url="https://github.com/o/r", + name="r", + status=RepoStatus.READY, + ) + RepositoryAccess.objects.create(user=user, repository=repo, role="owner") + + result_repo, err = get_user_repo_or_404_response(user, repo.id) + + assert err is None + assert result_repo is not None + assert result_repo.pk == repo.pk + + def test_returns_404_when_user_has_no_access_row(self): + owner = User.objects.create_user(username="owner", password="pw") + intruder = User.objects.create_user(username="intruder", password="pw") + repo = Repository.objects.create( + url="https://github.com/o/r", + name="r", + status=RepoStatus.READY, + ) + RepositoryAccess.objects.create(user=owner, repository=repo, role="owner") + + result_repo, err = get_user_repo_or_404_response(intruder, repo.id) + + assert result_repo is None + assert err is not None + assert err.status_code == 404 + assert err.data == {"error": "not found"} + + def test_returns_404_for_nonexistent_repo_id(self): + user = User.objects.create_user(username="ghost", password="pw") + + result_repo, err = get_user_repo_or_404_response(user, uuid.uuid4()) + + assert result_repo is None + assert err is not None + assert err.status_code == 404 + assert err.data == {"error": "not found"} + + +# --------------------------------------------------------------------------- +# RepoStatus enum +# --------------------------------------------------------------------------- + + +class TestRepoStatusEnum: + """The frontend mirrors these string values in + `frontend/src/constants/repoStatus.ts`; a value drift here is a + cross-stack bug. Lock the wire-strings down.""" + + def test_status_values_match_frontend_mirror(self): + assert RepoStatus.PENDING == "pending" + assert RepoStatus.CLONING == "cloning" + assert RepoStatus.PARSING == "parsing" + assert RepoStatus.EMBEDDING == "embedding" + assert RepoStatus.READY == "ready" + assert RepoStatus.FAILED == "failed" + + def test_labels_are_human_readable(self): + assert RepoStatus.PENDING.label == "Pending" + assert RepoStatus.CLONING.label == "Cloning" + assert RepoStatus.PARSING.label == "Parsing" + assert RepoStatus.EMBEDDING.label == "Embedding" + assert RepoStatus.READY.label == "Ready" + assert RepoStatus.FAILED.label == "Failed" + + def test_choices_count_matches_expected(self): + # Six states: pending / cloning / parsing / embedding / ready / failed. + assert len(RepoStatus.choices) == 6 From eca2aa559e86157bd4bdf5ec7fa07aeeed508592 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:55:37 +0530 Subject: [PATCH 10/18] fix(ui): preserve session on needs_reauth 401s --- frontend/src/api/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 9b6ca0d..407d63f 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -14,7 +14,7 @@ const http = axios.create({ http.interceptors.response.use( r => r, err => { - if (err?.response?.status === 401) { + if (err?.response?.status === 401 && !err.response?.data?.needs_reauth) { window.dispatchEvent(new CustomEvent('auth:unauthorized')) } return Promise.reject(err) From ff55cbd424568c8104fb9cc7dd8c9dc4d25c84bd Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:56:13 +0530 Subject: [PATCH 11/18] fix(ui): show active step during pending status, stop polling on ready --- frontend/src/components/Processing.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Processing.tsx b/frontend/src/components/Processing.tsx index 41436af..6057312 100644 --- a/frontend/src/components/Processing.tsx +++ b/frontend/src/components/Processing.tsx @@ -30,6 +30,7 @@ export function Processing({ repo: initial, onReady, switcher }: Props) { const updated = await api.getRepo(repo.id) setRepo(updated) if (updated.status === 'failed') clearInterval(timer.current!) + if (updated.status === 'ready') clearInterval(timer.current!) } catch {} }, 2000) @@ -37,7 +38,9 @@ export function Processing({ repo: initial, onReady, switcher }: Props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [repo.id, repo.status]) - const activeIdx = REPO_STATUSES.findIndex(s => s.key === repo.status) + const activeIdx = repo.status === 'pending' + ? 0 + : REPO_STATUSES.findIndex(s => s.key === repo.status) return (
@@ -48,6 +51,7 @@ export function Processing({ repo: initial, onReady, switcher }: Props) { let cls = '' if (repo.status === 'failed' && i === activeIdx) cls = 'error' else if (i < activeIdx || repo.status === 'ready') cls = 'done' + else if (i === 0 && (repo.status === 'pending' || repo.status === 'cloning')) cls = 'active' else if (i === activeIdx) cls = 'active' return (
From 98f58234172b7372db3b458e01494aed94a19153 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:57:47 +0530 Subject: [PATCH 12/18] refactor(github_api): return stable 3-tuple for errors --- backend/apps/auth_github/github_api.py | 18 +++++++++++++----- .../apps/auth_github/tests/test_github_api.py | 10 +++++----- backend/apps/auth_github/views.py | 6 +++--- backend/apps/repos/tasks.py | 5 ++++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/backend/apps/auth_github/github_api.py b/backend/apps/auth_github/github_api.py index ba0fe71..f5a933b 100644 --- a/backend/apps/auth_github/github_api.py +++ b/backend/apps/auth_github/github_api.py @@ -32,9 +32,17 @@ def decrypt_token_or_reauth(identity): def github_get(token, url, params=None, timeout=20): """Call GitHub's REST API with standard error mapping. - Returns either the raw `requests.Response` (on 2xx / non-rate-limit 4xx - the caller still wants to inspect) or a `(error_key, http_status)` tuple - suitable for the caller to surface as `Response({"error": key}, status=...)`. + Returns either the raw `requests.Response` (on 2xx / 401 / 404, where the + caller wants to inspect status semantically) or a stable 3-tuple of + `(error_key, http_status, retry_after)` for error mappings. `retry_after` + is the `Retry-After` header value for `rate_limited` and `None` for every + other error key. Callers can destructure unconditionally: + key, status, retry_after = result + + Error keys: + - ("github_unreachable", 503, None) — network/timeout failure + - ("rate_limited", 503, retry_after) — 403/429 + - ("github_error", 502, None) — other 4xx/5xx upstream """ try: resp = requests.get( @@ -48,7 +56,7 @@ def github_get(token, url, params=None, timeout=20): ) except requests.RequestException as e: logger.warning("GitHub network error on %s: %s", url, type(e).__name__) - return ("github_unreachable", 503) + return ("github_unreachable", 503, None) if resp.status_code in (403, 429): return ("rate_limited", 503, resp.headers.get("Retry-After")) @@ -57,6 +65,6 @@ def github_get(token, url, params=None, timeout=20): # 401 and 404 carry semantic meaning the caller handles — bubble the # response up. Other 4xx/5xx are folded into a generic upstream error. logger.warning("GitHub upstream error %s on %s", resp.status_code, url) - return ("github_error", 502) + return ("github_error", 502, None) return resp diff --git a/backend/apps/auth_github/tests/test_github_api.py b/backend/apps/auth_github/tests/test_github_api.py index 6e39229..0a00a25 100644 --- a/backend/apps/auth_github/tests/test_github_api.py +++ b/backend/apps/auth_github/tests/test_github_api.py @@ -170,7 +170,7 @@ def test_request_exception_maps_to_unreachable_503(self): side_effect=requests.ConnectionError("boom"), ): result = github_get("tok", "https://api.github.com/user") - assert result == ("github_unreachable", 503) + assert result == ("github_unreachable", 503, None) def test_timeout_maps_to_unreachable_503(self): with patch( @@ -178,7 +178,7 @@ def test_timeout_maps_to_unreachable_503(self): side_effect=requests.Timeout("slow"), ): result = github_get("tok", "https://api.github.com/user") - assert result == ("github_unreachable", 503) + assert result == ("github_unreachable", 503, None) def test_403_returns_rate_limited_with_retry_after(self): resp = _mock_resp(403, headers={"Retry-After": "60"}) @@ -202,20 +202,20 @@ def test_500_maps_to_github_error_502(self): resp = _mock_resp(500) with patch("apps.auth_github.github_api.requests.get", return_value=resp): result = github_get("tok", "https://api.github.com/user") - assert result == ("github_error", 502) + assert result == ("github_error", 502, None) def test_502_maps_to_github_error_502(self): resp = _mock_resp(502) with patch("apps.auth_github.github_api.requests.get", return_value=resp): result = github_get("tok", "https://api.github.com/user") - assert result == ("github_error", 502) + assert result == ("github_error", 502, None) def test_400_other_4xx_maps_to_github_error_502(self): """4xx that isn't 401, 403, 404, or 429 is folded to a generic upstream error.""" resp = _mock_resp(400) with patch("apps.auth_github.github_api.requests.get", return_value=resp): result = github_get("tok", "https://api.github.com/user") - assert result == ("github_error", 502) + assert result == ("github_error", 502, None) def test_params_are_forwarded(self): resp = _mock_resp(200) diff --git a/backend/apps/auth_github/views.py b/backend/apps/auth_github/views.py index e945266..4641fa8 100644 --- a/backend/apps/auth_github/views.py +++ b/backend/apps/auth_github/views.py @@ -260,10 +260,10 @@ def get(self, request): } result = github_get(token, GITHUB_REPOS_URL, params=params) if isinstance(result, tuple): - key, status = result[0], result[1] + key, status, retry_after = result r = Response({"error": key}, status=status) - if key == "rate_limited" and len(result) > 2 and result[2]: - r["Retry-After"] = result[2] + if key == "rate_limited" and retry_after: + r["Retry-After"] = retry_after return r resp = result diff --git a/backend/apps/repos/tasks.py b/backend/apps/repos/tasks.py index e1a2dcf..c55cdea 100644 --- a/backend/apps/repos/tasks.py +++ b/backend/apps/repos/tasks.py @@ -116,7 +116,10 @@ def ingest_repository(repo_id, user_id=None): f"https://api.github.com/repos/{owner}/{name}", timeout=15, ) - if not isinstance(probe, tuple): + if isinstance(probe, tuple): + key, _status, _retry = probe + logger.info("Repo privacy probe failed for %s: %s", repo_id, key) + else: if probe.status_code == 200: try: data = probe.json() From 83f6e3e918cb66b8133a46fa5853475177b1dcb6 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:59:18 +0530 Subject: [PATCH 13/18] fix(ui): replace stale blue accent literals with terracotta vars --- frontend/src/index.css | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/frontend/src/index.css b/frontend/src/index.css index b08fbe0..c63561c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -17,6 +17,10 @@ --warning: #e6b450; --node-bg: #241f1c; --node-border: #d97757; + /* Terracotta-derived translucent layers — replace stale rgba(88,166,255,*) + blue literals. Soft = hover, active = pressed/selected. */ + --accent-soft: rgba(217, 119, 87, 0.08); + --accent-active: rgba(217, 119, 87, 0.12); } body { @@ -90,8 +94,8 @@ body { transition: background 0.15s; } -.repo-item:hover { background: rgba(88,166,255,0.08); } -.repo-item.active { background: rgba(88,166,255,0.12); } +.repo-item:hover { background: var(--accent-soft); } +.repo-item.active { background: var(--accent-active); } .repo-item-dot { font-size: 10px; flex-shrink: 0; } @@ -377,12 +381,12 @@ body { } .tree-node:hover { - background: rgba(88, 166, 255, 0.08); + background: var(--accent-soft); color: var(--text); } .tree-node.selected { - background: rgba(88, 166, 255, 0.15); + background: rgba(217, 119, 87, 0.15); color: var(--accent); } @@ -421,7 +425,7 @@ body { } .fn-item:hover { - background: rgba(88, 166, 255, 0.08); + background: var(--accent-soft); } .fn-item .fn-name { @@ -482,7 +486,7 @@ body { .btn-mode.active { border-color: var(--accent); color: var(--accent); - background: rgba(88, 166, 255, 0.1); + background: rgba(217, 119, 87, 0.1); } .btn-mode:hover:not(.active) { @@ -732,8 +736,8 @@ body { } .msg-user .msg-bubble { - background: rgba(88, 166, 255, 0.15); - border: 1px solid rgba(88, 166, 255, 0.3); + background: rgba(217, 119, 87, 0.15); + border: 1px solid rgba(217, 119, 87, 0.3); color: var(--text); } @@ -753,8 +757,8 @@ body { .fn-tag { padding: 2px 8px; - background: rgba(88, 166, 255, 0.1); - border: 1px solid rgba(88, 166, 255, 0.2); + background: rgba(217, 119, 87, 0.1); + border: 1px solid rgba(217, 119, 87, 0.2); border-radius: 4px; font-size: 11px; font-family: monospace; @@ -763,7 +767,7 @@ body { } .fn-tag:hover { - background: rgba(88, 166, 255, 0.2); + background: rgba(217, 119, 87, 0.2); } .chat-input-row { @@ -997,7 +1001,7 @@ body { .landing-tabs button.active, .repo-switcher-tabs button.active { color: var(--text); border-color: var(--accent); - background: rgba(88,166,255,0.08); + background: var(--accent-active); } .repo-switcher-tabs { padding: 8px 12px 0; margin: 0; } From fc1c146fa4dffe08cabb8c96754c801f8fceccb8 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Mon, 11 May 2026 08:59:49 +0530 Subject: [PATCH 14/18] refactor(repos): remove dead get_user_repo_or_404 raising helper --- backend/apps/repos/utils.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/backend/apps/repos/utils.py b/backend/apps/repos/utils.py index cb22671..6cb0d4c 100644 --- a/backend/apps/repos/utils.py +++ b/backend/apps/repos/utils.py @@ -6,8 +6,6 @@ """ import re -from rest_framework.exceptions import NotFound - from .models import Repository @@ -18,18 +16,6 @@ def user_has_repo_access(user, repo_id) -> bool: return Repository.objects.filter(id=repo_id, accesses__user=user).exists() -def get_user_repo_or_404(user, repo_id) -> Repository: - """Return the Repository row for ``user``/``repo_id`` or raise NotFound.""" - repo = ( - Repository.objects.filter(id=repo_id, accesses__user=user).first() - if user is not None and getattr(user, "is_authenticated", False) - else None - ) - if not repo: - raise NotFound("not found") - return repo - - def parse_github_owner_repo(url: str): """Extract (owner, name) from a github.com URL or return (None, None). From 8e082fa18a5c2ef8d5c26d1d4d6d42f8d732c2aa Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Thu, 25 Jun 2026 13:09:27 +0530 Subject: [PATCH 15/18] feat(backend): streaming chat, resilient AI, server-side graph filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Stream the chat endpoint as SSE (meta citations -> tokens -> done); retrieval + access gate stay synchronous so 4xx errors stay clean JSON. - Switch Gemini SDK to REST transport to fix the gRPC c-ares DNS failures ("Could not contact DNS servers") seen during embedding/chat. - Update model gemini-2.0-flash -> gemini-2.5-flash (2.0 was retired, which surfaced as "generation failed"). - Make embeddings resilient: retry transient transport/availability errors with backoff + per-call timeout. - Drop the redundant second LLM call for chat follow-up suggestions. - Graph: hide dunder/boilerplate functions (__init__, …) at the DB-query level by default; opt back in with ?include_boilerplate=true, return a `hidden` count, and drop edges dangling to filtered-out nodes. - Test settings: stream-aware genai stub + GoogleAPICallError/RetryError. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/apps/chat/views.py | 94 +++++++++++++++++++++++-------- backend/apps/embeddings/client.py | 54 ++++++++++++++---- backend/apps/graph/views.py | 43 ++++++++++---- backend/core/test_settings.py | 22 ++++++++ 4 files changed, 166 insertions(+), 47 deletions(-) diff --git a/backend/apps/chat/views.py b/backend/apps/chat/views.py index fe87f57..a195849 100644 --- a/backend/apps/chat/views.py +++ b/backend/apps/chat/views.py @@ -1,5 +1,8 @@ +import json + import google.generativeai as genai from django.conf import settings +from django.http import StreamingHttpResponse from pgvector.django import L2Distance from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -12,10 +15,30 @@ TOP_K = 8 + +def _sse(event, data): + """Serialize a single Server-Sent Event frame.""" + return f"event: {event}\ndata: {json.dumps(data)}\n\n" + + +def _chunk_text(chunk): + """Safely pull text from a Gemini stream chunk. + + Accessing ``chunk.text`` raises when a chunk carries no text parts (e.g. a + safety-only chunk), so guard it rather than let it kill the stream. + """ + try: + return chunk.text or "" + except (ValueError, AttributeError): + return "" + + class ChatView(APIView): permission_classes = [IsAuthenticated] def post(self, request, repo_id): + # Gate + validation run synchronously *before* the stream opens so these + # stay normal JSON 4xx responses rather than mid-stream errors. if not user_has_repo_access(request.user, repo_id): return Response({"error": "not found"}, status=404) query = request.data.get("query", "").strip() @@ -41,35 +64,56 @@ def post(self, request, repo_id): ): expanded_ids.add(edge.target_id) - functions = FunctionNode.objects.filter( - id__in=expanded_ids, repository_id=repo_id - ).select_related("file") + # Materialize eagerly: the generator below must not lazily evaluate the + # ORM once the streaming response has started. + functions = list( + FunctionNode.objects.filter( + id__in=expanded_ids, repository_id=repo_id + ).select_related("file") + ) - context_parts = [] - for fn in functions: - context_parts.append(f"# {fn.file.path} :: {fn.name} (line {fn.start_line})\n{fn.source}") + context_parts = [ + f"# {fn.file.path} :: {fn.name} (line {fn.start_line})\n{fn.source}" + for fn in functions + ] context = "\n\n".join(context_parts[:12]) - genai.configure(api_key=settings.GOOGLE_API_KEY) - model = genai.GenerativeModel("gemini-2.0-flash") + citations = [ + { + "id": fn.id, + # node_id is the graph node id the frontend uses to focus/center + # a function in GraphPanel — the load-bearing link field. + "node_id": str(fn.id), + "name": fn.name, + "file": fn.file.path, + "start_line": fn.start_line, + "summary": fn.summary, + } + for fn in functions + ] + + genai.configure(api_key=settings.GOOGLE_API_KEY, transport="rest") + model = genai.GenerativeModel("gemini-2.5-flash") prompt = ( "You are a code assistant. Answer questions about the codebase using the provided function context. " - "Be concise and reference function names and file paths.\n\n" + "Be concise and reference function names and file paths. Use Markdown formatting.\n\n" f"Context:\n{context}\n\nQuestion: {query}" ) - response = model.generate_content(prompt) - answer = response.text - - return Response({ - "answer": answer, - "functions": [ - { - "id": fn.id, - "name": fn.name, - "file": fn.file.path, - "start_line": fn.start_line, - "summary": fn.summary, - } - for fn in functions - ], - }) + + def event_stream(): + yield _sse("meta", {"functions": citations}) + try: + for chunk in model.generate_content(prompt, stream=True): + text = _chunk_text(chunk) + if text: + yield _sse("token", {"text": text}) + except Exception: + yield _sse("error", {"error": "generation failed"}) + yield _sse("done", {}) + + response = StreamingHttpResponse( + event_stream(), content_type="text/event-stream" + ) + response["Cache-Control"] = "no-cache" + response["X-Accel-Buffering"] = "no" # disable nginx buffering + return response diff --git a/backend/apps/embeddings/client.py b/backend/apps/embeddings/client.py index b631b2f..7198f14 100644 --- a/backend/apps/embeddings/client.py +++ b/backend/apps/embeddings/client.py @@ -1,22 +1,56 @@ +import time from concurrent.futures import ThreadPoolExecutor, as_completed import google.generativeai as genai from django.conf import settings +from google.api_core import exceptions as gax EMBEDDING_MODEL = "models/gemini-embedding-001" EMBEDDING_DIM = 3072 -MAX_WORKERS = 20 +BATCH_SIZE = 50 +MAX_PARALLEL_BATCHES = 4 +MAX_TEXT_CHARS = 8000 +MAX_RETRIES = 5 +REQUEST_TIMEOUT = 30 +_RETRYABLE = (gax.GoogleAPICallError, gax.RetryError) -def _embed_one(text: str) -> list[float]: - genai.configure(api_key=settings.GOOGLE_API_KEY) - return genai.embed_content(model=EMBEDDING_MODEL, content=text)["embedding"] +_configured = False -def embed_texts(texts: list[str]) -> list[list[float]]: - results: list[list[float] | None] = [None] * len(texts) - with ThreadPoolExecutor(max_workers=MAX_WORKERS) as pool: - futures = {pool.submit(_embed_one, text): i for i, text in enumerate(texts)} +def _configure_once(): + global _configured + if not _configured: + genai.configure(api_key=settings.GOOGLE_API_KEY, transport="rest") + _configured = True + + +def _embed_batch(texts): + _configure_once() + for attempt in range(MAX_RETRIES): + try: + resp = genai.embed_content( + model=EMBEDDING_MODEL, + content=texts, + request_options={"timeout": REQUEST_TIMEOUT}, + ) + return resp["embedding"] + except _RETRYABLE: + if attempt == MAX_RETRIES - 1: + raise + time.sleep(min(2 ** attempt, 20)) + + +def embed_texts(texts): + if not texts: + return [] + capped = [t[:MAX_TEXT_CHARS] for t in texts] + batches = [(i, capped[i : i + BATCH_SIZE]) for i in range(0, len(capped), BATCH_SIZE)] + results = [None] * len(capped) + with ThreadPoolExecutor(max_workers=MAX_PARALLEL_BATCHES) as pool: + futures = {pool.submit(_embed_batch, chunk): start for start, chunk in batches} for future in as_completed(futures): - results[futures[future]] = future.result() - return results # type: ignore[return-value] + start = futures[future] + for offset, vec in enumerate(future.result()): + results[start + offset] = vec + return results diff --git a/backend/apps/graph/views.py b/backend/apps/graph/views.py index 74b3389..b017307 100644 --- a/backend/apps/graph/views.py +++ b/backend/apps/graph/views.py @@ -1,5 +1,6 @@ import google.generativeai as genai from django.conf import settings +from django.db.models import Q from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -8,6 +9,10 @@ from .models import FunctionEdge, FunctionNode +# Dunder/boilerplate functions (__init__, __repr__, …) — name starts and ends +# with a double underscore. Hidden by default so the graph shows real logic. +BOILERPLATE = Q(name__startswith="__") & Q(name__endswith="__") + def serialize_node(n): return { @@ -38,6 +43,10 @@ def get(self, request, repo_id): file_id = request.query_params.get("file_id") dir_prefix = request.query_params.get("dir") node_id = request.query_params.get("node_id") + include_bp = request.query_params.get("include_boilerplate", "").lower() in ("1", "true") + + def visible(qs): + return qs if include_bp else qs.exclude(BOILERPLATE) if node_id: # Validate node_id parses and belongs to this repo. A node_id from @@ -71,7 +80,7 @@ def get(self, request, repo_id): elif file_id: file_nodes = list( - FunctionNode.objects.filter(repository_id=repo_id, file_id=file_id).select_related("file") + visible(FunctionNode.objects.filter(repository_id=repo_id, file_id=file_id)).select_related("file") ) file_node_ids = {n.id for n in file_nodes} out_edges = list( @@ -92,9 +101,9 @@ def get(self, request, repo_id): if e.target_id not in file_node_ids: neighbor_ids.add(e.target_id) neighbor_nodes = list( - FunctionNode.objects.filter( + visible(FunctionNode.objects.filter( id__in=neighbor_ids, repository_id=repo_id - ).select_related("file") + )).select_related("file") ) if neighbor_ids else [] nodes_qs = file_nodes + neighbor_nodes @@ -105,7 +114,7 @@ def get(self, request, repo_id): ) dir_file_ids = set(dir_files.values_list("id", flat=True)) dir_nodes = list( - FunctionNode.objects.filter(repository_id=repo_id, file_id__in=dir_file_ids).select_related("file") + visible(FunctionNode.objects.filter(repository_id=repo_id, file_id__in=dir_file_ids)).select_related("file") ) dir_node_ids = {n.id for n in dir_nodes} out_edges = list( @@ -126,15 +135,15 @@ def get(self, request, repo_id): if e.target_id not in dir_node_ids: neighbor_ids.add(e.target_id) neighbor_nodes = list( - FunctionNode.objects.filter( + visible(FunctionNode.objects.filter( id__in=neighbor_ids, repository_id=repo_id - ).select_related("file") + )).select_related("file") ) if neighbor_ids else [] nodes_qs = dir_nodes + neighbor_nodes else: nodes_qs = list( - FunctionNode.objects.filter(repository_id=repo_id).select_related("file") + visible(FunctionNode.objects.filter(repository_id=repo_id)).select_related("file") ) node_ids = {n.id for n in nodes_qs} all_edges = list( @@ -143,16 +152,26 @@ def get(self, request, repo_id): ) ) + # Drop edges to nodes hidden by the boilerplate filter so no edge dangles. + node_id_set = {n.id for n in nodes_qs} seen = set() unique_edges = [] for e in all_edges: - if e.id not in seen: - seen.add(e.id) - unique_edges.append(e) + if e.id in seen: + continue + if e.source_id not in node_id_set or e.target_id not in node_id_set: + continue + seen.add(e.id) + unique_edges.append(e) + + hidden = 0 if include_bp else ( + FunctionNode.objects.filter(repository_id=repo_id).filter(BOILERPLATE).count() + ) return Response({ "nodes": [serialize_node(n) for n in nodes_qs], "edges": [serialize_edge(e) for e in unique_edges], + "hidden": hidden, }) @@ -225,8 +244,8 @@ def get(self, request, repo_id, node_id): "No markdown, no headers, plain text only." ) - genai.configure(api_key=settings.GOOGLE_API_KEY) - response = genai.GenerativeModel("gemini-2.0-flash").generate_content(prompt) + genai.configure(api_key=settings.GOOGLE_API_KEY, transport="rest") + response = genai.GenerativeModel("gemini-2.5-flash").generate_content(prompt) return Response({ "name": fn.name, diff --git a/backend/core/test_settings.py b/backend/core/test_settings.py index 5d8a2cf..a1b14ee 100644 --- a/backend/core/test_settings.py +++ b/backend/core/test_settings.py @@ -37,6 +37,10 @@ def __init__(self, *args, **kwargs): def generate_content(self, *args, **kwargs): class _R: text = "" + # In streaming mode the real client yields chunk objects; mirror + # that so `for chunk in generate_content(..., stream=True)` works. + if kwargs.get("stream"): + return iter([_R()]) return _R() _genai_stub.configure = _genai_configure @@ -45,6 +49,24 @@ class _R: sys.modules["google.generativeai"] = _genai_stub sys.modules["google"].generativeai = _genai_stub +if "google.api_core" not in sys.modules: + _api_core_pkg = types.ModuleType("google.api_core") + _api_core_pkg.__path__ = [] + _api_core_exc = types.ModuleType("google.api_core.exceptions") + + class _GoogleAPIError(Exception): + pass + + _api_core_exc.GoogleAPICallError = _GoogleAPIError + _api_core_exc.RetryError = type("RetryError", (Exception,), {}) + _api_core_exc.ResourceExhausted = type("ResourceExhausted", (_GoogleAPIError,), {}) + _api_core_exc.ServiceUnavailable = type("ServiceUnavailable", (_GoogleAPIError,), {}) + _api_core_exc.DeadlineExceeded = type("DeadlineExceeded", (_GoogleAPIError,), {}) + _api_core_pkg.exceptions = _api_core_exc + sys.modules["google.api_core"] = _api_core_pkg + sys.modules["google.api_core.exceptions"] = _api_core_exc + sys.modules["google"].api_core = _api_core_pkg + # --- django.contrib.postgres.fields.ArrayField stub ------------------------ # `apps.graph.models` imports ArrayField at module load; the real package From 6e92ddbeffc3fd03438400a6dcd6f3e31ba29d07 Mon Sep 17 00:00:00 2001 From: MohamedAklamaash Date: Thu, 25 Jun 2026 13:09:27 +0530 Subject: [PATCH 16/18] feat(ui): AI-first chat, design-system overhaul, enterprise landing - Streaming chat client (fetch + ReadableStream) with markdown, citation chips that focus/center the cited node in the graph, and a Cmd/Ctrl-K Ask palette; chat state lifted into a shared useChat hook. - Design system: Tailwind v4 + tokenized zinc+indigo palette with light mode + toggle, Inter/JetBrains Mono, focus rings, custom scrollbars. - react-router for shareable URLs (/r/:repoId); reserved /share + /orgs. - Resizable dashboard panels; Explorer & Chat are collapsible (open by default) via top-bar toggles; mobile stacked layout + bottom tab bar. - Tasteful framer-motion animations (palette, dropdown, messages, inspector, processing steps). - Enterprise marketing landing/sign-in page (hero, product mockup, feature grid, steps, CTA) with a two-tone wordmark. - Graph: user-defined vs all-functions toggle (server-driven), empty state, theme-aware colors, focus/center on citation. - Processing retry button (pull latest & re-analyze); strip repo names to the project; new node-graph favicon; topbar dropdown z-index fix. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/index.html | 8 +- frontend/package.json | 10 +- frontend/pnpm-lock.yaml | 1298 ++++++++++++++++-- frontend/public/favicon.svg | 8 +- frontend/src/App.tsx | 96 +- frontend/src/api/index.ts | 86 +- frontend/src/components/AskPalette.tsx | 85 ++ frontend/src/components/ChatPanel.tsx | 104 +- frontend/src/components/Dashboard.tsx | 177 ++- frontend/src/components/GithubRepoPicker.tsx | 2 +- frontend/src/components/GraphPanel.tsx | 194 ++- frontend/src/components/Landing.tsx | 2 +- frontend/src/components/Login.tsx | 235 +++- frontend/src/components/Processing.tsx | 30 +- frontend/src/components/RepoSwitcher.tsx | 23 +- frontend/src/components/Sidebar.tsx | 4 +- frontend/src/components/ThemeToggle.tsx | 23 + frontend/src/hooks/useChat.ts | 67 + frontend/src/hooks/useMediaQuery.ts | 15 + frontend/src/index.css | 873 ++++++++++-- frontend/src/main.tsx | 13 +- frontend/src/theme.ts | 16 + frontend/src/types/index.ts | 14 +- frontend/vite.config.js | 10 +- 24 files changed, 3032 insertions(+), 361 deletions(-) create mode 100644 frontend/src/components/AskPalette.tsx create mode 100644 frontend/src/components/ThemeToggle.tsx create mode 100644 frontend/src/hooks/useChat.ts create mode 100644 frontend/src/hooks/useMediaQuery.ts create mode 100644 frontend/src/theme.ts diff --git a/frontend/index.html b/frontend/index.html index 38343fb..901da3b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,13 @@ - codegraph + + + + CodeGraph
diff --git a/frontend/package.json b/frontend/package.json index b89c3ad..fce18c1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,14 +13,19 @@ }, "dependencies": { "@types/d3": "^7.4.3", - "@xyflow/react": "^12.6.4", "axios": "^1.7.9", "d3": "^7.9.0", + "framer-motion": "^11.18.2", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "react-markdown": "^10.1.0", + "react-resizable-panels": "^2.1.7", + "react-router-dom": "^7.18.0", + "remark-gfm": "^4.0.1" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@tailwindcss/vite": "^4.3.1", "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", @@ -31,6 +36,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "tailwindcss": "^4.3.1", "typescript": "^6.0.3", "vite": "^8.0.4" } diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 0135773..295a202 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -11,25 +11,40 @@ importers: '@types/d3': specifier: ^7.4.3 version: 7.4.3 - '@xyflow/react': - specifier: ^12.6.4 - version: 12.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) axios: specifier: ^1.7.9 version: 1.16.0 d3: specifier: ^7.9.0 version: 7.9.0 + framer-motion: + specifier: ^11.18.2 + version: 11.18.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: specifier: ^19.2.4 version: 19.2.6 react-dom: specifier: ^19.2.4 version: 19.2.6(react@19.2.6) + react-markdown: + specifier: ^10.1.0 + version: 10.1.0(@types/react@19.2.14)(react@19.2.6) + react-resizable-panels: + specifier: ^2.1.7 + version: 2.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + react-router-dom: + specifier: ^7.18.0 + version: 7.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 devDependencies: '@eslint/js': specifier: ^9.39.4 version: 9.39.4 + '@tailwindcss/vite': + specifier: ^4.3.1 + version: 4.3.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)) '@types/node': specifier: ^25.6.0 version: 25.6.2 @@ -41,31 +56,34 @@ importers: version: 19.2.3(@types/react@19.2.14) '@typescript-eslint/eslint-plugin': specifier: ^8.58.2 - version: 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) + version: 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/parser': specifier: ^8.58.2 - version: 8.59.2(eslint@9.39.4)(typescript@6.0.3) + version: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) '@vitejs/plugin-react': specifier: ^6.0.1 - version: 6.0.1(vite@8.0.11(@types/node@25.6.2)) + version: 6.0.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0)) eslint: specifier: ^9.39.4 - version: 9.39.4 + version: 9.39.4(jiti@2.7.0) eslint-plugin-react-hooks: specifier: ^7.0.1 - version: 7.1.1(eslint@9.39.4) + version: 7.1.1(eslint@9.39.4(jiti@2.7.0)) eslint-plugin-react-refresh: specifier: ^0.5.2 - version: 0.5.2(eslint@9.39.4) + version: 0.5.2(eslint@9.39.4(jiti@2.7.0)) globals: specifier: ^17.4.0 version: 17.6.0 + tailwindcss: + specifier: ^4.3.1 + version: 4.3.1 typescript: specifier: ^6.0.3 version: 6.0.3 vite: specifier: ^8.0.4 - version: 8.0.11(@types/node@25.6.2) + version: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) packages: @@ -329,6 +347,96 @@ packages: '@rolldown/pluginutils@1.0.0-rc.7': resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + '@tailwindcss/node@4.3.1': + resolution: {integrity: sha512-6NDaqRoAMSXD1mr/RXu0HBvNE9a2n5tHPsxu9XHLws8o4Twes5rBM2205SUUiJ9goAtadrN6xTGX0UDEwp/N4A==} + + '@tailwindcss/oxide-android-arm64@4.3.1': + resolution: {integrity: sha512-SVlyf61g374l5cHyg8x9kf5xmLcOaxvOTsbsqDnSsDJaKOEFZ7GCvi84VAVGpxojYOs1+3K6M0UjXfqPU8vmOQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + resolution: {integrity: sha512-hVnWLwv+e/l7c4WKyVtHVrIPvYdqWHjRB3MDIqARynzFtnQg85kmQEFCbV9Ja0VVx4xXTIiDWY60Y7iz/iNoDA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.1': + resolution: {integrity: sha512-Cf7abu0WVgbhU7ANgPUnSAvm7nCvMweusHb8FnaHlLfv/Caq4GYaEZg7ZImzzmjx4lIAfuS8q+eLIS7A7IzxIg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + resolution: {integrity: sha512-ZZqzX2Y+GXtXXfqSfpJhDm60OoZfvLHLCgm+J7NVqgHHJjG/m9ugZI77RwTsVd4fnBJuCFP6Ae6kTJb71UdS8g==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + resolution: {integrity: sha512-/Ah/xik0LaMYfv9DZ0S/t4pBlBNYOcqtRwusjgovHkvT8ixueWCLyJjsaF5kQIckjb4IT8Q6K6p/iPmZMixYgg==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + resolution: {integrity: sha512-gqdFoVJlw444GvpnheZLHmvTzSxI/cOUUh2KSNejQjTcYkW062SVD+En0rUgD+QV91bz1XGIGtt1HJd48xUGbQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + resolution: {integrity: sha512-Bwv9KwOvE0VKa86xPFif9b9c3Y1NxOV1P0gLti/IYaWEsQYZXDlxfGEtA8mdDZ7SG3wyNXAWYT5SIn3giL57oA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + resolution: {integrity: sha512-Ymi8O8T15HYQdOUWUtTI6ldN0neHP85FC+Qz32xTcZ7iJXtem/x8ITev0o1e9e5rkqj4lONZfTRLvkmin1+tKg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + resolution: {integrity: sha512-M+P/91qJ6uILLw4k2G93GMDRAXj61SMvFQYt39AqvUqYgExXpLL5aepfns7sj4HiAQeolirQF9E0lzRvdf4zPQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + resolution: {integrity: sha512-zsM8uOeqvVGHsAXsJxsT28ttosFahLJKCLOTUBqRAtKnVgGSRitds9T432QiT8b77Yga7JIBkulIRRlJPtYhRA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + resolution: {integrity: sha512-aiNvSq9BsVk8V513lDKlrCFAgf8qBMPZTpgEhInL+NwQqs97mYmupVMrPrgBBSL8Pv/0zXu9MrMF9rMun1ZeNg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + resolution: {integrity: sha512-xDEyu1rg290472FEGaKHnzyDyh5QH+AlWvsU5hMoMtPpzmKlRI0jaYKCgSHDYtaQWZOYbMaduSyCwFwY4n1HmA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.1': + resolution: {integrity: sha512-yVPyo8RNkabVr3O2EhHEE0Rewu7YKzc1DhIqfL46LKveFrmu9XbDazNOJY7/GRuvw1h6u3utWnR29H/p5JPlgA==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.1': + resolution: {integrity: sha512-hItDHuIIlEV61R+faXu66s1K36aTurO/Qw0e45Vskz57gXl9pWOT6eg3zmcEui6CZXddbN7zd41bwmvag4JGwQ==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + '@tybys/wasm-util@0.10.2': resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} @@ -425,15 +533,30 @@ packages: '@types/d3@7.4.3': resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.6.2': resolution: {integrity: sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw==} @@ -445,6 +568,12 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@typescript-eslint/eslint-plugin@8.59.2': resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -504,6 +633,9 @@ packages: resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.2': + resolution: {integrity: sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==} + '@vitejs/plugin-react@6.0.1': resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -517,15 +649,6 @@ packages: babel-plugin-react-compiler: optional: true - '@xyflow/react@12.10.2': - resolution: {integrity: sha512-CgIi6HwlcHXwlkTpr0fxLv/0sRVNZ8IdwKLzzeCscaYBwpvfcH1QFOCeaTCuEn1FQEs/B8CjnTSjhs8udgmBgQ==} - peerDependencies: - react: '>=17' - react-dom: '>=17' - - '@xyflow/system@0.0.76': - resolution: {integrity: sha512-hvwvnRS1B3REwVDlWexsq7YQaPZeG3/mKo1jv38UmnpWmxihp14bW6VtEOuHEwJX2FvzFw8k77LyKSk/wiZVNA==} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -552,6 +675,9 @@ packages: axios@1.16.0: resolution: {integrity: sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==} + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -587,12 +713,24 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - classcat@5.0.5: - resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} @@ -605,6 +743,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -615,6 +756,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -758,6 +903,9 @@ packages: supports-color: optional: true + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -768,10 +916,17 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -779,6 +934,10 @@ packages: electron-to-chromium@1.5.353: resolution: {integrity: sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==} + enhanced-resolve@5.21.6: + resolution: {integrity: sha512-aNnGCvbJ/RIyWo1IuhNdVjnNF+EjH9wpzpNHt+ci/m9He9LJvUN8wrCcXjp9cWsGNAuvSpVFTx/vraAFQ8qGjQ==} + engines: {node: '>=10.13.0'} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -803,6 +962,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-plugin-react-hooks@7.1.1: resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} engines: {node: '>=18'} @@ -856,10 +1019,16 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -906,6 +1075,20 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + framer-motion@11.18.2: + resolution: {integrity: sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -942,6 +1125,9 @@ packages: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -958,12 +1144,21 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -984,10 +1179,22 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -996,9 +1203,20 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1113,13 +1331,151 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.3: + resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1135,6 +1491,12 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + motion-dom@11.18.1: + resolution: {integrity: sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==} + + motion-utils@11.18.1: + resolution: {integrity: sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1165,6 +1527,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1188,6 +1553,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proxy-from-env@2.1.0: resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} engines: {node: '>=10'} @@ -1201,10 +1569,51 @@ packages: peerDependencies: react: ^19.2.6 + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-resizable-panels@2.1.9: + resolution: {integrity: sha512-z77+X08YDIrgAes4jl8xhnUu1LNIRp4+E7cv4xHmLOxxUPO/ML7PSrE813b90vj7xvQ1lcf7g2uA9GeMZonjhQ==} + peerDependencies: + react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-router-dom@7.18.0: + resolution: {integrity: sha512-Fi0yY6kgtKae/Th2xibdWK0KSdYZ4B53Gyf6wRtomOKWgpNm7H7+DyfDhncdz9FKbpS+1jmDhg3F4WoGJ+yFOA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.18.0: + resolution: {integrity: sha512-pTTGt8J+ji1NOmYnjzT+bAJy/1zD+Jp4ziO6cL7T3ZLvXKtusO7BpFqlRXitqpcPVqllsIXFHRMt+2/k3Xn6HQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.6: resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==} engines: {node: '>=0.10.0'} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1235,6 +1644,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1247,18 +1659,43 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} + tailwindcss@4.3.1: + resolution: {integrity: sha512-hk+TB1m+K8CYNrP6rjQaq/Y+4Zylwpa87mLYBKCunwnnQ9p+fHb7kmSfGqyEJoxF/O6CDyABWVFEafNSYKll+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + ts-api-utils@2.5.0: resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} engines: {node: '>=18.12'} @@ -1280,6 +1717,24 @@ packages: undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1289,10 +1744,11 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} vite@8.0.11: resolution: {integrity: sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==} @@ -1362,20 +1818,8 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} - zustand@4.5.7: - resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} - engines: {node: '>=12.7.0'} - peerDependencies: - '@types/react': '>=16.8' - immer: '>=9.0.6' - react: '>=16.8' - peerDependenciesMeta: - '@types/react': - optional: true - immer: - optional: true - react: - optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: @@ -1495,9 +1939,9 @@ snapshots: tslib: 2.8.1 optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': dependencies: - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} @@ -1638,6 +2082,74 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.7': {} + '@tailwindcss/node@4.3.1': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.6 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.1 + + '@tailwindcss/oxide-android-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.1': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.1': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.1': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.1': + optional: true + + '@tailwindcss/oxide@4.3.1': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-arm64': 4.3.1 + '@tailwindcss/oxide-darwin-x64': 4.3.1 + '@tailwindcss/oxide-freebsd-x64': 4.3.1 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.1 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.1 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.1 + '@tailwindcss/oxide-linux-x64-musl': 4.3.1 + '@tailwindcss/oxide-wasm32-wasi': 4.3.1 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.1 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.1 + + '@tailwindcss/vite@4.3.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0))': + dependencies: + '@tailwindcss/node': 4.3.1 + '@tailwindcss/oxide': 4.3.1 + tailwindcss: 4.3.1 + vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) + '@tybys/wasm-util@0.10.2': dependencies: tslib: 2.8.1 @@ -1760,12 +2272,30 @@ snapshots: '@types/d3-transition': 3.0.9 '@types/d3-zoom': 3.0.8 + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.9 + '@types/estree@1.0.9': {} '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/ms@2.1.0': {} + '@types/node@25.6.2': dependencies: undici-types: 7.19.2 @@ -1778,15 +2308,19 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/scope-manager': 8.59.2 - '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4)(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.2 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@6.0.3) @@ -1794,14 +2328,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.2(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) '@typescript-eslint/visitor-keys': 8.59.2 debug: 4.4.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1824,13 +2358,13 @@ snapshots: dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: '@typescript-eslint/types': 8.59.2 '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) - '@typescript-eslint/utils': 8.59.2(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) debug: 4.4.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) ts-api-utils: 2.5.0(typescript@6.0.3) typescript: 6.0.3 transitivePeerDependencies: @@ -1853,13 +2387,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.2(eslint@9.39.4)(typescript@6.0.3)': + '@typescript-eslint/utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@typescript-eslint/scope-manager': 8.59.2 '@typescript-eslint/types': 8.59.2 '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) typescript: 6.0.3 transitivePeerDependencies: - supports-color @@ -1869,33 +2403,12 @@ snapshots: '@typescript-eslint/types': 8.59.2 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@25.6.2))': - dependencies: - '@rolldown/pluginutils': 1.0.0-rc.7 - vite: 8.0.11(@types/node@25.6.2) - - '@xyflow/react@12.10.2(@types/react@19.2.14)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': - dependencies: - '@xyflow/system': 0.0.76 - classcat: 5.0.5 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - zustand: 4.5.7(@types/react@19.2.14)(react@19.2.6) - transitivePeerDependencies: - - '@types/react' - - immer + '@ungap/structured-clone@1.3.2': {} - '@xyflow/system@0.0.76': + '@vitejs/plugin-react@6.0.1(vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0))': dependencies: - '@types/d3-drag': 3.0.7 - '@types/d3-interpolate': 3.0.4 - '@types/d3-selection': 3.0.11 - '@types/d3-transition': 3.0.9 - '@types/d3-zoom': 3.0.8 - d3-drag: 3.0.0 - d3-interpolate: 3.0.1 - d3-selection: 3.0.0 - d3-zoom: 3.0.0 + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.11(@types/node@25.6.2)(jiti@2.7.0) acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -1926,6 +2439,8 @@ snapshots: transitivePeerDependencies: - debug + bail@2.0.2: {} + balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -1958,12 +2473,20 @@ snapshots: caniuse-lite@1.0.30001792: {} + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - classcat@5.0.5: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} color-convert@2.0.1: dependencies: @@ -1975,12 +2498,16 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@7.2.0: {} concat-map@0.0.1: {} convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2145,6 +2672,10 @@ snapshots: dependencies: ms: 2.1.3 + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + deep-is@0.1.4: {} delaunator@5.1.0: @@ -2153,8 +2684,14 @@ snapshots: delayed-stream@1.0.0: {} + dequal@2.0.3: {} + detect-libc@2.1.2: {} + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -2163,6 +2700,11 @@ snapshots: electron-to-chromium@1.5.353: {} + enhanced-resolve@5.21.6: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + es-define-property@1.0.1: {} es-errors@1.3.0: {} @@ -2182,20 +2724,22 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-plugin-react-hooks@7.1.1(eslint@9.39.4): + escape-string-regexp@5.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@9.39.4(jiti@2.7.0)): dependencies: '@babel/core': 7.29.0 '@babel/parser': 7.29.3 - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) hermes-parser: 0.25.1 zod: 4.4.3 zod-validation-error: 4.0.2(zod@4.4.3) transitivePeerDependencies: - supports-color - eslint-plugin-react-refresh@0.5.2(eslint@9.39.4): + eslint-plugin-react-refresh@0.5.2(eslint@9.39.4(jiti@2.7.0)): dependencies: - eslint: 9.39.4 + eslint: 9.39.4(jiti@2.7.0) eslint-scope@8.4.0: dependencies: @@ -2208,9 +2752,9 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.4: + eslint@9.39.4(jiti@2.7.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 @@ -2244,6 +2788,8 @@ snapshots: minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 transitivePeerDependencies: - supports-color @@ -2263,8 +2809,12 @@ snapshots: estraverse@5.3.0: {} + estree-util-is-identifier-name@3.0.0: {} + esutils@2.0.3: {} + extend@3.0.2: {} + fast-deep-equal@3.1.3: {} fast-json-stable-stringify@2.1.0: {} @@ -2301,6 +2851,15 @@ snapshots: hasown: 2.0.3 mime-types: 2.1.35 + framer-motion@11.18.2(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + motion-dom: 11.18.1 + motion-utils: 11.18.1 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + fsevents@2.3.3: optional: true @@ -2336,6 +2895,8 @@ snapshots: gopd@1.2.0: {} + graceful-fs@4.2.11: {} + has-flag@4.0.0: {} has-symbols@1.1.0: {} @@ -2348,12 +2909,38 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.9 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: dependencies: hermes-estree: 0.25.1 + html-url-attributes@3.0.1: {} + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -2369,16 +2956,33 @@ snapshots: imurmurhash@0.1.4: {} + inline-style-parser@0.2.7: {} + internmap@2.0.3: {} + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 + is-hexadecimal@2.0.1: {} + + is-plain-obj@4.1.0: {} + isexe@2.0.0: {} + jiti@2.7.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.1: @@ -2459,12 +3063,364 @@ snapshots: lodash.merge@4.6.2: {} + longest-streak@3.1.0: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.3: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.2 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + mime-db@1.52.0: {} mime-types@2.1.35: @@ -2479,6 +3435,12 @@ snapshots: dependencies: brace-expansion: 1.1.14 + motion-dom@11.18.1: + dependencies: + motion-utils: 11.18.1 + + motion-utils@11.18.1: {} + ms@2.1.3: {} nanoid@3.3.12: {} @@ -2508,6 +3470,16 @@ snapshots: dependencies: callsites: 3.1.0 + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -2524,6 +3496,8 @@ snapshots: prelude-ls@1.2.1: {} + property-information@7.2.0: {} + proxy-from-env@2.1.0: {} punycode@2.3.1: {} @@ -2533,8 +3507,79 @@ snapshots: react: 19.2.6 scheduler: 0.27.0 + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.6): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.6 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-resizable-panels@2.1.9(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + + react-router-dom@7.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + react: 19.2.6 + react-dom: 19.2.6(react@19.2.6) + react-router: 7.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + + react-router@7.18.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6): + dependencies: + cookie: 1.1.1 + react: 19.2.6 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.6(react@19.2.6) + react@19.2.6: {} + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + resolve-from@4.0.0: {} robust-predicates@3.0.3: {} @@ -2570,6 +3615,8 @@ snapshots: semver@7.8.0: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2578,23 +3625,45 @@ snapshots: source-map-js@1.2.1: {} + space-separated-tokens@2.0.2: {} + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-json-comments@3.1.1: {} + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 + tailwindcss@4.3.1: {} + + tapable@2.3.3: {} + tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + trim-lines@3.0.1: {} + + trough@2.2.0: {} + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: typescript: 6.0.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} type-check@0.4.0: dependencies: @@ -2604,6 +3673,39 @@ snapshots: undici-types@7.19.2: {} + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 @@ -2614,11 +3716,17 @@ snapshots: dependencies: punycode: 2.3.1 - use-sync-external-store@1.6.0(react@19.2.6): + vfile-message@4.0.3: dependencies: - react: 19.2.6 + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 - vite@8.0.11(@types/node@25.6.2): + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.0.11(@types/node@25.6.2)(jiti@2.7.0): dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 @@ -2628,6 +3736,7 @@ snapshots: optionalDependencies: '@types/node': 25.6.2 fsevents: 2.3.3 + jiti: 2.7.0 which@2.0.2: dependencies: @@ -2645,9 +3754,4 @@ snapshots: zod@4.4.3: {} - zustand@4.5.7(@types/react@19.2.14)(react@19.2.6): - dependencies: - use-sync-external-store: 1.6.0(react@19.2.6) - optionalDependencies: - '@types/react': 19.2.14 - react: 19.2.6 + zwitch@2.0.4: {} diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 6893eb1..90f840a 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1 +1,7 @@ - \ No newline at end of file + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2d44be6..86d3183 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react' +import { Navigate, Route, Routes, useNavigate, useParams } from 'react-router-dom' import type { Repository } from './types' import { api } from './api' import { Landing } from './components/Landing' @@ -6,10 +7,9 @@ import { Processing } from './components/Processing' import { Dashboard } from './components/Dashboard' import { RepoSwitcher } from './components/RepoSwitcher' import { Login } from './components/Login' +import { ThemeToggle } from './components/ThemeToggle' import { useAuth } from './auth/AuthContext' -type View = 'landing' | 'processing' | 'dashboard' - const STORAGE_KEY = 'codegraph_last_repo_id' function UserMenu() { @@ -26,47 +26,63 @@ function UserMenu() { ) } -function AuthedApp() { - const [view, setView] = useState('landing') +// Topbar cluster reused across screens. Navigation is URL-driven now. +function Switcher({ current }: { current: Repository | null }) { + const navigate = useNavigate() + const goToRepo = (r: Repository) => navigate(`/r/${r.id}`) + return ( +
+ + + +
+ ) +} + +function Home() { + const navigate = useNavigate() + const savedId = localStorage.getItem(STORAGE_KEY) + // Restore the last repo by redirecting to its URL; RepoView clears the key + // and bounces back here if it no longer resolves (so this can't loop). + if (savedId) return + return navigate(`/r/${r.id}`)} switcher={} /> +} + +function RepoView() { + const { repoId } = useParams<{ repoId: string }>() + const navigate = useNavigate() const [repo, setRepo] = useState(null) - const [restoring, setRestoring] = useState(true) + const [loading, setLoading] = useState(true) useEffect(() => { - const savedId = localStorage.getItem(STORAGE_KEY) - if (!savedId) { setRestoring(false); return } - api.getRepo(savedId) + if (!repoId) return + setLoading(true) + api.getRepo(repoId) .then(r => { setRepo(r) - setView(r.status === 'ready' ? 'dashboard' : 'processing') + localStorage.setItem(STORAGE_KEY, r.id) }) - .catch(() => localStorage.removeItem(STORAGE_KEY)) - .finally(() => setRestoring(false)) - }, []) - - const goTo = (r: Repository) => { - setRepo(r) - localStorage.setItem(STORAGE_KEY, r.id) - setView(r.status === 'ready' ? 'dashboard' : 'processing') - } + .catch(() => { + localStorage.removeItem(STORAGE_KEY) + navigate('/', { replace: true }) + }) + .finally(() => setLoading(false)) + }, [repoId, navigate]) - const handleReady = (r: Repository) => { - setRepo(r) - localStorage.setItem(STORAGE_KEY, r.id) - setView('dashboard') - } + if (loading || !repo) return null - if (restoring) return null + const switcher = - const switcher = ( -
- - -
- ) - - if (view === 'landing') return - if (view === 'processing') return - return setView('processing')} switcher={switcher} /> + if (repo.status === 'ready') { + return ( + api.getRepo(repo.id).then(setRepo)} + switcher={switcher} + /> + ) + } + return } export default function App() { @@ -74,5 +90,15 @@ export default function App() { if (status === 'loading') return null if (status === 'anon') return - return + + return ( + + } /> + } /> + {/* Reserved for Phase C (collaboration): */} + {/* } /> */} + {/* } /> */} + } /> + + ) } diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 407d63f..89962c5 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,5 +1,17 @@ import axios from 'axios' -import type { Repository, RepoFile, FileFn, GraphData, User, GitHubRepo } from '../types' +import type { Repository, RepoFile, FileFn, GraphData, User, GitHubRepo, Citation } from '../types' + +function readCookie(name: string): string { + const match = document.cookie.match(new RegExp('(^|;\\s*)' + name + '=([^;]*)')) + return match ? decodeURIComponent(match[2]) : '' +} + +export interface ChatStreamHandlers { + onMeta?: (citations: Citation[]) => void + onToken?: (text: string) => void + onError?: (message: string) => void + onDone?: () => void +} // xsrf* config lets axios read the Django CSRF cookie and attach it as the // header on same-origin unsafe requests — required because we use @@ -57,7 +69,7 @@ export const api = { getFileFunctions: (repoId: string, fileId: number) => http.get(`/files/${repoId}/files/${fileId}/functions/`).then(r => r.data), - getGraph: (repoId: string, params?: { file_id?: number; node_id?: string; dir?: string }) => + getGraph: (repoId: string, params?: { file_id?: number; node_id?: string; dir?: string; include_boilerplate?: boolean }) => http.get(`/graph/${repoId}/`, { params }).then(r => r.data), traceNode: (repoId: string, nodeId: string) => @@ -69,6 +81,72 @@ export const api = { explanation: string }>(`/graph/${repoId}/trace/${nodeId}/`).then(r => r.data), - chat: (repoId: string, query: string) => - http.post<{ answer: string; functions: FileFn[] }>(`/chat/${repoId}/`, { query }).then(r => r.data), + // Streaming chat. Axios can't surface incremental bodies and EventSource is + // GET-only (can't send the CSRF header), so we use fetch + ReadableStream and + // parse the SSE frames by hand. The 401 -> auth:unauthorized dispatch is + // replicated here because this path bypasses the axios interceptor. + chatStream: async ( + repoId: string, + query: string, + handlers: ChatStreamHandlers, + signal?: AbortSignal, + ) => { + const res = await fetch(`/api/chat/${repoId}/`, { + method: 'POST', + credentials: 'include', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': readCookie('csrftoken'), + }, + body: JSON.stringify({ query }), + signal, + }) + + if (res.status === 401) { + window.dispatchEvent(new CustomEvent('auth:unauthorized')) + handlers.onError?.('Session expired. Please sign in again.') + return + } + if (!res.ok || !res.body) { + let message = 'Something went wrong.' + try { + message = (await res.json())?.error || message + } catch { /* non-JSON error body */ } + handlers.onError?.(message) + return + } + + const reader = res.body.getReader() + const decoder = new TextDecoder() + let buffer = '' + + const dispatch = (frame: string) => { + const lines = frame.split('\n') + let event = 'message' + let data = '' + for (const line of lines) { + if (line.startsWith('event:')) event = line.slice(6).trim() + else if (line.startsWith('data:')) data += line.slice(5).trim() + } + if (!data) return + let payload: Record + try { payload = JSON.parse(data) } catch { return } + if (event === 'meta') handlers.onMeta?.(payload.functions as Citation[]) + else if (event === 'token') handlers.onToken?.(payload.text as string) + else if (event === 'error') handlers.onError?.((payload.error as string) || 'Something went wrong.') + else if (event === 'done') handlers.onDone?.() + } + + for (;;) { + const { done, value } = await reader.read() + if (done) break + buffer += decoder.decode(value, { stream: true }) + let idx + while ((idx = buffer.indexOf('\n\n')) !== -1) { + dispatch(buffer.slice(0, idx)) + buffer = buffer.slice(idx + 2) + } + } + if (buffer.trim()) dispatch(buffer) + }, } diff --git a/frontend/src/components/AskPalette.tsx b/frontend/src/components/AskPalette.tsx new file mode 100644 index 0000000..5818051 --- /dev/null +++ b/frontend/src/components/AskPalette.tsx @@ -0,0 +1,85 @@ +import { useEffect, useRef, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' + +interface Props { + /** Send a query into the shared chat conversation. */ + send: (query: string) => void + streaming: boolean +} + +/** + * Global Cmd/Ctrl-K "Ask" entry point. Opens a centered input that feeds the + * same chat send path as the side panel, so users can ask from anywhere. + * + * NOTE: this is a minimal modal; Phase B swaps it onto the Radix Dialog + * primitive once the design-system layer lands. + */ +export function AskPalette({ send, streaming }: Props) { + const [open, setOpen] = useState(false) + const [value, setValue] = useState('') + const inputRef = useRef(null) + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault() + setOpen(o => !o) + } else if (e.key === 'Escape') { + setOpen(false) + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, []) + + useEffect(() => { + if (open) { + setValue('') + inputRef.current?.focus() + } + }, [open]) + + const submit = () => { + if (!value.trim() || streaming) return + send(value) + setOpen(false) + } + + return ( + + {open && ( + setOpen(false)} + initial={{ opacity: 0 }} + animate={{ opacity: 1 }} + exit={{ opacity: 0 }} + transition={{ duration: 0.15 }} + > + e.stopPropagation()} + initial={{ opacity: 0, scale: 0.96, y: -12 }} + animate={{ opacity: 1, scale: 1, y: 0 }} + exit={{ opacity: 0, scale: 0.96, y: -12 }} + transition={{ duration: 0.18, ease: [0.16, 1, 0.3, 1] }} + > + setValue(e.target.value)} + onKeyDown={e => { + if (e.key === 'Enter') submit() + }} + /> +
+ Enter to ask · Esc to close +
+
+
+ )} +
+ ) +} diff --git a/frontend/src/components/ChatPanel.tsx b/frontend/src/components/ChatPanel.tsx index cf2611f..2476ea2 100644 --- a/frontend/src/components/ChatPanel.tsx +++ b/frontend/src/components/ChatPanel.tsx @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from 'react' -import type { ChatMessage, FileFn } from '../types' -import { api } from '../api' +import { motion } from 'framer-motion' +import ReactMarkdown from 'react-markdown' +import remarkGfm from 'remark-gfm' +import type { ChatMessage } from '../types' const SUGGESTIONS = [ 'Where is authentication handled?', @@ -9,39 +11,25 @@ const SUGGESTIONS = [ ] interface Props { - repoId: string - onFocusFn: (fn: FileFn) => void - switcher: React.ReactNode | null + messages: ChatMessage[] + streaming: boolean + send: (query: string) => void + /** Focus/center a cited function in the graph by its node id. */ + onCiteNode: (nodeId: string) => void } -export function ChatPanel({ repoId, onFocusFn }: Props) { - const [messages, setMessages] = useState([]) +export function ChatPanel({ messages, streaming, send, onCiteNode }: Props) { const [input, setInput] = useState('') - const [loading, setLoading] = useState(false) const bottomRef = useRef(null) - useEffect(() => { - setMessages([]) - setInput('') - }, [repoId]) - useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) - const send = async (query: string) => { - if (!query.trim() || loading) return + const submit = (query: string) => { + if (!query.trim() || streaming) return + send(query) setInput('') - setMessages(m => [...m, { role: 'user', content: query.trim() }]) - setLoading(true) - try { - const data = await api.chat(repoId, query.trim()) - setMessages(m => [...m, { role: 'assistant', content: data.answer, functions: data.functions }]) - } catch (err: unknown) { - setMessages(m => [...m, { role: 'assistant', content: (err as { response?: { data?: { error?: string } } }).response?.data?.error || 'Something went wrong.' }]) - } finally { - setLoading(false) - } } return ( @@ -51,41 +39,63 @@ export function ChatPanel({ repoId, onFocusFn }: Props) {

Ask anything about this repo

    - {SUGGESTIONS.map(s =>
  • send(s)}>{s}
  • )} + {SUGGESTIONS.map(s =>
  • submit(s)}>{s}
  • )}
)} - {messages.map((msg, i) => ( -
-
{msg.content}
- {msg.functions && msg.functions.length > 0 && ( -
- {msg.functions.map(fn => ( - onFocusFn(fn)}> - {fn.name} - - ))} + {messages.map((msg, i) => { + const isLast = i === messages.length - 1 + const pending = msg.role === 'assistant' && !msg.content && streaming && isLast + return ( + +
+ {pending ? ( + Thinking… + ) : msg.role === 'assistant' ? ( +
+ {msg.content} +
+ ) : ( + msg.content + )}
- )} -
- ))} - {loading && ( -
-
Thinking…
-
- )} + + {msg.citations && msg.citations.length > 0 && ( +
+ {msg.citations.map(c => ( + + ))} +
+ )} + + ) + })}
+
setInput(e.target.value)} - onKeyDown={e => e.key === 'Enter' && send(input)} - disabled={loading} + onKeyDown={e => e.key === 'Enter' && submit(input)} + disabled={streaming} /> -
diff --git a/frontend/src/components/Dashboard.tsx b/frontend/src/components/Dashboard.tsx index 5fc1b59..5b0d484 100644 --- a/frontend/src/components/Dashboard.tsx +++ b/frontend/src/components/Dashboard.tsx @@ -1,9 +1,13 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' +import { Panel, PanelGroup, PanelResizeHandle, type ImperativePanelHandle } from 'react-resizable-panels' import type { Repository, RepoFile } from '../types' import { api } from '../api' +import { useChat } from '../hooks/useChat' +import { useMediaQuery } from '../hooks/useMediaQuery' import { Sidebar } from './Sidebar' import { GraphPanel } from './GraphPanel' import { ChatPanel } from './ChatPanel' +import { AskPalette } from './AskPalette' interface Props { repo: Repository @@ -11,9 +15,26 @@ interface Props { switcher: React.ReactNode } +type MobileTab = 'files' | 'graph' | 'chat' + export function Dashboard({ repo, onReanalyze, switcher }: Props) { const [selectedFile, setSelectedFile] = useState(null) const [selectedDir, setSelectedDir] = useState(null) + const [focus, setFocus] = useState<{ id: string; n: number }>({ id: '', n: 0 }) + const [mobileTab, setMobileTab] = useState('graph') + const [sidebarOpen, setSidebarOpen] = useState(true) + const [chatOpen, setChatOpen] = useState(true) + + const sidebarRef = useRef(null) + const chatRef = useRef(null) + + const isMobile = useMediaQuery('(max-width: 820px)') + const { messages, streaming, send } = useChat(repo.id) + + const toggle = (panel: ImperativePanelHandle | null) => { + if (!panel) return + panel.isCollapsed() ? panel.expand() : panel.collapse() + } const handleReanalyze = async () => { await api.submitRepo(repo.url) @@ -23,37 +44,149 @@ export function Dashboard({ repo, onReanalyze, switcher }: Props) { const handleSelectFile = (f: RepoFile) => { setSelectedFile(f) setSelectedDir(null) + if (isMobile) setMobileTab('graph') } const handleSelectDir = (dir: string) => { setSelectedDir(dir) setSelectedFile(null) + if (isMobile) setMobileTab('graph') } + const citeNode = (nodeId: string) => { + setFocus(f => ({ id: nodeId, n: f.n + 1 })) + if (isMobile) setMobileTab('graph') + } + + const sidebar = ( + {}} + onReanalyze={handleReanalyze} + /> + ) + + const graph = ( + {}} + focusNodeId={focus.id || null} + focusNonce={focus.n} + /> + ) + + const chat = ( +
+
+ Ask about the codebase +
+ +
+ ) + return ( -
- {}} - onReanalyze={handleReanalyze} - /> - {}} - /> -
-
- Ask about the codebase +
+
+
+ {!isMobile && ( + + )} + + + + + + + {repo.name} +
+
+ {!isMobile && ( + + )} {switcher}
- {}} switcher={null} /> -
+ + + {isMobile ? ( + <> +
+ {mobileTab === 'files' && sidebar} + {mobileTab === 'graph' && graph} + {mobileTab === 'chat' && chat} +
+ + + ) : ( +
+ + setSidebarOpen(false)} + onExpand={() => setSidebarOpen(true)} + > + {sidebar} + + + {graph} + + setChatOpen(false)} + onExpand={() => setChatOpen(true)} + > + {chat} + + +
+ )} + +
) } diff --git a/frontend/src/components/GithubRepoPicker.tsx b/frontend/src/components/GithubRepoPicker.tsx index a08d8dd..c6a7b17 100644 --- a/frontend/src/components/GithubRepoPicker.tsx +++ b/frontend/src/components/GithubRepoPicker.tsx @@ -97,7 +97,7 @@ export function GithubRepoPicker({ onAnalyze }: Props) { {repos.map(r => (
- {r.full_name} + {r.name} {r.private && Private} {relativeTime(r.pushed_at)}
diff --git a/frontend/src/components/GraphPanel.tsx b/frontend/src/components/GraphPanel.tsx index c20f537..a6ef171 100644 --- a/frontend/src/components/GraphPanel.tsx +++ b/frontend/src/components/GraphPanel.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' import * as d3 from 'd3' import type { RepoFile, FunctionNode, FunctionEdge } from '../types' import { api } from '../api' @@ -31,19 +32,29 @@ interface Props { selectedFile: RepoFile | null selectedDir: string | null onNodeSelect: (id: string, name: string) => void + /** Node to focus/center (set by chat citations). */ + focusNodeId?: string | null + /** Bumped on every focus request so repeat clicks on the same node re-trigger. */ + focusNonce?: number } const FILE_COLORS = [ - '#d97757', '#7a9b6e', '#e6b450', '#b08968', - '#8e7cc3', '#5a8a8a', '#c25450', '#a89b8c', + '#6366f1', '#22d3ee', '#34d399', '#fbbf24', + '#f472b6', '#a78bfa', '#fb923c', '#38bdf8', ] -export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: Props) { +export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect, focusNodeId, focusNonce }: Props) { const svgRef = useRef(null) const simRef = useRef | null>(null) + // focusOn for the current drawing; lets the focus effect center a node + // without re-running draw. pendingFocus holds an id to apply after a redraw. + const focusFnRef = useRef<((id: string) => boolean) | null>(null) + const pendingFocusRef = useRef(null) const [mode, setMode] = useState('full') const [selected, setSelected] = useState(null) const [nodeCount, setNodeCount] = useState(0) + const [hiddenCount, setHiddenCount] = useState(0) + const [showAll, setShowAll] = useState(false) const [traceNodeId, setTraceNodeId] = useState(null) const [traceNodeName, setTraceNodeName] = useState(null) @@ -51,18 +62,22 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: const svg = svgRef.current if (!svg || m === 'flow') return - const params: { file_id?: number; dir?: string } = {} + const params: { file_id?: number; dir?: string; include_boilerplate?: boolean } = {} if (m === 'file' && selectedFile) params.file_id = selectedFile.id if (m === 'dir' && selectedDir) params.dir = selectedDir + if (showAll) params.include_boilerplate = true - let rawNodes: FunctionNode[], rawEdges: FunctionEdge[] + // Boilerplate (__init__, dunders) is filtered server-side unless "Show all". + let rawNodes: FunctionNode[], rawEdges: FunctionEdge[], hidden: number try { const data = await api.getGraph(repoId, params) rawNodes = data.nodes rawEdges = data.edges + hidden = data.hidden ?? 0 } catch { return } setNodeCount(rawNodes.length) + setHiddenCount(hidden) simRef.current?.stop() d3.select(svg).selectAll('*').remove() @@ -71,6 +86,11 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: const H = svg.clientHeight || 600 const R = 18 // node radius + // Read brand colors from CSS vars so the graph re-themes with light/dark. + const cs = getComputedStyle(svg) + const accentColor = cs.getPropertyValue('--accent').trim() || '#d97757' + const textColor = cs.getPropertyValue('--text').trim() || '#f0e6dc' + const fileIds = [...new Set(rawNodes.map(n => n.file_id))] const colorMap = new Map(fileIds.map((fid, i) => [fid, FILE_COLORS[i % FILE_COLORS.length]])) @@ -94,11 +114,10 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: const root = d3.select(svg) const g = root.append('g') - root.call( - d3.zoom() - .scaleExtent([0.05, 4]) - .on('zoom', e => g.attr('transform', e.transform)) - ) + const zoomBehavior = d3.zoom() + .scaleExtent([0.05, 4]) + .on('zoom', e => g.attr('transform', e.transform)) + root.call(zoomBehavior) root.append('defs').append('marker') .attr('id', 'arrow') @@ -110,11 +129,11 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: .attr('orient', 'auto') .append('path') .attr('d', 'M0,-5L10,0L0,5') - .attr('fill', '#d97757') + .attr('fill', accentColor) const link = g.append('g').selectAll('line') .data(edges).join('line') - .attr('stroke', '#d97757') + .attr('stroke', accentColor) .attr('stroke-width', 1.2) .attr('stroke-opacity', 0.6) .attr('marker-end', 'url(#arrow)') @@ -148,11 +167,11 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: onNodeSelect(d.id, d.name) setTraceNodeId(d.id) setTraceNodeName(d.name) - node.select('circle').attr('stroke', (n: NodeDatum) => n.id === d.id ? '#f0e6dc' : 'none').attr('stroke-width', 2) + node.select('circle').attr('stroke', (n: NodeDatum) => n.id === d.id ? textColor : 'none').attr('stroke-width', 2) link .attr('stroke', (e: EdgeDatum) => { const s = (e.source as NodeDatum).id, t = (e.target as NodeDatum).id - return s === d.id || t === d.id ? '#f0e6dc' : '#d97757' + return s === d.id || t === d.id ? textColor : accentColor }) .attr('stroke-opacity', (e: EdgeDatum) => { const s = (e.source as NodeDatum).id, t = (e.target as NodeDatum).id @@ -163,12 +182,49 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: root.on('click', () => { setSelected(null) node.select('circle').attr('stroke', 'none') - link.attr('stroke', '#d97757').attr('stroke-opacity', 0.6) + link.attr('stroke', accentColor).attr('stroke-opacity', 0.6) }) + // Imperative focus used by chat citations: highlight a node + its edges and + // pan/zoom it to the center. Returns false when the id isn't in this view. + const focusOn = (id: string): boolean => { + const d = nodes.find(n => n.id === id) + if (!d) return false + const deps = edges + .filter(e => (e.source as NodeDatum).id === d.id || (e.target as NodeDatum).id === d.id) + .map(e => { + const src = e.source as NodeDatum + const tgt = e.target as NodeDatum + const other = src.id === d.id ? tgt : src + return { id: other.id, name: other.name, file: other.file, direction: src.id === d.id ? 'calls' as const : 'called-by' as const } + }) + setSelected({ node: d, deps }) + setTraceNodeId(d.id) + setTraceNodeName(d.name) + node.select('circle').attr('stroke', (n: NodeDatum) => n.id === d.id ? textColor : 'none').attr('stroke-width', 2) + link + .attr('stroke', (e: EdgeDatum) => { + const s = (e.source as NodeDatum).id, t = (e.target as NodeDatum).id + return s === d.id || t === d.id ? '#f0e6dc' : '#d97757' + }) + .attr('stroke-opacity', (e: EdgeDatum) => { + const s = (e.source as NodeDatum).id, t = (e.target as NodeDatum).id + return s === d.id || t === d.id ? 1 : 0.2 + }) + const scale = 1.4 + const tx = W / 2 - (d.x ?? W / 2) * scale + const ty = H / 2 - (d.y ?? H / 2) * scale + root.transition().duration(500).call( + zoomBehavior.transform, + d3.zoomIdentity.translate(tx, ty).scale(scale), + ) + return true + } + focusFnRef.current = focusOn + node.append('circle') .attr('r', R) - .attr('fill', (d: NodeDatum) => colorMap.get(d.file_id) ?? '#d97757') + .attr('fill', (d: NodeDatum) => colorMap.get(d.file_id) ?? accentColor) .attr('fill-opacity', (d: NodeDatum) => d.isExternal ? 0.35 : 0.85) .attr('stroke', 'none') @@ -177,7 +233,7 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: .attr('text-anchor', 'middle') .attr('dy', R + 13) .attr('font-size', 10) - .attr('fill', '#f0e6dc') + .attr('fill', textColor) .attr('pointer-events', 'none') const sim = d3.forceSimulation(nodes) @@ -193,9 +249,28 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: .attr('y2', (d: EdgeDatum) => (d.target as NodeDatum).y!) node.attr('transform', (d: NodeDatum) => `translate(${d.x},${d.y})`) }) + .on('end', () => { + // Apply a focus requested before/while the layout was settling, now + // that node positions are final. + if (pendingFocusRef.current && focusOn(pendingFocusRef.current)) { + pendingFocusRef.current = null + } + }) simRef.current = sim - }, [repoId, selectedFile, selectedDir, onNodeSelect]) + }, [repoId, selectedFile, selectedDir, onNodeSelect, showAll]) + + // A citation was clicked: ensure the full graph is shown (so the node exists) + // and center it. Try immediately for the already-drawn case; otherwise let the + // redraw's 'end' handler apply the pending focus. + useEffect(() => { + if (!focusNodeId) return + setMode('full') + pendingFocusRef.current = focusNodeId + if (focusFnRef.current?.(focusNodeId)) { + pendingFocusRef.current = null + } + }, [focusNodeId, focusNonce]) useEffect(() => { draw(mode) }, [mode, selectedFile, selectedDir, draw]) @@ -213,6 +288,13 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: return () => obs.disconnect() }, [draw, mode]) + // Re-read CSS-var colors when the theme flips. + useEffect(() => { + const onThemeChange = () => draw(mode) + window.addEventListener('theme:change', onThemeChange) + return () => window.removeEventListener('theme:change', onThemeChange) + }, [draw, mode]) + return (
@@ -232,7 +314,20 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: > Flow {traceNodeName ? `· ${traceNodeName}` : ''} - {mode !== 'flow' && {nodeCount} nodes} + {mode !== 'flow' && ( + + )} + {mode !== 'flow' && ( + + {nodeCount} nodes{!showAll && hiddenCount > 0 ? ` · ${hiddenCount} hidden` : ''} + + )}
{mode === 'flow' ? ( @@ -240,31 +335,46 @@ export function GraphPanel({ repoId, selectedFile, selectedDir, onNodeSelect }: ) : ( <> - {selected && ( -
-
- {selected.node.name}() - -
-
File{selected.node.file}
-
Line{selected.node.start_line}
- {selected.node.summary && ( -
Summary{selected.node.summary}
- )} - {selected.deps.length > 0 && ( -
-
Dependencies
- {selected.deps.map(d => ( -
- {d.direction === 'calls' ? '→' : '←'} - {d.name} - {d.file.split('/').pop()} -
- ))} -
- )} + {nodeCount === 0 && ( +
+
+

No functions to show here

+ Pick a file or switch to Full Graph
)} + + {selected && ( + +
+ {selected.node.name}() + +
+
File{selected.node.file}
+
Line{selected.node.start_line}
+ {selected.node.summary && ( +
Summary{selected.node.summary}
+ )} + {selected.deps.length > 0 && ( +
+
Dependencies
+ {selected.deps.map(d => ( +
+ {d.direction === 'calls' ? '→' : '←'} + {d.name} + {d.file.split('/').pop()} +
+ ))} +
+ )} +
+ )} +
)}
diff --git a/frontend/src/components/Landing.tsx b/frontend/src/components/Landing.tsx index f01aeb8..92efbb3 100644 --- a/frontend/src/components/Landing.tsx +++ b/frontend/src/components/Landing.tsx @@ -33,7 +33,7 @@ export function Landing({ onSubmit, switcher }: Props) { return (
{switcher}
-

CodeGraph

+

CodeGraph

Explore any GitHub repository as a function-level knowledge graph

diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx index db59400..6913333 100644 --- a/frontend/src/components/Login.tsx +++ b/frontend/src/components/Login.tsx @@ -1,6 +1,110 @@ import { useEffect, useState } from 'react' +import { motion } from 'framer-motion' import { api } from '../api' +const LANGS = ['Python', 'TypeScript', 'JavaScript', 'Go', 'Rust', 'Java', 'C / C++', 'Kotlin'] + +const FEATURES = [ + { + title: 'Interactive call graph', + body: 'Every function is a node, every call an edge. Pan, zoom, filter by file, and watch how execution actually flows — no more reading file-by-file.', + icon: ( + <> + + + + ), + }, + { + title: 'Ask your codebase', + body: 'A semantic AI assistant grounded in your real functions. Answers cite actual code and jump straight to the node in the graph.', + icon: ( + <> + + + + ), + }, + { + title: 'Trace any flow', + body: 'Follow a function’s full call chain across files in a single click. Understand dependencies and impact before you change a line.', + icon: ( + <> + + + + ), + }, + { + title: 'Eight languages, zero setup', + body: 'Point CodeGraph at any GitHub repo — public or private. We clone, parse, map, and index it automatically in seconds.', + icon: ( + <> + + + ), + }, +] + +const STEPS = [ + { n: '01', title: 'Connect GitHub', body: 'Sign in and choose a repository — yours, your team’s, or any public project.' }, + { n: '02', title: 'We build the graph', body: 'CodeGraph extracts functions, maps call relationships, and embeds everything for semantic search.' }, + { n: '03', title: 'Explore & ask', body: 'Navigate the graph, trace execution flows, and ask questions in plain English.' }, +] + +function Logo({ size = 22 }: { size?: number }) { + return ( + + + + + + + ) +} + +function GithubIcon() { + return ( + + + + ) +} + +function HeroGraphic() { + const nodes = [ + { x: 70, y: 60 }, { x: 210, y: 40 }, { x: 300, y: 130 }, + { x: 150, y: 150 }, { x: 60, y: 200 }, { x: 250, y: 230 }, { x: 160, y: 270 }, + ] + const edges = [[0, 3], [1, 2], [1, 3], [3, 4], [3, 5], [5, 6], [4, 6], [2, 5]] + const colors = ['#6366f1', '#22d3ee', '#34d399', '#fbbf24', '#f472b6', '#a78bfa', '#38bdf8'] + return ( + + {edges.map(([a, b], i) => ( + + ))} + {nodes.map((n, i) => ( + + ))} + + ) +} + export function Login() { const [loading, setLoading] = useState(false) const [error, setError] = useState('') @@ -15,7 +119,7 @@ export function Login() { } }, []) - const handleClick = async () => { + const signIn = async () => { setLoading(true) setError('') try { @@ -27,16 +131,125 @@ export function Login() { } return ( -
-

CodeGraph

-

Sign in to explore your repositories as function-level knowledge graphs.

- -

- We request the repo scope so you can clone private repositories you choose. -

- {error &&

{error}

} +
+ + +
+
+ + AI-native code intelligence +

Understand any codebase
in minutes, not weeks.

+

+ CodeGraph turns any GitHub repository into an interactive, function-level + knowledge graph — then lets you ask it anything. Onboard faster, review + smarter, and ship with confidence. +

+
+ + Free to start · Reads only the repos you choose +
+ {error &&

{error}

} +
+ {LANGS.map(l => {l})} +
+
+ +
+
+
+
+
+
+ +
+ +

Stop reading code line by line.

+

Everything you need to understand an unfamiliar codebase — in one view.

+
+
+ {FEATURES.map((f, i) => ( + + + {f.icon} + +

{f.title}

+

{f.body}

+
+ ))} +
+
+ +
+ +

From repo to insight in three steps.

+
+
+ {STEPS.map((s, i) => ( + + {s.n} +

{s.title}

+

{s.body}

+
+ ))} +
+
+ +
+
+

See your codebase clearly.

+

Connect a repository and explore it as a living graph in under a minute.

+ +
+ +
+
CodeGraph
+ Function-level code intelligence · We request the repo scope to clone the private repositories you choose. +
) } diff --git a/frontend/src/components/Processing.tsx b/frontend/src/components/Processing.tsx index 6057312..ca78dcc 100644 --- a/frontend/src/components/Processing.tsx +++ b/frontend/src/components/Processing.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { motion } from 'framer-motion' import type { Repository } from '../types' import { api } from '../api' import { REPO_STATUSES } from '../constants/repoStatus' @@ -14,8 +15,20 @@ interface Props { export function Processing({ repo: initial, onReady, switcher }: Props) { const [repo, setRepo] = useState(initial) const [readyHolding, setReadyHolding] = useState(false) + const [retrying, setRetrying] = useState(false) const timer = useRef | null>(null) + const retry = async () => { + setRetrying(true) + try { + setRepo(await api.submitRepo(repo.url)) + } catch { + /* keep the failed state; the user can retry again */ + } finally { + setRetrying(false) + } + } + useEffect(() => { if (repo.status === 'failed') return @@ -54,7 +67,13 @@ export function Processing({ repo: initial, onReady, switcher }: Props) { else if (i === 0 && (repo.status === 'pending' || repo.status === 'cloning')) cls = 'active' else if (i === activeIdx) cls = 'active' return ( -
+ {cls === 'done' ? '✓' : cls === 'error' ? '✗' : cls === 'active' ? : '○'} @@ -62,7 +81,7 @@ export function Processing({ repo: initial, onReady, switcher }: Props) {
{step.label}
{step.description}
-
+ ) })}
@@ -70,7 +89,12 @@ export function Processing({ repo: initial, onReady, switcher }: Props) {

Done — opening dashboard…

)} {repo.status === 'failed' && ( -

{repo.status_message}

+
+

{repo.status_message}

+ +
)}
) diff --git a/frontend/src/components/RepoSwitcher.tsx b/frontend/src/components/RepoSwitcher.tsx index 9f4a2db..8d0c89a 100644 --- a/frontend/src/components/RepoSwitcher.tsx +++ b/frontend/src/components/RepoSwitcher.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import { AnimatePresence, motion } from 'framer-motion' import type { Repository } from '../types' import { api } from '../api' import { GithubRepoPicker } from './GithubRepoPicker' @@ -73,10 +74,10 @@ export function RepoSwitcher({ current, onSelect, onNew }: Props) { onNew(repo) } - const statusDot = (status: Repository['status']) => { - if (status === 'ready') return '🟢' - if (status === 'failed') return '🔴' - return '🟡' + const statusClass = (status: Repository['status']) => { + if (status === 'ready') return 'ready' + if (status === 'failed') return 'failed' + return 'pending' } return ( @@ -88,8 +89,15 @@ export function RepoSwitcher({ current, onSelect, onNew }: Props) { {open ? '▴' : '▾'} + {open && ( -
+
@@ -104,7 +112,7 @@ export function RepoSwitcher({ current, onSelect, onNew }: Props) { className={`repo-item ${current?.id === r.id ? 'active' : ''}`} onClick={() => { onSelect(r); setOpen(false) }} > - {statusDot(r.status)} + {r.name} {r.status !== 'ready' && ( {r.status} @@ -139,8 +147,9 @@ export function RepoSwitcher({ current, onSelect, onNew }: Props) { )} )} -
+
)} +
) } diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 8a61dbb..01f1dfa 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -107,8 +107,8 @@ export function Sidebar({ repoId, repoName, selectedFile, onSelectFile, onSelect return (
-

{repoName}

- +

Explorer

+
{sortEntries(Object.entries(tree)).map(([k, v]) => ( diff --git a/frontend/src/components/ThemeToggle.tsx b/frontend/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..a2d394d --- /dev/null +++ b/frontend/src/components/ThemeToggle.tsx @@ -0,0 +1,23 @@ +import { useState } from 'react' +import { applyTheme, getInitialTheme, type Theme } from '../theme' + +export function ThemeToggle() { + const [theme, setTheme] = useState(getInitialTheme()) + + const toggle = () => { + const next: Theme = theme === 'dark' ? 'light' : 'dark' + setTheme(next) + applyTheme(next) + } + + return ( + + ) +} diff --git a/frontend/src/hooks/useChat.ts b/frontend/src/hooks/useChat.ts new file mode 100644 index 0000000..6e5d00e --- /dev/null +++ b/frontend/src/hooks/useChat.ts @@ -0,0 +1,67 @@ +import { useCallback, useEffect, useRef, useState } from 'react' +import type { ChatMessage } from '../types' +import { api } from '../api' + +/** + * Owns chat state + the streaming send path for a repo. Lifted out of ChatPanel + * so multiple surfaces (the right-rail panel and the Cmd/Ctrl-K palette) can + * share one conversation and one send path. + */ +export function useChat(repoId: string) { + const [messages, setMessages] = useState([]) + const [streaming, setStreaming] = useState(false) + const abortRef = useRef(null) + + // Reset the conversation when the active repo changes, and abort any + // in-flight stream so its tokens don't bleed into the next repo. + useEffect(() => { + abortRef.current?.abort() + setMessages([]) + setStreaming(false) + }, [repoId]) + + useEffect(() => () => abortRef.current?.abort(), []) + + // Mutate the trailing assistant placeholder as tokens/meta arrive. + const patchLast = useCallback((patch: (m: ChatMessage) => ChatMessage) => { + setMessages(prev => { + if (prev.length === 0) return prev + const copy = [...prev] + copy[copy.length - 1] = patch(copy[copy.length - 1]) + return copy + }) + }, []) + + const send = useCallback((raw: string) => { + const query = raw.trim() + if (!query || streaming) return + + setMessages(m => [ + ...m, + { role: 'user', content: query }, + { role: 'assistant', content: '' }, + ]) + setStreaming(true) + + const controller = new AbortController() + abortRef.current = controller + + api.chatStream( + repoId, + query, + { + onMeta: citations => patchLast(m => ({ ...m, citations })), + onToken: text => patchLast(m => ({ ...m, content: m.content + text })), + onError: message => + patchLast(m => ({ ...m, content: m.content || message })), + onDone: () => setStreaming(false), + }, + controller.signal, + ).catch(() => { + patchLast(m => ({ ...m, content: m.content || 'Something went wrong.' })) + setStreaming(false) + }).finally(() => setStreaming(false)) + }, [repoId, streaming, patchLast]) + + return { messages, streaming, send } +} diff --git a/frontend/src/hooks/useMediaQuery.ts b/frontend/src/hooks/useMediaQuery.ts new file mode 100644 index 0000000..532902d --- /dev/null +++ b/frontend/src/hooks/useMediaQuery.ts @@ -0,0 +1,15 @@ +import { useEffect, useState } from 'react' + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(() => window.matchMedia(query).matches) + + useEffect(() => { + const mql = window.matchMedia(query) + const handler = () => setMatches(mql.matches) + mql.addEventListener('change', handler) + setMatches(mql.matches) + return () => mql.removeEventListener('change', handler) + }, [query]) + + return matches +} diff --git a/frontend/src/index.css b/frontend/src/index.css index c63561c..5105a92 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,37 +1,112 @@ +/* Tailwind v4 — import theme + utilities only (no preflight) so the new + utility classes coexist with the existing hand-written component CSS during + the incremental migration. */ +@import 'tailwindcss/theme.css' layer(theme); +@import 'tailwindcss/utilities.css' layer(utilities); + +/* Map the design tokens onto Tailwind color utilities (bg-surface, text-accent, + …). `inline` makes utilities reference the live var() so they re-theme at + runtime when [data-theme] flips. */ +@theme inline { + --color-bg: var(--bg); + --color-surface: var(--surface); + --color-surface-2: var(--surface-2); + --color-border: var(--border); + --color-text: var(--text); + --color-muted: var(--text-muted); + --color-accent: var(--accent); + --color-accent-hover: var(--accent-hover); + --color-success: var(--success); + --color-error: var(--error); + --color-warning: var(--warning); + --font-sans: 'Inter', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; + --font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace; +} + * { box-sizing: border-box; margin: 0; padding: 0; } -:root { - --bg: #1a1614; - --surface: #241f1c; - --border: #3a322d; - --text: #f0e6dc; - --text-muted: #a89b8c; - --accent: #d97757; - --accent-hover: #c2613f; - --success: #7a9b6e; - --error: #c25450; - --warning: #e6b450; - --node-bg: #241f1c; - --node-border: #d97757; - /* Terracotta-derived translucent layers — replace stale rgba(88,166,255,*) - blue literals. Soft = hover, active = pressed/selected. */ - --accent-soft: rgba(217, 119, 87, 0.08); - --accent-active: rgba(217, 119, 87, 0.12); +:root, +[data-theme='dark'] { + --bg: #09090b; + --surface: #121214; + --surface-2: #1a1a1e; + --border: #27272a; + --text: #fafafa; + --text-muted: #a1a1aa; + --accent: #6366f1; + --accent-hover: #4f46e5; + --accent-fg: #ffffff; + --success: #22c55e; + --error: #f87171; + --warning: #fbbf24; + --node-bg: #1a1a1e; + --node-border: #6366f1; + --accent-soft: rgba(99, 102, 241, 0.12); + --accent-active: rgba(99, 102, 241, 0.2); + --ring: rgba(99, 102, 241, 0.35); + --shadow: 0 12px 40px rgba(0, 0, 0, 0.5); + color-scheme: dark; +} + +[data-theme='light'] { + --bg: #fbfbfc; + --surface: #ffffff; + --surface-2: #f4f4f5; + --border: #e4e4e7; + --text: #18181b; + --text-muted: #71717a; + --accent: #4f46e5; + --accent-hover: #4338ca; + --accent-fg: #ffffff; + --success: #16a34a; + --error: #dc2626; + --warning: #d97706; + --node-bg: #ffffff; + --node-border: #4f46e5; + --accent-soft: rgba(79, 70, 229, 0.08); + --accent-active: rgba(79, 70, 229, 0.14); + --ring: rgba(79, 70, 229, 0.3); + --shadow: 0 12px 40px rgba(24, 24, 27, 0.12); + color-scheme: light; +} + +html, body, #root { + height: 100%; } body { background: var(--bg); color: var(--text); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; + font-family: var(--font-sans); font-size: 14px; - height: 100vh; overflow: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.01em; +} + +::-webkit-scrollbar { width: 10px; height: 10px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 6px; + border: 2px solid var(--bg); +} +::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } + +button, input, textarea { font-family: inherit; } + +:focus-visible { + outline: 2px solid var(--ring); + outline-offset: 2px; } +button:not(:disabled) { transition: background 0.15s, border-color 0.15s, color 0.15s, transform 0.1s; } + /* ── Topbar (removed — switcher lives in chat header) ── */ /* ── Repo Switcher ── */ @@ -73,8 +148,9 @@ body { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; - box-shadow: 0 8px 32px rgba(0,0,0,0.5); + box-shadow: var(--shadow); overflow: hidden; + z-index: 50; } .repo-list { @@ -97,7 +173,23 @@ body { .repo-item:hover { background: var(--accent-soft); } .repo-item.active { background: var(--accent-active); } -.repo-item-dot { font-size: 10px; flex-shrink: 0; } +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; +} +.status-dot.ready { background: var(--success); box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 22%, transparent); } +.status-dot.failed { background: var(--error); box-shadow: 0 0 0 3px color-mix(in srgb, var(--error) 22%, transparent); } +.status-dot.pending { + background: var(--warning); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--warning) 22%, transparent); + animation: dot-pulse 1.4s ease-in-out infinite; +} +@keyframes dot-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} .repo-item-name { flex: 1; @@ -131,12 +223,12 @@ body { min-width: 0; } -.repo-new-form input:focus { border-color: var(--accent); } +.repo-new-form input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } .repo-new-form button { padding: 7px 12px; background: var(--accent); - color: var(--bg); + color: var(--accent-fg); border: none; border-radius: 6px; font-size: 12px; @@ -159,22 +251,67 @@ body { flex-direction: column; align-items: center; justify-content: center; - height: 100vh; - gap: 24px; + min-height: 100%; + gap: 22px; padding: 24px; + overflow: hidden; } +.landing::before { + content: ''; + position: absolute; + inset: 0; + pointer-events: none; + background: + radial-gradient(48% 42% at 50% 32%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 72%), + radial-gradient(36% 30% at 78% 78%, color-mix(in srgb, #22d3ee 8%, transparent), transparent 70%); +} + +.landing > * { position: relative; z-index: 1; } + .landing h1 { - font-size: 2.2rem; + font-size: clamp(2.1rem, 5vw, 2.9rem); font-weight: 700; - background: linear-gradient(135deg, var(--accent), var(--warning)); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + letter-spacing: -0.045em; + color: var(--text); + line-height: 1; } +.landing h1 span { color: var(--accent); } + .landing p { color: var(--text-muted); font-size: 1rem; + max-width: 480px; + text-align: center; + line-height: 1.55; +} + +.hero-logo { + display: grid; + place-items: center; + width: 52px; + height: 52px; + border-radius: 14px; + color: var(--accent); + background: var(--accent-soft); + border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent); + margin-bottom: 2px; +} + +.btn-github { + display: inline-flex; + align-items: center; + gap: 9px; + padding: 12px 22px; + font-size: 14px; + border-radius: 10px; +} + +.hero-note { + font-size: 12.5px; + color: var(--text-muted); + margin-top: 6px; } .url-form { @@ -198,24 +335,30 @@ body { .url-form input:focus { border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); } .btn-primary { padding: 12px 24px; background: var(--accent); - color: var(--bg); + color: var(--accent-fg); border: none; border-radius: 8px; font-weight: 600; cursor: pointer; - transition: background 0.2s; + transition: background 0.18s, transform 0.1s, box-shadow 0.18s; white-space: nowrap; + box-shadow: 0 1px 2px rgba(0,0,0,0.15); } .btn-primary:hover:not(:disabled) { background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 14px var(--accent-soft); } +.btn-primary:active:not(:disabled) { transform: translateY(0); box-shadow: 0 1px 2px rgba(0,0,0,0.15); } + .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; @@ -236,6 +379,33 @@ body { color: var(--text-muted); } +.processing-failed { + display: flex; + flex-direction: column; + align-items: center; + gap: 14px; + max-width: 560px; + text-align: center; +} +.processing-error { + color: var(--error); + font-size: 13px; + line-height: 1.5; +} +.btn-retry { + padding: 9px 18px; + background: var(--accent); + color: var(--accent-fg); + border: none; + border-radius: 8px; + font-weight: 600; + font-size: 13px; + cursor: pointer; + transition: background 0.2s; +} +.btn-retry:hover:not(:disabled) { background: var(--accent-hover); } +.btn-retry:disabled { opacity: 0.5; cursor: not-allowed; } + .steps { display: flex; flex-direction: column; @@ -331,21 +501,28 @@ body { } .sidebar-header { - padding: 12px 16px; + padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; + gap: 8px; + min-height: 44px; } .sidebar-header h3 { - font-size: 12px; + font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } +.btn-reanalyze { flex-shrink: 0; white-space: nowrap; } + .btn-reanalyze { padding: 4px 10px; background: transparent; @@ -386,8 +563,10 @@ body { } .tree-node.selected { - background: rgba(217, 119, 87, 0.15); + background: var(--accent-active); color: var(--accent); + font-weight: 500; + box-shadow: inset 2px 0 0 var(--accent); } .tree-node .indent { @@ -430,7 +609,7 @@ body { .fn-item .fn-name { color: var(--accent); - font-family: monospace; + font-family: var(--font-mono); font-size: 12px; } @@ -472,21 +651,41 @@ body { padding: 0 6px; } +.graph-empty { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 6px; + pointer-events: none; + color: var(--text-muted); +} +.graph-empty-glyph { + font-size: 32px; + opacity: 0.35; + margin-bottom: 4px; +} +.graph-empty p { font-size: 14px; color: var(--text); font-weight: 500; } +.graph-empty span { font-size: 12px; } + .btn-mode { - padding: 6px 12px; - background: var(--surface); + padding: 6px 13px; + background: var(--surface-2); border: 1px solid var(--border); - border-radius: 6px; + border-radius: 7px; color: var(--text-muted); font-size: 12px; + font-weight: 500; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s, border-color 0.15s, color 0.15s; } .btn-mode.active { - border-color: var(--accent); + border-color: color-mix(in srgb, var(--accent) 50%, transparent); color: var(--accent); - background: rgba(217, 119, 87, 0.1); + background: var(--accent-soft); } .btn-mode:hover:not(.active) { @@ -516,7 +715,7 @@ body { } .inspector-name { - font-family: monospace; + font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--accent); @@ -550,7 +749,7 @@ body { .inspector-row code { color: var(--text); - font-family: monospace; + font-family: var(--font-mono); font-size: 11px; word-break: break-all; } @@ -588,7 +787,7 @@ body { .dep-badge.called-by { color: var(--success); } .dep-name { - font-family: monospace; + font-family: var(--font-mono); color: var(--text); flex: 1; overflow: hidden; @@ -736,8 +935,8 @@ body { } .msg-user .msg-bubble { - background: rgba(217, 119, 87, 0.15); - border: 1px solid rgba(217, 119, 87, 0.3); + background: color-mix(in srgb, var(--accent) 16%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 32%, transparent); color: var(--text); } @@ -757,17 +956,17 @@ body { .fn-tag { padding: 2px 8px; - background: rgba(217, 119, 87, 0.1); - border: 1px solid rgba(217, 119, 87, 0.2); + background: color-mix(in srgb, var(--accent) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--accent) 22%, transparent); border-radius: 4px; font-size: 11px; - font-family: monospace; + font-family: var(--font-mono); color: var(--accent); cursor: pointer; } .fn-tag:hover { - background: rgba(217, 119, 87, 0.2); + background: color-mix(in srgb, var(--accent) 22%, transparent); } .chat-input-row { @@ -786,35 +985,112 @@ body { color: var(--text); font-size: 13px; outline: none; - transition: border-color 0.2s; + transition: border-color 0.15s, box-shadow 0.15s; } .chat-input-row input:focus { border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); } .btn-send { padding: 10px 16px; background: var(--accent); - color: var(--bg); + color: var(--accent-fg); border: none; border-radius: 8px; font-weight: 600; cursor: pointer; font-size: 13px; - transition: background 0.2s; + transition: background 0.18s, transform 0.1s; } .btn-send:hover:not(:disabled) { background: var(--accent-hover); + transform: translateY(-1px); } +.btn-send:active:not(:disabled) { transform: translateY(0); } .btn-send:disabled { opacity: 0.5; cursor: not-allowed; } - +/* ── Markdown in chat ── */ +.md { white-space: normal; } +.md > :first-child { margin-top: 0; } +.md > :last-child { margin-bottom: 0; } +.md p { margin: 0 0 8px; } +.md ul, .md ol { margin: 0 0 8px; padding-left: 18px; } +.md li { margin: 2px 0; } +.md code { + font-family: var(--font-mono); + font-size: 12px; + background: color-mix(in srgb, var(--accent) 12%, transparent); + padding: 1px 5px; + border-radius: 4px; +} +.md pre { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 10px 12px; + overflow-x: auto; + margin: 0 0 8px; +} +.md pre code { background: none; padding: 0; } +.md a { color: var(--accent); } +.md h1, .md h2, .md h3 { font-size: 14px; margin: 10px 0 6px; } +.md table { border-collapse: collapse; margin: 0 0 8px; } +.md th, .md td { border: 1px solid var(--border); padding: 4px 8px; font-size: 12px; } + +.msg-pending { color: var(--text-muted); } + +/* Citations are now