diff --git a/.github/workflows/deploy-production.yml b/.github/workflows/deploy-production.yml new file mode 100644 index 0000000..7c58e8f --- /dev/null +++ b/.github/workflows/deploy-production.yml @@ -0,0 +1,392 @@ +# ============================================================================= +# LARUN MVP - Production Deployment Workflow +# ============================================================================= +# Deploys to production on version tags (v*.*.*) +# Requires manual approval for safety +# ============================================================================= + +name: Deploy to Production + +on: + push: + tags: + - 'v*.*.*' + + workflow_dispatch: + inputs: + version: + description: 'Version tag to deploy (e.g., v1.0.0)' + required: true + type: string + confirm_production: + description: 'Type "deploy-production" to confirm' + required: true + type: string + +env: + REGISTRY: ghcr.io + API_IMAGE: ghcr.io/${{ github.repository }}/larun-api + WEB_IMAGE: ghcr.io/${{ github.repository }}/larun-web + +jobs: + # =========================================================================== + # Validate Deployment + # =========================================================================== + validate: + name: Validate Deployment + runs-on: ubuntu-latest + + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + - name: Validate manual trigger + if: github.event_name == 'workflow_dispatch' + run: | + if [ "${{ github.event.inputs.confirm_production }}" != "deploy-production" ]; then + echo "::error::Production deployment not confirmed. Please type 'deploy-production' to confirm." + exit 1 + fi + + - name: Get version + id: version + run: | + if [ "${{ github.event_name }}" == "push" ]; then + VERSION=${GITHUB_REF#refs/tags/} + else + VERSION=${{ github.event.inputs.version }} + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Deploying version: $VERSION" + + # =========================================================================== + # Run Full Test Suite + # =========================================================================== + test: + name: Run Full Tests + runs-on: ubuntu-latest + needs: [validate] + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: larun + POSTGRES_PASSWORD: password + POSTGRES_DB: larun_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate.outputs.version }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run API tests + env: + DATABASE_URL: postgresql://larun:password@localhost:5432/larun_test + REDIS_URL: redis://localhost:6379 + JWT_SECRET: test-jwt-secret-min-32-characters + run: | + pytest tests/ -v --tb=short --cov=api --cov-report=xml + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: Install web dependencies + working-directory: web + run: npm ci + + - name: Run web tests + working-directory: web + run: npm test -- --passWithNoTests --coverage + + - name: Run web build + working-directory: web + run: npm run build + env: + NEXT_PUBLIC_API_URL: https://api.larun.ai + + # =========================================================================== + # Build and Push Production Images + # =========================================================================== + build: + name: Build Production Images + runs-on: ubuntu-latest + needs: [validate, test] + + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate.outputs.version }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for API + id: meta-api + uses: docker/metadata-action@v5 + with: + images: ${{ env.API_IMAGE }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=production + type=raw,value=latest + + - name: Build and push API image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.api + target: runtime + push: true + tags: ${{ steps.meta-api.outputs.tags }} + labels: ${{ steps.meta-api.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Extract metadata for Web + id: meta-web + uses: docker/metadata-action@v5 + with: + images: ${{ env.WEB_IMAGE }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=production + type=raw,value=latest + + - name: Build and push Web image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.web + target: runner + push: true + tags: ${{ steps.meta-web.outputs.tags }} + labels: ${{ steps.meta-web.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + NEXT_PUBLIC_API_URL=${{ secrets.PRODUCTION_API_URL }} + + # =========================================================================== + # Deploy to Production (Requires Approval) + # =========================================================================== + deploy: + name: Deploy to Production + runs-on: ubuntu-latest + needs: [validate, build] + environment: + name: production + url: https://larun.ai + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate.outputs.version }} + + # Option A: Deploy to Railway + # - name: Deploy to Railway + # uses: railwayapp/railway-github-action@v1 + # with: + # service: larun-production + # env: + # RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN_PROD }} + + # Option B: Deploy to Fly.io + # - name: Deploy to Fly.io + # uses: superfly/flyctl-actions/setup-flyctl@master + # - run: flyctl deploy --remote-only --app larun-production + # env: + # FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN_PROD }} + + # Option C: Deploy to AWS/GCP/Azure + # (Configure based on your cloud provider) + + # Placeholder deployment step + - name: Deploy notification + run: | + echo "==========================================" + echo " PRODUCTION DEPLOYMENT READY" + echo "==========================================" + echo "" + echo "Version: ${{ needs.validate.outputs.version }}" + echo "" + echo "Docker images built and pushed:" + echo " - API: ${{ env.API_IMAGE }}:${{ needs.validate.outputs.version }}" + echo " - Web: ${{ env.WEB_IMAGE }}:${{ needs.validate.outputs.version }}" + echo "" + echo "Configure your deployment platform by uncommenting" + echo "the appropriate deployment step above." + + # =========================================================================== + # Post-Deployment Verification + # =========================================================================== + verify: + name: Verify Deployment + runs-on: ubuntu-latest + needs: [deploy] + + steps: + - name: Wait for deployment to stabilize + run: sleep 60 + + - name: Health check - API + run: | + for i in {1..10}; do + if curl -sf https://api.larun.ai/health; then + echo "API health check passed" + exit 0 + fi + echo "Waiting for API... ($i/10)" + sleep 10 + done + echo "::warning::API health check failed after deployment" + + - name: Health check - Web + run: | + for i in {1..10}; do + if curl -sf https://larun.ai; then + echo "Web health check passed" + exit 0 + fi + echo "Waiting for Web... ($i/10)" + sleep 10 + done + echo "::warning::Web health check failed after deployment" + + # =========================================================================== + # Create GitHub Release + # =========================================================================== + release: + name: Create Release + runs-on: ubuntu-latest + needs: [validate, verify] + if: startsWith(github.ref, 'refs/tags/') + + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ needs.validate.outputs.version }} + + - name: Generate changelog + id: changelog + run: | + # Get previous tag + PREV_TAG=$(git describe --abbrev=0 --tags HEAD^ 2>/dev/null || echo "") + if [ -n "$PREV_TAG" ]; then + CHANGELOG=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges) + else + CHANGELOG="Initial release" + fi + echo "changelog<> $GITHUB_OUTPUT + echo "$CHANGELOG" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + name: LARUN ${{ needs.validate.outputs.version }} + body: | + ## What's Changed + + ${{ steps.changelog.outputs.changelog }} + + ## Docker Images + + - API: `${{ env.API_IMAGE }}:${{ needs.validate.outputs.version }}` + - Web: `${{ env.WEB_IMAGE }}:${{ needs.validate.outputs.version }}` + + ## Deployment Status + + - [x] Tests passed + - [x] Docker images built + - [x] Production deployed + - [x] Health checks verified + draft: false + prerelease: false + + # =========================================================================== + # Notify on Completion + # =========================================================================== + notify: + name: Notify + runs-on: ubuntu-latest + needs: [validate, verify] + if: always() + + steps: + - name: Send Slack notification + if: ${{ secrets.SLACK_WEBHOOK_URL != '' }} + uses: 8398a7/action-slack@v3 + with: + status: ${{ needs.verify.result }} + text: | + Production deployment ${{ needs.verify.result }} + Version: ${{ needs.validate.outputs.version }} + Repository: ${{ github.repository }} + Author: ${{ github.actor }} + webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Summary + run: | + echo "==========================================" + echo " PRODUCTION DEPLOYMENT COMPLETE" + echo "==========================================" + echo "" + echo "Version: ${{ needs.validate.outputs.version }}" + echo "Status: ${{ needs.verify.result }}" + echo "" + echo "URLs:" + echo " - Web: https://larun.ai" + echo " - API: https://api.larun.ai" diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000..b1a6e71 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,276 @@ +# ============================================================================= +# LARUN MVP - Staging Deployment Workflow +# ============================================================================= +# Automatically deploys to staging when PRs are merged to main +# Manual trigger also available for ad-hoc deployments +# ============================================================================= + +name: Deploy to Staging + +on: + push: + branches: + - main + paths: + - 'api/**' + - 'web/**' + - 'src/**' + - 'docker/**' + - '.github/workflows/deploy-staging.yml' + + workflow_dispatch: + inputs: + skip_tests: + description: 'Skip tests before deployment' + required: false + default: 'false' + type: boolean + +env: + REGISTRY: ghcr.io + API_IMAGE: ghcr.io/${{ github.repository }}/larun-api + WEB_IMAGE: ghcr.io/${{ github.repository }}/larun-web + +jobs: + # =========================================================================== + # Run Tests + # =========================================================================== + test: + name: Run Tests + runs-on: ubuntu-latest + if: ${{ github.event.inputs.skip_tests != 'true' }} + + services: + postgres: + image: postgres:15-alpine + env: + POSTGRES_USER: larun + POSTGRES_PASSWORD: password + POSTGRES_DB: larun_test + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + + redis: + image: redis:7-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + + - name: Install dependencies + run: | + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Run API tests + env: + DATABASE_URL: postgresql://larun:password@localhost:5432/larun_test + REDIS_URL: redis://localhost:6379 + JWT_SECRET: test-jwt-secret-min-32-characters + run: | + pytest tests/ -v --tb=short + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package-lock.json + + - name: Install web dependencies + working-directory: web + run: npm ci + + - name: Run web tests + working-directory: web + run: npm test -- --passWithNoTests + + - name: Run web type check + working-directory: web + run: npm run type-check || true # Don't fail on type errors initially + + # =========================================================================== + # Build and Push Docker Images + # =========================================================================== + build: + name: Build Docker Images + runs-on: ubuntu-latest + needs: [test] + if: always() && (needs.test.result == 'success' || needs.test.result == 'skipped') + + permissions: + contents: read + packages: write + + outputs: + api_image: ${{ steps.meta-api.outputs.tags }} + web_image: ${{ steps.meta-web.outputs.tags }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for API + id: meta-api + uses: docker/metadata-action@v5 + with: + images: ${{ env.API_IMAGE }} + tags: | + type=ref,event=branch + type=sha,prefix=staging- + type=raw,value=staging + + - name: Build and push API image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.api + target: runtime + push: true + tags: ${{ steps.meta-api.outputs.tags }} + labels: ${{ steps.meta-api.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Extract metadata for Web + id: meta-web + uses: docker/metadata-action@v5 + with: + images: ${{ env.WEB_IMAGE }} + tags: | + type=ref,event=branch + type=sha,prefix=staging- + type=raw,value=staging + + - name: Build and push Web image + uses: docker/build-push-action@v5 + with: + context: . + file: docker/Dockerfile.web + target: runner + push: true + tags: ${{ steps.meta-web.outputs.tags }} + labels: ${{ steps.meta-web.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + build-args: | + NEXT_PUBLIC_API_URL=${{ secrets.STAGING_API_URL }} + + # =========================================================================== + # Deploy to Staging + # =========================================================================== + deploy: + name: Deploy to Staging + runs-on: ubuntu-latest + needs: [build] + environment: + name: staging + url: https://staging.larun.ai + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Option A: Deploy to Railway + # - name: Deploy to Railway + # uses: railwayapp/railway-github-action@v1 + # with: + # service: larun-staging + # env: + # RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }} + + # Option B: Deploy to Fly.io + # - name: Deploy to Fly.io + # uses: superfly/flyctl-actions/setup-flyctl@master + # - run: flyctl deploy --remote-only --app larun-staging + # env: + # FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + + # Option C: Deploy to Vercel (Web only) + # - name: Deploy Web to Vercel + # uses: amondnet/vercel-action@v25 + # with: + # vercel-token: ${{ secrets.VERCEL_TOKEN }} + # vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} + # vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }} + # working-directory: ./web + + # Placeholder deployment step + - name: Deploy notification + run: | + echo "==========================================" + echo " STAGING DEPLOYMENT READY" + echo "==========================================" + echo "" + echo "Docker images built and pushed:" + echo " - API: ${{ env.API_IMAGE }}:staging" + echo " - Web: ${{ env.WEB_IMAGE }}:staging" + echo "" + echo "Configure your deployment platform (Railway, Fly.io, Vercel)" + echo "by uncommenting the appropriate deployment step above." + echo "" + echo "Required secrets:" + echo " - STAGING_API_URL: URL of your staging API" + echo " - For Railway: RAILWAY_TOKEN" + echo " - For Fly.io: FLY_API_TOKEN" + echo " - For Vercel: VERCEL_TOKEN, VERCEL_ORG_ID, VERCEL_PROJECT_ID" + + # =========================================================================== + # Notify on Completion + # =========================================================================== + notify: + name: Notify + runs-on: ubuntu-latest + needs: [deploy] + if: always() + + steps: + - name: Send Slack notification + if: ${{ secrets.SLACK_WEBHOOK_URL != '' }} + uses: 8398a7/action-slack@v3 + with: + status: ${{ needs.deploy.result }} + text: | + Staging deployment ${{ needs.deploy.result }} + Repository: ${{ github.repository }} + Commit: ${{ github.sha }} + Author: ${{ github.actor }} + webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} + + - name: Create deployment status + run: | + if [ "${{ needs.deploy.result }}" == "success" ]; then + echo "Staging deployment successful!" + echo "URL: https://staging.larun.ai" + else + echo "Staging deployment failed" + exit 1 + fi diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index ae758cc..6106fed 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -47,11 +47,28 @@ jobs: runs-on: ubuntu-latest steps: - name: Check PR description - uses: jadrol/pr-description-checker@v1 + uses: actions/github-script@v7 with: - github-token: ${{ secrets.GITHUB_TOKEN }} - min-length: 50 - exempt-labels: 'skip-description-check,dependencies' + script: | + const pr = context.payload.pull_request; + const body = pr.body || ''; + const minLength = 50; + + // Check for exempt labels + const exemptLabels = ['skip-description-check', 'dependencies']; + const prLabels = pr.labels.map(l => l.name); + const isExempt = exemptLabels.some(label => prLabels.includes(label)); + + if (isExempt) { + console.log('PR is exempt from description check'); + return; + } + + if (body.length < minLength) { + core.setFailed(`PR description must be at least ${minLength} characters. Current: ${body.length}`); + } else { + console.log(`PR description length: ${body.length} characters ✓`); + } conventional-commits: name: Conventional Commits Check diff --git a/PARALLEL_IMPLEMENTATION_PLAN.md b/PARALLEL_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..6d6682f --- /dev/null +++ b/PARALLEL_IMPLEMENTATION_PLAN.md @@ -0,0 +1,832 @@ +# LARUN.SPACE MVP - Parallel Implementation Plan +## 4-Agent Concurrent Development Strategy + +**Document:** PARALLEL-MVP-2026-001 +**Version:** 1.0 +**Date:** February 2, 2026 +**Target:** 4-week accelerated MVP delivery + +--- + +``` +╔══════════════════════════════════════════════════════════════════════════════╗ +║ 4-AGENT PARALLEL DEVELOPMENT ║ +╠══════════════════════════════════════════════════════════════════════════════╣ +║ ║ +║ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ║ +║ │ ALPHA │ │ BETA │ │ GAMMA │ │ DELTA │ ║ +║ │ Detection │ │ Backend │ │ Frontend │ │ Platform │ ║ +║ │ Engine │ │ API │ │ UI │ │ DevOps │ ║ +║ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ ║ +║ │ │ │ │ ║ +║ └────────────────┴────────────────┴────────────────┘ ║ +║ │ ║ +║ ┌───────┴───────┐ ║ +║ │ COORDINATION │ ║ +║ │ LAYER │ ║ +║ └───────────────┘ ║ +║ ║ +╚══════════════════════════════════════════════════════════════════════════════╝ +``` + +--- + +## 1. AGENT ASSIGNMENTS + +### Agent ALPHA - Detection Engine +``` +Focus: Core astronomical analysis pipeline +Owner: Claude Agent 1 +Branch: claude/mvp-alpha-detection + +Responsibilities: +├── Transit detection model optimization +├── BLS periodogram enhancements +├── Phase folding accuracy +├── Vetting suite refinement +└── Detection API service layer + +Files Owned (exclusive write access): +├── src/skills/periodogram.py +├── src/skills/vetting.py +├── src/skills/transit_fitting.py (new) +├── src/detection/ +│ ├── __init__.py +│ ├── detector.py +│ ├── bls_engine.py +│ └── phase_folder.py +├── src/services/detection_service.py (new) +└── tests/test_detection/ + +Interface Contract: +- Exposes: DetectionService class with analyze() method +- Input: TIC ID or light curve data +- Output: DetectionResult dataclass +``` + +### Agent BETA - Backend API +``` +Focus: REST API, database, job processing +Owner: Claude Agent 2 +Branch: claude/mvp-beta-backend + +Responsibilities: +├── FastAPI endpoint implementation +├── Database schema and migrations +├── Background job queue +├── User management APIs +└── Analysis result storage + +Files Owned (exclusive write access): +├── src/api/ +│ ├── __init__.py +│ ├── main.py +│ ├── routes/ +│ │ ├── auth.py +│ │ ├── analysis.py +│ │ ├── user.py +│ │ └── subscription.py +│ ├── models/ +│ │ ├── database.py +│ │ ├── user.py +│ │ ├── analysis.py +│ │ └── subscription.py +│ └── services/ +│ ├── job_queue.py +│ └── email_service.py +├── alembic/ (migrations) +└── tests/test_api/ + +Interface Contract: +- Exposes: REST API at /api/v1/* +- Consumes: DetectionService from ALPHA +- Database: PostgreSQL with defined schema +``` + +### Agent GAMMA - Frontend UI +``` +Focus: Web interface, visualizations, UX +Owner: Claude Agent 3 +Branch: claude/mvp-gamma-frontend + +Responsibilities: +├── Next.js application setup +├── Landing page +├── Analysis interface +├── Results visualization (Plotly) +├── User dashboard +└── Responsive design + +Files Owned (exclusive write access): +├── web/ +│ ├── package.json +│ ├── next.config.js +│ ├── tailwind.config.js +│ ├── src/ +│ │ ├── app/ +│ │ │ ├── page.tsx (landing) +│ │ │ ├── analyze/ +│ │ │ ├── dashboard/ +│ │ │ ├── auth/ +│ │ │ └── layout.tsx +│ │ ├── components/ +│ │ │ ├── LightCurvePlot.tsx +│ │ │ ├── PeriodogramPlot.tsx +│ │ │ ├── VettingResults.tsx +│ │ │ └── AnalysisForm.tsx +│ │ ├── lib/ +│ │ │ └── api-client.ts +│ │ └── styles/ +│ └── public/ +└── tests/test_frontend/ + +Interface Contract: +- Consumes: REST API from BETA +- API Client: Typed fetch wrapper +- State: React Query for server state +``` + +### Agent DELTA - Platform & DevOps +``` +Focus: Authentication, payments, infrastructure +Owner: Claude Agent 4 +Branch: claude/mvp-delta-platform + +Responsibilities: +├── Stripe integration +├── NextAuth.js setup +├── Docker configuration +├── CI/CD pipelines +├── Environment configuration +└── Deployment scripts + +Files Owned (exclusive write access): +├── web/src/app/api/auth/ (NextAuth) +├── web/src/app/api/stripe/ +├── web/src/lib/ +│ ├── auth.ts +│ ├── stripe.ts +│ └── config.ts +├── docker/ +│ ├── Dockerfile.api +│ ├── Dockerfile.web +│ └── docker-compose.yml +├── .github/workflows/ +│ ├── deploy.yml +│ └── test.yml +├── infrastructure/ +│ ├── terraform/ (optional) +│ └── scripts/ +└── .env.example + +Interface Contract: +- Provides: Auth middleware for API +- Provides: Stripe webhook handlers +- Provides: Deployment configuration +``` + +--- + +## 2. COORDINATION SYSTEM + +### 2.1 Directory Structure +``` +.coordination/ +├── STATUS.md # Real-time status of all agents +├── INTERFACES.md # API contracts between components +├── FILE_LOCKS.md # Current file ownership +├── HANDOFF_QUEUE.md # Tasks waiting for dependencies +├── INTEGRATION_LOG.md # Integration test results +└── agents/ + ├── ALPHA_WORKLOG.md # Agent Alpha's progress + ├── BETA_WORKLOG.md # Agent Beta's progress + ├── GAMMA_WORKLOG.md # Agent Gamma's progress + └── DELTA_WORKLOG.md # Agent Delta's progress +``` + +### 2.2 Status Board Format +```markdown +# AGENT STATUS BOARD +Last Updated: [timestamp] + +## Current Sprint: Week 1 - Foundation + +| Agent | Status | Current Task | Blocked By | ETA | +|-------|--------|--------------|------------|-----| +| ALPHA | 🟢 Active | BLS optimization | - | 2h | +| BETA | 🟡 Waiting | DB schema | DELTA (env) | 4h | +| GAMMA | 🟢 Active | Landing page | - | 3h | +| DELTA | 🟢 Active | Docker setup | - | 1h | + +## Blockers +- [ ] BETA waiting for DATABASE_URL from DELTA + +## Today's Integration Points +- [ ] 14:00 - ALPHA provides DetectionService interface +- [ ] 16:00 - DELTA provides auth middleware +- [ ] 18:00 - Integration test: BETA + ALPHA +``` + +### 2.3 Interface Contract Template +```markdown +# Interface: DetectionService + +Provider: ALPHA +Consumers: BETA + +## Python Interface +```python +from dataclasses import dataclass +from typing import List, Optional +import numpy as np + +@dataclass +class DetectionResult: + tic_id: str + detection: bool + confidence: float # 0.0 - 1.0 + period_days: Optional[float] + depth_ppm: Optional[float] + duration_hours: Optional[float] + epoch_btjd: Optional[float] + snr: Optional[float] + vetting: VettingResult + phase_folded: PhaseFoldedData + raw_lightcurve: LightCurveData + +@dataclass +class VettingResult: + disposition: str # "PLANET_CANDIDATE" | "LIKELY_FALSE_POSITIVE" | "INCONCLUSIVE" + confidence: float + odd_even: TestResult + v_shape: TestResult + secondary: TestResult + +class DetectionService: + async def analyze(self, tic_id: str) -> DetectionResult: + """Main entry point for transit analysis.""" + pass + + async def analyze_lightcurve( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None + ) -> DetectionResult: + """Analyze provided light curve data.""" + pass +``` + +Status: 🟡 Draft | 🟢 Approved | 🔵 Implemented +Current: 🟡 Draft +``` + +--- + +## 3. WEEKLY SPRINT PLAN + +### Week 1: Foundation (Days 1-7) + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ WEEK 1: PARALLEL FOUNDATION WORK │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ALPHA (Detection) BETA (Backend) │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Day 1-2: │ │ Day 1-2: │ │ +│ │ - Refactor BLS │ │ - DB schema │ │ +│ │ - DetectionSvc │ │ - Alembic setup │ │ +│ │ interface │ │ - Base models │ │ +│ ├──────────────────┤ ├──────────────────┤ │ +│ │ Day 3-4: │ │ Day 3-4: │ │ +│ │ - Phase folding │ │ - Auth endpoints │ │ +│ │ - Vetting tests │ │ - User CRUD │ │ +│ ├──────────────────┤ ├──────────────────┤ │ +│ │ Day 5-7: │ │ Day 5-7: │ │ +│ │ - Unit tests │ │ - Analysis API │ │ +│ │ - Integration │ ──► │ - Job queue │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ +│ GAMMA (Frontend) DELTA (Platform) │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Day 1-2: │ │ Day 1-2: │ │ +│ │ - Next.js setup │ │ - Docker configs │ │ +│ │ - Tailwind │ │ - .env setup │ │ +│ │ - Component lib │ │ - CI pipeline │ │ +│ ├──────────────────┤ ├──────────────────┤ │ +│ │ Day 3-4: │ │ Day 3-4: │ │ +│ │ - Landing page │ │ - NextAuth.js │ │ +│ │ - Auth UI │ ◄── │ - Auth config │ │ +│ ├──────────────────┤ ├──────────────────┤ │ +│ │ Day 5-7: │ │ Day 5-7: │ │ +│ │ - Analysis form │ │ - Stripe setup │ │ +│ │ - Mock results │ │ - Webhooks │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ +│ INTEGRATION CHECKPOINT: Day 7 │ +│ - ALPHA DetectionService callable from BETA │ +│ - DELTA auth working with GAMMA │ +│ - All agents can run locally with docker-compose │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +#### Week 1 Deliverables by Agent + +**ALPHA - Detection Engine** +``` +□ Day 1: Create src/detection/ module structure +□ Day 1: Define DetectionResult, VettingResult dataclasses +□ Day 2: Refactor BLS from skills to detection module +□ Day 2: Create DetectionService class with interface +□ Day 3: Implement phase_fold() with sub-second accuracy +□ Day 3: Refactor vetting tests to new structure +□ Day 4: Add comprehensive logging +□ Day 4: Create detection CLI for testing +□ Day 5: Write unit tests (>80% coverage) +□ Day 6: Integration tests with sample TIC IDs +□ Day 7: Documentation and interface finalization +``` + +**BETA - Backend API** +``` +□ Day 1: Create src/api/ module structure +□ Day 1: Set up SQLAlchemy + Alembic +□ Day 2: Define database models (User, Analysis, Subscription) +□ Day 2: Create initial migration +□ Day 3: Implement /api/auth/* endpoints +□ Day 3: Implement /api/user/* endpoints +□ Day 4: Implement /api/analyze endpoint (stub) +□ Day 4: Set up Redis + job queue +□ Day 5: Connect to ALPHA's DetectionService +□ Day 6: Implement job status polling +□ Day 7: API documentation (OpenAPI/Swagger) +``` + +**GAMMA - Frontend** +``` +□ Day 1: npx create-next-app with TypeScript + Tailwind +□ Day 1: Set up project structure +□ Day 2: Create component library (Button, Card, Input, etc.) +□ Day 2: Set up API client with types +□ Day 3: Build landing page (hero, features, pricing) +□ Day 3: Build auth pages (login, register, forgot-password) +□ Day 4: Connect auth UI to DELTA's NextAuth +□ Day 4: Build analysis form component +□ Day 5: Build results display (mock data) +□ Day 6: Build light curve visualization (Plotly) +□ Day 7: Responsive testing + polish +``` + +**DELTA - Platform** +``` +□ Day 1: Create docker/ directory structure +□ Day 1: Dockerfile for API (Python) +□ Day 1: docker-compose.yml with all services +□ Day 2: Set up .env.example with all variables +□ Day 2: Create GitHub Actions test workflow +□ Day 3: Implement NextAuth.js configuration +□ Day 3: Set up auth providers (email, optional OAuth) +□ Day 4: Create Stripe products and prices +□ Day 4: Implement checkout session creation +□ Day 5: Implement Stripe webhooks +□ Day 5: Usage limit enforcement logic +□ Day 6: Deployment scripts (Vercel + Railway) +□ Day 7: Production environment setup +``` + +--- + +### Week 2: Integration (Days 8-14) + +``` +┌────────────────────────────────────────────────────────────────────────────┐ +│ WEEK 2: INTEGRATION & FEATURE COMPLETION │ +├────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ALPHA ──────────► BETA ──────────► GAMMA │ +│ Detection API UI │ +│ Service Endpoints Components │ +│ │ +│ Day 8-10: Full Pipeline Integration │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ User enters TIC ID → API queues job → Detection runs → Results show │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Day 11-12: Payment Integration │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ User subscribes → Stripe checkout → Webhook → Account activated │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +│ Day 13-14: End-to-End Testing │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ Complete user journey from signup to analysis to results │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└────────────────────────────────────────────────────────────────────────────┘ +``` + +#### Week 2 Deliverables by Agent + +**ALPHA - Detection Engine** +``` +□ Day 8: Optimize BLS for <30s execution +□ Day 9: Add caching for repeated TIC queries +□ Day 10: Performance benchmarking (20 targets) +□ Day 11: Error handling improvements +□ Day 12: Edge case handling (no data, partial data) +□ Day 13: Load testing support +□ Day 14: Final accuracy validation +``` + +**BETA - Backend API** +``` +□ Day 8: Full integration with DetectionService +□ Day 9: Analysis history endpoints +□ Day 10: Usage tracking implementation +□ Day 11: Subscription status in responses +□ Day 12: Rate limiting implementation +□ Day 13: Error response standardization +□ Day 14: API load testing +``` + +**GAMMA - Frontend** +``` +□ Day 8: Connect analysis form to real API +□ Day 9: Real-time job status polling +□ Day 10: Results page with real data +□ Day 11: Dashboard with analysis history +□ Day 12: Subscription management UI +□ Day 13: Error states and edge cases +□ Day 14: Mobile responsiveness final pass +``` + +**DELTA - Platform** +``` +□ Day 8: Full auth flow testing +□ Day 9: Stripe subscription flow testing +□ Day 10: Usage limit enforcement testing +□ Day 11: Production deployment (staging) +□ Day 12: SSL and domain configuration +□ Day 13: Monitoring setup (Sentry, analytics) +□ Day 14: Security audit checklist +``` + +--- + +### Week 3: Polish & Testing (Days 15-21) + +``` +□ All agents: Bug fixes from integration +□ All agents: Performance optimization +□ ALPHA: Accuracy improvements if needed +□ BETA: Database optimization +□ GAMMA: UI/UX polish +□ DELTA: Security hardening +□ Integration: Full E2E test suite +□ Integration: Load testing (50 concurrent users) +``` + +--- + +### Week 4: Launch Prep (Days 22-28) + +``` +□ Beta testing with 10 users +□ Bug fixes from beta feedback +□ Documentation completion +□ Marketing page content +□ Support email setup +□ Soft launch +□ Monitor and hotfix +``` + +--- + +## 4. COMMUNICATION PROTOCOL + +### 4.1 Handoff Messages + +When an agent completes work that another agent depends on: + +```markdown +## HANDOFF: ALPHA → BETA +Date: 2026-02-03 14:00 UTC +From: Agent ALPHA +To: Agent BETA + +### Completed +- DetectionService class implemented +- All tests passing (47/47) +- Interface matches INTERFACES.md spec + +### Files Changed +- src/detection/service.py (new) +- src/detection/models.py (new) +- tests/test_detection/test_service.py (new) + +### How to Use +```python +from src.detection import DetectionService + +service = DetectionService() +result = await service.analyze("TIC 12345678") +print(result.detection) # True/False +print(result.confidence) # 0.87 +``` + +### Known Issues +- None + +### Next Steps for BETA +- Import DetectionService in analysis endpoint +- Call service.analyze() in job worker + +### Branch +claude/mvp-alpha-detection @ commit abc123 +``` + +### 4.2 Blocking Notifications + +When an agent is blocked: + +```markdown +## BLOCKER: BETA blocked by DELTA +Date: 2026-02-03 10:00 UTC +From: Agent BETA +Blocking Agent: DELTA + +### What I Need +DATABASE_URL environment variable and database credentials + +### Why I'm Blocked +Cannot run migrations or test database models + +### Impact +- 4 tasks blocked +- Estimated delay: 2 hours after resolution + +### Workaround Attempted +- Using SQLite locally (partial success) +- Need PostgreSQL for full compatibility + +### Priority +HIGH - Blocking critical path +``` + +### 4.3 Daily Sync Format + +Each agent updates STATUS.md at start of day: + +```markdown +## ALPHA Daily Update - 2026-02-03 + +### Yesterday +- ✅ Refactored BLS to detection module +- ✅ Created DetectionService interface +- ⚠️ Phase folding 90% complete (edge case found) + +### Today +- [ ] Fix phase folding edge case +- [ ] Complete vetting test refactor +- [ ] Write unit tests + +### Blockers +- None + +### Need from Others +- BETA: Confirmation on DetectionResult schema +- DELTA: None + +### Integration Ready +- DetectionService.analyze() ready for BETA integration +``` + +--- + +## 5. GIT WORKFLOW + +### 5.1 Branch Strategy + +``` +main +├── develop +│ ├── claude/mvp-alpha-detection (Agent ALPHA) +│ ├── claude/mvp-beta-backend (Agent BETA) +│ ├── claude/mvp-gamma-frontend (Agent GAMMA) +│ └── claude/mvp-delta-platform (Agent DELTA) +│ +└── Integration branches (created as needed) + ├── integrate/alpha-beta + ├── integrate/gamma-delta + └── integrate/full-stack +``` + +### 5.2 Merge Rules + +1. **Daily**: Agents push to their own branches +2. **Integration Points**: Create integration branches +3. **End of Week**: Merge integration branches to develop +4. **Launch**: Merge develop to main + +### 5.3 Conflict Resolution + +If two agents need the same file: +1. First agent to claim in FILE_LOCKS.md owns it +2. Second agent creates interface request +3. Owning agent exposes interface +4. Never edit files you don't own + +--- + +## 6. SHARED RESOURCES + +### 6.1 Shared Types (all agents can read) + +``` +shared/ +├── types/ +│ ├── detection.py # DetectionResult, VettingResult +│ ├── user.py # UserProfile, Subscription +│ └── api.py # APIResponse, APIError +└── constants/ + ├── config.py # Shared configuration + └── enums.py # Status enums, etc. +``` + +### 6.2 Shared Dependencies + +``` +# requirements.txt (DELTA maintains) +# All agents use same versions + +numpy==1.24.0 +pandas==2.0.0 +astropy==5.3.0 +lightkurve==2.4.0 +fastapi==0.100.0 +sqlalchemy==2.0.0 +pydantic==2.0.0 +``` + +--- + +## 7. AGENT STARTUP INSTRUCTIONS + +### For Agent ALPHA (Detection) +```markdown +You are Agent ALPHA, responsible for the Detection Engine. + +Your branch: claude/mvp-alpha-detection +Your files: src/detection/*, src/skills/*, tests/test_detection/* + +DO NOT modify files owned by other agents. + +Your first task: +1. Read .coordination/STATUS.md +2. Read .coordination/INTERFACES.md +3. Create your branch from develop +4. Start with Week 1, Day 1 tasks +5. Update .coordination/agents/ALPHA_WORKLOG.md daily + +Interface you must provide: +- DetectionService class with analyze(tic_id) method +- Must return DetectionResult dataclass +- See INTERFACES.md for exact specification + +When complete, create HANDOFF message for BETA. +``` + +### For Agent BETA (Backend) +```markdown +You are Agent BETA, responsible for the Backend API. + +Your branch: claude/mvp-beta-backend +Your files: src/api/*, alembic/*, tests/test_api/* + +DO NOT modify files owned by other agents. + +Your first task: +1. Read .coordination/STATUS.md +2. Read .coordination/INTERFACES.md +3. Create your branch from develop +4. Start with Week 1, Day 1 tasks +5. Update .coordination/agents/BETA_WORKLOG.md daily + +You will consume: +- DetectionService from ALPHA (wait for HANDOFF) +- Auth middleware from DELTA (wait for HANDOFF) + +You will provide: +- REST API endpoints for GAMMA +- See INTERFACES.md for API specification +``` + +### For Agent GAMMA (Frontend) +```markdown +You are Agent GAMMA, responsible for the Frontend UI. + +Your branch: claude/mvp-gamma-frontend +Your files: web/*, tests/test_frontend/* + +DO NOT modify files owned by other agents. + +Your first task: +1. Read .coordination/STATUS.md +2. Read .coordination/INTERFACES.md +3. Create your branch from develop +4. Start with Week 1, Day 1 tasks +5. Update .coordination/agents/GAMMA_WORKLOG.md daily + +You will consume: +- REST API from BETA +- Auth config from DELTA + +Start with mock data, replace with real API when BETA ready. +``` + +### For Agent DELTA (Platform) +```markdown +You are Agent DELTA, responsible for Platform & DevOps. + +Your branch: claude/mvp-delta-platform +Your files: docker/*, infrastructure/*, web/src/lib/auth.ts, + web/src/lib/stripe.ts, .github/workflows/* + +DO NOT modify files owned by other agents. + +Your first task: +1. Read .coordination/STATUS.md +2. Read .coordination/INTERFACES.md +3. Create your branch from develop +4. Start with Week 1, Day 1 tasks +5. Update .coordination/agents/DELTA_WORKLOG.md daily + +You provide to all agents: +- Docker configuration +- Environment variables +- Auth middleware +- Stripe integration +- CI/CD pipelines + +Unblock other agents ASAP - they depend on your infrastructure. +``` + +--- + +## 8. QUICK REFERENCE + +### File Ownership Matrix + +| Directory/File | ALPHA | BETA | GAMMA | DELTA | +|----------------|-------|------|-------|-------| +| src/detection/ | ✅ Write | Read | - | - | +| src/skills/ | ✅ Write | Read | - | - | +| src/api/ | Read | ✅ Write | - | - | +| alembic/ | - | ✅ Write | - | - | +| web/src/app/ | - | - | ✅ Write | - | +| web/src/components/ | - | - | ✅ Write | - | +| web/src/lib/auth.ts | - | - | Read | ✅ Write | +| web/src/lib/stripe.ts | - | - | Read | ✅ Write | +| docker/ | Read | Read | Read | ✅ Write | +| .github/workflows/ | - | - | - | ✅ Write | +| shared/ | Read | Read | Read | ✅ Write | +| .coordination/ | ✅ Write | ✅ Write | ✅ Write | ✅ Write | + +### Integration Checkpoints + +| Day | Checkpoint | Agents | Verification | +|-----|------------|--------|--------------| +| 7 | Detection callable | ALPHA + BETA | Unit test passes | +| 7 | Auth working | DELTA + GAMMA | Login flow works | +| 10 | Full analysis | ALL | TIC → Results | +| 14 | Payment flow | DELTA + GAMMA + BETA | Subscribe works | +| 21 | E2E complete | ALL | Full user journey | + +--- + +## 9. EMERGENCY PROCEDURES + +### If Agent Goes Offline +1. Other agents continue on non-blocked tasks +2. Mark blocked tasks in HANDOFF_QUEUE.md +3. New agent can pick up from worklog + +### If Integration Fails +1. Identify which interface contract broken +2. Both agents review INTERFACES.md +3. Resolve in integration branch +4. Update contract if needed + +### If Behind Schedule +1. Identify critical path items +2. Defer non-essential features +3. Focus all agents on blockers +4. Consider scope reduction + +--- + +*Document: PARALLEL-MVP-2026-001* +*Version: 1.0* +*For: Multi-Agent Claude Development* diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api new file mode 100644 index 0000000..ccf1079 --- /dev/null +++ b/docker/Dockerfile.api @@ -0,0 +1,93 @@ +# ============================================================================= +# LARUN MVP - Python FastAPI Dockerfile +# ============================================================================= +# Multi-stage build for optimized image size +# Supports both CPU and GPU training (GPU requires nvidia-docker) +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: Builder - Install dependencies +# ----------------------------------------------------------------------------- +FROM python:3.11-slim as builder + +WORKDIR /app + +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + curl \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +COPY requirements.txt requirements-dev.txt ./ + +# Create virtual environment and install dependencies +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# ----------------------------------------------------------------------------- +# Stage 2: Runtime - Production image +# ----------------------------------------------------------------------------- +FROM python:3.11-slim as runtime + +WORKDIR /app + +# Install runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + libgomp1 \ + && rm -rf /var/lib/apt/lists/* + +# Copy virtual environment from builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Create non-root user for security +RUN groupadd -r larun && useradd -r -g larun larun + +# Copy application code +COPY --chown=larun:larun api/ ./api/ +COPY --chown=larun:larun src/ ./src/ +COPY --chown=larun:larun larun.py ./ +COPY --chown=larun:larun pyproject.toml ./ + +# Create directories for models and output +RUN mkdir -p /app/models /app/output /app/data && \ + chown -R larun:larun /app/models /app/output /app/data + +# Set environment variables +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PYTHONPATH=/app + +# Switch to non-root user +USER larun + +# Expose API port +EXPOSE 8000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD curl -f http://localhost:8000/health || exit 1 + +# Default command - uvicorn with hot reload disabled for production +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] + +# ----------------------------------------------------------------------------- +# Stage 3: Development - With dev dependencies and hot reload +# ----------------------------------------------------------------------------- +FROM runtime as development + +USER root + +# Install development dependencies +RUN pip install --no-cache-dir pytest pytest-cov httpx + +USER larun + +# Enable hot reload for development +CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] diff --git a/docker/Dockerfile.web b/docker/Dockerfile.web new file mode 100644 index 0000000..773d260 --- /dev/null +++ b/docker/Dockerfile.web @@ -0,0 +1,115 @@ +# ============================================================================= +# LARUN MVP - Next.js Web Frontend Dockerfile +# ============================================================================= +# Multi-stage build for optimized production image +# Uses standalone output mode for minimal image size +# ============================================================================= + +# ----------------------------------------------------------------------------- +# Stage 1: Dependencies - Install node modules +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS deps + +WORKDIR /app + +# Install dependencies needed for node-gyp +RUN apk add --no-cache libc6-compat + +# Copy package files +COPY web/package.json web/package-lock.json* ./ + +# Install dependencies +RUN npm ci + +# ----------------------------------------------------------------------------- +# Stage 2: Builder - Build the application +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS builder + +WORKDIR /app + +# Copy dependencies from deps stage +COPY --from=deps /app/node_modules ./node_modules + +# Copy source files +COPY web/ ./ + +# Set environment variables for build +ENV NEXT_TELEMETRY_DISABLED=1 \ + NODE_ENV=production + +# Build the application +RUN npm run build + +# ----------------------------------------------------------------------------- +# Stage 3: Runner - Production image +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS runner + +WORKDIR /app + +# Set environment variables +ENV NODE_ENV=production \ + NEXT_TELEMETRY_DISABLED=1 + +# Create non-root user for security +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy necessary files from builder +COPY --from=builder /app/public ./public + +# Set up standalone output directory +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Switch to non-root user +USER nextjs + +# Expose port +EXPOSE 3000 + +# Set port environment variable +ENV PORT=3000 \ + HOSTNAME="0.0.0.0" + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1 + +# Start the application +CMD ["node", "server.js"] + +# ----------------------------------------------------------------------------- +# Stage 4: Development - With hot reload +# ----------------------------------------------------------------------------- +FROM node:20-alpine AS development + +WORKDIR /app + +# Install dependencies needed for development +RUN apk add --no-cache libc6-compat + +# Create non-root user +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy package files and install dependencies +COPY web/package.json web/package-lock.json* ./ +RUN npm ci + +# Copy source files +COPY --chown=nextjs:nodejs web/ ./ + +# Switch to non-root user +USER nextjs + +# Expose port +EXPOSE 3000 + +# Set environment for development +ENV NODE_ENV=development \ + NEXT_TELEMETRY_DISABLED=1 + +# Start in development mode with hot reload +CMD ["npm", "run", "dev"] diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml new file mode 100644 index 0000000..4af5246 --- /dev/null +++ b/docker/docker-compose.prod.yml @@ -0,0 +1,146 @@ +# ============================================================================= +# LARUN MVP - Docker Compose for Production +# ============================================================================= +# This extends the base docker-compose.yml with production settings +# Usage: docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d +# ============================================================================= + +version: "3.9" + +services: + # =========================================================================== + # PostgreSQL Database - Production Settings + # =========================================================================== + db: + restart: always + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} # Use secret from env + deploy: + resources: + limits: + memory: 1G + reservations: + memory: 512M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # =========================================================================== + # Redis - Production Settings + # =========================================================================== + redis: + restart: always + command: > + redis-server + --appendonly yes + --requirepass ${REDIS_PASSWORD} + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # =========================================================================== + # Python FastAPI Backend - Production Settings + # =========================================================================== + api: + restart: always + build: + context: .. + dockerfile: docker/Dockerfile.api + target: runtime # Use production stage, not development + environment: + - LOG_LEVEL=WARNING + - ENVIRONMENT=production + volumes: [] # Don't mount source code in production + deploy: + resources: + limits: + memory: 2G + reservations: + memory: 1G + logging: + driver: "json-file" + options: + max-size: "50m" + max-file: "5" + + # =========================================================================== + # Next.js Frontend - Production Settings + # =========================================================================== + web: + restart: always + build: + context: .. + dockerfile: docker/Dockerfile.web + target: runner # Use production stage, not development + environment: + - NODE_ENV=production + volumes: [] # Don't mount source code in production + deploy: + resources: + limits: + memory: 512M + reservations: + memory: 256M + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # =========================================================================== + # Nginx Reverse Proxy (Production only) + # =========================================================================== + nginx: + image: nginx:alpine + container_name: larun_nginx + restart: always + ports: + - "80:80" + - "443:443" + volumes: + - ../infrastructure/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ../infrastructure/nginx/ssl:/etc/nginx/ssl:ro + - certbot_data:/var/www/certbot:ro + depends_on: + - api + - web + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # =========================================================================== + # Certbot for SSL certificates (Production only) + # =========================================================================== + certbot: + image: certbot/certbot:latest + container_name: larun_certbot + volumes: + - ../infrastructure/nginx/ssl:/etc/letsencrypt + - certbot_data:/var/www/certbot + entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" + +# ============================================================================= +# Additional Volumes for Production +# ============================================================================= +volumes: + certbot_data: + driver: local + +# ============================================================================= +# Networks +# ============================================================================= +networks: + default: + name: larun_production diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..cfffd00 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,156 @@ +# ============================================================================= +# LARUN MVP - Docker Compose for Local Development +# ============================================================================= +# Usage: +# docker compose up -d # Start all services +# docker compose up -d db redis # Start only database and redis +# docker compose logs -f api # View API logs +# docker compose down # Stop all services +# ============================================================================= + +version: "3.9" + +services: + # =========================================================================== + # PostgreSQL Database + # =========================================================================== + db: + image: postgres:15-alpine + container_name: larun_db + environment: + POSTGRES_USER: larun + POSTGRES_PASSWORD: password + POSTGRES_DB: larun_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init-db:/docker-entrypoint-initdb.d # Optional: init scripts + healthcheck: + test: ["CMD-SHELL", "pg_isready -U larun -d larun_db"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + + # =========================================================================== + # Redis - Job Queue & Caching + # =========================================================================== + redis: + image: redis:7-alpine + container_name: larun_redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + restart: unless-stopped + command: redis-server --appendonly yes + + # =========================================================================== + # Python FastAPI Backend (Agent BETA will implement) + # =========================================================================== + api: + build: + context: .. + dockerfile: docker/Dockerfile.api + container_name: larun_api + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + environment: + - DATABASE_URL=postgresql://larun:password@db:5432/larun_db + - REDIS_URL=redis://redis:6379 + env_file: + - ../.env + ports: + - "8000:8000" + volumes: + # Mount source for hot reload during development + - ../api:/app/api:ro + - ../src:/app/src:ro + - ../models:/app/models + - ../output:/app/output + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] + interval: 30s + timeout: 10s + retries: 3 + + # =========================================================================== + # Next.js Frontend (Agent GAMMA will implement) + # =========================================================================== + web: + build: + context: .. + dockerfile: docker/Dockerfile.web + container_name: larun_web + depends_on: + - api + env_file: + - ../.env + environment: + - NEXT_PUBLIC_API_URL=http://api:8000 + - NEXTAUTH_URL=http://localhost:3000 + ports: + - "3000:3000" + volumes: + # Mount source for hot reload during development + - ../web:/app:ro + - /app/node_modules # Exclude node_modules from mount + - /app/.next # Exclude .next from mount + restart: unless-stopped + + # =========================================================================== + # Redis Commander - Database Admin UI (Development only) + # =========================================================================== + redis-commander: + image: rediscommander/redis-commander:latest + container_name: larun_redis_ui + environment: + - REDIS_HOSTS=local:redis:6379 + ports: + - "8081:8081" + depends_on: + - redis + profiles: + - tools # Only start with: docker compose --profile tools up + + # =========================================================================== + # pgAdmin - PostgreSQL Admin UI (Development only) + # =========================================================================== + pgadmin: + image: dpage/pgadmin4:latest + container_name: larun_pgadmin + environment: + PGADMIN_DEFAULT_EMAIL: admin@larun.local + PGADMIN_DEFAULT_PASSWORD: admin + ports: + - "5050:80" + depends_on: + - db + profiles: + - tools # Only start with: docker compose --profile tools up + +# ============================================================================= +# Named Volumes +# ============================================================================= +volumes: + postgres_data: + driver: local + redis_data: + driver: local + +# ============================================================================= +# Networks (using default bridge network) +# ============================================================================= +networks: + default: + name: larun_network diff --git a/docs/coordination-templates/MVP_INTERFACES.md b/docs/coordination-templates/MVP_INTERFACES.md new file mode 100644 index 0000000..d44d166 --- /dev/null +++ b/docs/coordination-templates/MVP_INTERFACES.md @@ -0,0 +1,593 @@ +# MVP Interface Contracts +**Version:** 1.0 +**Last Updated:** 2026-02-02 + +This document defines the API contracts between agents. All agents MUST adhere to these interfaces. + +--- + +## 1. Detection Service Interface (ALPHA → BETA) + +**Provider:** Agent ALPHA +**Consumer:** Agent BETA +**Status:** 🟡 Draft + +### Python Types + +```python +# File: shared/types/detection.py + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from enum import Enum +import numpy as np + + +class Disposition(str, Enum): + PLANET_CANDIDATE = "PLANET_CANDIDATE" + LIKELY_FALSE_POSITIVE = "LIKELY_FALSE_POSITIVE" + INCONCLUSIVE = "INCONCLUSIVE" + + +class TestFlag(str, Enum): + PASS = "PASS" + WARNING = "WARNING" + FAIL = "FAIL" + + +@dataclass +class TestResult: + """Result of a single vetting test.""" + test_name: str + flag: TestFlag + confidence: float # 0.0 - 1.0 + value: float # The measured value + threshold: float # The threshold for passing + message: str + details: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class VettingResult: + """Combined vetting results.""" + disposition: Disposition + confidence: float # 0.0 - 1.0 + tests_passed: int + tests_failed: int + tests_warning: int + odd_even: TestResult + v_shape: TestResult + secondary_eclipse: TestResult + recommendation: str + + +@dataclass +class LightCurveData: + """Light curve data for visualization.""" + time: List[float] # BJD or BTJD + flux: List[float] # Normalized flux + flux_err: List[float] # Flux errors + quality: List[int] # Quality flags + + +@dataclass +class PhaseFoldedData: + """Phase-folded light curve.""" + phase: List[float] # -0.5 to 0.5 + flux: List[float] + flux_err: List[float] + binned_phase: List[float] # Binned for visualization + binned_flux: List[float] + binned_flux_err: List[float] + + +@dataclass +class PeriodogramData: + """BLS periodogram results.""" + periods: List[float] # Days + powers: List[float] # BLS power + best_period: float + best_power: float + top_periods: List[float] # Top 3 candidates + top_powers: List[float] + + +@dataclass +class DetectionResult: + """Complete detection analysis result.""" + # Target identification + tic_id: str + ra: Optional[float] = None + dec: Optional[float] = None + + # Detection result + detection: bool = False + confidence: float = 0.0 # 0.0 - 1.0 + + # Transit parameters (if detected) + period_days: Optional[float] = None + depth_ppm: Optional[float] = None + duration_hours: Optional[float] = None + epoch_btjd: Optional[float] = None + snr: Optional[float] = None + + # Detailed results + vetting: Optional[VettingResult] = None + periodogram: Optional[PeriodogramData] = None + phase_folded: Optional[PhaseFoldedData] = None + raw_lightcurve: Optional[LightCurveData] = None + + # Metadata + sectors_used: List[int] = field(default_factory=list) + processing_time_seconds: float = 0.0 + error: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + # Implementation here + pass +``` + +### Service Interface + +```python +# File: src/detection/service.py + +from abc import ABC, abstractmethod +from shared.types.detection import DetectionResult +import numpy as np + + +class IDetectionService(ABC): + """Interface for detection service.""" + + @abstractmethod + async def analyze(self, tic_id: str) -> DetectionResult: + """ + Analyze a target by TIC ID. + + Args: + tic_id: TESS Input Catalog ID (e.g., "TIC 12345678" or "12345678") + + Returns: + DetectionResult with all analysis data + + Raises: + TargetNotFoundError: If TIC ID not found in MAST + DataUnavailableError: If no light curve data available + AnalysisError: If analysis fails + """ + pass + + @abstractmethod + async def analyze_lightcurve( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + tic_id: Optional[str] = None + ) -> DetectionResult: + """ + Analyze provided light curve data directly. + + Args: + time: Time array (BJD or BTJD) + flux: Normalized flux array + flux_err: Optional flux error array + tic_id: Optional TIC ID for metadata + + Returns: + DetectionResult with all analysis data + """ + pass + + @abstractmethod + async def get_status(self) -> Dict[str, Any]: + """Get service health status.""" + pass +``` + +### Usage Example + +```python +from src.detection import DetectionService + +# In BETA's analysis endpoint +service = DetectionService() + +# Analyze by TIC ID +result = await service.analyze("TIC 470710327") + +if result.detection: + print(f"Planet candidate found!") + print(f"Period: {result.period_days:.4f} days") + print(f"Confidence: {result.confidence:.1%}") + print(f"Disposition: {result.vetting.disposition}") +else: + print("No transit detected") +``` + +--- + +## 2. REST API Interface (BETA → GAMMA) + +**Provider:** Agent BETA +**Consumer:** Agent GAMMA +**Status:** 🟡 Draft + +### Base URL +``` +Development: http://localhost:8000/api/v1 +Production: https://api.larun.space/api/v1 +``` + +### Authentication Endpoints + +```typescript +// POST /api/v1/auth/register +interface RegisterRequest { + email: string; + password: string; + name?: string; +} + +interface RegisterResponse { + user: { + id: string; + email: string; + name: string; + created_at: string; + }; + message: string; +} + +// POST /api/v1/auth/login +interface LoginRequest { + email: string; + password: string; +} + +interface LoginResponse { + access_token: string; + token_type: "bearer"; + expires_in: number; + user: User; +} + +// POST /api/v1/auth/logout +// Requires: Authorization header +interface LogoutResponse { + message: string; +} + +// POST /api/v1/auth/reset-password +interface ResetPasswordRequest { + email: string; +} + +interface ResetPasswordResponse { + message: string; +} +``` + +### Analysis Endpoints + +```typescript +// POST /api/v1/analyze +// Requires: Authorization header +interface AnalyzeRequest { + tic_id: string; +} + +interface AnalyzeResponse { + analysis_id: string; + status: "pending" | "processing" | "completed" | "failed"; + message: string; +} + +// GET /api/v1/analyze/:id +// Requires: Authorization header +interface AnalysisResult { + id: string; + tic_id: string; + status: "pending" | "processing" | "completed" | "failed"; + created_at: string; + completed_at: string | null; + + // Only present when status === "completed" + result?: { + detection: boolean; + confidence: number; + period_days: number | null; + depth_ppm: number | null; + duration_hours: number | null; + epoch_btjd: number | null; + snr: number | null; + + vetting: { + disposition: "PLANET_CANDIDATE" | "LIKELY_FALSE_POSITIVE" | "INCONCLUSIVE"; + confidence: number; + tests_passed: number; + tests_failed: number; + odd_even: TestResult; + v_shape: TestResult; + secondary_eclipse: TestResult; + recommendation: string; + }; + + periodogram: { + periods: number[]; + powers: number[]; + best_period: number; + }; + + phase_folded: { + phase: number[]; + flux: number[]; + binned_phase: number[]; + binned_flux: number[]; + }; + + raw_lightcurve: { + time: number[]; + flux: number[]; + }; + + sectors_used: number[]; + processing_time_seconds: number; + }; + + // Only present when status === "failed" + error?: string; +} + +// GET /api/v1/analyses +// Requires: Authorization header +interface AnalysesListResponse { + analyses: AnalysisResult[]; + total: number; + page: number; + per_page: number; +} + +// DELETE /api/v1/analyses/:id +// Requires: Authorization header +interface DeleteResponse { + message: string; +} +``` + +### User Endpoints + +```typescript +// GET /api/v1/user/profile +// Requires: Authorization header +interface UserProfile { + id: string; + email: string; + name: string; + created_at: string; + subscription: { + plan: "hobbyist_monthly" | "hobbyist_annual" | null; + status: "active" | "canceled" | "past_due" | null; + current_period_end: string | null; + }; +} + +// GET /api/v1/user/usage +// Requires: Authorization header +interface UsageResponse { + analyses_this_month: number; + analyses_limit: number; + period_start: string; + period_end: string; +} +``` + +### Subscription Endpoints + +```typescript +// POST /api/v1/subscription/create-checkout +// Requires: Authorization header +interface CreateCheckoutRequest { + plan: "hobbyist_monthly" | "hobbyist_annual"; + success_url: string; + cancel_url: string; +} + +interface CreateCheckoutResponse { + checkout_url: string; + session_id: string; +} + +// GET /api/v1/subscription/portal +// Requires: Authorization header +interface PortalResponse { + portal_url: string; +} +``` + +### Error Response Format + +```typescript +interface APIError { + error: { + code: string; + message: string; + details?: Record; + }; +} + +// Common error codes: +// - "unauthorized": Missing or invalid auth token +// - "forbidden": Insufficient permissions +// - "not_found": Resource not found +// - "validation_error": Invalid request data +// - "rate_limited": Too many requests +// - "usage_limit_exceeded": Monthly analysis limit reached +// - "internal_error": Server error +``` + +--- + +## 3. Auth Configuration Interface (DELTA → GAMMA) + +**Provider:** Agent DELTA +**Consumer:** Agent GAMMA +**Status:** 🟡 Draft + +### NextAuth Configuration + +```typescript +// File: web/src/lib/auth.ts (DELTA provides) + +import { NextAuthOptions } from "next-auth"; +import CredentialsProvider from "next-auth/providers/credentials"; + +export const authOptions: NextAuthOptions = { + providers: [ + CredentialsProvider({ + name: "credentials", + credentials: { + email: { label: "Email", type: "email" }, + password: { label: "Password", type: "password" } + }, + async authorize(credentials) { + // Calls BETA's /api/v1/auth/login + // Returns user object or null + } + }) + ], + callbacks: { + async jwt({ token, user }) { /* ... */ }, + async session({ session, token }) { /* ... */ } + }, + pages: { + signIn: "/auth/login", + signOut: "/auth/logout", + error: "/auth/error", + } +}; + +// Session type for GAMMA to use +declare module "next-auth" { + interface Session { + user: { + id: string; + email: string; + name: string; + accessToken: string; + }; + } +} +``` + +### Usage in GAMMA + +```typescript +// In GAMMA's components +import { useSession, signIn, signOut } from "next-auth/react"; + +function AnalyzeButton() { + const { data: session } = useSession(); + + if (!session) { + return ; + } + + return ; +} +``` + +--- + +## 4. Stripe Configuration Interface (DELTA → BETA/GAMMA) + +**Provider:** Agent DELTA +**Consumer:** Agents BETA, GAMMA +**Status:** 🟡 Draft + +### Stripe Product IDs + +```typescript +// File: shared/constants/stripe.ts (DELTA provides) + +export const STRIPE_PRODUCTS = { + HOBBYIST_MONTHLY: { + priceId: process.env.STRIPE_PRICE_HOBBYIST_MONTHLY!, + name: "Hobbyist Monthly", + price: 9, + currency: "usd", + interval: "month", + analysisLimit: 25, + }, + HOBBYIST_ANNUAL: { + priceId: process.env.STRIPE_PRICE_HOBBYIST_ANNUAL!, + name: "Hobbyist Annual", + price: 89, + currency: "usd", + interval: "year", + analysisLimit: 25, // per month + }, +} as const; +``` + +### Webhook Events (DELTA handles, BETA receives) + +```typescript +// Events DELTA forwards to BETA's internal API + +interface SubscriptionActivated { + event: "subscription.activated"; + user_id: string; + stripe_customer_id: string; + stripe_subscription_id: string; + plan: "hobbyist_monthly" | "hobbyist_annual"; + current_period_end: string; +} + +interface SubscriptionCanceled { + event: "subscription.canceled"; + user_id: string; + stripe_subscription_id: string; +} + +interface SubscriptionUpdated { + event: "subscription.updated"; + user_id: string; + stripe_subscription_id: string; + plan: "hobbyist_monthly" | "hobbyist_annual"; + status: "active" | "past_due" | "canceled"; + current_period_end: string; +} +``` + +--- + +## Interface Status Summary + +| Interface | Provider | Consumer | Status | Version | +|-----------|----------|----------|--------|---------| +| DetectionService | ALPHA | BETA | 🟡 Draft | 1.0 | +| REST API | BETA | GAMMA | 🟡 Draft | 1.0 | +| Auth Config | DELTA | GAMMA | 🟡 Draft | 1.0 | +| Stripe Config | DELTA | BETA, GAMMA | 🟡 Draft | 1.0 | + +**Status Legend:** +- 🟡 Draft - Interface defined, not yet implemented +- 🟢 Approved - Interface reviewed and approved +- 🔵 Implemented - Interface implemented by provider +- ✅ Verified - Interface tested by consumer + +--- + +## Change Log + +| Date | Author | Change | +|------|--------|--------| +| 2026-02-02 | System | Initial interface definitions | + +--- + +*All agents must notify others before changing interfaces.* +*Use HANDOFF_NOTES.md to communicate interface changes.* diff --git a/docs/coordination-templates/MVP_STATUS.md b/docs/coordination-templates/MVP_STATUS.md new file mode 100644 index 0000000..63ad617 --- /dev/null +++ b/docs/coordination-templates/MVP_STATUS.md @@ -0,0 +1,129 @@ +# MVP AGENT STATUS BOARD +**Last Updated:** 2026-02-02 16:00 UTC +**Sprint:** Week 1 - Foundation +**Target Launch:** 4 weeks + +--- + +## Agent Status Overview + +| Agent | Role | Status | Branch | Current Task | Blocked By | Progress | +|-------|------|--------|--------|--------------|------------|----------| +| ALPHA | Detection Engine | 🔵 Ready | `claude/mvp-alpha-detection` | Waiting to start | - | 0% | +| BETA | Backend API | 🔵 Ready | `claude/mvp-beta-backend` | Waiting to start | - | 0% | +| GAMMA | Frontend UI | 🔵 Ready | `claude/mvp-gamma-frontend` | Waiting to start | - | 0% | +| DELTA | Platform/DevOps | 🔵 Ready | `claude/mvp-delta-platform` | Waiting to start | - | 0% | + +**Status Legend:** +- 🔵 Ready - Waiting to start +- 🟢 Active - Working on tasks +- 🟡 Waiting - Blocked by dependency +- 🔴 Blocked - Critical issue +- ✅ Complete - Phase finished + +--- + +## Current Blockers + +| ID | Agent | Blocked By | Description | Priority | Resolution | +|----|-------|------------|-------------|----------|------------| +| - | - | - | No blockers | - | - | + +--- + +## Today's Integration Points + +| Time (UTC) | Integration | Agents | Status | +|------------|-------------|--------|--------| +| - | No integrations scheduled | - | - | + +--- + +## Week 1 Progress + +### ALPHA - Detection Engine +``` +Week 1 Tasks (0/11 complete): +□ Create src/detection/ module structure +□ Define DetectionResult, VettingResult dataclasses +□ Refactor BLS from skills to detection module +□ Create DetectionService class with interface +□ Implement phase_fold() with sub-second accuracy +□ Refactor vetting tests to new structure +□ Add comprehensive logging +□ Create detection CLI for testing +□ Write unit tests (>80% coverage) +□ Integration tests with sample TIC IDs +□ Documentation and interface finalization +``` + +### BETA - Backend API +``` +Week 1 Tasks (0/11 complete): +□ Create src/api/ module structure +□ Set up SQLAlchemy + Alembic +□ Define database models (User, Analysis, Subscription) +□ Create initial migration +□ Implement /api/auth/* endpoints +□ Implement /api/user/* endpoints +□ Implement /api/analyze endpoint (stub) +□ Set up Redis + job queue +□ Connect to ALPHA's DetectionService +□ Implement job status polling +□ API documentation (OpenAPI/Swagger) +``` + +### GAMMA - Frontend UI +``` +Week 1 Tasks (0/11 complete): +□ npx create-next-app with TypeScript + Tailwind +□ Set up project structure +□ Create component library (Button, Card, Input, etc.) +□ Set up API client with types +□ Build landing page (hero, features, pricing) +□ Build auth pages (login, register, forgot-password) +□ Connect auth UI to DELTA's NextAuth +□ Build analysis form component +□ Build results display (mock data) +□ Build light curve visualization (Plotly) +□ Responsive testing + polish +``` + +### DELTA - Platform/DevOps +``` +Week 1 Tasks (0/12 complete): +□ Create docker/ directory structure +□ Dockerfile for API (Python) +□ docker-compose.yml with all services +□ Set up .env.example with all variables +□ Create GitHub Actions test workflow +□ Implement NextAuth.js configuration +□ Set up auth providers (email, optional OAuth) +□ Create Stripe products and prices +□ Implement checkout session creation +□ Implement Stripe webhooks +□ Deployment scripts (Vercel + Railway) +□ Production environment setup +``` + +--- + +## Handoff Queue + +| From | To | Item | Status | Date | +|------|----|------|--------|------| +| - | - | No pending handoffs | - | - | + +--- + +## Notes + +- All agents should update this file when changing status +- Use HANDOFF_NOTES.md for detailed handoff information +- Check INTERFACES.md before implementing cross-agent features +- Update FILE_LOCKS.md before modifying shared files + +--- + +*Updated by: System* +*Next sync: When agents start* diff --git a/docs/coordination-templates/agents/ALPHA_WORKLOG.md b/docs/coordination-templates/agents/ALPHA_WORKLOG.md new file mode 100644 index 0000000..d95a17d --- /dev/null +++ b/docs/coordination-templates/agents/ALPHA_WORKLOG.md @@ -0,0 +1,134 @@ +# Agent ALPHA Worklog - Detection Engine + +**Branch:** `claude/mvp-alpha-detection` +**Owner:** Claude Agent 1 +**Status:** 🔵 Ready to Start + +--- + +## My Responsibilities + +- Transit detection model optimization +- BLS periodogram implementation +- Phase folding accuracy +- Vetting suite (odd-even, V-shape, secondary eclipse) +- Detection service layer for BETA + +## My Files (Exclusive Write Access) + +``` +src/detection/ +├── __init__.py +├── service.py # DetectionService class +├── detector.py # Transit detection logic +├── bls_engine.py # BLS periodogram +├── phase_folder.py # Phase folding +└── models.py # Data classes + +src/skills/ +├── periodogram.py # Existing (refactor) +├── vetting.py # Existing (refactor) +└── transit_fitting.py # New + +tests/test_detection/ +├── __init__.py +├── test_service.py +├── test_bls.py +├── test_vetting.py +└── test_phase_fold.py +``` + +--- + +## Daily Log + +### Day 0 - Setup (Date: ______) + +**Status:** Not started + +**Tasks:** +- [ ] Create branch `claude/mvp-alpha-detection` +- [ ] Create `src/detection/` directory structure +- [ ] Review existing `src/skills/periodogram.py` +- [ ] Review existing `src/skills/vetting.py` +- [ ] Read MVP_INTERFACES.md + +**Notes:** +- (Add notes here) + +**Blockers:** +- None + +--- + +### Day 1 (Date: ______) + +**Status:** (🔵 Ready | 🟢 Active | 🟡 Waiting | 🔴 Blocked) + +**Yesterday:** +- (What was completed) + +**Today:** +- [ ] Task 1 +- [ ] Task 2 + +**Blockers:** +- (List any blockers) + +**Handoffs:** +- (List any handoffs to other agents) + +--- + +## Interface I Provide + +```python +# DetectionService - for BETA to consume + +from src.detection import DetectionService + +service = DetectionService() +result = await service.analyze("TIC 12345678") + +# result.detection: bool +# result.confidence: float (0-1) +# result.period_days: float +# result.vetting: VettingResult +# result.phase_folded: PhaseFoldedData +``` + +**Interface Status:** 🟡 Draft → 🟢 Approved → 🔵 Implemented → ✅ Verified + +--- + +## Dependencies I Need + +| From | What | Status | +|------|------|--------| +| - | No external dependencies | ✅ | + +--- + +## My Progress + +| Week | Day | Task | Status | +|------|-----|------|--------| +| 1 | 1 | Create detection module structure | ⬜ | +| 1 | 1 | Define dataclasses | ⬜ | +| 1 | 2 | Refactor BLS | ⬜ | +| 1 | 2 | Create DetectionService interface | ⬜ | +| 1 | 3 | Phase folding accuracy | ⬜ | +| 1 | 3 | Refactor vetting | ⬜ | +| 1 | 4 | Logging | ⬜ | +| 1 | 4 | Detection CLI | ⬜ | +| 1 | 5 | Unit tests | ⬜ | +| 1 | 6 | Integration tests | ⬜ | +| 1 | 7 | Documentation | ⬜ | + +**Legend:** ⬜ Not Started | 🔄 In Progress | ✅ Complete | ⛔ Blocked + +--- + +## Notes + +(Add any notes, decisions, or observations here) diff --git a/docs/coordination-templates/agents/BETA_WORKLOG.md b/docs/coordination-templates/agents/BETA_WORKLOG.md new file mode 100644 index 0000000..9c7fc42 --- /dev/null +++ b/docs/coordination-templates/agents/BETA_WORKLOG.md @@ -0,0 +1,162 @@ +# Agent BETA Worklog - Backend API + +**Branch:** `claude/mvp-beta-backend` +**Owner:** Claude Agent 2 +**Status:** 🔵 Ready to Start + +--- + +## My Responsibilities + +- FastAPI REST API implementation +- Database schema and migrations (PostgreSQL) +- Background job queue (Redis) +- User management APIs +- Analysis result storage +- Integration with ALPHA's DetectionService + +## My Files (Exclusive Write Access) + +``` +src/api/ +├── __init__.py +├── main.py # FastAPI app +├── config.py # API configuration +├── dependencies.py # Dependency injection +├── routes/ +│ ├── __init__.py +│ ├── auth.py # /api/v1/auth/* +│ ├── analysis.py # /api/v1/analyze/* +│ ├── user.py # /api/v1/user/* +│ └── subscription.py # /api/v1/subscription/* +├── models/ +│ ├── __init__.py +│ ├── database.py # SQLAlchemy setup +│ ├── user.py # User model +│ ├── analysis.py # Analysis model +│ └── subscription.py # Subscription model +├── schemas/ +│ ├── __init__.py +│ ├── auth.py # Auth request/response +│ ├── analysis.py # Analysis request/response +│ └── user.py # User request/response +└── services/ + ├── __init__.py + ├── job_queue.py # Background job processing + └── email_service.py # Email notifications + +alembic/ +├── env.py +├── versions/ +│ └── 001_initial.py +└── alembic.ini + +tests/test_api/ +├── __init__.py +├── test_auth.py +├── test_analysis.py +├── test_user.py +└── conftest.py +``` + +--- + +## Daily Log + +### Day 0 - Setup (Date: ______) + +**Status:** Not started + +**Tasks:** +- [ ] Create branch `claude/mvp-beta-backend` +- [ ] Create `src/api/` directory structure +- [ ] Review existing `api.py` +- [ ] Read MVP_INTERFACES.md +- [ ] Wait for DATABASE_URL from DELTA + +**Notes:** +- Need PostgreSQL connection from DELTA + +**Blockers:** +- Waiting for DELTA to provide environment setup + +--- + +### Day 1 (Date: ______) + +**Status:** (🔵 Ready | 🟢 Active | 🟡 Waiting | 🔴 Blocked) + +**Yesterday:** +- (What was completed) + +**Today:** +- [ ] Task 1 +- [ ] Task 2 + +**Blockers:** +- (List any blockers) + +**Handoffs:** +- (List any handoffs to other agents) + +--- + +## Interface I Provide + +```typescript +// REST API for GAMMA to consume + +Base URL: http://localhost:8000/api/v1 + +Endpoints: +- POST /auth/register +- POST /auth/login +- POST /auth/logout +- GET /user/profile +- GET /user/usage +- POST /analyze +- GET /analyze/:id +- GET /analyses +- DELETE /analyses/:id +- POST /subscription/create-checkout +- GET /subscription/portal +``` + +**Interface Status:** 🟡 Draft → 🟢 Approved → 🔵 Implemented → ✅ Verified + +--- + +## Dependencies I Need + +| From | What | Status | +|------|------|--------| +| ALPHA | DetectionService class | 🟡 Waiting | +| DELTA | DATABASE_URL env var | 🟡 Waiting | +| DELTA | Auth middleware config | 🟡 Waiting | +| DELTA | Stripe webhook secret | 🟡 Waiting | + +--- + +## My Progress + +| Week | Day | Task | Status | +|------|-----|------|--------| +| 1 | 1 | Create API module structure | ⬜ | +| 1 | 1 | Set up SQLAlchemy + Alembic | ⬜ | +| 1 | 2 | Define database models | ⬜ | +| 1 | 2 | Create initial migration | ⬜ | +| 1 | 3 | Auth endpoints | ⬜ | +| 1 | 3 | User endpoints | ⬜ | +| 1 | 4 | Analysis endpoint (stub) | ⬜ | +| 1 | 4 | Redis + job queue | ⬜ | +| 1 | 5 | Connect to DetectionService | ⬜ | +| 1 | 6 | Job status polling | ⬜ | +| 1 | 7 | API documentation | ⬜ | + +**Legend:** ⬜ Not Started | 🔄 In Progress | ✅ Complete | ⛔ Blocked + +--- + +## Notes + +(Add any notes, decisions, or observations here) diff --git a/docs/coordination-templates/agents/DELTA_WORKLOG.md b/docs/coordination-templates/agents/DELTA_WORKLOG.md new file mode 100644 index 0000000..2f8caf6 --- /dev/null +++ b/docs/coordination-templates/agents/DELTA_WORKLOG.md @@ -0,0 +1,263 @@ +# Agent DELTA Worklog - Platform & DevOps + +**Branch:** `claude/mvp-delta-platform` +**Owner:** Claude Agent 4 +**Status:** 🔵 Ready to Start + +--- + +## My Responsibilities + +- Docker configuration for all services +- Environment setup and secrets management +- NextAuth.js authentication setup +- Stripe integration (products, checkout, webhooks) +- CI/CD pipelines (GitHub Actions) +- Deployment scripts (Vercel, Railway) +- Security configuration + +## My Files (Exclusive Write Access) + +``` +docker/ +├── Dockerfile.api # Python API container +├── Dockerfile.web # Next.js container +├── docker-compose.yml # Full stack local dev +└── docker-compose.prod.yml # Production config + +web/src/lib/ +├── auth.ts # NextAuth configuration +├── stripe.ts # Stripe client setup +└── config.ts # Environment config + +web/src/app/api/ +├── auth/ +│ └── [...nextauth]/route.ts # NextAuth API route +└── stripe/ + └── webhook/route.ts # Stripe webhook handler + +infrastructure/ +├── scripts/ +│ ├── deploy-api.sh +│ ├── deploy-web.sh +│ └── setup-env.sh +└── terraform/ # Optional IaC + └── main.tf + +.github/workflows/ +├── ci.yml # Existing (enhance) +├── deploy-staging.yml # New +└── deploy-production.yml # New + +.env.example # Template for all env vars +``` + +--- + +## Daily Log + +### Day 0 - Setup (Date: ______) + +**Status:** Not started + +**Tasks:** +- [ ] Create branch `claude/mvp-delta-platform` +- [ ] Create `docker/` directory structure +- [ ] Create `.env.example` with all required variables +- [ ] Read MVP_INTERFACES.md +- [ ] PRIORITY: Unblock other agents with env setup + +**Notes:** +- Other agents depend on my environment setup +- Should complete Docker + .env first to unblock BETA + +**Blockers:** +- None + +--- + +### Day 1 (Date: ______) + +**Status:** (🔵 Ready | 🟢 Active | 🟡 Waiting | 🔴 Blocked) + +**Yesterday:** +- (What was completed) + +**Today:** +- [ ] Task 1 +- [ ] Task 2 + +**Blockers:** +- (List any blockers) + +**Handoffs:** +- (List any handoffs to other agents) + +--- + +## Interfaces I Provide + +### To BETA (Environment) +```bash +# Environment variables BETA needs +DATABASE_URL=postgresql://user:pass@localhost:5432/larun +REDIS_URL=redis://localhost:6379 +JWT_SECRET=your-jwt-secret +STRIPE_WEBHOOK_SECRET=whsec_xxx +``` + +### To GAMMA (Auth Config) +```typescript +// web/src/lib/auth.ts - GAMMA imports this +import { NextAuthOptions } from "next-auth"; + +export const authOptions: NextAuthOptions = { + // Full configuration for NextAuth +}; +``` + +### To GAMMA (Stripe Config) +```typescript +// web/src/lib/stripe.ts - GAMMA imports this +import Stripe from "stripe"; + +export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); + +export const STRIPE_PRODUCTS = { + HOBBYIST_MONTHLY: { priceId: "price_xxx", ... }, + HOBBYIST_ANNUAL: { priceId: "price_xxx", ... }, +}; +``` + +### To ALL (Docker) +```yaml +# docker-compose.yml - All agents use for local dev +services: + api: # Python FastAPI + web: # Next.js + db: # PostgreSQL + redis: # Job queue +``` + +--- + +## Dependencies I Need + +| From | What | Status | +|------|------|--------| +| - | No dependencies | ✅ | + +**Note:** I am the foundation - other agents depend on me! + +--- + +## Agents Waiting on Me + +| Agent | What They Need | Priority | Status | +|-------|----------------|----------|--------| +| BETA | DATABASE_URL, REDIS_URL | HIGH | 🟡 | +| BETA | STRIPE_WEBHOOK_SECRET | MEDIUM | 🟡 | +| GAMMA | auth.ts (NextAuth) | HIGH | 🟡 | +| GAMMA | stripe.ts (Stripe) | MEDIUM | 🟡 | +| ALL | docker-compose.yml | HIGH | 🟡 | + +--- + +## My Progress + +| Week | Day | Task | Status | +|------|-----|------|--------| +| 1 | 1 | Create docker/ structure | ⬜ | +| 1 | 1 | Dockerfile.api (Python) | ⬜ | +| 1 | 1 | docker-compose.yml | ⬜ | +| 1 | 2 | .env.example complete | ⬜ | +| 1 | 2 | GitHub Actions test workflow | ⬜ | +| 1 | 3 | NextAuth.js configuration | ⬜ | +| 1 | 3 | Auth providers setup | ⬜ | +| 1 | 4 | Stripe products (Dashboard) | ⬜ | +| 1 | 4 | Checkout session creation | ⬜ | +| 1 | 5 | Stripe webhooks | ⬜ | +| 1 | 5 | Usage limit logic | ⬜ | +| 1 | 6 | Deployment scripts | ⬜ | +| 1 | 7 | Production environment | ⬜ | + +**Legend:** ⬜ Not Started | 🔄 In Progress | ✅ Complete | ⛔ Blocked + +--- + +## Environment Variables Template + +```bash +# .env.example - Complete list + +# =================== +# DATABASE +# =================== +DATABASE_URL=postgresql://larun:password@localhost:5432/larun_db + +# =================== +# REDIS +# =================== +REDIS_URL=redis://localhost:6379 + +# =================== +# AUTH +# =================== +NEXTAUTH_URL=http://localhost:3000 +NEXTAUTH_SECRET=your-nextauth-secret-min-32-chars +JWT_SECRET=your-jwt-secret-min-32-chars + +# =================== +# STRIPE +# =================== +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_PUBLISHABLE_KEY=pk_test_xxx +STRIPE_WEBHOOK_SECRET=whsec_xxx +STRIPE_PRICE_HOBBYIST_MONTHLY=price_xxx +STRIPE_PRICE_HOBBYIST_ANNUAL=price_xxx + +# =================== +# EMAIL (Optional for MVP) +# =================== +SMTP_HOST=smtp.sendgrid.net +SMTP_PORT=587 +SMTP_USER=apikey +SMTP_PASS=SG.xxx + +# =================== +# MONITORING (Optional) +# =================== +SENTRY_DSN=https://xxx@sentry.io/xxx +``` + +--- + +## Stripe Setup Checklist + +``` +□ Create Stripe account (or use existing) +□ Switch to Test mode +□ Create Product: "LARUN Hobbyist" +□ Create Price: $9/month (monthly) +□ Create Price: $89/year (annual) +□ Copy price IDs to .env +□ Set up webhook endpoint +□ Configure webhook events: + □ checkout.session.completed + □ customer.subscription.updated + □ customer.subscription.deleted + □ invoice.payment_failed +□ Test with Stripe CLI +□ Document for team +``` + +--- + +## Notes + +**Priority:** Unblock other agents ASAP! +- Day 1 focus: docker-compose.yml + .env.example +- Day 2 focus: NextAuth for GAMMA +- Day 3 focus: Stripe setup + +(Add any other notes here) diff --git a/docs/coordination-templates/agents/GAMMA_WORKLOG.md b/docs/coordination-templates/agents/GAMMA_WORKLOG.md new file mode 100644 index 0000000..ff7f042 --- /dev/null +++ b/docs/coordination-templates/agents/GAMMA_WORKLOG.md @@ -0,0 +1,204 @@ +# Agent GAMMA Worklog - Frontend UI + +**Branch:** `claude/mvp-gamma-frontend` +**Owner:** Claude Agent 3 +**Status:** 🔵 Ready to Start + +--- + +## My Responsibilities + +- Next.js application setup +- Landing page with pricing +- User authentication UI +- Analysis interface (TIC ID input → results) +- Interactive visualizations (Plotly) +- User dashboard +- Responsive design + +## My Files (Exclusive Write Access) + +``` +web/ +├── package.json +├── next.config.js +├── tailwind.config.js +├── tsconfig.json +├── src/ +│ ├── app/ +│ │ ├── layout.tsx +│ │ ├── page.tsx # Landing page +│ │ ├── globals.css +│ │ ├── analyze/ +│ │ │ └── page.tsx # Analysis interface +│ │ ├── results/ +│ │ │ └── [id]/page.tsx # Results display +│ │ ├── dashboard/ +│ │ │ └── page.tsx # User dashboard +│ │ ├── auth/ +│ │ │ ├── login/page.tsx +│ │ │ ├── register/page.tsx +│ │ │ └── forgot-password/page.tsx +│ │ └── pricing/ +│ │ └── page.tsx +│ ├── components/ +│ │ ├── ui/ # Base components +│ │ │ ├── Button.tsx +│ │ │ ├── Card.tsx +│ │ │ ├── Input.tsx +│ │ │ └── ... +│ │ ├── layout/ +│ │ │ ├── Header.tsx +│ │ │ ├── Footer.tsx +│ │ │ └── Sidebar.tsx +│ │ ├── analysis/ +│ │ │ ├── AnalysisForm.tsx +│ │ │ ├── AnalysisProgress.tsx +│ │ │ └── AnalysisCard.tsx +│ │ ├── results/ +│ │ │ ├── DetectionBadge.tsx +│ │ │ ├── VettingResults.tsx +│ │ │ └── TransitParameters.tsx +│ │ └── visualizations/ +│ │ ├── LightCurvePlot.tsx +│ │ ├── PhaseFoldedPlot.tsx +│ │ └── PeriodogramPlot.tsx +│ ├── lib/ +│ │ ├── api-client.ts # API wrapper (read auth.ts from DELTA) +│ │ └── utils.ts +│ ├── hooks/ +│ │ ├── useAnalysis.ts +│ │ └── useUser.ts +│ └── types/ +│ └── index.ts +└── public/ + ├── logo.svg + └── images/ + +tests/test_frontend/ +├── components/ +└── pages/ +``` + +--- + +## Daily Log + +### Day 0 - Setup (Date: ______) + +**Status:** Not started + +**Tasks:** +- [ ] Create branch `claude/mvp-gamma-frontend` +- [ ] Run `npx create-next-app@latest` +- [ ] Set up Tailwind CSS +- [ ] Read MVP_INTERFACES.md +- [ ] Review BETA's API spec + +**Notes:** +- Can start with mock data while waiting for BETA + +**Blockers:** +- None (can work independently initially) + +--- + +### Day 1 (Date: ______) + +**Status:** (🔵 Ready | 🟢 Active | 🟡 Waiting | 🔴 Blocked) + +**Yesterday:** +- (What was completed) + +**Today:** +- [ ] Task 1 +- [ ] Task 2 + +**Blockers:** +- (List any blockers) + +**Handoffs:** +- (List any handoffs to other agents) + +--- + +## Interfaces I Consume + +### From BETA (REST API) +```typescript +// API endpoints I need to call +POST /api/v1/auth/login +POST /api/v1/auth/register +POST /api/v1/analyze +GET /api/v1/analyze/:id +GET /api/v1/analyses +GET /api/v1/user/profile +GET /api/v1/user/usage +POST /api/v1/subscription/create-checkout +``` + +### From DELTA (Auth Config) +```typescript +// NextAuth configuration +import { authOptions } from "@/lib/auth"; // DELTA provides +import { useSession } from "next-auth/react"; +``` + +--- + +## Dependencies I Need + +| From | What | Status | +|------|------|--------| +| BETA | REST API endpoints | 🟡 Waiting (use mocks) | +| DELTA | NextAuth config (auth.ts) | 🟡 Waiting | +| DELTA | Stripe config (stripe.ts) | 🟡 Waiting | + +--- + +## My Progress + +| Week | Day | Task | Status | +|------|-----|------|--------| +| 1 | 1 | Next.js + Tailwind setup | ⬜ | +| 1 | 1 | Project structure | ⬜ | +| 1 | 2 | UI component library | ⬜ | +| 1 | 2 | API client setup | ⬜ | +| 1 | 3 | Landing page | ⬜ | +| 1 | 3 | Auth pages | ⬜ | +| 1 | 4 | Connect auth to DELTA | ⬜ | +| 1 | 4 | Analysis form | ⬜ | +| 1 | 5 | Results display (mock) | ⬜ | +| 1 | 6 | Plotly visualizations | ⬜ | +| 1 | 7 | Responsive testing | ⬜ | + +**Legend:** ⬜ Not Started | 🔄 In Progress | ✅ Complete | ⛔ Blocked + +--- + +## Mock Data for Development + +```typescript +// Use this while waiting for BETA +const mockAnalysisResult = { + id: "mock-123", + tic_id: "TIC 470710327", + status: "completed", + result: { + detection: true, + confidence: 0.87, + period_days: 3.5247, + depth_ppm: 1250, + vetting: { + disposition: "PLANET_CANDIDATE", + // ... etc + } + } +}; +``` + +--- + +## Notes + +(Add any notes, decisions, or observations here) diff --git a/infrastructure/.env.example b/infrastructure/.env.example new file mode 100644 index 0000000..b2979bb --- /dev/null +++ b/infrastructure/.env.example @@ -0,0 +1,89 @@ +# ============================================================================= +# LARUN MVP Environment Variables Template +# ============================================================================= +# Copy this file to ../.env and fill in your values +# DO NOT commit your actual .env file to version control! +# ============================================================================= + +# =================== +# DATABASE (PostgreSQL) +# =================== +DATABASE_URL=postgresql://larun:password@localhost:5432/larun_db +# For production, use a secure password and SSL: +# DATABASE_URL=postgresql://larun:SECURE_PASSWORD@host:5432/larun_db?sslmode=require + +# =================== +# REDIS (Job Queue & Caching) +# =================== +REDIS_URL=redis://localhost:6379 +# For production with authentication: +# REDIS_URL=redis://:password@host:6379 + +# =================== +# AUTHENTICATION (NextAuth.js) +# =================== +# The URL of your Next.js application +NEXTAUTH_URL=http://localhost:3000 +# Generate with: openssl rand -base64 32 +NEXTAUTH_SECRET=your-nextauth-secret-minimum-32-characters-long +# JWT secret for API authentication (can be same as NEXTAUTH_SECRET or different) +JWT_SECRET=your-jwt-secret-minimum-32-characters-long + +# =================== +# STRIPE (Payments) +# =================== +# Get these from https://dashboard.stripe.com/test/apikeys +STRIPE_SECRET_KEY=sk_test_xxx +STRIPE_PUBLISHABLE_KEY=pk_test_xxx +# Get webhook secret from Stripe CLI or Dashboard +STRIPE_WEBHOOK_SECRET=whsec_xxx + +# Stripe Price IDs - Create in Stripe Dashboard +STRIPE_PRICE_HOBBYIST_MONTHLY=price_xxx +STRIPE_PRICE_HOBBYIST_ANNUAL=price_xxx + +# =================== +# API CONFIGURATION +# =================== +API_URL=http://localhost:8000 +API_HOST=0.0.0.0 +API_PORT=8000 +# python logging level: DEBUG, INFO, WARNING, ERROR +LOG_LEVEL=INFO + +# =================== +# WEB CONFIGURATION +# =================== +NEXT_PUBLIC_API_URL=http://localhost:8000 +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx + +# =================== +# GPU/ML CONFIGURATION +# =================== +# Set to 'true' to enable GPU training (requires NVIDIA GPU) +ENABLE_GPU=false +# Maximum concurrent training jobs per user (Hobbyist tier) +MAX_CONCURRENT_JOBS=1 +# Maximum training duration in seconds (Hobbyist tier: 1 hour) +MAX_TRAINING_DURATION=3600 + +# =================== +# EMAIL (Optional for MVP) +# =================== +SMTP_HOST=smtp.sendgrid.net +SMTP_PORT=587 +SMTP_USER=apikey +SMTP_PASS=SG.xxx +FROM_EMAIL=noreply@larun.ai + +# =================== +# MONITORING (Optional) +# =================== +SENTRY_DSN=https://xxx@sentry.io/xxx + +# =================== +# ENVIRONMENT +# =================== +NODE_ENV=development +# Options: development, staging, production +ENVIRONMENT=development diff --git a/infrastructure/scripts/deploy.sh b/infrastructure/scripts/deploy.sh new file mode 100755 index 0000000..4d52f30 --- /dev/null +++ b/infrastructure/scripts/deploy.sh @@ -0,0 +1,171 @@ +#!/bin/bash +# ============================================================================= +# LARUN MVP - Deployment Script +# ============================================================================= +# Deploys LARUN to staging or production environment +# Usage: ./deploy.sh [staging|production] +# ============================================================================= + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +# ----------------------------------------------------------------------------- +# Parse arguments +# ----------------------------------------------------------------------------- +ENVIRONMENT="${1:-staging}" + +if [[ "$ENVIRONMENT" != "staging" && "$ENVIRONMENT" != "production" ]]; then + echo -e "${RED}Error: Invalid environment. Use 'staging' or 'production'${NC}" + echo "Usage: $0 [staging|production]" + exit 1 +fi + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} LARUN MVP - Deploy to ${ENVIRONMENT}${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Safety checks for production +# ----------------------------------------------------------------------------- +if [ "$ENVIRONMENT" == "production" ]; then + echo -e "${YELLOW}WARNING: You are about to deploy to PRODUCTION${NC}" + echo "" + + # Check for required environment variables + REQUIRED_VARS=( + "DATABASE_URL" + "REDIS_URL" + "NEXTAUTH_SECRET" + "JWT_SECRET" + "STRIPE_SECRET_KEY" + "STRIPE_WEBHOOK_SECRET" + ) + + MISSING_VARS=() + for var in "${REQUIRED_VARS[@]}"; do + if [ -z "${!var}" ]; then + MISSING_VARS+=("$var") + fi + done + + if [ ${#MISSING_VARS[@]} -gt 0 ]; then + echo -e "${RED}Error: Missing required environment variables:${NC}" + for var in "${MISSING_VARS[@]}"; do + echo " - $var" + done + exit 1 + fi + + # Confirm deployment + read -p "Type 'deploy' to confirm production deployment: " CONFIRM + if [ "$CONFIRM" != "deploy" ]; then + echo -e "${YELLOW}Deployment cancelled${NC}" + exit 0 + fi +fi + +# ----------------------------------------------------------------------------- +# Build Docker images +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Building Docker images...${NC}" + +cd "$PROJECT_ROOT" + +# Build API image +echo "Building API image..." +docker build -t larun-api:latest -f docker/Dockerfile.api . + +# Build Web image +echo "Building Web image..." +docker build -t larun-web:latest -f docker/Dockerfile.web . + +echo -e "${GREEN} [OK] Docker images built${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Run tests +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Running tests...${NC}" + +# Run API tests +echo "Running API tests..." +docker run --rm larun-api:latest pytest tests/ -v || { + echo -e "${RED}API tests failed. Aborting deployment.${NC}" + exit 1 +} + +echo -e "${GREEN} [OK] All tests passed${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Deploy based on environment +# ----------------------------------------------------------------------------- +if [ "$ENVIRONMENT" == "staging" ]; then + echo -e "${YELLOW}Deploying to staging...${NC}" + + # Tag images for staging + docker tag larun-api:latest larun-api:staging + docker tag larun-web:latest larun-web:staging + + # Push to registry (configure your registry here) + # docker push your-registry/larun-api:staging + # docker push your-registry/larun-web:staging + + # Deploy to staging server + # This would typically be: + # - Railway: railway up + # - Fly.io: fly deploy + # - AWS: aws ecs update-service + # - K8s: kubectl apply -f k8s/staging/ + + echo -e "${GREEN} [OK] Deployed to staging${NC}" + echo "" + echo "Staging URLs:" + echo " - Web: https://staging.larun.ai" + echo " - API: https://api.staging.larun.ai" + +else + echo -e "${YELLOW}Deploying to production...${NC}" + + # Tag images for production + GIT_SHA=$(git rev-parse --short HEAD) + docker tag larun-api:latest larun-api:$GIT_SHA + docker tag larun-api:latest larun-api:production + docker tag larun-web:latest larun-web:$GIT_SHA + docker tag larun-web:latest larun-web:production + + # Push to registry (configure your registry here) + # docker push your-registry/larun-api:$GIT_SHA + # docker push your-registry/larun-api:production + # docker push your-registry/larun-web:$GIT_SHA + # docker push your-registry/larun-web:production + + # Deploy to production server + # This would typically be: + # - Railway: railway up --environment production + # - Fly.io: fly deploy --app larun-production + # - AWS: aws ecs update-service --cluster production + # - K8s: kubectl apply -f k8s/production/ + + echo -e "${GREEN} [OK] Deployed to production${NC}" + echo "" + echo "Production URLs:" + echo " - Web: https://larun.ai" + echo " - API: https://api.larun.ai" +fi + +echo "" +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Deployment Complete!${NC}" +echo -e "${GREEN}========================================${NC}" diff --git a/infrastructure/scripts/setup-env.sh b/infrastructure/scripts/setup-env.sh new file mode 100755 index 0000000..7fe0682 --- /dev/null +++ b/infrastructure/scripts/setup-env.sh @@ -0,0 +1,198 @@ +#!/bin/bash +# ============================================================================= +# LARUN MVP - Environment Setup Script +# ============================================================================= +# This script sets up the development environment for LARUN +# Run this once when setting up a new development machine +# ============================================================================= + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +echo -e "${BLUE}========================================${NC}" +echo -e "${BLUE} LARUN MVP - Environment Setup${NC}" +echo -e "${BLUE}========================================${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Check prerequisites +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Checking prerequisites...${NC}" + +# Check Docker +if ! command -v docker &> /dev/null; then + echo -e "${RED}Error: Docker is not installed.${NC}" + echo "Please install Docker: https://docs.docker.com/get-docker/" + exit 1 +fi +echo -e "${GREEN} [OK] Docker installed${NC}" + +# Check Docker Compose +if ! command -v docker compose &> /dev/null; then + echo -e "${RED}Error: Docker Compose is not installed.${NC}" + echo "Please install Docker Compose: https://docs.docker.com/compose/install/" + exit 1 +fi +echo -e "${GREEN} [OK] Docker Compose installed${NC}" + +# Check Node.js (optional for local web development) +if command -v node &> /dev/null; then + NODE_VERSION=$(node --version) + echo -e "${GREEN} [OK] Node.js installed: ${NODE_VERSION}${NC}" +else + echo -e "${YELLOW} [WARN] Node.js not installed (optional for local web dev)${NC}" +fi + +# Check Python (optional for local API development) +if command -v python3 &> /dev/null; then + PYTHON_VERSION=$(python3 --version) + echo -e "${GREEN} [OK] Python installed: ${PYTHON_VERSION}${NC}" +else + echo -e "${YELLOW} [WARN] Python not installed (optional for local API dev)${NC}" +fi + +echo "" + +# ----------------------------------------------------------------------------- +# Create .env file if it doesn't exist +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Setting up environment file...${NC}" + +ENV_FILE="$PROJECT_ROOT/.env" +ENV_EXAMPLE="$PROJECT_ROOT/infrastructure/.env.example" + +if [ -f "$ENV_FILE" ]; then + echo -e "${YELLOW} [SKIP] .env file already exists${NC}" + echo " If you want to recreate it, delete .env and run this script again" +else + if [ -f "$ENV_EXAMPLE" ]; then + cp "$ENV_EXAMPLE" "$ENV_FILE" + + # Generate secure secrets + NEXTAUTH_SECRET=$(openssl rand -base64 32) + JWT_SECRET=$(openssl rand -base64 32) + + # Replace placeholder secrets (macOS/Linux compatible) + if [[ "$OSTYPE" == "darwin"* ]]; then + # macOS + sed -i '' "s|NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$NEXTAUTH_SECRET|" "$ENV_FILE" + sed -i '' "s|JWT_SECRET=.*|JWT_SECRET=$JWT_SECRET|" "$ENV_FILE" + else + # Linux + sed -i "s|NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$NEXTAUTH_SECRET|" "$ENV_FILE" + sed -i "s|JWT_SECRET=.*|JWT_SECRET=$JWT_SECRET|" "$ENV_FILE" + fi + + echo -e "${GREEN} [OK] Created .env file with secure secrets${NC}" + echo -e "${YELLOW} [ACTION] Please update Stripe keys in .env${NC}" + else + echo -e "${RED}Error: .env.example not found at $ENV_EXAMPLE${NC}" + exit 1 + fi +fi + +echo "" + +# ----------------------------------------------------------------------------- +# Create necessary directories +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Creating directories...${NC}" + +mkdir -p "$PROJECT_ROOT/models" +mkdir -p "$PROJECT_ROOT/output" +mkdir -p "$PROJECT_ROOT/data" +mkdir -p "$PROJECT_ROOT/web" +mkdir -p "$PROJECT_ROOT/api" + +echo -e "${GREEN} [OK] Created necessary directories${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Pull Docker images +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Pulling Docker images...${NC}" + +docker pull postgres:15-alpine +docker pull redis:7-alpine +docker pull node:20-alpine +docker pull python:3.11-slim + +echo -e "${GREEN} [OK] Docker images pulled${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Start services +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Starting database and redis services...${NC}" + +cd "$PROJECT_ROOT/docker" +docker compose up -d db redis + +echo -e "${GREEN} [OK] Services started${NC}" +echo "" + +# ----------------------------------------------------------------------------- +# Wait for services to be healthy +# ----------------------------------------------------------------------------- +echo -e "${YELLOW}Waiting for services to be healthy...${NC}" + +# Wait for PostgreSQL +for i in {1..30}; do + if docker compose exec -T db pg_isready -U larun -d larun_db > /dev/null 2>&1; then + echo -e "${GREEN} [OK] PostgreSQL is ready${NC}" + break + fi + if [ $i -eq 30 ]; then + echo -e "${RED}Error: PostgreSQL did not become ready in time${NC}" + exit 1 + fi + sleep 1 +done + +# Wait for Redis +for i in {1..30}; do + if docker compose exec -T redis redis-cli ping > /dev/null 2>&1; then + echo -e "${GREEN} [OK] Redis is ready${NC}" + break + fi + if [ $i -eq 30 ]; then + echo -e "${RED}Error: Redis did not become ready in time${NC}" + exit 1 + fi + sleep 1 +done + +echo "" + +# ----------------------------------------------------------------------------- +# Summary +# ----------------------------------------------------------------------------- +echo -e "${GREEN}========================================${NC}" +echo -e "${GREEN} Setup Complete!${NC}" +echo -e "${GREEN}========================================${NC}" +echo "" +echo "Services running:" +echo " - PostgreSQL: localhost:5432" +echo " - Redis: localhost:6379" +echo "" +echo "Next steps:" +echo " 1. Update Stripe keys in .env" +echo " 2. Start all services: cd docker && docker compose up" +echo " 3. Access the web app: http://localhost:3000" +echo " 4. Access the API: http://localhost:8000" +echo "" +echo "Useful commands:" +echo " - View logs: docker compose logs -f" +echo " - Stop services: docker compose down" +echo " - Admin tools: docker compose --profile tools up" +echo "" diff --git a/src/api/__init__.py b/src/api/__init__.py new file mode 100644 index 0000000..eee8231 --- /dev/null +++ b/src/api/__init__.py @@ -0,0 +1,14 @@ +""" +LARUN API Module + +FastAPI-based REST API for the LARUN exoplanet detection platform. +Provides endpoints for authentication, analysis, and user management. + +Agent: BETA +Branch: claude/mvp-beta-backend +""" + +from src.api.main import app + +__all__ = ["app"] +__version__ = "0.1.0" diff --git a/src/api/config.py b/src/api/config.py new file mode 100644 index 0000000..a87e223 --- /dev/null +++ b/src/api/config.py @@ -0,0 +1,75 @@ +""" +API Configuration + +Centralized configuration management using Pydantic settings. +Loads configuration from environment variables with sensible defaults. + +Agent: BETA +""" + +from functools import lru_cache +from typing import Optional +from pydantic_settings import BaseSettings + + +class Settings(BaseSettings): + """Application settings loaded from environment variables.""" + + # Application + APP_NAME: str = "LARUN API" + APP_VERSION: str = "0.1.0" + DEBUG: bool = False + ENVIRONMENT: str = "development" + + # API + API_V1_PREFIX: str = "/api/v1" + API_HOST: str = "0.0.0.0" + API_PORT: int = 8000 + + # Database + DATABASE_URL: str = "sqlite+aiosqlite:///./larun.db" + DATABASE_ECHO: bool = False + + # Security / JWT + SECRET_KEY: str = "development-secret-key-change-in-production" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 # 24 hours + + # Rate Limiting + RATE_LIMIT_REQUESTS: int = 100 + RATE_LIMIT_PERIOD: int = 60 # seconds + + # Analysis Limits (Hobbyist tier) + HOBBYIST_MONTHLY_LIMIT: int = 25 + + # Stripe (placeholder - DELTA will provide) + STRIPE_SECRET_KEY: Optional[str] = None + STRIPE_WEBHOOK_SECRET: Optional[str] = None + STRIPE_PRICE_HOBBYIST_MONTHLY: Optional[str] = None + STRIPE_PRICE_HOBBYIST_ANNUAL: Optional[str] = None + + # Redis (for job queue) + REDIS_URL: str = "redis://localhost:6379" + + # CORS + CORS_ORIGINS: list[str] = [ + "http://localhost:3000", + "http://localhost:8000", + "https://larun.space", + "https://www.larun.space", + ] + + class Config: + env_file = ".env" + env_file_encoding = "utf-8" + case_sensitive = True + + +@lru_cache() +def get_settings() -> Settings: + """Get cached settings instance.""" + return Settings() + + +# Convenience access +settings = get_settings() diff --git a/src/api/dependencies.py b/src/api/dependencies.py new file mode 100644 index 0000000..3ef5f69 --- /dev/null +++ b/src/api/dependencies.py @@ -0,0 +1,258 @@ +""" +API Dependencies + +FastAPI dependency injection functions for authentication, +database sessions, and service access. + +Agent: BETA +""" + +from datetime import datetime, timedelta +from typing import Optional, Annotated + +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from jose import JWTError, jwt +from passlib.context import CryptContext +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.config import settings +from src.api.models.database import get_db +from src.api.models.user import User +from src.api.models.subscription import Subscription +from src.api.schemas.auth import TokenData + + +# Security setup +security = HTTPBearer() +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + + +# Password utilities +def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + return pwd_context.verify(plain_password, hashed_password) + + +def get_password_hash(password: str) -> str: + """Hash a password for storage.""" + return pwd_context.hash(password) + + +# JWT utilities +def create_access_token( + data: dict, + expires_delta: Optional[timedelta] = None, +) -> str: + """Create a JWT access token.""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta( + minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES + ) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, + settings.SECRET_KEY, + algorithm=settings.ALGORITHM, + ) + return encoded_jwt + + +def decode_access_token(token: str) -> Optional[TokenData]: + """Decode and validate a JWT access token.""" + try: + payload = jwt.decode( + token, + settings.SECRET_KEY, + algorithms=[settings.ALGORITHM], + ) + user_id: Optional[int] = payload.get("sub") + email: Optional[str] = payload.get("email") + if user_id is None: + return None + return TokenData(user_id=user_id, email=email) + except JWTError: + return None + + +# Authentication dependencies +async def get_current_user( + credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> User: + """ + Get the current authenticated user from JWT token. + + Raises HTTPException if token is invalid or user not found. + """ + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "error": { + "code": "unauthorized", + "message": "Could not validate credentials", + } + }, + headers={"WWW-Authenticate": "Bearer"}, + ) + + token_data = decode_access_token(credentials.credentials) + if token_data is None: + raise credentials_exception + + result = await db.execute(select(User).where(User.id == token_data.user_id)) + user = result.scalar_one_or_none() + + if user is None: + raise credentials_exception + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "error": { + "code": "forbidden", + "message": "User account is deactivated", + } + }, + ) + + return user + + +async def get_current_active_user( + current_user: Annotated[User, Depends(get_current_user)], +) -> User: + """Get current user, ensuring they are active.""" + return current_user + + +# Subscription dependencies +async def get_user_subscription( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> Optional[Subscription]: + """Get the current user's subscription if any.""" + result = await db.execute( + select(Subscription).where(Subscription.user_id == current_user.id) + ) + return result.scalar_one_or_none() + + +async def require_active_subscription( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> Subscription: + """ + Require user to have an active subscription. + + Raises HTTPException if no active subscription. + """ + subscription = await get_user_subscription(current_user, db) + + if subscription is None or not subscription.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "error": { + "code": "forbidden", + "message": "Active subscription required", + } + }, + ) + + return subscription + + +async def check_usage_limit( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> Subscription: + """ + Check if user has remaining analyses in their quota. + + Raises HTTPException if usage limit exceeded. + """ + subscription = await require_active_subscription(current_user, db) + + if not subscription.can_analyze: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "error": { + "code": "usage_limit_exceeded", + "message": f"Monthly analysis limit ({subscription.analysis_limit}) reached", + "details": { + "limit": subscription.analysis_limit, + "used": subscription.analyses_this_period, + "period_end": subscription.current_period_end.isoformat() + if subscription.current_period_end + else None, + }, + } + }, + ) + + return subscription + + +# Mock Detection Service (until ALPHA delivers) +class MockDetectionService: + """ + Mock detection service for development. + + Will be replaced with real DetectionService from ALPHA. + """ + + async def analyze(self, tic_id: str) -> dict: + """Mock analysis that returns placeholder data.""" + import random + + return { + "tic_id": tic_id, + "detection": random.choice([True, False]), + "confidence": random.uniform(0.5, 0.99), + "period_days": random.uniform(1.0, 50.0) if random.random() > 0.3 else None, + "depth_ppm": random.uniform(100, 5000) if random.random() > 0.3 else None, + "duration_hours": random.uniform(1.0, 10.0) if random.random() > 0.3 else None, + "snr": random.uniform(5.0, 50.0), + "sectors_used": [1, 2, 3], + "processing_time_seconds": random.uniform(10.0, 30.0), + "vetting": { + "disposition": random.choice( + ["PLANET_CANDIDATE", "LIKELY_FALSE_POSITIVE", "INCONCLUSIVE"] + ), + "confidence": random.uniform(0.5, 0.99), + "tests_passed": random.randint(1, 3), + "tests_failed": random.randint(0, 2), + "recommendation": "Recommend follow-up observations", + }, + } + + async def get_status(self) -> dict: + """Get mock service status.""" + return { + "status": "healthy", + "version": "mock-0.1.0", + } + + +# Detection service dependency +_detection_service: Optional[MockDetectionService] = None + + +def get_detection_service() -> MockDetectionService: + """ + Get the detection service instance. + + Currently returns mock service. + Will be replaced with real DetectionService when ALPHA delivers. + """ + global _detection_service + if _detection_service is None: + _detection_service = MockDetectionService() + return _detection_service diff --git a/src/api/main.py b/src/api/main.py new file mode 100644 index 0000000..15bfa55 --- /dev/null +++ b/src/api/main.py @@ -0,0 +1,181 @@ +""" +LARUN API Main Application + +FastAPI application entry point with router configuration, +middleware setup, and lifecycle management. + +Agent: BETA +Branch: claude/mvp-beta-backend +""" + +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from fastapi import FastAPI, Request, status +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError + +from src.api.config import settings +from src.api.models.database import init_db +from src.api.routes import auth, analysis, user, subscription + + +@asynccontextmanager +async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: + """ + Application lifespan management. + + Handles startup and shutdown tasks: + - Initialize database tables on startup + - Clean up resources on shutdown + """ + # Startup + print(f"Starting {settings.APP_NAME} v{settings.APP_VERSION}") + await init_db() + print("Database initialized") + + yield + + # Shutdown + print("Shutting down...") + + +# Create FastAPI application +app = FastAPI( + title=settings.APP_NAME, + description=""" + LARUN Exoplanet Detection API + + A REST API for the LARUN platform that enables users to: + - Register and authenticate + - Submit exoplanet detection analysis requests + - Retrieve analysis results + - Manage subscriptions + + ## Authentication + Most endpoints require JWT bearer token authentication. + Obtain a token via the /api/v1/auth/login endpoint. + + ## Rate Limiting + API requests are rate-limited based on subscription tier. + """, + version=settings.APP_VERSION, + docs_url="/api/docs" if settings.DEBUG else None, + redoc_url="/api/redoc" if settings.DEBUG else None, + openapi_url="/api/openapi.json" if settings.DEBUG else None, + lifespan=lifespan, +) + +# CORS middleware +app.add_middleware( + CORSMiddleware, + allow_origins=settings.CORS_ORIGINS, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +# Exception handlers +@app.exception_handler(RequestValidationError) +async def validation_exception_handler( + request: Request, exc: RequestValidationError +) -> JSONResponse: + """Handle request validation errors with structured response.""" + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={ + "error": { + "code": "validation_error", + "message": "Invalid request data", + "details": exc.errors(), + } + }, + ) + + +@app.exception_handler(Exception) +async def general_exception_handler( + request: Request, exc: Exception +) -> JSONResponse: + """Handle unexpected errors gracefully.""" + # In debug mode, include error details + if settings.DEBUG: + message = str(exc) + else: + message = "An internal error occurred" + + return JSONResponse( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": { + "code": "internal_error", + "message": message, + } + }, + ) + + +# Include routers +app.include_router( + auth.router, + prefix=f"{settings.API_V1_PREFIX}/auth", + tags=["Authentication"], +) + +app.include_router( + analysis.router, + prefix=f"{settings.API_V1_PREFIX}", + tags=["Analysis"], +) + +app.include_router( + user.router, + prefix=f"{settings.API_V1_PREFIX}/user", + tags=["User"], +) + +app.include_router( + subscription.router, + prefix=f"{settings.API_V1_PREFIX}/subscription", + tags=["Subscription"], +) + + +# Health check endpoint +@app.get("/health", tags=["Health"]) +async def health_check() -> dict: + """ + Health check endpoint. + + Returns service status for load balancers and monitoring. + """ + return { + "status": "healthy", + "service": settings.APP_NAME, + "version": settings.APP_VERSION, + } + + +@app.get("/", tags=["Root"]) +async def root() -> dict: + """Root endpoint with API information.""" + return { + "name": settings.APP_NAME, + "version": settings.APP_VERSION, + "docs": "/api/docs" if settings.DEBUG else "disabled", + "api_prefix": settings.API_V1_PREFIX, + } + + +# For running with uvicorn directly +if __name__ == "__main__": + import uvicorn + + uvicorn.run( + "src.api.main:app", + host=settings.API_HOST, + port=settings.API_PORT, + reload=settings.DEBUG, + ) diff --git a/src/api/models/__init__.py b/src/api/models/__init__.py new file mode 100644 index 0000000..42d9f5e --- /dev/null +++ b/src/api/models/__init__.py @@ -0,0 +1,23 @@ +""" +Database Models Module + +SQLAlchemy ORM models for the LARUN database. +""" + +from src.api.models.database import Base, get_db, engine, async_session +from src.api.models.user import User +from src.api.models.analysis import Analysis, AnalysisStatus +from src.api.models.subscription import Subscription, SubscriptionPlan, SubscriptionStatus + +__all__ = [ + "Base", + "get_db", + "engine", + "async_session", + "User", + "Analysis", + "AnalysisStatus", + "Subscription", + "SubscriptionPlan", + "SubscriptionStatus", +] diff --git a/src/api/models/analysis.py b/src/api/models/analysis.py new file mode 100644 index 0000000..e34509d --- /dev/null +++ b/src/api/models/analysis.py @@ -0,0 +1,160 @@ +""" +Analysis Model + +SQLAlchemy model for exoplanet detection analysis jobs and results. + +Agent: BETA +""" + +from datetime import datetime +from enum import Enum +from typing import Optional, Any, Dict, TYPE_CHECKING + +from sqlalchemy import String, DateTime, Float, Integer, Text, ForeignKey, JSON, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.api.models.database import Base + +if TYPE_CHECKING: + from src.api.models.user import User + + +class AnalysisStatus(str, Enum): + """Status of an analysis job.""" + + PENDING = "pending" + PROCESSING = "processing" + COMPLETED = "completed" + FAILED = "failed" + + +class Analysis(Base): + """ + Analysis job model. + + Stores analysis requests and results from the detection engine. + Each analysis is associated with a user and tracks a specific TIC ID. + """ + + __tablename__ = "analyses" + + # Primary key + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + + # Foreign key to user + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), + nullable=False, + index=True, + ) + + # Target identification + tic_id: Mapped[str] = mapped_column( + String(50), + nullable=False, + index=True, + ) + + # Job status + status: Mapped[AnalysisStatus] = mapped_column( + String(20), + default=AnalysisStatus.PENDING, + nullable=False, + index=True, + ) + + # Detection results (populated when completed) + detection: Mapped[Optional[bool]] = mapped_column(nullable=True) + confidence: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + + # Transit parameters + period_days: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + depth_ppm: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + duration_hours: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + epoch_btjd: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + snr: Mapped[Optional[float]] = mapped_column(Float, nullable=True) + + # Full result JSON (from DetectionService) + result_json: Mapped[Optional[Dict[str, Any]]] = mapped_column( + JSON, + nullable=True, + ) + + # Error message if failed + error_message: Mapped[Optional[str]] = mapped_column(Text, nullable=True) + + # Processing metadata + processing_time_seconds: Mapped[Optional[float]] = mapped_column( + Float, + nullable=True, + ) + sectors_used: Mapped[Optional[str]] = mapped_column( + String(255), + nullable=True, + ) # Comma-separated list of sectors + + # Timestamps + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + index=True, + ) + started_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + completed_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + + # Relationships + user: Mapped["User"] = relationship("User", back_populates="analyses") + + def __repr__(self) -> str: + return f"" + + @property + def is_complete(self) -> bool: + """Check if analysis has completed (success or failure).""" + return self.status in (AnalysisStatus.COMPLETED, AnalysisStatus.FAILED) + + @property + def is_successful(self) -> bool: + """Check if analysis completed successfully.""" + return self.status == AnalysisStatus.COMPLETED + + def set_result( + self, + detection: bool, + confidence: float, + period_days: Optional[float] = None, + depth_ppm: Optional[float] = None, + duration_hours: Optional[float] = None, + epoch_btjd: Optional[float] = None, + snr: Optional[float] = None, + result_json: Optional[Dict[str, Any]] = None, + processing_time: Optional[float] = None, + sectors: Optional[list[int]] = None, + ) -> None: + """Set analysis results after successful detection.""" + self.status = AnalysisStatus.COMPLETED + self.detection = detection + self.confidence = confidence + self.period_days = period_days + self.depth_ppm = depth_ppm + self.duration_hours = duration_hours + self.epoch_btjd = epoch_btjd + self.snr = snr + self.result_json = result_json + self.processing_time_seconds = processing_time + if sectors: + self.sectors_used = ",".join(str(s) for s in sectors) + self.completed_at = datetime.utcnow() + + def set_error(self, error_message: str) -> None: + """Set analysis as failed with error message.""" + self.status = AnalysisStatus.FAILED + self.error_message = error_message + self.completed_at = datetime.utcnow() diff --git a/src/api/models/database.py b/src/api/models/database.py new file mode 100644 index 0000000..bf0df4a --- /dev/null +++ b/src/api/models/database.py @@ -0,0 +1,101 @@ +""" +Database Configuration + +SQLAlchemy async engine and session setup for PostgreSQL/SQLite. +Uses async/await throughout for non-blocking database operations. + +Agent: BETA +""" + +from contextlib import asynccontextmanager +from typing import AsyncGenerator + +from sqlalchemy.ext.asyncio import ( + AsyncSession, + async_sessionmaker, + create_async_engine, +) +from sqlalchemy.orm import declarative_base + +from src.api.config import settings + +# Create async engine +# For production, use PostgreSQL: postgresql+asyncpg://user:pass@host/db +# For development/testing, SQLite with aiosqlite works well +engine = create_async_engine( + settings.DATABASE_URL, + echo=settings.DATABASE_ECHO, + future=True, +) + +# Create async session factory +async_session = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + autocommit=False, + autoflush=False, +) + +# Base class for all ORM models +Base = declarative_base() + + +async def get_db() -> AsyncGenerator[AsyncSession, None]: + """ + Dependency injection for database sessions. + + Yields an async database session and ensures proper cleanup. + Use with FastAPI's Depends(): + + @router.get("/items") + async def get_items(db: AsyncSession = Depends(get_db)): + ... + """ + async with async_session() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +@asynccontextmanager +async def get_db_context() -> AsyncGenerator[AsyncSession, None]: + """ + Context manager for database sessions outside of request context. + + Useful for background tasks and CLI operations: + + async with get_db_context() as db: + user = await db.get(User, user_id) + """ + async with async_session() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +async def create_tables() -> None: + """Create all database tables. Call on startup.""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.create_all) + + +async def drop_tables() -> None: + """Drop all database tables. Use with caution!""" + async with engine.begin() as conn: + await conn.run_sync(Base.metadata.drop_all) + + +async def init_db() -> None: + """Initialize database with tables and any required seed data.""" + await create_tables() diff --git a/src/api/models/subscription.py b/src/api/models/subscription.py new file mode 100644 index 0000000..785c379 --- /dev/null +++ b/src/api/models/subscription.py @@ -0,0 +1,164 @@ +""" +Subscription Model + +SQLAlchemy model for user subscription plans and billing. + +Agent: BETA +""" + +from datetime import datetime +from enum import Enum +from typing import Optional, TYPE_CHECKING + +from sqlalchemy import String, DateTime, Integer, ForeignKey, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.api.models.database import Base + +if TYPE_CHECKING: + from src.api.models.user import User + + +class SubscriptionPlan(str, Enum): + """Available subscription plans.""" + + HOBBYIST_MONTHLY = "hobbyist_monthly" + HOBBYIST_ANNUAL = "hobbyist_annual" + + +class SubscriptionStatus(str, Enum): + """Subscription status states.""" + + ACTIVE = "active" + CANCELED = "canceled" + PAST_DUE = "past_due" + TRIALING = "trialing" + INCOMPLETE = "incomplete" + + +# Plan limits - analyses per month +PLAN_LIMITS = { + SubscriptionPlan.HOBBYIST_MONTHLY: 25, + SubscriptionPlan.HOBBYIST_ANNUAL: 25, +} + + +class Subscription(Base): + """ + User subscription model. + + Tracks Stripe subscription status and usage limits. + Managed primarily through Stripe webhooks (DELTA handles). + """ + + __tablename__ = "subscriptions" + + # Primary key + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + + # Foreign key to user (one-to-one relationship) + user_id: Mapped[int] = mapped_column( + ForeignKey("users.id", ondelete="CASCADE"), + unique=True, + nullable=False, + index=True, + ) + + # Stripe references + stripe_subscription_id: Mapped[str] = mapped_column( + String(255), + unique=True, + nullable=False, + index=True, + ) + stripe_price_id: Mapped[Optional[str]] = mapped_column( + String(255), + nullable=True, + ) + + # Subscription details + plan: Mapped[SubscriptionPlan] = mapped_column( + String(50), + nullable=False, + ) + status: Mapped[SubscriptionStatus] = mapped_column( + String(50), + default=SubscriptionStatus.ACTIVE, + nullable=False, + index=True, + ) + + # Billing period + current_period_start: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + current_period_end: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + + # Usage tracking for current period + analyses_this_period: Mapped[int] = mapped_column( + Integer, + default=0, + nullable=False, + ) + + # Timestamps + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + canceled_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + + # Relationships + user: Mapped["User"] = relationship("User", back_populates="subscription") + + def __repr__(self) -> str: + return f"" + + @property + def is_active(self) -> bool: + """Check if subscription is currently active.""" + return self.status in (SubscriptionStatus.ACTIVE, SubscriptionStatus.TRIALING) + + @property + def analysis_limit(self) -> int: + """Get the monthly analysis limit for this plan.""" + return PLAN_LIMITS.get(self.plan, 0) + + @property + def analyses_remaining(self) -> int: + """Get remaining analyses for current period.""" + return max(0, self.analysis_limit - self.analyses_this_period) + + @property + def can_analyze(self) -> bool: + """Check if user can perform more analyses.""" + return self.is_active and self.analyses_remaining > 0 + + def increment_usage(self) -> bool: + """ + Increment usage counter. + + Returns True if successful, False if limit exceeded. + """ + if not self.can_analyze: + return False + self.analyses_this_period += 1 + return True + + def reset_usage(self) -> None: + """Reset usage counter for new billing period.""" + self.analyses_this_period = 0 diff --git a/src/api/models/user.py b/src/api/models/user.py new file mode 100644 index 0000000..1c0e3e1 --- /dev/null +++ b/src/api/models/user.py @@ -0,0 +1,97 @@ +""" +User Model + +SQLAlchemy model for user accounts and authentication. + +Agent: BETA +""" + +from datetime import datetime +from typing import Optional, List, TYPE_CHECKING + +from sqlalchemy import String, DateTime, Boolean, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from src.api.models.database import Base + +if TYPE_CHECKING: + from src.api.models.analysis import Analysis + from src.api.models.subscription import Subscription + + +class User(Base): + """ + User account model. + + Stores user credentials, profile information, and Stripe customer reference. + Related to Analysis and Subscription models. + """ + + __tablename__ = "users" + + # Primary key + id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) + + # Authentication fields + email: Mapped[str] = mapped_column( + String(255), + unique=True, + nullable=False, + index=True, + ) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + + # Profile fields + name: Mapped[Optional[str]] = mapped_column(String(255), nullable=True) + is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + is_verified: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False) + + # Stripe integration (set by DELTA's Stripe webhooks) + stripe_customer_id: Mapped[Optional[str]] = mapped_column( + String(255), + unique=True, + nullable=True, + index=True, + ) + + # Timestamps + created_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + nullable=False, + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + server_default=func.now(), + onupdate=func.now(), + nullable=False, + ) + last_login_at: Mapped[Optional[datetime]] = mapped_column( + DateTime(timezone=True), + nullable=True, + ) + + # Relationships + analyses: Mapped[List["Analysis"]] = relationship( + "Analysis", + back_populates="user", + cascade="all, delete-orphan", + lazy="selectin", + ) + subscription: Mapped[Optional["Subscription"]] = relationship( + "Subscription", + back_populates="user", + uselist=False, + cascade="all, delete-orphan", + lazy="selectin", + ) + + def __repr__(self) -> str: + return f"" + + @property + def has_active_subscription(self) -> bool: + """Check if user has an active subscription.""" + if self.subscription is None: + return False + return self.subscription.is_active diff --git a/src/api/routes/__init__.py b/src/api/routes/__init__.py new file mode 100644 index 0000000..9293d7f --- /dev/null +++ b/src/api/routes/__init__.py @@ -0,0 +1,9 @@ +""" +API Routes Module + +Contains all FastAPI route handlers organized by feature. +""" + +from src.api.routes import auth, analysis, user, subscription + +__all__ = ["auth", "analysis", "user", "subscription"] diff --git a/src/api/routes/analysis.py b/src/api/routes/analysis.py new file mode 100644 index 0000000..822d4ae --- /dev/null +++ b/src/api/routes/analysis.py @@ -0,0 +1,380 @@ +""" +Analysis Routes + +Endpoints for submitting and retrieving exoplanet detection analyses. + +Agent: BETA +""" + +import re +from datetime import datetime +from typing import Annotated, Optional + +from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Query +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.config import settings +from src.api.models.database import get_db +from src.api.models.user import User +from src.api.models.analysis import Analysis, AnalysisStatus +from src.api.schemas.analysis import ( + AnalyzeRequest, + AnalyzeResponse, + AnalysisResult, + AnalysisResultDetail, + AnalysesListResponse, + DeleteAnalysisResponse, + VettingResultSchema, +) +from src.api.dependencies import ( + get_current_user, + check_usage_limit, + get_detection_service, + MockDetectionService, +) + +router = APIRouter() + + +def normalize_tic_id(tic_id: str) -> str: + """Normalize TIC ID to standard format (digits only).""" + return re.sub(r"[^0-9]", "", tic_id) + + +async def run_analysis( + analysis_id: int, + tic_id: str, + detection_service: MockDetectionService, +) -> None: + """ + Background task to run analysis. + + This will be replaced with proper job queue (Redis/Celery) + when integrating with ALPHA's DetectionService. + """ + from src.api.models.database import get_db_context + + async with get_db_context() as db: + # Get the analysis + result = await db.execute( + select(Analysis).where(Analysis.id == analysis_id) + ) + analysis = result.scalar_one_or_none() + + if analysis is None: + return + + try: + # Update status to processing + analysis.status = AnalysisStatus.PROCESSING + analysis.started_at = datetime.utcnow() + await db.flush() + + # Run detection (mock for now) + detection_result = await detection_service.analyze(tic_id) + + # Update analysis with results + analysis.set_result( + detection=detection_result.get("detection", False), + confidence=detection_result.get("confidence", 0.0), + period_days=detection_result.get("period_days"), + depth_ppm=detection_result.get("depth_ppm"), + duration_hours=detection_result.get("duration_hours"), + epoch_btjd=detection_result.get("epoch_btjd"), + snr=detection_result.get("snr"), + result_json=detection_result, + processing_time=detection_result.get("processing_time_seconds"), + sectors=detection_result.get("sectors_used"), + ) + + except Exception as e: + analysis.set_error(str(e)) + + await db.commit() + + +@router.post( + "/analyze", + response_model=AnalyzeResponse, + status_code=status.HTTP_202_ACCEPTED, + summary="Submit analysis request", + description="Submit a TIC ID for exoplanet transit detection analysis.", +) +async def submit_analysis( + request: AnalyzeRequest, + background_tasks: BackgroundTasks, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], + subscription: Annotated[None, Depends(check_usage_limit)], + detection_service: Annotated[MockDetectionService, Depends(get_detection_service)], +) -> AnalyzeResponse: + """ + Submit a TIC ID for analysis. + + - **tic_id**: TESS Input Catalog ID (e.g., "TIC 470710327" or "470710327") + + Returns an analysis ID to poll for results. + Requires active subscription with available usage quota. + """ + # Normalize TIC ID + normalized_tic = normalize_tic_id(request.tic_id) + + if not normalized_tic: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "error": { + "code": "invalid_tic_id", + "message": "Invalid TIC ID format", + } + }, + ) + + # Create analysis record + analysis = Analysis( + user_id=current_user.id, + tic_id=normalized_tic, + status=AnalysisStatus.PENDING, + ) + + db.add(analysis) + await db.flush() + await db.refresh(analysis) + + # Increment usage (subscription already validated by check_usage_limit) + if current_user.subscription: + current_user.subscription.increment_usage() + await db.flush() + + # Queue background analysis + background_tasks.add_task( + run_analysis, + analysis.id, + normalized_tic, + detection_service, + ) + + return AnalyzeResponse( + analysis_id=analysis.id, + status="pending", + message="Analysis queued. Poll GET /analyze/{id} for results.", + ) + + +@router.get( + "/analyze/{analysis_id}", + response_model=AnalysisResult, + summary="Get analysis result", + description="Get the status and results of a submitted analysis.", +) +async def get_analysis( + analysis_id: int, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +) -> AnalysisResult: + """ + Get analysis status and results. + + - **analysis_id**: ID returned from POST /analyze + + Returns current status and results when completed. + """ + result = await db.execute( + select(Analysis).where( + Analysis.id == analysis_id, + Analysis.user_id == current_user.id, + ) + ) + analysis = result.scalar_one_or_none() + + if analysis is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "error": { + "code": "not_found", + "message": "Analysis not found", + } + }, + ) + + # Build response + response = AnalysisResult( + id=analysis.id, + tic_id=analysis.tic_id, + status=analysis.status.value, + created_at=analysis.created_at, + completed_at=analysis.completed_at, + ) + + # Add result details if completed + if analysis.status == AnalysisStatus.COMPLETED: + sectors = [] + if analysis.sectors_used: + sectors = [int(s) for s in analysis.sectors_used.split(",")] + + # Build vetting result if available + vetting = None + if analysis.result_json and "vetting" in analysis.result_json: + v = analysis.result_json["vetting"] + vetting = VettingResultSchema( + disposition=v.get("disposition", "INCONCLUSIVE"), + confidence=v.get("confidence", 0.0), + tests_passed=v.get("tests_passed", 0), + tests_failed=v.get("tests_failed", 0), + recommendation=v.get("recommendation", ""), + ) + + response.result = AnalysisResultDetail( + detection=analysis.detection or False, + confidence=analysis.confidence or 0.0, + period_days=analysis.period_days, + depth_ppm=analysis.depth_ppm, + duration_hours=analysis.duration_hours, + epoch_btjd=analysis.epoch_btjd, + snr=analysis.snr, + vetting=vetting, + sectors_used=sectors, + processing_time_seconds=analysis.processing_time_seconds, + ) + + elif analysis.status == AnalysisStatus.FAILED: + response.error = analysis.error_message + + return response + + +@router.get( + "/analyses", + response_model=AnalysesListResponse, + summary="List user analyses", + description="Get paginated list of user's analysis history.", +) +async def list_analyses( + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], + page: int = Query(1, ge=1, description="Page number"), + per_page: int = Query(10, ge=1, le=100, description="Items per page"), + status_filter: Optional[str] = Query( + None, + alias="status", + description="Filter by status (pending, processing, completed, failed)", + ), +) -> AnalysesListResponse: + """ + List user's analyses with pagination. + + - **page**: Page number (default 1) + - **per_page**: Items per page (default 10, max 100) + - **status**: Optional status filter + """ + # Build query + query = select(Analysis).where(Analysis.user_id == current_user.id) + + if status_filter: + try: + status_enum = AnalysisStatus(status_filter) + query = query.where(Analysis.status == status_enum) + except ValueError: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "error": { + "code": "invalid_status", + "message": f"Invalid status: {status_filter}", + } + }, + ) + + # Get total count + count_query = select(func.count()).select_from(query.subquery()) + total_result = await db.execute(count_query) + total = total_result.scalar() or 0 + + # Get paginated results + offset = (page - 1) * per_page + query = query.order_by(Analysis.created_at.desc()).offset(offset).limit(per_page) + result = await db.execute(query) + analyses = result.scalars().all() + + # Build response + analysis_results = [] + for analysis in analyses: + ar = AnalysisResult( + id=analysis.id, + tic_id=analysis.tic_id, + status=analysis.status.value, + created_at=analysis.created_at, + completed_at=analysis.completed_at, + ) + + if analysis.status == AnalysisStatus.COMPLETED: + sectors = [] + if analysis.sectors_used: + sectors = [int(s) for s in analysis.sectors_used.split(",")] + + ar.result = AnalysisResultDetail( + detection=analysis.detection or False, + confidence=analysis.confidence or 0.0, + period_days=analysis.period_days, + depth_ppm=analysis.depth_ppm, + duration_hours=analysis.duration_hours, + snr=analysis.snr, + sectors_used=sectors, + processing_time_seconds=analysis.processing_time_seconds, + ) + elif analysis.status == AnalysisStatus.FAILED: + ar.error = analysis.error_message + + analysis_results.append(ar) + + return AnalysesListResponse( + analyses=analysis_results, + total=total, + page=page, + per_page=per_page, + ) + + +@router.delete( + "/analyses/{analysis_id}", + response_model=DeleteAnalysisResponse, + summary="Delete analysis", + description="Delete an analysis record.", +) +async def delete_analysis( + analysis_id: int, + db: Annotated[AsyncSession, Depends(get_db)], + current_user: Annotated[User, Depends(get_current_user)], +) -> DeleteAnalysisResponse: + """ + Delete an analysis. + + - **analysis_id**: ID of analysis to delete + + Only the owner can delete their analyses. + """ + result = await db.execute( + select(Analysis).where( + Analysis.id == analysis_id, + Analysis.user_id == current_user.id, + ) + ) + analysis = result.scalar_one_or_none() + + if analysis is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "error": { + "code": "not_found", + "message": "Analysis not found", + } + }, + ) + + await db.delete(analysis) + await db.flush() + + return DeleteAnalysisResponse(message="Analysis deleted successfully") diff --git a/src/api/routes/auth.py b/src/api/routes/auth.py new file mode 100644 index 0000000..ff1476a --- /dev/null +++ b/src/api/routes/auth.py @@ -0,0 +1,238 @@ +""" +Authentication Routes + +Endpoints for user registration, login, and password management. + +Agent: BETA +""" + +from datetime import datetime, timedelta +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.config import settings +from src.api.models.database import get_db +from src.api.models.user import User +from src.api.schemas.auth import ( + RegisterRequest, + RegisterResponse, + LoginRequest, + LoginResponse, + LogoutResponse, + ResetPasswordRequest, + ResetPasswordResponse, + UserResponse, +) +from src.api.dependencies import ( + get_password_hash, + verify_password, + create_access_token, + get_current_user, +) + +router = APIRouter() + + +@router.post( + "/register", + response_model=RegisterResponse, + status_code=status.HTTP_201_CREATED, + summary="Register a new user", + description="Create a new user account with email and password.", +) +async def register( + request: RegisterRequest, + db: Annotated[AsyncSession, Depends(get_db)], +) -> RegisterResponse: + """ + Register a new user. + + - **email**: Valid email address (must be unique) + - **password**: Strong password (min 8 chars, uppercase, lowercase, digit) + - **name**: Optional display name + """ + # Check if email already exists + result = await db.execute(select(User).where(User.email == request.email)) + existing_user = result.scalar_one_or_none() + + if existing_user: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail={ + "error": { + "code": "email_exists", + "message": "An account with this email already exists", + } + }, + ) + + # Create new user + user = User( + email=request.email, + password_hash=get_password_hash(request.password), + name=request.name, + is_active=True, + is_verified=False, # Email verification not implemented yet + ) + + db.add(user) + await db.flush() + await db.refresh(user) + + return RegisterResponse( + user=UserResponse( + id=user.id, + email=user.email, + name=user.name, + created_at=user.created_at, + ), + message="Registration successful. Please check your email for verification.", + ) + + +@router.post( + "/login", + response_model=LoginResponse, + summary="Login user", + description="Authenticate with email and password to receive JWT token.", +) +async def login( + request: LoginRequest, + db: Annotated[AsyncSession, Depends(get_db)], +) -> LoginResponse: + """ + Login and receive access token. + + - **email**: Registered email address + - **password**: Account password + + Returns JWT bearer token for API authentication. + """ + # Find user by email + result = await db.execute(select(User).where(User.email == request.email)) + user = result.scalar_one_or_none() + + if user is None or not verify_password(request.password, user.password_hash): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail={ + "error": { + "code": "invalid_credentials", + "message": "Invalid email or password", + } + }, + ) + + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "error": { + "code": "account_disabled", + "message": "Account has been deactivated", + } + }, + ) + + # Update last login + user.last_login_at = datetime.utcnow() + await db.flush() + + # Create access token + access_token = create_access_token( + data={ + "sub": user.id, + "email": user.email, + } + ) + + return LoginResponse( + access_token=access_token, + token_type="bearer", + expires_in=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60, + user=UserResponse( + id=user.id, + email=user.email, + name=user.name, + created_at=user.created_at, + ), + ) + + +@router.post( + "/logout", + response_model=LogoutResponse, + summary="Logout user", + description="Logout current user (token invalidation).", +) +async def logout( + current_user: Annotated[User, Depends(get_current_user)], +) -> LogoutResponse: + """ + Logout current user. + + Note: JWT tokens are stateless. For full logout, client should + discard the token. Token blacklisting can be implemented later + with Redis. + """ + # In a production system, we would add the token to a blacklist + # For now, just return success + return LogoutResponse(message="Logout successful") + + +@router.post( + "/reset-password", + response_model=ResetPasswordResponse, + summary="Request password reset", + description="Send password reset email to registered address.", +) +async def reset_password( + request: ResetPasswordRequest, + db: Annotated[AsyncSession, Depends(get_db)], +) -> ResetPasswordResponse: + """ + Request password reset. + + - **email**: Registered email address + + If the email exists, a reset link will be sent. + Response is the same whether email exists or not (security). + """ + # Find user by email + result = await db.execute(select(User).where(User.email == request.email)) + user = result.scalar_one_or_none() + + if user: + # TODO: Generate reset token and send email + # For now, just log (placeholder for email service) + print(f"Password reset requested for: {request.email}") + + # Always return same response (security - don't reveal if email exists) + return ResetPasswordResponse( + message="If the email exists, a reset link has been sent" + ) + + +@router.get( + "/me", + response_model=UserResponse, + summary="Get current user", + description="Get the currently authenticated user's info.", +) +async def get_me( + current_user: Annotated[User, Depends(get_current_user)], +) -> UserResponse: + """ + Get current authenticated user. + + Requires valid JWT token in Authorization header. + """ + return UserResponse( + id=current_user.id, + email=current_user.email, + name=current_user.name, + created_at=current_user.created_at, + ) diff --git a/src/api/routes/subscription.py b/src/api/routes/subscription.py new file mode 100644 index 0000000..997b781 --- /dev/null +++ b/src/api/routes/subscription.py @@ -0,0 +1,242 @@ +""" +Subscription Routes + +Endpoints for subscription management and Stripe integration. + +Agent: BETA +Note: Stripe integration handled by DELTA. These are stub endpoints. +""" + +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.config import settings +from src.api.models.database import get_db +from src.api.models.user import User +from src.api.models.subscription import Subscription, SubscriptionPlan, SubscriptionStatus +from src.api.schemas.user import ( + CreateCheckoutRequest, + CreateCheckoutResponse, + PortalResponse, + SubscriptionResponse, +) +from src.api.dependencies import get_current_user, get_user_subscription + +router = APIRouter() + + +@router.post( + "/create-checkout", + response_model=CreateCheckoutResponse, + summary="Create checkout session", + description="Create a Stripe checkout session for subscription.", +) +async def create_checkout( + request: CreateCheckoutRequest, + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> CreateCheckoutResponse: + """ + Create Stripe checkout session. + + - **plan**: Subscription plan (hobbyist_monthly or hobbyist_annual) + - **success_url**: URL to redirect after successful payment + - **cancel_url**: URL to redirect if user cancels + + Returns checkout URL to redirect user to Stripe. + + NOTE: Full Stripe integration implemented by DELTA. + This is a stub that returns a mock checkout URL. + """ + # Check if user already has active subscription + if current_user.subscription and current_user.subscription.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "error": { + "code": "already_subscribed", + "message": "User already has an active subscription. Use the customer portal to modify.", + } + }, + ) + + # Validate plan + if request.plan not in ["hobbyist_monthly", "hobbyist_annual"]: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "error": { + "code": "invalid_plan", + "message": f"Invalid plan: {request.plan}", + } + }, + ) + + # TODO: Create actual Stripe checkout session (DELTA handles) + # For now, return mock response + + if settings.STRIPE_SECRET_KEY: + # When Stripe is configured, create real checkout session + # This will be implemented when DELTA provides Stripe integration + pass + + # Mock response for development + mock_session_id = f"cs_mock_{current_user.id}_{request.plan}" + mock_checkout_url = f"https://checkout.stripe.com/mock/{mock_session_id}" + + return CreateCheckoutResponse( + checkout_url=mock_checkout_url, + session_id=mock_session_id, + ) + + +@router.get( + "/portal", + response_model=PortalResponse, + summary="Get customer portal URL", + description="Get Stripe customer portal URL for subscription management.", +) +async def get_portal( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> PortalResponse: + """ + Get Stripe customer portal URL. + + Returns URL to redirect user to manage their subscription + (update payment method, cancel, view invoices). + + NOTE: Full Stripe integration implemented by DELTA. + This is a stub that returns a mock portal URL. + """ + if not current_user.stripe_customer_id: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "error": { + "code": "no_customer", + "message": "No Stripe customer found. Subscribe first.", + } + }, + ) + + # TODO: Create actual Stripe portal session (DELTA handles) + # For now, return mock response + + if settings.STRIPE_SECRET_KEY: + # When Stripe is configured, create real portal session + pass + + # Mock response for development + mock_portal_url = f"https://billing.stripe.com/mock/{current_user.stripe_customer_id}" + + return PortalResponse(portal_url=mock_portal_url) + + +@router.get( + "/status", + response_model=SubscriptionResponse, + summary="Get subscription status", + description="Get the current user's subscription details.", +) +async def get_subscription_status( + current_user: Annotated[User, Depends(get_current_user)], + subscription: Annotated[Subscription, Depends(get_user_subscription)], +) -> SubscriptionResponse: + """ + Get current subscription status. + + Returns subscription details including usage. + """ + if subscription is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "error": { + "code": "no_subscription", + "message": "No subscription found", + } + }, + ) + + return SubscriptionResponse( + id=subscription.id, + plan=subscription.plan.value, + status=subscription.status.value, + current_period_start=subscription.current_period_start, + current_period_end=subscription.current_period_end, + analyses_this_period=subscription.analyses_this_period, + analyses_remaining=subscription.analyses_remaining, + ) + + +@router.post( + "/cancel", + summary="Cancel subscription", + description="Cancel the current subscription at period end.", +) +async def cancel_subscription( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> dict: + """ + Cancel current subscription. + + Subscription remains active until the end of the billing period. + + NOTE: Full Stripe integration implemented by DELTA. + """ + if not current_user.subscription: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail={ + "error": { + "code": "no_subscription", + "message": "No subscription to cancel", + } + }, + ) + + if current_user.subscription.status == SubscriptionStatus.CANCELED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail={ + "error": { + "code": "already_canceled", + "message": "Subscription is already canceled", + } + }, + ) + + # TODO: Cancel via Stripe API (DELTA handles) + # For now, just update local status + current_user.subscription.status = SubscriptionStatus.CANCELED + + await db.flush() + + return { + "message": "Subscription will be canceled at the end of the billing period", + "period_end": current_user.subscription.current_period_end.isoformat() + if current_user.subscription.current_period_end + else None, + } + + +# Webhook endpoint (DELTA handles actual Stripe webhooks) +@router.post( + "/webhook", + include_in_schema=False, + summary="Stripe webhook handler", +) +async def stripe_webhook(): + """ + Handle Stripe webhook events. + + NOTE: This is a placeholder. DELTA handles actual Stripe webhooks + and forwards relevant events to update subscription status. + """ + # DELTA will call internal API to update subscriptions + # based on Stripe webhook events + return {"received": True} diff --git a/src/api/routes/user.py b/src/api/routes/user.py new file mode 100644 index 0000000..50e528c --- /dev/null +++ b/src/api/routes/user.py @@ -0,0 +1,183 @@ +""" +User Routes + +Endpoints for user profile and usage management. + +Agent: BETA +""" + +from datetime import datetime +from typing import Annotated, Optional + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy import select, func +from sqlalchemy.ext.asyncio import AsyncSession + +from src.api.models.database import get_db +from src.api.models.user import User +from src.api.models.analysis import Analysis +from src.api.schemas.user import ( + UserProfile, + UsageResponse, + UpdateProfileRequest, + UpdateProfileResponse, + SubscriptionInfo, +) +from src.api.dependencies import get_current_user + +router = APIRouter() + + +@router.get( + "/profile", + response_model=UserProfile, + summary="Get user profile", + description="Get the current user's profile with subscription info.", +) +async def get_profile( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> UserProfile: + """ + Get current user's profile. + + Returns user info and active subscription details. + """ + # Build subscription info + subscription_info = None + if current_user.subscription: + subscription_info = SubscriptionInfo( + plan=current_user.subscription.plan.value, + status=current_user.subscription.status.value, + current_period_end=current_user.subscription.current_period_end, + ) + + return UserProfile( + id=current_user.id, + email=current_user.email, + name=current_user.name, + created_at=current_user.created_at, + subscription=subscription_info, + ) + + +@router.patch( + "/profile", + response_model=UpdateProfileResponse, + summary="Update user profile", + description="Update the current user's profile.", +) +async def update_profile( + request: UpdateProfileRequest, + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> UpdateProfileResponse: + """ + Update current user's profile. + + - **name**: New display name + """ + if request.name is not None: + current_user.name = request.name + + await db.flush() + await db.refresh(current_user) + + # Build subscription info + subscription_info = None + if current_user.subscription: + subscription_info = SubscriptionInfo( + plan=current_user.subscription.plan.value, + status=current_user.subscription.status.value, + current_period_end=current_user.subscription.current_period_end, + ) + + return UpdateProfileResponse( + user=UserProfile( + id=current_user.id, + email=current_user.email, + name=current_user.name, + created_at=current_user.created_at, + subscription=subscription_info, + ), + message="Profile updated successfully", + ) + + +@router.get( + "/usage", + response_model=UsageResponse, + summary="Get usage statistics", + description="Get the current user's analysis usage for the billing period.", +) +async def get_usage( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> UsageResponse: + """ + Get current usage statistics. + + Returns analyses used this period vs. limit. + """ + if current_user.subscription is None: + return UsageResponse( + analyses_this_month=0, + analyses_limit=0, + period_start=None, + period_end=None, + ) + + sub = current_user.subscription + + return UsageResponse( + analyses_this_month=sub.analyses_this_period, + analyses_limit=sub.analysis_limit, + period_start=sub.current_period_start, + period_end=sub.current_period_end, + ) + + +@router.get( + "/stats", + summary="Get user statistics", + description="Get aggregate statistics for the user's analyses.", +) +async def get_stats( + current_user: Annotated[User, Depends(get_current_user)], + db: Annotated[AsyncSession, Depends(get_db)], +) -> dict: + """ + Get aggregate statistics. + + Returns counts and averages for user's analyses. + """ + # Total analyses + total_result = await db.execute( + select(func.count(Analysis.id)).where(Analysis.user_id == current_user.id) + ) + total_analyses = total_result.scalar() or 0 + + # Completed analyses with detections + detections_result = await db.execute( + select(func.count(Analysis.id)).where( + Analysis.user_id == current_user.id, + Analysis.detection == True, + ) + ) + total_detections = detections_result.scalar() or 0 + + # Average confidence (for completed analyses) + avg_confidence_result = await db.execute( + select(func.avg(Analysis.confidence)).where( + Analysis.user_id == current_user.id, + Analysis.confidence.isnot(None), + ) + ) + avg_confidence = avg_confidence_result.scalar() or 0.0 + + return { + "total_analyses": total_analyses, + "total_detections": total_detections, + "detection_rate": total_detections / total_analyses if total_analyses > 0 else 0, + "average_confidence": round(avg_confidence, 3), + } diff --git a/src/api/schemas/__init__.py b/src/api/schemas/__init__.py new file mode 100644 index 0000000..1061a3b --- /dev/null +++ b/src/api/schemas/__init__.py @@ -0,0 +1,50 @@ +""" +Pydantic Schemas Module + +Request and response schemas for API validation. +""" + +from src.api.schemas.auth import ( + RegisterRequest, + RegisterResponse, + LoginRequest, + LoginResponse, + TokenData, +) +from src.api.schemas.analysis import ( + AnalyzeRequest, + AnalyzeResponse, + AnalysisResult, + AnalysesListResponse, + VettingResultSchema, + PeriodogramSchema, + PhaseFoldedSchema, + LightCurveSchema, +) +from src.api.schemas.user import ( + UserProfile, + UsageResponse, + SubscriptionInfo, +) + +__all__ = [ + # Auth schemas + "RegisterRequest", + "RegisterResponse", + "LoginRequest", + "LoginResponse", + "TokenData", + # Analysis schemas + "AnalyzeRequest", + "AnalyzeResponse", + "AnalysisResult", + "AnalysesListResponse", + "VettingResultSchema", + "PeriodogramSchema", + "PhaseFoldedSchema", + "LightCurveSchema", + # User schemas + "UserProfile", + "UsageResponse", + "SubscriptionInfo", +] diff --git a/src/api/schemas/analysis.py b/src/api/schemas/analysis.py new file mode 100644 index 0000000..dce0c79 --- /dev/null +++ b/src/api/schemas/analysis.py @@ -0,0 +1,148 @@ +""" +Analysis Schemas + +Pydantic models for analysis request/response validation. + +Agent: BETA +""" + +from datetime import datetime +from typing import Optional, List, Any, Dict + +from pydantic import BaseModel, Field + + +class VettingTestSchema(BaseModel): + """Individual vetting test result.""" + + test_name: str + flag: str # PASS, WARNING, FAIL + confidence: float = Field(..., ge=0.0, le=1.0) + value: Optional[float] = None + threshold: Optional[float] = None + message: str + + +class VettingResultSchema(BaseModel): + """Combined vetting results.""" + + disposition: str # PLANET_CANDIDATE, LIKELY_FALSE_POSITIVE, INCONCLUSIVE + confidence: float = Field(..., ge=0.0, le=1.0) + tests_passed: int + tests_failed: int + tests_warning: Optional[int] = 0 + odd_even: Optional[VettingTestSchema] = None + v_shape: Optional[VettingTestSchema] = None + secondary_eclipse: Optional[VettingTestSchema] = None + recommendation: str + + +class PeriodogramSchema(BaseModel): + """Periodogram data for visualization.""" + + periods: List[float] + powers: List[float] + best_period: float + best_power: Optional[float] = None + top_periods: Optional[List[float]] = None + top_powers: Optional[List[float]] = None + + +class PhaseFoldedSchema(BaseModel): + """Phase-folded light curve data.""" + + phase: List[float] + flux: List[float] + flux_err: Optional[List[float]] = None + binned_phase: List[float] + binned_flux: List[float] + binned_flux_err: Optional[List[float]] = None + + +class LightCurveSchema(BaseModel): + """Raw light curve data.""" + + time: List[float] + flux: List[float] + flux_err: Optional[List[float]] = None + quality: Optional[List[int]] = None + + +class AnalyzeRequest(BaseModel): + """Request body for analysis submission.""" + + tic_id: str = Field( + ..., + description="TESS Input Catalog ID (e.g., 'TIC 12345678' or '12345678')", + pattern=r"^(TIC\s*)?\d+$", + ) + + +class AnalyzeResponse(BaseModel): + """Response for analysis submission.""" + + analysis_id: int + status: str = "pending" + message: str = "Analysis queued" + + +class AnalysisResultDetail(BaseModel): + """Detailed analysis result when completed.""" + + detection: bool + confidence: float = Field(..., ge=0.0, le=1.0) + period_days: Optional[float] = None + depth_ppm: Optional[float] = None + duration_hours: Optional[float] = None + epoch_btjd: Optional[float] = None + snr: Optional[float] = None + + vetting: Optional[VettingResultSchema] = None + periodogram: Optional[PeriodogramSchema] = None + phase_folded: Optional[PhaseFoldedSchema] = None + raw_lightcurve: Optional[LightCurveSchema] = None + + sectors_used: List[int] = [] + processing_time_seconds: Optional[float] = None + + +class AnalysisResult(BaseModel): + """Complete analysis response.""" + + id: int + tic_id: str + status: str # pending, processing, completed, failed + created_at: datetime + completed_at: Optional[datetime] = None + + # Only present when completed + result: Optional[AnalysisResultDetail] = None + + # Only present when failed + error: Optional[str] = None + + class Config: + from_attributes = True + + +class AnalysesListResponse(BaseModel): + """Response for analysis list endpoint.""" + + analyses: List[AnalysisResult] + total: int + page: int + per_page: int + + +class DeleteAnalysisResponse(BaseModel): + """Response for analysis deletion.""" + + message: str = "Analysis deleted successfully" + + +class AnalysisStatusUpdate(BaseModel): + """Internal model for analysis status updates.""" + + status: str + result: Optional[Dict[str, Any]] = None + error: Optional[str] = None diff --git a/src/api/schemas/auth.py b/src/api/schemas/auth.py new file mode 100644 index 0000000..1ac048a --- /dev/null +++ b/src/api/schemas/auth.py @@ -0,0 +1,117 @@ +""" +Authentication Schemas + +Pydantic models for authentication request/response validation. + +Agent: BETA +""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, EmailStr, Field, field_validator + + +class RegisterRequest(BaseModel): + """Request body for user registration.""" + + email: EmailStr = Field(..., description="User email address") + password: str = Field( + ..., + min_length=8, + max_length=128, + description="Password (minimum 8 characters)", + ) + name: Optional[str] = Field( + None, + max_length=255, + description="Display name", + ) + + @field_validator("password") + @classmethod + def validate_password(cls, v: str) -> str: + """Validate password strength.""" + if len(v) < 8: + raise ValueError("Password must be at least 8 characters") + if not any(c.isupper() for c in v): + raise ValueError("Password must contain at least one uppercase letter") + if not any(c.islower() for c in v): + raise ValueError("Password must contain at least one lowercase letter") + if not any(c.isdigit() for c in v): + raise ValueError("Password must contain at least one digit") + return v + + +class UserResponse(BaseModel): + """User data in responses.""" + + id: int + email: str + name: Optional[str] + created_at: datetime + + class Config: + from_attributes = True + + +class RegisterResponse(BaseModel): + """Response for successful registration.""" + + user: UserResponse + message: str = "Registration successful" + + +class LoginRequest(BaseModel): + """Request body for user login.""" + + email: EmailStr = Field(..., description="User email address") + password: str = Field(..., description="User password") + + +class LoginResponse(BaseModel): + """Response for successful login.""" + + access_token: str + token_type: str = "bearer" + expires_in: int = Field(..., description="Token expiration time in seconds") + user: UserResponse + + +class LogoutResponse(BaseModel): + """Response for successful logout.""" + + message: str = "Logout successful" + + +class ResetPasswordRequest(BaseModel): + """Request body for password reset.""" + + email: EmailStr = Field(..., description="User email address") + + +class ResetPasswordResponse(BaseModel): + """Response for password reset request.""" + + message: str = "If the email exists, a reset link has been sent" + + +class TokenData(BaseModel): + """JWT token payload data.""" + + user_id: int + email: Optional[str] = None + + +class RefreshTokenRequest(BaseModel): + """Request body for token refresh.""" + + refresh_token: str + + +class RefreshTokenResponse(BaseModel): + """Response for token refresh.""" + + access_token: str + token_type: str = "bearer" + expires_in: int diff --git a/src/api/schemas/user.py b/src/api/schemas/user.py new file mode 100644 index 0000000..eec2d20 --- /dev/null +++ b/src/api/schemas/user.py @@ -0,0 +1,98 @@ +""" +User Schemas + +Pydantic models for user profile and subscription validation. + +Agent: BETA +""" + +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel, Field + + +class SubscriptionInfo(BaseModel): + """Subscription information in user profile.""" + + plan: Optional[str] = None # hobbyist_monthly, hobbyist_annual + status: Optional[str] = None # active, canceled, past_due + current_period_end: Optional[datetime] = None + + class Config: + from_attributes = True + + +class UserProfile(BaseModel): + """User profile response.""" + + id: int + email: str + name: Optional[str] = None + created_at: datetime + subscription: Optional[SubscriptionInfo] = None + + class Config: + from_attributes = True + + +class UsageResponse(BaseModel): + """Usage statistics response.""" + + analyses_this_month: int + analyses_limit: int + period_start: Optional[datetime] = None + period_end: Optional[datetime] = None + + +class UpdateProfileRequest(BaseModel): + """Request body for profile update.""" + + name: Optional[str] = Field(None, max_length=255) + + +class UpdateProfileResponse(BaseModel): + """Response for profile update.""" + + user: UserProfile + message: str = "Profile updated successfully" + + +class CreateCheckoutRequest(BaseModel): + """Request body for creating Stripe checkout session.""" + + plan: str = Field( + ..., + pattern=r"^(hobbyist_monthly|hobbyist_annual)$", + description="Subscription plan", + ) + success_url: str = Field(..., description="URL to redirect on success") + cancel_url: str = Field(..., description="URL to redirect on cancel") + + +class CreateCheckoutResponse(BaseModel): + """Response for checkout session creation.""" + + checkout_url: str + session_id: str + + +class PortalResponse(BaseModel): + """Response for Stripe customer portal.""" + + portal_url: str + + +class SubscriptionResponse(BaseModel): + """Full subscription details response.""" + + id: int + plan: str + status: str + current_period_start: Optional[datetime] = None + current_period_end: Optional[datetime] = None + analyses_this_period: int + analyses_remaining: int + + class Config: + from_attributes = True diff --git a/src/detection/__init__.py b/src/detection/__init__.py new file mode 100644 index 0000000..30da71d --- /dev/null +++ b/src/detection/__init__.py @@ -0,0 +1,49 @@ +""" +LARUN Detection Module +====================== +Core astronomical analysis pipeline for transit detection. + +This module provides: +- DetectionService: Main service for analyzing targets +- TransitDetector: Low-level transit detection +- BLSEngine: Box Least Squares periodogram wrapper +- PhaseFolding: Phase folding utilities + +Author: Agent ALPHA +Version: 1.0.0 (MVP) +""" + +from .models import ( + DetectionResult, + VettingResult, + PhaseFoldedData, + PeriodogramData, + LightCurveData, + TestResult, + TestFlag, + Disposition, +) +from .service import DetectionService +from .detector import TransitDetector +from .bls_engine import BLSEngine +from .phase_folder import PhaseFolding + +__all__ = [ + # Main service + "DetectionService", + # Detection components + "TransitDetector", + "BLSEngine", + "PhaseFolding", + # Data models + "DetectionResult", + "VettingResult", + "PhaseFoldedData", + "PeriodogramData", + "LightCurveData", + "TestResult", + "TestFlag", + "Disposition", +] + +__version__ = "1.0.0" diff --git a/src/detection/bls_engine.py b/src/detection/bls_engine.py new file mode 100644 index 0000000..80bda67 --- /dev/null +++ b/src/detection/bls_engine.py @@ -0,0 +1,280 @@ +""" +BLS Engine +========== +Wrapper around the existing BLS periodogram implementation. + +This module provides a clean interface for BLS analysis, wrapping +the existing src/skills/periodogram.py implementation. + +Author: Agent ALPHA +""" + +import numpy as np +from typing import Optional, Tuple, List +import logging + +from .models import PeriodogramData + +# Import existing BLS implementation +from src.skills.periodogram import ( + BLSPeriodogram, + PeriodogramResult as SkillPeriodogramResult, + TransitCandidate, +) + +logger = logging.getLogger(__name__) + + +class BLSEngine: + """ + Box Least Squares periodogram engine. + + Wraps the existing BLSPeriodogram class from src/skills/periodogram.py + and provides a simplified interface for the detection service. + + Example: + >>> engine = BLSEngine(min_period=0.5, max_period=20.0) + >>> result = engine.compute(time, flux) + >>> print(f"Best period: {result.best_period:.4f} days") + """ + + def __init__( + self, + min_period: float = 0.5, + max_period: float = 50.0, + min_duration_fraction: float = 0.01, + max_duration_fraction: float = 0.15, + n_periods: int = 10000, + n_durations: int = 10, + oversample: int = 5, + min_snr: float = 7.0, + ): + """ + Initialize the BLS engine. + + Args: + min_period: Minimum period to search (days) + max_period: Maximum period to search (days) + min_duration_fraction: Minimum transit duration as fraction of period + max_duration_fraction: Maximum transit duration as fraction of period + n_periods: Number of period samples + n_durations: Number of duration samples + oversample: Frequency oversampling factor + min_snr: Minimum SNR for detection threshold + """ + self.min_period = min_period + self.max_period = max_period + self.min_snr = min_snr + + # Initialize underlying BLS + self._bls = BLSPeriodogram( + min_period=min_period, + max_period=max_period, + min_duration=min_duration_fraction, + max_duration=max_duration_fraction, + n_periods=n_periods, + n_durations=n_durations, + oversample=oversample, + ) + + logger.info( + f"BLSEngine initialized: period=[{min_period}, {max_period}] days, " + f"min_snr={min_snr}" + ) + + def compute( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + ) -> Tuple[PeriodogramData, Optional[TransitCandidate]]: + """ + Compute BLS periodogram. + + Args: + time: Time array (days, BJD/BTJD) + flux: Normalized flux array (median ~1.0) + flux_err: Optional flux uncertainties + + Returns: + Tuple of (PeriodogramData, best TransitCandidate or None) + """ + logger.info(f"Computing BLS on {len(time)} data points") + + # Run the underlying BLS computation + skill_result = self._bls.compute( + time=time, + flux=flux, + flux_err=flux_err, + min_snr=self.min_snr, + ) + + # Convert to our PeriodogramData model + periodogram_data = self._convert_to_periodogram_data(skill_result) + + # Get best candidate + best_candidate = None + if skill_result.candidates: + best_candidate = skill_result.candidates[0] + + logger.info( + f"BLS complete: best_period={periodogram_data.best_period:.4f}d, " + f"candidates={len(skill_result.candidates)}" + ) + + return periodogram_data, best_candidate + + def _convert_to_periodogram_data( + self, result: SkillPeriodogramResult + ) -> PeriodogramData: + """Convert skill result to PeriodogramData model.""" + # Get top N periods (up to 3) + n_top = min(3, len(result.periods)) + + # Sort by power to get top periods + sorted_idx = np.argsort(result.power)[::-1][:n_top] + top_periods = result.periods[sorted_idx].tolist() + top_powers = result.power[sorted_idx].tolist() + + return PeriodogramData( + periods=result.periods.tolist(), + powers=result.power.tolist(), + best_period=float(result.best_period), + best_power=float(result.best_power), + top_periods=top_periods, + top_powers=top_powers, + ) + + def estimate_transit_parameters( + self, + time: np.ndarray, + flux: np.ndarray, + period: float, + t0: float, + ) -> dict: + """ + Estimate transit parameters at a given period. + + Args: + time: Time array + flux: Flux array + period: Orbital period (days) + t0: Mid-transit epoch + + Returns: + Dictionary with depth, duration, snr estimates + """ + # Phase fold + phase = ((time - t0) % period) / period + phase[phase > 0.5] -= 1.0 + + # Find in-transit points + in_transit = np.abs(phase) < 0.05 + out_transit = np.abs(phase) > 0.15 + + if np.sum(in_transit) < 3 or np.sum(out_transit) < 10: + return { + "depth": 0.0, + "depth_ppm": 0.0, + "duration_hours": 0.0, + "snr": 0.0, + } + + # Calculate depth + baseline = np.median(flux[out_transit]) + in_transit_flux = np.mean(flux[in_transit]) + depth = baseline - in_transit_flux + + # Estimate duration + threshold = baseline - 0.5 * depth + below_threshold = flux < threshold + duration_fraction = np.sum(below_threshold & (np.abs(phase) < 0.2)) / len(flux) + duration_days = duration_fraction * period + duration_hours = duration_days * 24 + + # Estimate SNR + noise = np.std(flux[out_transit]) + snr = depth / noise if noise > 0 else 0.0 + + return { + "depth": float(depth), + "depth_ppm": float(depth * 1e6), + "duration_hours": float(duration_hours), + "snr": float(snr), + } + + @staticmethod + def validate_light_curve( + time: np.ndarray, + flux: np.ndarray, + min_points: int = 100, + ) -> Tuple[bool, str]: + """ + Validate light curve data before BLS analysis. + + Args: + time: Time array + flux: Flux array + min_points: Minimum required data points + + Returns: + Tuple of (is_valid, error_message) + """ + # Check array lengths + if len(time) != len(flux): + return False, "Time and flux arrays have different lengths" + + # Check minimum points + valid_mask = np.isfinite(time) & np.isfinite(flux) + n_valid = np.sum(valid_mask) + + if n_valid < min_points: + return False, f"Insufficient valid data points ({n_valid} < {min_points})" + + # Check baseline + baseline = time[valid_mask].max() - time[valid_mask].min() + if baseline < 1.0: + return False, f"Insufficient time baseline ({baseline:.2f} days < 1 day)" + + # Check flux range + flux_valid = flux[valid_mask] + flux_range = flux_valid.max() - flux_valid.min() + if flux_range < 1e-6: + return False, "Flux values have no variation" + + return True, "" + + +def run_bls_analysis( + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + min_period: float = 0.5, + max_period: float = 50.0, + min_snr: float = 7.0, +) -> Tuple[PeriodogramData, Optional[dict]]: + """ + Convenience function to run BLS analysis. + + Args: + time: Time array (days) + flux: Normalized flux array + flux_err: Optional flux errors + min_period: Minimum period (days) + max_period: Maximum period (days) + min_snr: Minimum SNR threshold + + Returns: + Tuple of (PeriodogramData, candidate_dict or None) + """ + engine = BLSEngine( + min_period=min_period, + max_period=max_period, + min_snr=min_snr, + ) + + periodogram, candidate = engine.compute(time, flux, flux_err) + + candidate_dict = candidate.to_dict() if candidate else None + + return periodogram, candidate_dict diff --git a/src/detection/detector.py b/src/detection/detector.py new file mode 100644 index 0000000..9eaf79e --- /dev/null +++ b/src/detection/detector.py @@ -0,0 +1,405 @@ +""" +Transit Detector +================ +Core transit detection logic combining BLS and vetting. + +This module orchestrates the detection pipeline, using BLS for +period finding and vetting tests for validation. + +Author: Agent ALPHA +""" + +import numpy as np +from typing import Optional, Tuple, Dict, Any +import logging +import time as time_module + +from .models import ( + DetectionResult, + VettingResult, + TestResult, + TestFlag, + Disposition, + PeriodogramData, + PhaseFoldedData, + LightCurveData, +) +from .bls_engine import BLSEngine +from .phase_folder import PhaseFolding + +# Import existing vetting from skills +from src.skills.vetting import TransitVetter, VettingResult as SkillVettingResult + +logger = logging.getLogger(__name__) + + +class TransitDetector: + """ + Transit detection and vetting pipeline. + + Combines BLS periodogram analysis with vetting tests to detect + and validate transit signals in light curve data. + + Example: + >>> detector = TransitDetector() + >>> result = detector.detect(time, flux, tic_id="TIC 470710327") + >>> if result.detection: + ... print(f"Found planet candidate with period {result.period_days:.4f} days") + """ + + def __init__( + self, + min_period: float = 0.5, + max_period: float = 50.0, + min_snr: float = 7.0, + n_bins: int = 100, + run_vetting: bool = True, + ): + """ + Initialize the transit detector. + + Args: + min_period: Minimum period to search (days) + max_period: Maximum period to search (days) + min_snr: Minimum SNR threshold for detection + n_bins: Number of bins for phase-folded data + run_vetting: Whether to run vetting tests + """ + self.min_period = min_period + self.max_period = max_period + self.min_snr = min_snr + self.n_bins = n_bins + self.run_vetting = run_vetting + + # Initialize components + self.bls_engine = BLSEngine( + min_period=min_period, + max_period=max_period, + min_snr=min_snr, + ) + self.phase_folder = PhaseFolding(n_bins=n_bins) + self.vetter = TransitVetter() + + logger.info( + f"TransitDetector initialized: period=[{min_period}, {max_period}]d, " + f"min_snr={min_snr}" + ) + + def detect( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + tic_id: str = "unknown", + ra: Optional[float] = None, + dec: Optional[float] = None, + sectors: Optional[list] = None, + ) -> DetectionResult: + """ + Run full transit detection pipeline. + + Args: + time: Time array (days, BJD/BTJD) + flux: Normalized flux array + flux_err: Optional flux errors + tic_id: TESS Input Catalog ID + ra: Right ascension (optional) + dec: Declination (optional) + sectors: List of TESS sectors used (optional) + + Returns: + DetectionResult with full analysis results + """ + start_time = time_module.time() + logger.info(f"Starting detection for {tic_id}") + + # Validate input + is_valid, error_msg = self.bls_engine.validate_light_curve(time, flux) + if not is_valid: + logger.warning(f"Invalid light curve: {error_msg}") + return DetectionResult( + tic_id=tic_id, + ra=ra, + dec=dec, + detection=False, + error=error_msg, + processing_time_seconds=time_module.time() - start_time, + ) + + # Clean data + time_clean, flux_clean, flux_err_clean = self._clean_data( + time, flux, flux_err + ) + + # Create raw light curve data + raw_lightcurve = LightCurveData.from_arrays( + time_clean, flux_clean, flux_err_clean + ) + + try: + # Run BLS + periodogram, candidate = self.bls_engine.compute( + time_clean, flux_clean, flux_err_clean + ) + + # Check if we have a detection + if candidate is None: + logger.info(f"No significant transit signal found for {tic_id}") + return DetectionResult( + tic_id=tic_id, + ra=ra, + dec=dec, + detection=False, + confidence=0.0, + periodogram=periodogram, + raw_lightcurve=raw_lightcurve, + sectors_used=sectors or [], + processing_time_seconds=time_module.time() - start_time, + ) + + # Extract transit parameters + period = candidate.period + t0 = candidate.t0 + depth = candidate.depth + snr = candidate.snr + + logger.info( + f"Candidate found: P={period:.4f}d, depth={depth*1e6:.1f}ppm, " + f"SNR={snr:.1f}" + ) + + # Phase fold + phase_folded = self.phase_folder.fold( + time_clean, flux_clean, period, t0, flux_err_clean + ) + + # Estimate duration + duration_hours = self.phase_folder.estimate_duration( + phase_folded, period, depth + ) + + # Run vetting tests + vetting_result = None + if self.run_vetting: + vetting_result = self._run_vetting( + time_clean, flux_clean, period, t0 + ) + + # Compute overall confidence + confidence = self._compute_confidence(snr, vetting_result) + + # Determine if this is a detection + detection = snr >= self.min_snr + + result = DetectionResult( + tic_id=tic_id, + ra=ra, + dec=dec, + detection=detection, + confidence=confidence, + period_days=float(period), + depth_ppm=float(depth * 1e6), + duration_hours=float(duration_hours) if duration_hours > 0 else None, + epoch_btjd=float(t0), + snr=float(snr), + vetting=vetting_result, + periodogram=periodogram, + phase_folded=phase_folded, + raw_lightcurve=raw_lightcurve, + sectors_used=sectors or [], + processing_time_seconds=time_module.time() - start_time, + ) + + logger.info( + f"Detection complete for {tic_id}: " + f"detection={detection}, confidence={confidence:.2%}" + ) + + return result + + except Exception as e: + logger.error(f"Detection failed for {tic_id}: {e}") + return DetectionResult( + tic_id=tic_id, + ra=ra, + dec=dec, + detection=False, + error=str(e), + raw_lightcurve=raw_lightcurve, + processing_time_seconds=time_module.time() - start_time, + ) + + def _clean_data( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray], + ) -> Tuple[np.ndarray, np.ndarray, Optional[np.ndarray]]: + """Clean and normalize input data.""" + # Remove NaN/Inf values + mask = np.isfinite(time) & np.isfinite(flux) + if flux_err is not None: + mask &= np.isfinite(flux_err) + + time_clean = np.asarray(time[mask], dtype=np.float64) + flux_clean = np.asarray(flux[mask], dtype=np.float64) + + if flux_err is not None: + flux_err_clean = np.asarray(flux_err[mask], dtype=np.float64) + else: + flux_err_clean = None + + # Normalize flux to median = 1 + median_flux = np.median(flux_clean) + if median_flux > 0 and not np.isclose(median_flux, 1.0, rtol=0.01): + flux_clean = flux_clean / median_flux + if flux_err_clean is not None: + flux_err_clean = flux_err_clean / median_flux + + logger.debug( + f"Data cleaned: {len(time_clean)} points, " + f"baseline={time_clean.max()-time_clean.min():.1f} days" + ) + + return time_clean, flux_clean, flux_err_clean + + def _run_vetting( + self, + time: np.ndarray, + flux: np.ndarray, + period: float, + t0: float, + ) -> VettingResult: + """ + Run vetting tests and convert to our VettingResult format. + + Args: + time: Cleaned time array + flux: Cleaned flux array + period: Best-fit period + t0: Mid-transit epoch + + Returns: + VettingResult with all test results + """ + logger.debug("Running vetting tests...") + + # Use existing vetter + skill_result = self.vetter.run_all(time, flux, period, t0) + + # Convert individual test results + test_results = {} + for test in skill_result.tests: + test_name = test.test_name.lower().replace(" ", "_").replace("-", "_") + + # Determine flag + if test.passed: + flag = TestFlag.PASS + elif test.confidence < 0.5: + flag = TestFlag.FAIL + else: + flag = TestFlag.WARNING + + # Extract value and threshold from details if available + value = test.details.get("difference_sigma", 0.0) + threshold = 3.0 # Default significance threshold + + test_results[test_name] = TestResult( + test_name=test.test_name, + flag=flag, + confidence=test.confidence, + value=value, + threshold=threshold, + message=test.message, + details=test.details, + ) + + # Ensure we have all required tests (create defaults if missing) + if "odd_even_depth" not in test_results: + test_results["odd_even_depth"] = self._create_default_test("Odd-Even Depth") + if "v_shape_grazing" not in test_results and "v_shape_(grazing)" not in test_results: + test_results["v_shape"] = self._create_default_test("V-Shape (Grazing)") + if "secondary_eclipse" not in test_results: + test_results["secondary_eclipse"] = self._create_default_test("Secondary Eclipse") + + # Get the test results (handle varying key names) + odd_even = test_results.get("odd_even_depth", test_results.get("odd_even", self._create_default_test("Odd-Even Depth"))) + v_shape = test_results.get("v_shape_(grazing)", test_results.get("v_shape", self._create_default_test("V-Shape (Grazing)"))) + secondary = test_results.get("secondary_eclipse", self._create_default_test("Secondary Eclipse")) + + # Create VettingResult + return VettingResult.from_test_results(odd_even, v_shape, secondary) + + def _create_default_test(self, name: str) -> TestResult: + """Create a default inconclusive test result.""" + return TestResult( + test_name=name, + flag=TestFlag.WARNING, + confidence=0.5, + value=0.0, + threshold=0.0, + message="Test not performed - insufficient data", + details={}, + ) + + def _compute_confidence( + self, + snr: float, + vetting: Optional[VettingResult], + ) -> float: + """ + Compute overall detection confidence. + + Args: + snr: Signal-to-noise ratio from BLS + vetting: Vetting result (if available) + + Returns: + Confidence score (0.0-1.0) + """ + # SNR contribution (caps at 1.0 for SNR >= 20) + snr_confidence = min(1.0, snr / 20.0) + + if vetting is None: + return snr_confidence * 0.7 # Reduce if no vetting + + # Combine with vetting confidence + vetting_weight = 0.4 + snr_weight = 0.6 + + confidence = ( + snr_weight * snr_confidence + + vetting_weight * vetting.confidence + ) + + # Apply disposition penalty + if vetting.disposition == Disposition.LIKELY_FALSE_POSITIVE: + confidence *= 0.3 + elif vetting.disposition == Disposition.INCONCLUSIVE: + confidence *= 0.7 + + return min(1.0, max(0.0, confidence)) + + +def detect_transits( + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + tic_id: str = "unknown", + **kwargs, +) -> DetectionResult: + """ + Convenience function for transit detection. + + Args: + time: Time array (days) + flux: Flux array + flux_err: Optional flux errors + tic_id: Target identifier + **kwargs: Additional arguments for TransitDetector + + Returns: + DetectionResult + """ + detector = TransitDetector(**kwargs) + return detector.detect(time, flux, flux_err, tic_id) diff --git a/src/detection/models.py b/src/detection/models.py new file mode 100644 index 0000000..5fe2e23 --- /dev/null +++ b/src/detection/models.py @@ -0,0 +1,441 @@ +""" +Detection Data Models +===================== +Dataclasses for the detection pipeline results. + +These models define the interface contract between Agent ALPHA (detection) +and Agent BETA (API service). + +Reference: .coordination/MVP_INTERFACES.md +""" + +from dataclasses import dataclass, field +from typing import Optional, List, Dict, Any +from enum import Enum +import json + + +class Disposition(str, Enum): + """Classification disposition for a transit candidate.""" + PLANET_CANDIDATE = "PLANET_CANDIDATE" + LIKELY_FALSE_POSITIVE = "LIKELY_FALSE_POSITIVE" + INCONCLUSIVE = "INCONCLUSIVE" + + +class TestFlag(str, Enum): + """Result flag for individual vetting tests.""" + PASS = "PASS" + WARNING = "WARNING" + FAIL = "FAIL" + + +@dataclass +class TestResult: + """ + Result of a single vetting test. + + Attributes: + test_name: Name of the test (e.g., "odd_even", "v_shape") + flag: Test result flag (PASS/WARNING/FAIL) + confidence: Confidence in the result (0.0-1.0) + value: The measured value from the test + threshold: The threshold used for pass/fail decision + message: Human-readable description of result + details: Additional test-specific details + """ + test_name: str + flag: TestFlag + confidence: float # 0.0 - 1.0 + value: float + threshold: float + message: str + details: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + return { + "test_name": self.test_name, + "flag": self.flag.value, + "confidence": round(self.confidence, 4), + "value": round(self.value, 6) if self.value is not None else None, + "threshold": round(self.threshold, 6) if self.threshold is not None else None, + "message": self.message, + "details": self.details, + } + + +@dataclass +class VettingResult: + """ + Combined vetting results from all tests. + + Attributes: + disposition: Overall classification (PLANET_CANDIDATE, etc.) + confidence: Combined confidence score (0.0-1.0) + tests_passed: Number of tests that passed + tests_failed: Number of tests that failed + tests_warning: Number of tests with warnings + odd_even: Result of odd-even depth test + v_shape: Result of V-shape test + secondary_eclipse: Result of secondary eclipse search + recommendation: Human-readable recommendation + """ + disposition: Disposition + confidence: float # 0.0 - 1.0 + tests_passed: int + tests_failed: int + tests_warning: int + odd_even: TestResult + v_shape: TestResult + secondary_eclipse: TestResult + recommendation: str + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + return { + "disposition": self.disposition.value, + "confidence": round(self.confidence, 4), + "tests_passed": self.tests_passed, + "tests_failed": self.tests_failed, + "tests_warning": self.tests_warning, + "odd_even": self.odd_even.to_dict(), + "v_shape": self.v_shape.to_dict(), + "secondary_eclipse": self.secondary_eclipse.to_dict(), + "recommendation": self.recommendation, + } + + @classmethod + def from_test_results( + cls, + odd_even: TestResult, + v_shape: TestResult, + secondary_eclipse: TestResult + ) -> "VettingResult": + """ + Create VettingResult from individual test results. + + Args: + odd_even: Odd-even depth test result + v_shape: V-shape test result + secondary_eclipse: Secondary eclipse test result + + Returns: + VettingResult with computed disposition and recommendation + """ + tests = [odd_even, v_shape, secondary_eclipse] + + tests_passed = sum(1 for t in tests if t.flag == TestFlag.PASS) + tests_failed = sum(1 for t in tests if t.flag == TestFlag.FAIL) + tests_warning = sum(1 for t in tests if t.flag == TestFlag.WARNING) + + # Compute overall confidence as weighted average + weights = [0.35, 0.35, 0.30] # Weights for each test + confidence = sum(t.confidence * w for t, w in zip(tests, weights)) + + # Determine disposition + if tests_failed >= 2: + disposition = Disposition.LIKELY_FALSE_POSITIVE + recommendation = "Multiple vetting tests failed. Likely a false positive (eclipsing binary or systematic)." + elif tests_failed == 1 and tests_warning >= 1: + disposition = Disposition.INCONCLUSIVE + recommendation = "Some vetting tests raised concerns. Manual review recommended." + elif tests_failed == 1: + disposition = Disposition.INCONCLUSIVE + recommendation = f"One test failed ({[t.test_name for t in tests if t.flag == TestFlag.FAIL][0]}). Further investigation needed." + elif tests_warning >= 2: + disposition = Disposition.INCONCLUSIVE + recommendation = "Multiple warnings raised. Consider additional observations." + else: + disposition = Disposition.PLANET_CANDIDATE + recommendation = "All vetting tests passed. Strong planet candidate for follow-up." + + return cls( + disposition=disposition, + confidence=confidence, + tests_passed=tests_passed, + tests_failed=tests_failed, + tests_warning=tests_warning, + odd_even=odd_even, + v_shape=v_shape, + secondary_eclipse=secondary_eclipse, + recommendation=recommendation, + ) + + +@dataclass +class LightCurveData: + """ + Light curve data for visualization. + + Attributes: + time: Time array (BJD or BTJD) + flux: Normalized flux values + flux_err: Flux error values + quality: Quality flags for each point + """ + time: List[float] # BJD or BTJD + flux: List[float] # Normalized flux + flux_err: List[float] # Flux errors + quality: List[int] # Quality flags + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + return { + "time": self.time, + "flux": self.flux, + "flux_err": self.flux_err, + "quality": self.quality, + } + + @classmethod + def from_arrays( + cls, + time, + flux, + flux_err=None, + quality=None + ) -> "LightCurveData": + """Create from numpy arrays.""" + import numpy as np + + time_list = time.tolist() if hasattr(time, 'tolist') else list(time) + flux_list = flux.tolist() if hasattr(flux, 'tolist') else list(flux) + + if flux_err is not None: + flux_err_list = flux_err.tolist() if hasattr(flux_err, 'tolist') else list(flux_err) + else: + flux_err_list = [0.0] * len(flux_list) + + if quality is not None: + quality_list = quality.tolist() if hasattr(quality, 'tolist') else list(quality) + else: + quality_list = [0] * len(flux_list) + + return cls( + time=time_list, + flux=flux_list, + flux_err=flux_err_list, + quality=quality_list, + ) + + +@dataclass +class PhaseFoldedData: + """ + Phase-folded light curve data. + + Attributes: + phase: Phase values (-0.5 to 0.5, transit at 0) + flux: Flux values sorted by phase + flux_err: Flux error values + binned_phase: Binned phase centers + binned_flux: Binned flux values + binned_flux_err: Binned flux errors + """ + phase: List[float] # -0.5 to 0.5 + flux: List[float] + flux_err: List[float] + binned_phase: List[float] # Binned for visualization + binned_flux: List[float] + binned_flux_err: List[float] + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + return { + "phase": self.phase, + "flux": self.flux, + "flux_err": self.flux_err, + "binned_phase": self.binned_phase, + "binned_flux": self.binned_flux, + "binned_flux_err": self.binned_flux_err, + } + + +@dataclass +class PeriodogramData: + """ + BLS periodogram results. + + Attributes: + periods: Array of periods searched (days) + powers: BLS power for each period + best_period: Best-fit period (days) + best_power: Power at best period + top_periods: Top N period candidates (days) + top_powers: Powers of top candidates + """ + periods: List[float] # Days + powers: List[float] # BLS power + best_period: float + best_power: float + top_periods: List[float] # Top 3 candidates + top_powers: List[float] + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + return { + "periods": self.periods, + "powers": self.powers, + "best_period": round(self.best_period, 6), + "best_power": round(self.best_power, 6), + "top_periods": [round(p, 6) for p in self.top_periods], + "top_powers": [round(p, 6) for p in self.top_powers], + } + + +@dataclass +class DetectionResult: + """ + Complete detection analysis result. + + This is the main output from DetectionService.analyze(). + Contains all information about the detection attempt including + transit parameters, vetting results, and visualization data. + + Attributes: + tic_id: TESS Input Catalog ID + ra: Right ascension (degrees) + dec: Declination (degrees) + detection: Whether a transit was detected + confidence: Overall detection confidence (0.0-1.0) + period_days: Orbital period (days) + depth_ppm: Transit depth in parts per million + duration_hours: Transit duration (hours) + epoch_btjd: Mid-transit time (BTJD) + snr: Signal-to-noise ratio + vetting: Vetting test results + periodogram: BLS periodogram data + phase_folded: Phase-folded light curve + raw_lightcurve: Original light curve data + sectors_used: List of TESS sectors used + processing_time_seconds: Analysis duration + error: Error message if analysis failed + """ + # Target identification + tic_id: str + ra: Optional[float] = None + dec: Optional[float] = None + + # Detection result + detection: bool = False + confidence: float = 0.0 # 0.0 - 1.0 + + # Transit parameters (if detected) + period_days: Optional[float] = None + depth_ppm: Optional[float] = None + duration_hours: Optional[float] = None + epoch_btjd: Optional[float] = None + snr: Optional[float] = None + + # Detailed results + vetting: Optional[VettingResult] = None + periodogram: Optional[PeriodogramData] = None + phase_folded: Optional[PhaseFoldedData] = None + raw_lightcurve: Optional[LightCurveData] = None + + # Metadata + sectors_used: List[int] = field(default_factory=list) + processing_time_seconds: float = 0.0 + error: Optional[str] = None + + def to_dict(self) -> Dict[str, Any]: + """Convert to JSON-serializable dictionary.""" + result = { + "tic_id": self.tic_id, + "ra": self.ra, + "dec": self.dec, + "detection": self.detection, + "confidence": round(self.confidence, 4), + "period_days": round(self.period_days, 6) if self.period_days else None, + "depth_ppm": round(self.depth_ppm, 2) if self.depth_ppm else None, + "duration_hours": round(self.duration_hours, 4) if self.duration_hours else None, + "epoch_btjd": round(self.epoch_btjd, 6) if self.epoch_btjd else None, + "snr": round(self.snr, 2) if self.snr else None, + "vetting": self.vetting.to_dict() if self.vetting else None, + "periodogram": self.periodogram.to_dict() if self.periodogram else None, + "phase_folded": self.phase_folded.to_dict() if self.phase_folded else None, + "raw_lightcurve": self.raw_lightcurve.to_dict() if self.raw_lightcurve else None, + "sectors_used": self.sectors_used, + "processing_time_seconds": round(self.processing_time_seconds, 3), + "error": self.error, + } + return result + + def to_json(self) -> str: + """Convert to JSON string.""" + return json.dumps(self.to_dict(), indent=2) + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "DetectionResult": + """Create from dictionary.""" + # Handle nested objects + vetting = None + if data.get("vetting"): + # Would need to reconstruct VettingResult + pass + + return cls( + tic_id=data["tic_id"], + ra=data.get("ra"), + dec=data.get("dec"), + detection=data.get("detection", False), + confidence=data.get("confidence", 0.0), + period_days=data.get("period_days"), + depth_ppm=data.get("depth_ppm"), + duration_hours=data.get("duration_hours"), + epoch_btjd=data.get("epoch_btjd"), + snr=data.get("snr"), + sectors_used=data.get("sectors_used", []), + processing_time_seconds=data.get("processing_time_seconds", 0.0), + error=data.get("error"), + ) + + def summary(self) -> str: + """Generate human-readable summary.""" + lines = [ + f"Detection Result for {self.tic_id}", + "=" * 40, + ] + + if self.error: + lines.append(f"ERROR: {self.error}") + return "\n".join(lines) + + if self.detection: + lines.append(f"TRANSIT DETECTED (confidence: {self.confidence:.1%})") + lines.append(f" Period: {self.period_days:.4f} days") + lines.append(f" Depth: {self.depth_ppm:.1f} ppm") + if self.duration_hours: + lines.append(f" Duration: {self.duration_hours:.2f} hours") + if self.snr: + lines.append(f" SNR: {self.snr:.1f}") + if self.vetting: + lines.append(f" Disposition: {self.vetting.disposition.value}") + lines.append(f" Recommendation: {self.vetting.recommendation}") + else: + lines.append("NO TRANSIT DETECTED") + + lines.append(f"Processing time: {self.processing_time_seconds:.2f}s") + + return "\n".join(lines) + + +# Custom exceptions for the detection module +class DetectionError(Exception): + """Base exception for detection errors.""" + pass + + +class TargetNotFoundError(DetectionError): + """Raised when target TIC ID is not found in MAST.""" + pass + + +class DataUnavailableError(DetectionError): + """Raised when no light curve data is available.""" + pass + + +class AnalysisError(DetectionError): + """Raised when analysis fails.""" + pass diff --git a/src/detection/phase_folder.py b/src/detection/phase_folder.py new file mode 100644 index 0000000..6b96f34 --- /dev/null +++ b/src/detection/phase_folder.py @@ -0,0 +1,251 @@ +""" +Phase Folding Module +==================== +Utilities for phase folding light curves. + +This module wraps the existing phase folding functions from +src/skills/periodogram.py and provides enhanced functionality. + +Author: Agent ALPHA +""" + +import numpy as np +from typing import Optional, Tuple +import logging + +from .models import PhaseFoldedData + +# Import existing phase folding functions +from src.skills.periodogram import phase_fold, bin_phase_curve + +logger = logging.getLogger(__name__) + + +class PhaseFolding: + """ + Phase folding utilities for transit analysis. + + Example: + >>> folder = PhaseFolding(n_bins=100) + >>> result = folder.fold(time, flux, period=3.5, t0=2458000.0) + >>> print(f"Transit depth estimate: {result.estimate_depth():.1f} ppm") + """ + + def __init__(self, n_bins: int = 100): + """ + Initialize phase folder. + + Args: + n_bins: Number of bins for binned output + """ + self.n_bins = n_bins + logger.debug(f"PhaseFolding initialized with {n_bins} bins") + + def fold( + self, + time: np.ndarray, + flux: np.ndarray, + period: float, + t0: float = 0.0, + flux_err: Optional[np.ndarray] = None, + ) -> PhaseFoldedData: + """ + Phase fold a light curve. + + Args: + time: Time array (days, BJD/BTJD) + flux: Flux array + period: Orbital period (days) + t0: Reference epoch / mid-transit time + flux_err: Optional flux errors + + Returns: + PhaseFoldedData with raw and binned phase-folded data + """ + logger.info(f"Phase folding {len(time)} points at period={period:.4f}d") + + # Use existing phase_fold function + phase, flux_sorted = phase_fold(time, flux, period, t0) + + # Handle flux errors + if flux_err is not None: + # Sort errors the same way as flux + sort_idx = np.argsort(((time - t0) % period) / period) + phase_for_sort = ((time - t0) % period) / period + phase_for_sort[phase_for_sort > 0.5] -= 1.0 + sort_idx = np.argsort(phase_for_sort) + flux_err_sorted = flux_err[sort_idx] + else: + flux_err_sorted = np.full_like(flux_sorted, np.std(flux_sorted)) + + # Bin the phase-folded data + bin_centers, bin_flux, bin_err = bin_phase_curve( + phase, flux_sorted, self.n_bins + ) + + # Handle NaN in binned data + valid_bins = ~np.isnan(bin_flux) + if not np.all(valid_bins): + # Interpolate over NaN bins + bin_flux = np.interp( + bin_centers, + bin_centers[valid_bins], + bin_flux[valid_bins] + ) + bin_err = np.interp( + bin_centers, + bin_centers[valid_bins], + bin_err[valid_bins] + ) + + return PhaseFoldedData( + phase=phase.tolist(), + flux=flux_sorted.tolist(), + flux_err=flux_err_sorted.tolist(), + binned_phase=bin_centers.tolist(), + binned_flux=bin_flux.tolist(), + binned_flux_err=bin_err.tolist(), + ) + + def estimate_depth(self, phase_data: PhaseFoldedData) -> float: + """ + Estimate transit depth from phase-folded data. + + Args: + phase_data: Phase-folded light curve data + + Returns: + Transit depth in fractional units + """ + binned_flux = np.array(phase_data.binned_flux) + binned_phase = np.array(phase_data.binned_phase) + + # Get out-of-transit baseline + out_of_transit = np.abs(binned_phase) > 0.15 + baseline = np.median(binned_flux[out_of_transit]) + + # Get minimum flux (transit center) + min_flux = np.min(binned_flux) + + depth = baseline - min_flux + return float(depth) + + def estimate_duration( + self, + phase_data: PhaseFoldedData, + period: float, + depth: Optional[float] = None + ) -> float: + """ + Estimate transit duration from phase-folded data. + + Args: + phase_data: Phase-folded light curve data + period: Orbital period (days) + depth: Transit depth (if known, otherwise estimated) + + Returns: + Transit duration in hours + """ + binned_flux = np.array(phase_data.binned_flux) + binned_phase = np.array(phase_data.binned_phase) + + if depth is None: + depth = self.estimate_depth(phase_data) + + # Get baseline + out_of_transit = np.abs(binned_phase) > 0.15 + baseline = np.median(binned_flux[out_of_transit]) + + # Find points below half-depth + threshold = baseline - 0.5 * depth + in_transit = binned_flux < threshold + + if np.sum(in_transit) < 2: + return 0.0 + + # Duration is range of phases in transit + transit_phases = binned_phase[in_transit] + duration_phase = np.max(transit_phases) - np.min(transit_phases) + duration_days = duration_phase * period + duration_hours = duration_days * 24 + + return float(duration_hours) + + def find_transit_center(self, phase_data: PhaseFoldedData) -> float: + """ + Find the phase of transit center. + + Args: + phase_data: Phase-folded light curve data + + Returns: + Phase of transit center (-0.5 to 0.5) + """ + binned_flux = np.array(phase_data.binned_flux) + binned_phase = np.array(phase_data.binned_phase) + + min_idx = np.argmin(binned_flux) + return float(binned_phase[min_idx]) + + +def phase_fold_lightcurve( + time: np.ndarray, + flux: np.ndarray, + period: float, + t0: float = 0.0, + flux_err: Optional[np.ndarray] = None, + n_bins: int = 100, +) -> PhaseFoldedData: + """ + Convenience function to phase fold a light curve. + + Args: + time: Time array (days) + flux: Flux array + period: Orbital period (days) + t0: Reference epoch + flux_err: Optional flux errors + n_bins: Number of bins + + Returns: + PhaseFoldedData with folded light curve + """ + folder = PhaseFolding(n_bins=n_bins) + return folder.fold(time, flux, period, t0, flux_err) + + +def compute_transit_mask( + time: np.ndarray, + period: float, + t0: float, + duration_hours: float, + buffer_factor: float = 1.5, +) -> np.ndarray: + """ + Compute a boolean mask for in-transit points. + + Args: + time: Time array (days) + period: Orbital period (days) + t0: Mid-transit epoch + duration_hours: Transit duration (hours) + buffer_factor: Factor to expand transit window + + Returns: + Boolean array marking in-transit points + """ + duration_days = duration_hours / 24.0 + half_duration = duration_days / 2.0 * buffer_factor + + # Phase fold + phase = ((time - t0) % period) / period + phase[phase > 0.5] -= 1.0 + + # Convert duration to phase + duration_phase = half_duration / period + + # Mark in-transit points + in_transit = np.abs(phase) < duration_phase + + return in_transit diff --git a/src/detection/service.py b/src/detection/service.py new file mode 100644 index 0000000..11a359d --- /dev/null +++ b/src/detection/service.py @@ -0,0 +1,487 @@ +""" +Detection Service +================= +Main service class for transit detection. + +This is the primary interface for Agent BETA to use. It provides +async methods for analyzing TESS targets and light curves. + +Author: Agent ALPHA +Reference: .coordination/MVP_INTERFACES.md +""" + +import asyncio +import numpy as np +from abc import ABC, abstractmethod +from typing import Optional, Dict, Any, List +import logging +import time as time_module + +from .models import ( + DetectionResult, + LightCurveData, + TargetNotFoundError, + DataUnavailableError, + AnalysisError, +) +from .detector import TransitDetector + +logger = logging.getLogger(__name__) + + +class IDetectionService(ABC): + """ + Interface for detection service. + + This abstract base class defines the contract that DetectionService + must implement. Agent BETA should code against this interface. + """ + + @abstractmethod + async def analyze(self, tic_id: str) -> DetectionResult: + """ + Analyze a target by TIC ID. + + Args: + tic_id: TESS Input Catalog ID (e.g., "TIC 12345678" or "12345678") + + Returns: + DetectionResult with all analysis data + + Raises: + TargetNotFoundError: If TIC ID not found in MAST + DataUnavailableError: If no light curve data available + AnalysisError: If analysis fails + """ + pass + + @abstractmethod + async def analyze_lightcurve( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + tic_id: Optional[str] = None, + ) -> DetectionResult: + """ + Analyze provided light curve data directly. + + Args: + time: Time array (BJD or BTJD) + flux: Normalized flux array + flux_err: Optional flux error array + tic_id: Optional TIC ID for metadata + + Returns: + DetectionResult with all analysis data + """ + pass + + @abstractmethod + async def get_status(self) -> Dict[str, Any]: + """Get service health status.""" + pass + + +class DetectionService(IDetectionService): + """ + Main detection service for transit analysis. + + This service wraps the TransitDetector and provides async methods + for analyzing targets. It handles data fetching from MAST and + coordinates the full detection pipeline. + + Example: + >>> service = DetectionService() + >>> result = await service.analyze("TIC 470710327") + >>> if result.detection: + ... print(f"Planet candidate: P={result.period_days:.4f}d") + + For direct light curve analysis: + >>> result = await service.analyze_lightcurve(time, flux) + """ + + def __init__( + self, + min_period: float = 0.5, + max_period: float = 50.0, + min_snr: float = 7.0, + cache_dir: str = "data/cache", + ): + """ + Initialize the detection service. + + Args: + min_period: Minimum search period (days) + max_period: Maximum search period (days) + min_snr: Minimum SNR threshold + cache_dir: Directory for caching data + """ + self.min_period = min_period + self.max_period = max_period + self.min_snr = min_snr + self.cache_dir = cache_dir + + # Initialize detector + self.detector = TransitDetector( + min_period=min_period, + max_period=max_period, + min_snr=min_snr, + ) + + # Track service stats + self._analyses_count = 0 + self._start_time = time_module.time() + + logger.info( + f"DetectionService initialized: period=[{min_period}, {max_period}]d" + ) + + async def analyze(self, tic_id: str) -> DetectionResult: + """ + Analyze a target by TIC ID. + + Fetches light curve data from MAST and runs the full detection + pipeline including BLS periodogram and vetting tests. + + Args: + tic_id: TESS Input Catalog ID (e.g., "TIC 12345678" or "12345678") + + Returns: + DetectionResult with all analysis data + + Raises: + TargetNotFoundError: If TIC ID not found in MAST + DataUnavailableError: If no light curve data available + AnalysisError: If analysis fails + """ + start_time = time_module.time() + logger.info(f"Starting analysis for {tic_id}") + + # Normalize TIC ID + tic_id = self._normalize_tic_id(tic_id) + + try: + # Fetch light curve data + time, flux, flux_err, metadata = await self._fetch_lightcurve(tic_id) + + # Run detection + result = await self.analyze_lightcurve( + time=time, + flux=flux, + flux_err=flux_err, + tic_id=tic_id, + ) + + # Update with metadata + result.ra = metadata.get("ra") + result.dec = metadata.get("dec") + result.sectors_used = metadata.get("sectors", []) + + self._analyses_count += 1 + + logger.info( + f"Analysis complete for {tic_id}: " + f"detection={result.detection}, " + f"time={time_module.time() - start_time:.2f}s" + ) + + return result + + except TargetNotFoundError: + raise + except DataUnavailableError: + raise + except Exception as e: + logger.error(f"Analysis failed for {tic_id}: {e}") + raise AnalysisError(f"Analysis failed: {e}") from e + + async def analyze_lightcurve( + self, + time: np.ndarray, + flux: np.ndarray, + flux_err: Optional[np.ndarray] = None, + tic_id: Optional[str] = None, + ) -> DetectionResult: + """ + Analyze provided light curve data directly. + + Use this method when you already have light curve data and + don't need to fetch from MAST. + + Args: + time: Time array (BJD or BTJD) + flux: Normalized flux array + flux_err: Optional flux error array + tic_id: Optional TIC ID for metadata + + Returns: + DetectionResult with all analysis data + """ + # Ensure arrays are numpy + time = np.asarray(time, dtype=np.float64) + flux = np.asarray(flux, dtype=np.float64) + if flux_err is not None: + flux_err = np.asarray(flux_err, dtype=np.float64) + + # Run detection in executor to not block event loop + loop = asyncio.get_event_loop() + result = await loop.run_in_executor( + None, + self.detector.detect, + time, + flux, + flux_err, + tic_id or "direct_input", + ) + + self._analyses_count += 1 + + return result + + async def analyze_batch( + self, + tic_ids: List[str], + max_concurrent: int = 5, + ) -> List[DetectionResult]: + """ + Analyze multiple targets in parallel. + + Args: + tic_ids: List of TIC IDs to analyze + max_concurrent: Maximum concurrent analyses + + Returns: + List of DetectionResult objects + """ + semaphore = asyncio.Semaphore(max_concurrent) + + async def analyze_with_limit(tic_id: str) -> DetectionResult: + async with semaphore: + try: + return await self.analyze(tic_id) + except Exception as e: + logger.error(f"Batch analysis failed for {tic_id}: {e}") + return DetectionResult( + tic_id=tic_id, + detection=False, + error=str(e), + ) + + tasks = [analyze_with_limit(tic_id) for tic_id in tic_ids] + results = await asyncio.gather(*tasks) + + return list(results) + + async def get_status(self) -> Dict[str, Any]: + """ + Get service health status. + + Returns: + Dictionary with service status information + """ + uptime = time_module.time() - self._start_time + + return { + "status": "healthy", + "version": "1.0.0", + "uptime_seconds": round(uptime, 2), + "analyses_completed": self._analyses_count, + "config": { + "min_period": self.min_period, + "max_period": self.max_period, + "min_snr": self.min_snr, + }, + } + + async def _fetch_lightcurve( + self, tic_id: str + ) -> tuple[np.ndarray, np.ndarray, Optional[np.ndarray], Dict[str, Any]]: + """ + Fetch light curve data from MAST. + + Args: + tic_id: Normalized TIC ID + + Returns: + Tuple of (time, flux, flux_err, metadata) + + Raises: + TargetNotFoundError: If target not found + DataUnavailableError: If no data available + """ + logger.info(f"Fetching light curve for {tic_id}") + + try: + # Try to use lightkurve + from lightkurve import search_lightcurve + + # Search for TESS data + search_result = search_lightcurve( + tic_id, + mission="TESS", + author="SPOC", # Prefer SPOC pipeline + ) + + if len(search_result) == 0: + # Try without SPOC filter + search_result = search_lightcurve(tic_id, mission="TESS") + + if len(search_result) == 0: + raise DataUnavailableError(f"No TESS data found for {tic_id}") + + # Download all available sectors + logger.info(f"Found {len(search_result)} sectors for {tic_id}") + + # Download and stitch light curves + lc_collection = search_result.download_all() + + if lc_collection is None or len(lc_collection) == 0: + raise DataUnavailableError(f"Failed to download data for {tic_id}") + + # Stitch together + lc = lc_collection.stitch() + + # Use PDCSAP flux if available (de-trended) + if hasattr(lc, 'flux'): + flux = lc.flux.value + time = lc.time.value + else: + raise DataUnavailableError("No flux data in light curve") + + # Get flux errors + flux_err = None + if hasattr(lc, 'flux_err') and lc.flux_err is not None: + flux_err = lc.flux_err.value + + # Get metadata + sectors = [] + for item in search_result: + if hasattr(item, 'sequence_number'): + sectors.append(int(item.sequence_number)) + elif hasattr(item, 'sector'): + sectors.append(int(item.sector)) + + # Get coordinates if available + ra = None + dec = None + if hasattr(lc, 'meta'): + ra = lc.meta.get('RA') + dec = lc.meta.get('DEC') + + metadata = { + "sectors": list(set(sectors)), + "ra": ra, + "dec": dec, + "n_points": len(time), + "baseline_days": float(time.max() - time.min()) if len(time) > 0 else 0, + } + + logger.info( + f"Downloaded {len(time)} points from {len(metadata['sectors'])} sectors" + ) + + return time, flux, flux_err, metadata + + except ImportError: + logger.warning("lightkurve not available, using fallback") + return await self._fetch_lightcurve_fallback(tic_id) + + except Exception as e: + if "not found" in str(e).lower(): + raise TargetNotFoundError(f"Target {tic_id} not found in MAST") + if "no data" in str(e).lower(): + raise DataUnavailableError(f"No data available for {tic_id}") + raise + + async def _fetch_lightcurve_fallback( + self, tic_id: str + ) -> tuple[np.ndarray, np.ndarray, Optional[np.ndarray], Dict[str, Any]]: + """ + Fallback light curve fetch using astroquery. + + Used when lightkurve is not available. + """ + try: + from src.pipeline.nasa_pipeline import NASADataPipeline + + pipeline = NASADataPipeline({}, cache_dir=self.cache_dir) + results = await pipeline.fetch_tess_lightcurve(tic_id) + + if not results: + raise DataUnavailableError(f"No data found for {tic_id}") + + # Combine all results + all_time = [] + all_flux = [] + all_flux_err = [] + + for data in results: + if data.time is not None: + all_time.extend(data.time) + all_flux.extend(data.flux) + if data.flux_error is not None: + all_flux_err.extend(data.flux_error) + + time = np.array(all_time) + flux = np.array(all_flux) + flux_err = np.array(all_flux_err) if all_flux_err else None + + metadata = { + "sectors": [], + "ra": None, + "dec": None, + "n_points": len(time), + } + + return time, flux, flux_err, metadata + + except Exception as e: + raise DataUnavailableError(f"Failed to fetch data: {e}") + + def _normalize_tic_id(self, tic_id: str) -> str: + """ + Normalize TIC ID format. + + Accepts formats: "TIC 12345678", "TIC12345678", "12345678" + Returns: "TIC 12345678" + """ + # Remove common prefixes and whitespace + tic_id = tic_id.strip().upper() + + if tic_id.startswith("TIC"): + tic_id = tic_id[3:].strip() + + # Ensure it's numeric + if not tic_id.isdigit(): + raise ValueError(f"Invalid TIC ID format: {tic_id}") + + return f"TIC {tic_id}" + + +# Factory function for creating service with config +def create_detection_service( + config: Optional[Dict[str, Any]] = None +) -> DetectionService: + """ + Create a DetectionService with configuration. + + Args: + config: Optional configuration dictionary with keys: + - min_period: Minimum search period (default 0.5) + - max_period: Maximum search period (default 50.0) + - min_snr: Minimum SNR threshold (default 7.0) + - cache_dir: Cache directory (default "data/cache") + + Returns: + Configured DetectionService instance + """ + if config is None: + config = {} + + return DetectionService( + min_period=config.get("min_period", 0.5), + max_period=config.get("max_period", 50.0), + min_snr=config.get("min_snr", 7.0), + cache_dir=config.get("cache_dir", "data/cache"), + ) diff --git a/tests/test_api/__init__.py b/tests/test_api/__init__.py new file mode 100644 index 0000000..e8b60b3 --- /dev/null +++ b/tests/test_api/__init__.py @@ -0,0 +1,5 @@ +""" +API Test Suite + +Tests for the LARUN REST API endpoints. +""" diff --git a/tests/test_detection/__init__.py b/tests/test_detection/__init__.py new file mode 100644 index 0000000..c8882ff --- /dev/null +++ b/tests/test_detection/__init__.py @@ -0,0 +1,3 @@ +""" +Unit tests for the detection module. +""" diff --git a/tests/test_detection/test_models.py b/tests/test_detection/test_models.py new file mode 100644 index 0000000..9385226 --- /dev/null +++ b/tests/test_detection/test_models.py @@ -0,0 +1,321 @@ +""" +Tests for detection models. +""" + +import pytest +import json +from src.detection.models import ( + DetectionResult, + VettingResult, + TestResult, + TestFlag, + Disposition, + PhaseFoldedData, + PeriodogramData, + LightCurveData, + DetectionError, + TargetNotFoundError, + DataUnavailableError, + AnalysisError, +) + + +class TestTestFlag: + """Tests for TestFlag enum.""" + + def test_values(self): + assert TestFlag.PASS.value == "PASS" + assert TestFlag.WARNING.value == "WARNING" + assert TestFlag.FAIL.value == "FAIL" + + def test_string_conversion(self): + assert str(TestFlag.PASS) == "TestFlag.PASS" + + +class TestDisposition: + """Tests for Disposition enum.""" + + def test_values(self): + assert Disposition.PLANET_CANDIDATE.value == "PLANET_CANDIDATE" + assert Disposition.LIKELY_FALSE_POSITIVE.value == "LIKELY_FALSE_POSITIVE" + assert Disposition.INCONCLUSIVE.value == "INCONCLUSIVE" + + +class TestTestResult: + """Tests for TestResult dataclass.""" + + def test_creation(self): + result = TestResult( + test_name="odd_even", + flag=TestFlag.PASS, + confidence=0.95, + value=1.2, + threshold=3.0, + message="Depths consistent", + details={"depth_odd": 100, "depth_even": 102}, + ) + assert result.test_name == "odd_even" + assert result.flag == TestFlag.PASS + assert result.confidence == 0.95 + + def test_to_dict(self): + result = TestResult( + test_name="v_shape", + flag=TestFlag.WARNING, + confidence=0.6, + value=0.35, + threshold=0.3, + message="Slight V-shape detected", + ) + d = result.to_dict() + assert d["test_name"] == "v_shape" + assert d["flag"] == "WARNING" + assert d["confidence"] == 0.6 + + +class TestVettingResult: + """Tests for VettingResult dataclass.""" + + @pytest.fixture + def test_results(self): + """Create test results for testing.""" + odd_even = TestResult( + test_name="Odd-Even", + flag=TestFlag.PASS, + confidence=0.9, + value=1.0, + threshold=3.0, + message="Pass", + ) + v_shape = TestResult( + test_name="V-Shape", + flag=TestFlag.PASS, + confidence=0.85, + value=0.1, + threshold=0.3, + message="Pass", + ) + secondary = TestResult( + test_name="Secondary Eclipse", + flag=TestFlag.PASS, + confidence=0.8, + value=0.5, + threshold=3.0, + message="Pass", + ) + return odd_even, v_shape, secondary + + def test_from_test_results_all_pass(self, test_results): + odd_even, v_shape, secondary = test_results + result = VettingResult.from_test_results(odd_even, v_shape, secondary) + + assert result.disposition == Disposition.PLANET_CANDIDATE + assert result.tests_passed == 3 + assert result.tests_failed == 0 + assert "Strong planet candidate" in result.recommendation + + def test_from_test_results_some_fail(self, test_results): + odd_even, v_shape, secondary = test_results + odd_even.flag = TestFlag.FAIL + odd_even.confidence = 0.3 + v_shape.flag = TestFlag.FAIL + v_shape.confidence = 0.2 + + result = VettingResult.from_test_results(odd_even, v_shape, secondary) + + assert result.disposition == Disposition.LIKELY_FALSE_POSITIVE + assert result.tests_failed == 2 + + def test_to_dict(self, test_results): + odd_even, v_shape, secondary = test_results + result = VettingResult.from_test_results(odd_even, v_shape, secondary) + d = result.to_dict() + + assert "disposition" in d + assert "confidence" in d + assert "odd_even" in d + assert d["disposition"] == "PLANET_CANDIDATE" + + +class TestLightCurveData: + """Tests for LightCurveData dataclass.""" + + def test_creation(self): + lc = LightCurveData( + time=[0.0, 1.0, 2.0], + flux=[1.0, 0.99, 1.0], + flux_err=[0.001, 0.001, 0.001], + quality=[0, 0, 0], + ) + assert len(lc.time) == 3 + assert len(lc.flux) == 3 + + def test_from_arrays(self): + import numpy as np + + time = np.array([0.0, 1.0, 2.0]) + flux = np.array([1.0, 0.99, 1.0]) + + lc = LightCurveData.from_arrays(time, flux) + assert lc.time == [0.0, 1.0, 2.0] + assert lc.flux == [1.0, 0.99, 1.0] + + def test_to_dict(self): + lc = LightCurveData( + time=[0.0, 1.0], + flux=[1.0, 0.99], + flux_err=[0.001, 0.001], + quality=[0, 0], + ) + d = lc.to_dict() + assert "time" in d + assert "flux" in d + + +class TestPhaseFoldedData: + """Tests for PhaseFoldedData dataclass.""" + + def test_creation(self): + data = PhaseFoldedData( + phase=[-0.5, 0.0, 0.5], + flux=[1.0, 0.99, 1.0], + flux_err=[0.001, 0.001, 0.001], + binned_phase=[-0.25, 0.0, 0.25], + binned_flux=[1.0, 0.99, 1.0], + binned_flux_err=[0.0005, 0.0005, 0.0005], + ) + assert len(data.phase) == 3 + assert len(data.binned_phase) == 3 + + +class TestPeriodogramData: + """Tests for PeriodogramData dataclass.""" + + def test_creation(self): + data = PeriodogramData( + periods=[1.0, 2.0, 3.0], + powers=[0.1, 0.5, 0.2], + best_period=2.0, + best_power=0.5, + top_periods=[2.0, 3.0, 1.0], + top_powers=[0.5, 0.2, 0.1], + ) + assert data.best_period == 2.0 + assert len(data.top_periods) == 3 + + def test_to_dict(self): + data = PeriodogramData( + periods=[1.0, 2.0], + powers=[0.1, 0.5], + best_period=2.0, + best_power=0.5, + top_periods=[2.0], + top_powers=[0.5], + ) + d = data.to_dict() + assert d["best_period"] == 2.0 + + +class TestDetectionResult: + """Tests for DetectionResult dataclass.""" + + def test_no_detection(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=False, + ) + assert result.tic_id == "TIC 12345678" + assert result.detection is False + assert result.period_days is None + + def test_with_detection(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=True, + confidence=0.85, + period_days=3.5, + depth_ppm=1000, + duration_hours=2.5, + snr=15.0, + ) + assert result.detection is True + assert result.period_days == 3.5 + assert result.snr == 15.0 + + def test_to_dict(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=True, + confidence=0.85, + period_days=3.5, + ) + d = result.to_dict() + assert d["tic_id"] == "TIC 12345678" + assert d["detection"] is True + assert d["period_days"] == 3.5 + + def test_to_json(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=True, + ) + json_str = result.to_json() + parsed = json.loads(json_str) + assert parsed["tic_id"] == "TIC 12345678" + + def test_summary(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=True, + confidence=0.85, + period_days=3.5, + depth_ppm=1000, + snr=15.0, + ) + summary = result.summary() + assert "TIC 12345678" in summary + assert "DETECTED" in summary + assert "3.5" in summary + + def test_summary_no_detection(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=False, + ) + summary = result.summary() + assert "NO TRANSIT" in summary + + def test_error_summary(self): + result = DetectionResult( + tic_id="TIC 12345678", + detection=False, + error="Insufficient data", + ) + summary = result.summary() + assert "ERROR" in summary + assert "Insufficient data" in summary + + +class TestExceptions: + """Tests for custom exceptions.""" + + def test_detection_error(self): + with pytest.raises(DetectionError): + raise DetectionError("Test error") + + def test_target_not_found_error(self): + with pytest.raises(TargetNotFoundError): + raise TargetNotFoundError("TIC not found") + + def test_data_unavailable_error(self): + with pytest.raises(DataUnavailableError): + raise DataUnavailableError("No data") + + def test_analysis_error(self): + with pytest.raises(AnalysisError): + raise AnalysisError("Analysis failed") + + def test_inheritance(self): + assert issubclass(TargetNotFoundError, DetectionError) + assert issubclass(DataUnavailableError, DetectionError) + assert issubclass(AnalysisError, DetectionError) diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/web/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000..05e726d --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,18 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + // Override default ignores of eslint-config-next. + globalIgnores([ + // Default ignores of eslint-config-next: + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), +]); + +export default eslintConfig; diff --git a/web/next.config.ts b/web/next.config.ts new file mode 100644 index 0000000..e9ffa30 --- /dev/null +++ b/web/next.config.ts @@ -0,0 +1,7 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + /* config options here */ +}; + +export default nextConfig; diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..831c367 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,6538 @@ +{ + "name": "web", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.1.0", + "dependencies": { + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "typescript": "^5" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.0.tgz", + "integrity": "sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==", + "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.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "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.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "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.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "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.2", + "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/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "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/@img/colour": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", + "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "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": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@next/env": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", + "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "license": "MIT" + }, + "node_modules/@next/eslint-plugin-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", + "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-glob": "3.3.1" + } + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", + "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", + "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", + "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", + "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", + "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", + "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", + "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", + "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nolyfill/is-core-module": { + "version": "1.0.39", + "resolved": "https://registry.npmjs.org/@nolyfill/is-core-module/-/is-core-module-1.0.39.tgz", + "integrity": "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.4.0" + } + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/postcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", + "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "postcss": "^8.4.41", + "tailwindcss": "4.1.18" + } + }, + "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/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/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/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.10.tgz", + "integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==", + "dev": 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.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz", + "integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/type-utils": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.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.54.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.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.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz", + "integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz", + "integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.54.0", + "@typescript-eslint/types": "^8.54.0", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz", + "integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.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/tsconfig-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz", + "integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==", + "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.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz", + "integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz", + "integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==", + "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.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz", + "integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.54.0", + "@typescript-eslint/tsconfig-utils": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/visitor-keys": "8.54.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.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.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz", + "integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.54.0", + "@typescript-eslint/types": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz", + "integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.54.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "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.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "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/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "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.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.30001767", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001767.tgz", + "integrity": "sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==", + "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/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "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/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==", + "dev": true, + "license": "MIT" + }, + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "devOptional": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "dev": true, + "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.283", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.283.tgz", + "integrity": "sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.4", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", + "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@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.12.4", + "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.2", + "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-config-next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", + "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@next/eslint-plugin-next": "16.1.6", + "eslint-import-resolver-node": "^0.3.6", + "eslint-import-resolver-typescript": "^3.5.2", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsx-a11y": "^6.10.0", + "eslint-plugin-react": "^7.37.0", + "eslint-plugin-react-hooks": "^7.0.0", + "globals": "16.4.0", + "typescript-eslint": "^8.46.0" + }, + "peerDependencies": { + "eslint": ">=9.0.0", + "typescript": ">=3.3.1" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", + "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "@nolyfill/is-core-module": "1.0.39", + "debug": "^4.4.0", + "get-tsconfig": "^4.10.0", + "is-bun-module": "^2.0.0", + "stable-hash": "^0.0.5", + "tinyglobby": "^0.2.13", + "unrs-resolver": "^1.6.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", + "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" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.1.tgz", + "integrity": "sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "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": "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/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "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-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "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==", + "dev": true, + "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/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/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-bun-module/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "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/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "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/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "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.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "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.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "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/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "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/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "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/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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/next": { + "version": "16.1.6", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", + "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "license": "MIT", + "dependencies": { + "@next/env": "16.1.6", + "@swc/helpers": "0.5.15", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=20.9.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "16.1.6", + "@next/swc-darwin-x64": "16.1.6", + "@next/swc-linux-arm64-gnu": "16.1.6", + "@next/swc-linux-arm64-musl": "16.1.6", + "@next/swc-linux-x64-gnu": "16.1.6", + "@next/swc-linux-x64-musl": "16.1.6", + "@next/swc-win32-arm64-msvc": "16.1.6", + "@next/swc-win32-x64-msvc": "16.1.6", + "sharp": "^0.34.4" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/next/node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "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.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "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/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "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/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stable-hash": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", + "integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/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/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "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==", + "license": "0BSD" + }, + "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/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz", + "integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.54.0", + "@typescript-eslint/parser": "8.54.0", + "@typescript-eslint/typescript-estree": "8.54.0", + "@typescript-eslint/utils": "8.54.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", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "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/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/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..bd60b9d --- /dev/null +++ b/web/package.json @@ -0,0 +1,26 @@ +{ + "name": "web", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "eslint" + }, + "dependencies": { + "next": "16.1.6", + "react": "19.2.3", + "react-dom": "19.2.3" + }, + "devDependencies": { + "@tailwindcss/postcss": "^4", + "@types/node": "^20", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.1.6", + "tailwindcss": "^4", + "typescript": "^5" + } +} diff --git a/web/postcss.config.mjs b/web/postcss.config.mjs new file mode 100644 index 0000000..61e3684 --- /dev/null +++ b/web/postcss.config.mjs @@ -0,0 +1,7 @@ +const config = { + plugins: { + "@tailwindcss/postcss": {}, + }, +}; + +export default config; diff --git a/web/public/file.svg b/web/public/file.svg new file mode 100644 index 0000000..004145c --- /dev/null +++ b/web/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/globe.svg b/web/public/globe.svg new file mode 100644 index 0000000..567f17b --- /dev/null +++ b/web/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/next.svg b/web/public/next.svg new file mode 100644 index 0000000..5174b28 --- /dev/null +++ b/web/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/vercel.svg b/web/public/vercel.svg new file mode 100644 index 0000000..7705396 --- /dev/null +++ b/web/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/public/window.svg b/web/public/window.svg new file mode 100644 index 0000000..b2b2a44 --- /dev/null +++ b/web/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web/src/app/analyze/page.tsx b/web/src/app/analyze/page.tsx new file mode 100644 index 0000000..3800397 --- /dev/null +++ b/web/src/app/analyze/page.tsx @@ -0,0 +1,270 @@ +'use client'; + +import { useState } from 'react'; +import { Button, Card, Input } from '@/components/ui'; +import { Header, Footer } from '@/components/layout'; + +interface AnalysisResult { + id: string; + tic_id: string; + status: 'pending' | 'processing' | 'completed' | 'failed'; + result?: { + detection: boolean; + confidence: number; + period_days: number | null; + depth_ppm: number | null; + duration_hours: number | null; + vetting: { + disposition: string; + odd_even: { flag: string; message: string }; + v_shape: { flag: string; message: string }; + secondary_eclipse: { flag: string; message: string }; + }; + }; +} + +export default function AnalyzePage() { + const [ticId, setTicId] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + + const handleAnalyze = async () => { + if (!ticId.trim()) { + setError('Please enter a TIC ID'); + return; + } + + setIsLoading(true); + setError(null); + setResult(null); + + try { + // Submit analysis + const response = await fetch('/api/v1/analyze', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ tic_id: ticId }), + }); + + if (!response.ok) { + throw new Error('Failed to start analysis'); + } + + const { analysis_id } = await response.json(); + + // Poll for results + let attempts = 0; + const maxAttempts = 60; // 5 minutes max + + while (attempts < maxAttempts) { + const statusResponse = await fetch(`/api/v1/analyze/${analysis_id}`); + const statusData = await statusResponse.json(); + + if (statusData.status === 'completed' || statusData.status === 'failed') { + setResult(statusData); + break; + } + + await new Promise(resolve => setTimeout(resolve, 5000)); + attempts++; + } + + if (attempts >= maxAttempts) { + setError('Analysis timed out. Please try again.'); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ +
+
+

Analyze Target

+

+ Enter a TESS Input Catalog (TIC) ID to search for exoplanet transit signals. +

+ + {/* Search Form */} + +
+ setTicId(e.target.value)} + className="flex-1" + disabled={isLoading} + /> + +
+ + {error && ( +

{error}

+ )} +
+ + {/* Loading State */} + {isLoading && ( + +
+

Analyzing TIC {ticId}...

+

+ This may take up to 2 minutes. We're searching for transit signals, + running vetting tests, and generating results. +

+
+ )} + + {/* Results */} + {result && result.status === 'completed' && result.result && ( +
+ {/* Detection Result */} + +

Detection Result

+ +
+ {result.result.detection ? '🌍 Planet Candidate Detected!' : 'No Transit Detected'} +
+ + {result.result.detection && ( +
+
+

Confidence

+

+ {(result.result.confidence * 100).toFixed(1)}% +

+
+
+

Period

+

+ {result.result.period_days?.toFixed(4)} days +

+
+
+

Depth

+

+ {result.result.depth_ppm?.toFixed(0)} ppm +

+
+
+

Duration

+

+ {result.result.duration_hours?.toFixed(2)} hrs +

+
+
+ )} +
+ + {/* Vetting Results */} + {result.result.vetting && ( + +

Vetting Results

+ +
+ {result.result.vetting.disposition.replace(/_/g, ' ')} +
+ +
+ {/* Odd-Even Test */} +
+
+

Odd-Even Depth Test

+

{result.result.vetting.odd_even.message}

+
+ + {result.result.vetting.odd_even.flag} + +
+ + {/* V-Shape Test */} +
+
+

V-Shape Detection

+

{result.result.vetting.v_shape.message}

+
+ + {result.result.vetting.v_shape.flag} + +
+ + {/* Secondary Eclipse */} +
+
+

Secondary Eclipse

+

{result.result.vetting.secondary_eclipse.message}

+
+ + {result.result.vetting.secondary_eclipse.flag} + +
+
+
+ )} + + {/* Actions */} +
+ + +
+
+ )} + + {/* Error State */} + {result && result.status === 'failed' && ( + +

Analysis Failed

+

+ Unable to analyze TIC {ticId}. This could be due to insufficient data + or the target not being observed by TESS. +

+ +
+ )} +
+
+ +
+
+ ); +} diff --git a/web/src/app/auth/login/page.tsx b/web/src/app/auth/login/page.tsx new file mode 100644 index 0000000..826fd5e --- /dev/null +++ b/web/src/app/auth/login/page.tsx @@ -0,0 +1,209 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/ui/Button'; +import Input from '@/components/ui/Input'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/Card'; + +export default function LoginPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + email: '', + password: '', + }); + const [errors, setErrors] = useState>({}); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + // Clear error when user starts typing + if (errors[name]) { + setErrors((prev) => ({ ...prev, [name]: '' })); + } + }; + + const validateForm = () => { + const newErrors: Record = {}; + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/\S+@\S+\.\S+/.test(formData.email)) { + newErrors.email = 'Please enter a valid email'; + } + + if (!formData.password) { + newErrors.password = 'Password is required'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsLoading(true); + + // TODO: Replace with actual API call + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // On success, redirect to dashboard + router.push('/dashboard'); + } catch (error) { + setErrors({ form: 'Invalid email or password' }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ + + +
+ + + +
+ + Welcome back + Sign in to your LARUN account +
+ + +
+ {errors.form && ( +
+ {errors.form} +
+ )} + + + + + } + /> + + + + + } + /> + +
+ + + Forgot password? + +
+ + + + +
+
+
+
+
+
+ Or continue with +
+
+ +
+ + +
+
+ +

+ Don't have an account?{' '} + + Sign up for free + +

+ + +
+
+ ); +} diff --git a/web/src/app/auth/register/page.tsx b/web/src/app/auth/register/page.tsx new file mode 100644 index 0000000..22edceb --- /dev/null +++ b/web/src/app/auth/register/page.tsx @@ -0,0 +1,259 @@ +'use client'; + +import { useState } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import Button from '@/components/ui/Button'; +import Input from '@/components/ui/Input'; +import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/Card'; + +export default function RegisterPage() { + const router = useRouter(); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + password: '', + confirmPassword: '', + }); + const [errors, setErrors] = useState>({}); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + // Clear error when user starts typing + if (errors[name]) { + setErrors((prev) => ({ ...prev, [name]: '' })); + } + }; + + const validateForm = () => { + const newErrors: Record = {}; + + if (!formData.name) { + newErrors.name = 'Name is required'; + } + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/\S+@\S+\.\S+/.test(formData.email)) { + newErrors.email = 'Please enter a valid email'; + } + + if (!formData.password) { + newErrors.password = 'Password is required'; + } else if (formData.password.length < 8) { + newErrors.password = 'Password must be at least 8 characters'; + } + + if (!formData.confirmPassword) { + newErrors.confirmPassword = 'Please confirm your password'; + } else if (formData.password !== formData.confirmPassword) { + newErrors.confirmPassword = 'Passwords do not match'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + setIsLoading(true); + + // TODO: Replace with actual API call + try { + // Simulate API call + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // On success, redirect to dashboard + router.push('/dashboard'); + } catch (error) { + setErrors({ form: 'Something went wrong. Please try again.' }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+ + + +
+ + + +
+ + Create your account + Start discovering exoplanets with LARUN +
+ + +
+ {errors.form && ( +
+ {errors.form} +
+ )} + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + +
+ + +
+ + + + +
+
+
+
+
+
+ Or continue with +
+
+ +
+ + +
+
+ +

+ Already have an account?{' '} + + Sign in + +

+ + +
+
+ ); +} diff --git a/web/src/app/dashboard/page.tsx b/web/src/app/dashboard/page.tsx new file mode 100644 index 0000000..c922d0a --- /dev/null +++ b/web/src/app/dashboard/page.tsx @@ -0,0 +1,238 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Link from 'next/link'; +import { Button, Card } from '@/components/ui'; +import { Header, Footer } from '@/components/layout'; + +interface Analysis { + id: string; + tic_id: string; + status: 'pending' | 'processing' | 'completed' | 'failed'; + created_at: string; + result?: { + detection: boolean; + confidence: number; + period_days: number | null; + vetting: { + disposition: string; + }; + }; +} + +interface UsageData { + analyses_this_month: number; + analyses_limit: number; + period_start: string; + period_end: string; +} + +export default function DashboardPage() { + const [analyses, setAnalyses] = useState([]); + const [usage, setUsage] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + // Fetch user's analyses + const fetchData = async () => { + try { + const [analysesRes, usageRes] = await Promise.all([ + fetch('/api/v1/analyses'), + fetch('/api/v1/user/usage'), + ]); + + if (analysesRes.ok) { + const data = await analysesRes.json(); + setAnalyses(data.analyses || []); + } + + if (usageRes.ok) { + const data = await usageRes.json(); + setUsage(data); + } + } catch (error) { + console.error('Failed to fetch dashboard data:', error); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + }, []); + + const getStatusBadge = (status: string) => { + const styles: Record = { + completed: 'bg-green-500/20 text-green-400', + processing: 'bg-blue-500/20 text-blue-400', + pending: 'bg-yellow-500/20 text-yellow-400', + failed: 'bg-red-500/20 text-red-400', + }; + return styles[status] || 'bg-gray-500/20 text-gray-400'; + }; + + const getDispositionBadge = (disposition: string) => { + if (disposition === 'PLANET_CANDIDATE') { + return 'bg-green-500/20 text-green-400'; + } else if (disposition === 'LIKELY_FALSE_POSITIVE') { + return 'bg-red-500/20 text-red-400'; + } + return 'bg-yellow-500/20 text-yellow-400'; + }; + + return ( +
+
+ +
+
+
+
+

Dashboard

+

Manage your exoplanet analyses

+
+ + + +
+ + {/* Usage Stats */} +
+ +

Analyses This Month

+

+ {usage?.analyses_this_month || 0} + + {' '}/ {usage?.analyses_limit || 25} + +

+
+
+
+ + + +

Planet Candidates

+

+ {analyses.filter(a => + a.result?.vetting?.disposition === 'PLANET_CANDIDATE' + ).length} +

+

+ From {analyses.filter(a => a.status === 'completed').length} completed analyses +

+
+ + +

Subscription

+

Hobbyist

+

+ $9/month • 25 analyses +

+ + Manage subscription → + +
+
+ + {/* Recent Analyses */} + +

Recent Analyses

+ + {isLoading ? ( +
+
+

Loading analyses...

+
+ ) : analyses.length === 0 ? ( +
+
🔭
+

No analyses yet

+ + + +
+ ) : ( +
+ + + + + + + + + + + + + + {analyses.map((analysis) => ( + + + + + + + + + + ))} + +
TIC IDDateStatusResultConfidencePeriodActions
+ TIC {analysis.tic_id} + + {new Date(analysis.created_at).toLocaleDateString()} + + + {analysis.status} + + + {analysis.result?.vetting?.disposition ? ( + + {analysis.result.vetting.disposition.replace(/_/g, ' ')} + + ) : ( + - + )} + + {analysis.result?.confidence + ? `${(analysis.result.confidence * 100).toFixed(1)}%` + : '-'} + + {analysis.result?.period_days + ? `${analysis.result.period_days.toFixed(4)}d` + : '-'} + + + View Details + +
+
+ )} +
+ + {/* Quick Tips */} + +

💡 Tips

+
    +
  • • Search for known exoplanet hosts to validate results (e.g., TIC 307210830 = TOI-700)
  • +
  • • Analyses with high confidence ({">"} 85%) and PLANET_CANDIDATE disposition are promising
  • +
  • • Check all 3 vetting tests pass for best candidates
  • +
  • • Need more analyses? Upgrade to Professional for unlimited targets
  • +
+
+
+
+ +
+
+ ); +} diff --git a/web/src/app/favicon.ico b/web/src/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/web/src/app/favicon.ico differ diff --git a/web/src/app/globals.css b/web/src/app/globals.css new file mode 100644 index 0000000..b04c04b --- /dev/null +++ b/web/src/app/globals.css @@ -0,0 +1,61 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #0f172a; + --primary: #4f46e5; + --primary-dark: #4338ca; + --secondary: #475569; + --accent: #8b5cf6; +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-primary: var(--primary); + --color-primary-dark: var(--primary-dark); + --color-secondary: var(--secondary); + --color-accent: var(--accent); + --font-sans: var(--font-inter); +} + +body { + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans), system-ui, sans-serif; +} + +/* Custom scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f5f9; +} + +::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +/* Smooth animations */ +* { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} + +/* Focus styles */ +*:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +/* Selection styles */ +::selection { + background-color: rgba(79, 70, 229, 0.2); +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx new file mode 100644 index 0000000..ae433af --- /dev/null +++ b/web/src/app/layout.tsx @@ -0,0 +1,28 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Header } from "@/components/layout"; +import { Footer } from "@/components/layout"; + +export const metadata: Metadata = { + title: "LARUN - Discover Exoplanets with AI", + description: "AI-powered light curve analysis for exoplanet discovery. Upload your astronomical data and let machine learning find hidden worlds.", + keywords: ["exoplanet", "astronomy", "AI", "light curve", "transit detection", "machine learning"], +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
+
+ {children} +
+