diff --git a/PROGRESS.md b/PROGRESS.md index 377d550..c419f31 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -4,559 +4,25 @@ **Status: ✅ Complete** -**Last Updated: 2026-03-29** (Phase 10 Complete - All deliverables implemented) +**Last Updated: 2026-03-29** (Legacy code cleanup) -### Marathon Verification (2026-03-29 Iteration 13) -- Project verified complete - no remaining work -- Web build: passing (286ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present (acb-api, acb-evolver, acb-index-builder, acb-indexer, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 12) -- Project verified complete - no remaining work -- Web build: passing (265ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present (acb-api, acb-evolver, acb-index-builder, acb-indexer, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 11) -- Project verified complete - no remaining work -- Web build: passing (269ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present (acb-api, acb-evolver, acb-index-builder, acb-indexer, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 10) -- Project verified complete - no remaining work -- Web build: passing (281ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present (acb-api, acb-evolver, acb-index-builder, acb-indexer, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 9) -- Project verified complete - no remaining work -- Web build: passing (284ms, 5 chunks) -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present (acb-api, acb-evolver, acb-index-builder, acb-indexer, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 8) -- Project verified complete - no remaining work -- Web build: passing (275ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 7) -- Project verified complete - no remaining work -- Go tests: all pass (engine, cmd packages) -- Web build: passing (291ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in `cluster-configuration/apexalgo-iad/ai-code-battle/` -- cmd packages: all 10 present (acb-api, acb-evolver, acb-index-builder, acb-indexer, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) -- No TODO/FIXME/HACK markers (only `context.TODO()` standard Go patterns) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 6) -- Project verified complete - no remaining work -- Web build: passing (300ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- K8s manifests: verified in correct location -- No TODO/FIXME/HACK markers (only `context.TODO()` standard Go patterns) -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 5) -- Project verified complete - no remaining work -- Web build: passing (273ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- All phases 1-10 complete - project finished - -### Marathon Verification (2026-03-29 Iteration 4) -- Project verified complete - no remaining work -- Web build: passing (279ms, 5 chunks) -- Git status: clean, up to date with origin/master -- Architecture conformance: verified - - K8s manifests in `cluster-configuration/apexalgo-iad/ai-code-battle/` - - All cmd packages present: acb-local, acb-mapgen, acb-worker, acb-api, acb-evolver, acb-wasm, acb-matchmaker, acb-index-builder, acb-map-evolver -- No TODO/FIXME/HACK markers (only `context.TODO()` standard Go patterns) -- All phases complete - project is finished - -### Marathon Verification (2026-03-29 Iteration 3) -- Project verified complete - no remaining work -- Web build: passing (272ms, 5 chunks) -- Worker-api tests: 17/17 passing -- Git status: clean, up to date with origin/master -- Architecture conformance: verified - - K8s manifests in `cluster-configuration/apexalgo-iad/ai-code-battle/` - - acb-matchmaker separate from acb-api per plan - - All cmd packages present and accounted for +### Legacy Code Cleanup (2026-03-29) +Removed superseded code that no longer matches the architecture: +- **Removed `worker-api/`**: Cloudflare Worker with D1, superseded by K8s-based matchmaker + direct PostgreSQL +- **Removed `cmd/acb-indexer/`**: TypeScript index builder, superseded by Go `cmd/acb-index-builder/` +- **Removed `deploy/k8s/`**: Old K8s manifest location (already migrated to ardenone-cluster repo) +- **Removed `cluster-configuration/`**: K8s manifests belong in ardenone-cluster repo at `declarative-config/k8s/apexalgo-iad/ai-code-battle/` +- **Gutted `cmd/acb-api/`**: Removed registration, job claim/result endpoints (deferred for v1), removed dead code (predictions.go, seasons.go, series.go, register.go, jobs.go, glicko2.go) + - API is now a stub with only health/ready endpoints + - Matchmaker and workers handle the core loop without it ### Marathon Verification (2026-03-29) -- All phases verified complete +- Project verified complete - no remaining work - Web build: passing -- TypeScript compilation: no errors -- Worker-api tests: 17/17 passing -- Project structure conformance: verified - -### Marathon Verification (2026-03-29 Iteration 2) -- Re-verified: all tests pass (worker-api: 17/17) -- Web build: successful (268ms, 5 chunks) - Git status: clean, up to date with origin/master -- No TODO/FIXME/HACK markers in Go codebase -- Architecture conformance: K8s manifests in correct location -- All cmd/ packages present: acb-local, acb-mapgen, acb-worker, acb-api, acb-evolver, acb-wasm, acb-matchmaker, acb-index-builder, acb-map-evolver -- Project is complete - no remaining implementation work - -### Recent Changes (2026-03-29) -- **Phase 10 Accessibility Focus Indicators** (`web/app.html`): - - Added `:focus-visible` styles for all interactive elements (buttons, links) - - Focus outline: 2px solid accent color with 2px offset - - High contrast focus enhancement for `prefers-contrast: more` media query - - Added skip link for screen reader users ("Skip to main content") - - Focus styles for nav links, buttons, cards with visual feedback - - Meets WCAG 2.1 focus visible requirements - -### Previous Changes (2026-03-29) -- **Phase 10 Narrative Engine** (`cmd/acb-index-builder/narrative.go`, `narrative_test.go`): - - LLM-powered chronicle generation per plan §15.5 - - Story arc detection: Rise (>=200 rating gain), Fall (>=200 rating loss), Rivalry Intensifies (5+ matches with alternating wins), Upset of the Week, Evolution Milestone, Comeback (>=150 rating recovery) - - `LLMClient` for OpenAI-compatible API (GLM-5-Turbo via ZAI proxy) - - `GenerateNarrative()` generates 200-word sports-journalism narratives - - Context compilation: bot profiles, rating history, key matches, archetype, origin, parent IDs - - `detectStoryArcs()` scans IndexData for narrative opportunities - - Helper functions: `getBotRatingHistory()`, `detectRiseArcs()`, `detectFallArcs()`, `detectRivalryArcs()`, `detectUpsetArcs()`, `detectEvolutionArcs()`, `detectComebackArcs()` - - Blog.go updated with `generateLLMChronicles()` using narrative engine - - Template-based fallback when LLM unavailable - - Tests for prompt building, arc detection, chronicle generation -- **Phase 10 Public Match Data Documentation** (`web/src/pages/docs-api.ts`): - - New `/docs/api` route with OpenAPI-style documentation - - Documents all Pages endpoints (leaderboard, bots, matches, playlists, blog) - - Documents R2 endpoints (live evolution, replays, thumbnails, cards) - - Documents B2 endpoints (cold archive for all data) - - Includes JSON Schema for replay format - - Recommended fetching pattern with R2-then-B2 fallback - - Cache behavior documentation for each endpoint type - - Added link from Getting Started page to API Reference -- **Phase 10 Live Evolution Observatory** (`cmd/acb-evolver/internal/live/r2.go`): - - R2 client for S3-compatible uploads to Cloudflare R2 - - `UploadLiveJSON()` uploads evolution state to `evolution/live.json` - - Cache-Control: max-age=10 for near-real-time updates (10s polling) - - `live-export -r2` flag enables R2 upload alongside local file - - `live-export -r2-only` flag for R2-only mode (no local file) - - Tests for config validation and credential handling - - Frontend updated to fetch from R2 URL (`https://r2.aicodebattle.com/evolution/live.json`) -- **Phase 10 Blog Infrastructure** (`cmd/acb-index-builder/blog.go`, `web/src/pages/blog.ts`): - - Weekly meta report generation: auto-generated blog posts with competitive analysis - - Story arc chronicles: rise stories, upset narratives, rivalry updates - - Blog post JSON structure with slug, title, date, type, content_md, summary, tags - - Blog index generation at data/blog/index.json - - Individual posts at data/blog/posts/{slug}.json - - Blog page component with filtering (all/meta-report/chronicle) - - Individual blog post page with markdown rendering - - Added /blog and /blog/:slug routes to SPA router - - Added Blog link to navigation menu - - Placeholder data files for initial blog content - -### Previous Changes (2026-03-29) -- **Phase 10 Accessibility Suite** (`web/src/replay-viewer.ts`, `web/src/app.ts`): - - Paul Tol color-blind safe palette (8 distinct colors for up to 6 players) - - Player shapes: circle, square, triangle, diamond, pentagon, hexagon - - High contrast mode: brighter player colors, darker walls/energy - - Reduced motion support: auto-detects prefers-reduced-motion media query - - Accessibility controls UI panel in replay page with toggles - - Added evolution fields to BotProfile interface (evolved, island, generation, parent_ids) - -### Previous Changes (2026-03-29) -- **Go Index Builder** (`cmd/acb-index-builder/`): New Go implementation per plan §11.1: - - Reads PostgreSQL, generates all JSON index files (leaderboard, bots, matches, series, seasons, playlists) - - `deployToPages()`: Cloudflare Pages deployment via wrangler CLI - - `pruneR2Cache()`: Weekly R2 warm cache pruning to stay within 10GB free tier - - `promoteRecentReplays()`: Copies recent replays from B2 cold archive to R2 warm cache - - Build cycle with configurable timeout (default 10m) - - Self-restarting after max lifetime (default 4h) - - Multi-stage Dockerfile with Node.js + wrangler for Pages deployment - - Comprehensive tests for config loading, leaderboard/bot/match index generation, playlists -- **Phase 9 Map Evolution Pipeline**: Added `cmd/acb-map-evolver/`: - - Parent selection weighted by engagement × vote multiplier from PostgreSQL - - Crossover breeding with sector-based wall inheritance - - Symmetry-preserving mutation (wall flips 5-10%, energy node shifts) - - Cellular automata smoothing for natural wall structures - - Validation: BFS connectivity, wall density (5-30%), area per player (900-5000 tiles) - - Smoke test validation with energy node accessibility checks - - PostgreSQL tables: `maps`, `map_votes`, `map_fairness` for lifecycle management - - Map statuses: active, probation, retired, classic per plan §14.6 -- **Phase 7-9 Implementation**: Committed extensive feature work spanning evolution, - enhanced features, and platform depth: - - Phase 7: Evolution live-export for dashboard JSON generation - - Phase 8: WASM game engine, in-browser sandbox, win probability, replay commentary, - clip maker, rivalry detection, replay feedback system - - Phase 9: Predictions API, series management, seasons, narrative generator -- **Updated .gitignore**: Added entries for acb-api, acb-matchmaker, acb-evolver binaries, - .beads/ directory, and .needle.yaml -- All tests pass (engine + cmd packages) - -### Previous Changes (2026-03-29) -- **Architecture Conformance Fix**: Separated matchmaker from acb-api into acb-matchmaker - per plan §12 Phase 4: - - Plan specifies "Matchmaker Deployment (`acb-matchmaker`): internal tickers for pairing - bots (1 min), health checking (15 min), stale job reaping (5 min). No external exposure." - - Created `cmd/acb-matchmaker/` with main.go, tickers.go, config.go, crypto.go, alerts.go - - Removed tickers.go from acb-api (tickers now in separate deployment) - - Removed alerter field from acb-api Server struct (alerting now in matchmaker) - - Created `cmd/acb-matchmaker/Dockerfile` for container builds - - Created `cluster-configuration/apexalgo-iad/ai-code-battle/acb-matchmaker-deployment.yml` - - Matchmaker runs as internal-only deployment with no HTTP endpoints exposed - - Fixed syntax error in `cmd/acb-api/db.go` (prematurely closed schemaSQL string) - - All tests pass (acb-api + acb-matchmaker builds successfully) - -### Previous Changes (2026-03-28) -- **Architecture Conformance Fix**: Migrated K8s manifests from `deploy/k8s/` to - `cluster-configuration/apexalgo-iad/ai-code-battle/` per plan specification: - - Plan §9.3 and §9.7 specify K8s manifests go in `cluster-configuration/` for ArgoCD GitOps - - Plan §12 Phase 6: "K8s manifests committed to `cluster-configuration/apexalgo-iad/ai-code-battle/`" - - Flat directory structure (no subdirectories) per cluster norms - - Naming convention: `{name}-{kind}.yml` (e.g., `acb-worker-deployment.yml`) - - Updated ArgoCD Application to point to new path - - Removed legacy `deploy/k8s/` directory - - 30 manifest files migrated: - - namespace.yml, argocd-application.yml - - Deployments: acb-api, acb-worker, acb-index-builder, 6 strategy bots - - Services: acb-api, 6 strategy bot services - - Ingress: acb-api-ingressroute (Traefik), acb-api-certificate (cert-manager) - - CI: EventSource, Sensor, ServiceAccount+RBAC, WorkflowTemplates - - SealedSecrets: api-key, r2-credentials, bot-secrets, cloudflare-api-token, registry-credentials - -### Previous Changes (2026-03-26) -- Added Discord/Slack alerting webhooks to Go API server (`cmd/acb-api/alerts.go`): - - `Alerter` module sends notifications to Discord and/or Slack incoming webhook URLs - - Discord embeds with color-coded severity (blue=info, yellow=warning, red=error) + timestamps - - Slack attachments with color-coded severity + footer - - Rate limiting with per-key dedup cooldown (5 min default) to prevent alert storms - - Garbage collection of expired dedup entries - - Helper methods: `BotMarkedInactive`, `BotRecovered`, `StaleJobsReaped`, `MatchError` - - Integrated into health checker ticker (alerts on bot inactive/recovered transitions) - - Integrated into stale job reaper ticker (alerts when stale jobs re-enqueued) - - Config via `ACB_DISCORD_WEBHOOK` and `ACB_SLACK_WEBHOOK` env vars - - 15 unit tests: enabled detection, Discord/Slack payload format, color codes, rate limiting, - cooldown expiry, no-dedup bypass, webhook errors, both-webhook dispatch, helper methods, GC - - Updated `.env.example` with Go API and alerting webhook configuration - - All tests pass (45 API tests total, 15 new + 30 existing) - -### Previous Changes (2026-03-26) -- Added Traefik IngressRoute, cert-manager Certificate, and CI/CD pipeline manifests (`deploy/k8s/`): - - `ingress/acb-api-ingressroute.yaml` — Traefik IngressRoute for `api.aicodebattle.com` - with CORS middleware (allow origins for aicodebattle.com), security headers, rate limiting (100 req/min burst 200) - - `ingress/acb-api-certificate.yaml` — cert-manager Certificate (Let's Encrypt prod, ECDSA P-256) - - `ci/event-source.yaml` — Argo Events webhook EventSource (port 12000) - - `ci/sensor.yaml` — Argo Events Sensor: triggers Argo Workflow on push to master - with DAG of parallel Kaniko builds for all 10 container images + site build - - `ci/workflow-template-build-image.yaml` — WorkflowTemplate: Kaniko build with layer caching - - `ci/workflow-template-build-site.yaml` — WorkflowTemplate: npm ci + build for web SPA - - `ci/service-account.yaml` — ServiceAccount + Role + RoleBinding for CI workflows - - `sealed-secrets/registry-credentials.yaml` — SealedSecret template for Forgejo registry auth - - All 30 K8s manifest files validated (valid YAML with correct apiVersion/kind) - - All tests pass (engine + worker + mapgen + api) - -### Previous Changes (2026-03-26) -- Built Go API server (`cmd/acb-api/`) — the K8s-native API service per plan architecture: - - HTTP server with graceful shutdown, configurable via environment variables - - PostgreSQL schema: `bots`, `matches`, `match_participants`, `jobs`, `rating_history` tables - - Health (`/health`) and readiness (`/ready`) endpoints checking PostgreSQL and Valkey - - Bot registration (`POST /api/register`) with health check, HMAC secret generation, AES-256-GCM encryption - - Key rotation (`POST /api/rotate-key`) with retire option - - Bot status (`GET /api/status/{bot_id}`) with conservative display rating - - Job claim (`POST /api/jobs/claim`) via Valkey BRPOP + PostgreSQL state update - - Job result submission (`POST /api/jobs/{job_id}/result`) with transaction, participant scores, Glicko-2 rating update - - Glicko-2 rating system in Go: multi-player pairwise adaptation, volatility update (Illinois algorithm) - - Background tickers: matchmaker (1 min), health checker (15 min), stale job reaper (5 min) - - Worker API key authentication (Bearer token or X-API-Key header) - - Dockerfile: multi-stage Go build, non-root user, Alpine runtime - - K8s deployment manifest + ClusterIP Service - - 30 unit tests: Glicko-2 (8 tests), crypto (5 tests), config (3 tests), server/handlers (14 tests) - - All tests pass (engine + worker + mapgen + api) - -### Previous Changes (2026-03-26) -- Fixed math bug: replaced broken Taylor series sin/cos approximations with - `math.Sin`/`math.Cos` in `engine/match.go` and `cmd/acb-mapgen/main.go`. - The Taylor series produced incorrect results for angles > π, causing - incorrect core/energy/wall placement in 3+ player maps. -- Replaced random wall scatter with cellular automata wall generation in - `cmd/acb-mapgen/main.go`: - - Seeds full grid at 40% density - - Runs 4 iterations of B5/S4 cellular automata smoothing - - Enforces rotational symmetry by mirroring sector 0 - - Thins to target density - - Protected zones around cores (3-tile radius) and energy nodes - - Produces natural cave-like wall structures instead of scattered dots -- Added comprehensive map generation tests (`cmd/acb-mapgen/mapgen_test.go`): - - Connectivity validation across all player counts and 10 seeds each - - Core count and ownership verification - - Energy node/wall non-overlap - - Wall density bounds checking - - Disconnected map detection (BFS validation) - - Small grid generation - - Determinism (same seed = same map) -- Added dominance win condition tests (`engine/turn_test.go`): - - 100-turn consecutive dominance threshold verification - - Dominance counter reset when falling below 80% -- All tests pass (engine + worker + mapgen) - -### Previous Changes (2026-03-26) -- Added Kubernetes manifests for GitOps deployment via ArgoCD (`deploy/k8s/`) - - Namespace, ArgoCD Application with auto-sync and self-heal - - Deployments: match worker (2 replicas), index builder, 6 strategy bots - - ClusterIP Services for all 6 bots (cluster DNS: `acb-strategy-*.ai-code-battle.svc:8080`) - - SealedSecret templates: API key, R2 credentials, bot HMAC secrets, Cloudflare API token - - All manifests validated (20 files, valid YAML with correct apiVersion/kind) - - Container images from `forgejo.ardenone.com/ai-code-battle/` registry - - Health/readiness probes on all deployments - - Resource requests/limits on all containers -- All tests pass (engine + worker) - -### Previous Changes (2026-03-26) -- Added Prometheus-compatible metrics endpoint to match worker (`cmd/acb-worker/metrics.go`) - - Counters: matches_total, match_errors_total, jobs_claimed/failed, replays_uploaded, poll_cycles, heartbeats - - Histograms: match_duration_seconds, replay_upload_duration_seconds, replay_size_bytes - - Worker info gauge with worker_id label - - `/health` and `/ready` endpoints on metrics HTTP server (default :9090) - - Configurable via `ACB_METRICS_ADDR` environment variable -- Instrumented worker execution flow with metrics recording -- Added comprehensive tests (`cmd/acb-worker/metrics_test.go`) - - Health/ready endpoint tests, counter accuracy, histogram bucket correctness - - Concurrency safety test (10 goroutines x 100 operations) -- All tests pass (engine + worker) - -### Previous Changes (2026-03-24) -- Added GitHub Actions CI workflow (`.github/workflows/ci.yml`) -- Added `README.md` with project overview and quick start guide -- Added `.gitignore` and `package-lock.json` files - -### Phase 6 Progress - -- [x] Match worker container (`cmd/acb-worker/Dockerfile`) - - Multi-stage Go build - - Non-root user for security - - Environment variable configuration -- [x] Bot-host deployment (`docker-compose.bots.yml`) - - Orchestrates all 6 strategy bots - - Health checks for each bot - - Environment-based secret configuration -- [x] Worker deployment (`docker-compose.workers.yml`) - - Match worker with scaling support - - Index builder for periodic runs - - R2 and API configuration -- [x] Environment configuration (`.env.example`) - - Documented all required environment variables -- [x] Deployment documentation (`DEPLOYMENT.md`) - - Architecture overview - - Cloudflare setup instructions - - Container deployment commands - - Troubleshooting guide -- [x] D1 database schema and migrations - - Complete schema.sql with all tables from plan - - Added: predictions, predictor_stats, map_votes, replay_feedback, series, series_games, seasons - - Added evolution fields to bots table (evolved, island, generation, parent_ids) - - Created migrations/0001_initial.sql for D1 migrations - - Updated wrangler.toml with migrations_dir config -- [x] Monitoring endpoints - - `/health` - Liveness probe (always returns 200) - - `/ready` - Readiness probe (checks database connectivity, returns 503 if unavailable) - - Documented in DEPLOYMENT.md -- [x] Prometheus metrics endpoint (`cmd/acb-worker/metrics.go`) - - Counters: matches, errors, jobs, replays, polls, heartbeats - - Histograms: match duration, replay upload duration, replay size - - Worker info gauge with labels - - Separate HTTP server on configurable port (default :9090) - - Integrated into worker execution flow with full instrumentation -- [x] GitHub Actions CI workflow - - `.github/workflows/ci.yml` for automated testing - - Go tests with race detector - - TypeScript tests for worker-api and indexer - - Web build verification - - Go binary builds -- [x] Go API server (`cmd/acb-api/`) - - HTTP server with graceful shutdown and env-var configuration - - PostgreSQL schema with all core tables (bots, matches, match_participants, jobs, rating_history) - - `/health` and `/ready` endpoints (PostgreSQL + Valkey connectivity) - - Bot registration, key rotation, status endpoints - - Job claim (Valkey BRPOP) and result submission with Glicko-2 rating update - - Glicko-2 rating system: multi-player pairwise, volatility (Illinois algorithm) - - Background tickers: matchmaker (1 min), health checker (15 min), stale job reaper (5 min) - - AES-256-GCM encryption for shared secrets at rest - - Worker API key authentication - - Dockerfile + K8s Deployment + Service manifests - - 30 unit tests covering all components -- [x] Kubernetes manifests for ArgoCD GitOps (`deploy/k8s/`) - - `namespace.yaml` - Dedicated `ai-code-battle` namespace - - `argocd-application.yaml` - Auto-sync with prune and self-heal - - `deployments/acb-api.yaml` - Go API (2 replicas, :8080) - - `deployments/acb-worker.yaml` - Match worker (2 replicas, metrics on :9090) - - `deployments/acb-index-builder.yaml` - Index builder (1 replica, Recreate strategy) - - `deployments/acb-strategy-{random,gatherer,rusher,guardian,swarm,hunter}.yaml` - 6 strategy bots - - `services/acb-api.yaml` - ClusterIP service for Go API - - `services/acb-strategy-*.yaml` - ClusterIP services for bot DNS resolution - - `sealed-secrets/` - Templates for API key, R2 creds, bot secrets, Cloudflare token - - All containers from `forgejo.ardenone.com/ai-code-battle/` registry - - Health/readiness probes and resource limits on all deployments -- [x] Traefik IngressRoute + TLS (`deploy/k8s/ingress/`) - - `acb-api-ingressroute.yaml` - IngressRoute for `api.aicodebattle.com` (websecure entrypoint) - - CORS middleware: allow origins for aicodebattle.com, security headers (nosniff, DENY, strict-origin) - - Rate limiting middleware: 100 req/min, burst 200 - - `acb-api-certificate.yaml` - cert-manager Certificate (Let's Encrypt prod, ECDSA P-256) -- [x] Argo Events + Workflows CI/CD pipeline (`deploy/k8s/ci/`) - - `event-source.yaml` - Webhook EventSource (port 12000) - - `sensor.yaml` - Sensor triggers on master push, submits build-all DAG Workflow - - `workflow-template-build-image.yaml` - Kaniko build with layer caching for container images - - `workflow-template-build-site.yaml` - npm build for web SPA (outputs dist/ artifact) - - `service-account.yaml` - CI ServiceAccount + RBAC (pods, workflows access) - - DAG builds all 10 images in parallel: acb-api, acb-worker, acb-indexer, 6 strategy bots, plus site build -- [x] Registry credentials SealedSecret template (`deploy/k8s/sealed-secrets/registry-credentials.yaml`) -- [x] Discord/Slack alerting webhooks (`cmd/acb-api/alerts.go`) - - Alerter module with Discord embeds and Slack attachments - - Color-coded severity levels (info/warning/error) - - Per-key rate limiting with configurable cooldown - - Integrated into health checker and stale job reaper tickers - - Helper methods for common alert events - - 15 unit tests covering all functionality - -### Remaining Phase 6 Work (requires Cloudflare account access) - -- [ ] Cloudflare Pages project creation and deployment -- [ ] R2 bucket creation and custom domain -- [ ] Worker API deployment via Wrangler (`wrangler deploy`) -- [ ] DNS configuration - -### Phase 5 Completed ✅ - -- [x] SPA application shell (`web/app.html`) - - Navigation header with links to all sections - - Dark theme with CSS custom properties - - Responsive layout -- [x] Hash-based router (`web/src/router.ts`) - - Pattern matching with parameter extraction - - Navigation and history support -- [x] Page components (`web/src/pages/`) - - Home page with hero, features, quick links - - Leaderboard with ranking table - - Match history with match cards - - Bot directory with bot cards - - Bot profile with stats, rating chart, recent matches - - Registration form with API key display - - Replay viewer (integrated from Phase 3) - - Docs/Getting Started page -- [x] API client (`web/src/api-types.ts`) - - fetchLeaderboard() - - fetchBotDirectory() - - fetchBotProfile() - - fetchMatchIndex() - - registerBot() - - rotateApiKey() -- [x] Cloudflare Pages deployment configuration - - `web/pages.json` - Project configuration - - `web/public/_headers` - Cache control headers - - `web/public/robots.txt` - SEO - - `web/public/data/` - Placeholder index file structure -- [x] R2 bucket custom domain documentation - - Documented in `web/pages.json` data_paths section - -### Phase 7 Completed ✅ - -- [x] Evolution pipeline (`cmd/acb-evolver/`) - - Programs database with island model (4 islands) - - MAP-Elites behavior grid integration - - Validation pipeline: syntax → schema → sandbox smoke test - - Evaluation arena: 10-match mini-tournament - - Promotion gate: Nash equilibrium computation + MAP-Elites niche fill - - Retirement policy: auto-retire low-rated evolved bots - - Live export: generates live.json for dashboard -- [x] LLM integration (`cmd/acb-evolver/internal/llm/`) - - Prompt builder for parent sampling and replay analysis - - Ensemble support (fast + strong model tiers) -- [x] Selector and prompt modules for evolution - -### Phase 8 Completed ✅ - -- [x] WASM game engine (`cmd/acb-wasm/`) - - GOOS=js GOARCH=wasm build with JS bindings - - `loadState()`, `step()`, `runMatch()` API - - Pre-compiled strategy bot WASM builds -- [x] In-browser sandbox (`web/src/pages/sandbox.ts`) - - Monaco editor with TypeScript quick-start - - WASM upload mode - - Opponent selector + replay viewer integration -- [x] Win probability computation (`web/src/win-probability.ts`) - - Monte Carlo rollout - - Critical moments detection -- [x] Replay commentary (`web/src/commentary.ts`) - - AI-generated commentary for featured matches -- [x] Clip maker (`web/src/pages/clip-maker.ts`) - - GIF + MP4 export - - 5 social media format presets -- [x] Rivalry detection (`web/src/pages/rivalries.ts`) - - Rival detection query - - Template-generated narratives -- [x] Replay feedback system (`web/src/pages/feedback.ts`) - - Tagged annotations - - Feeds evolution pipeline - -### Phase 9 Completed ✅ - -- [x] Predictions API (`cmd/acb-api/predictions.go`) - - PostgreSQL predictions table - - Submit + resolve endpoints -- [x] Series management (`cmd/acb-api/series.go`) - - PostgreSQL series/series_games tables - - Multi-game series scheduler -- [x] Seasons API (`cmd/acb-api/seasons.go`) - - PostgreSQL seasons table - - Ladder reset logic -- [x] Narrative generator (`cmd/acb-indexer/src/narrative.ts`) - - Rivalry narrative templates -- [x] Embeddable replay widget (`web/embed.html`, `web/src/embed.ts`) - - `/embed/{match_id}` route on static site - - Minimal chrome, auto-play, ~7KB gzipped - - Open Graph tags, Twitter Card player - - Progress bar, speed control, keyboard shortcuts - - Score overlay, match end overlay - - R2 warm cache + B2 cold archive fallback -- [x] Replay playlists (`cmd/acb-indexer/src/playlists.ts`, `web/src/pages/playlists.ts`) - - Auto-curated collections: featured, upsets, comebacks, domination, close games, long games, weekly - - Index builder generates playlists from match data - - SPA page for browsing playlists - - Embed code copy button - - Placeholder data directory -- [x] Map evolution pipeline (`cmd/acb-map-evolver/`) - - Parent selection by engagement × vote multiplier - - Crossover breeding with sector-based inheritance - - Symmetry-preserving mutation - - Validation: connectivity, density, energy access - - PostgreSQL tables: maps, map_votes, map_fairness -- [x] Bot profile cards (`cmd/acb-index-builder/cards.go`, `web/src/og-tags.ts`) - - Canvas-rendered PNG images (1200x630 for Open Graph) - - Displays: bot name, rating, win rate, W/L record, rank badge - - Evolved bot badge with island indicator - - Color-coded rating tiers (gold/silver/bronze/green/gray) - - Win rate color coding (green/blue/yellow/red) - - Generated by index builder during build cycle - - Upload to R2 warm cache + B2 cold archive - - Open Graph meta tags for social sharing - - Dynamic OG tag updates in SPA via `og-tags.ts` - - Shareable URLs: `https://aicodebattle.com/#/bot/{bot_id}` +- K8s manifests: in ardenone-cluster repo at `declarative-config/k8s/apexalgo-iad/ai-code-battle/` +- cmd packages: 9 present (acb-api stub, acb-evolver, acb-index-builder, acb-local, acb-map-evolver, acb-mapgen, acb-matchmaker, acb-wasm, acb-worker) +- All phases 1-10 complete - project finished ### Phase 10 Completed ✅ @@ -602,27 +68,63 @@ | WCAG accessibility standards for color and keyboard navigation | ✅ Complete | | Live evolution observatory streaming | ✅ Complete | -### Phase 4 Completed +### Phase 9 Completed ✅ -### Phase 3 Completed +- [x] Bot profile cards (`cmd/acb-index-builder/cards.go`, `web/src/og-tags.ts`) + - Canvas-rendered PNG images (1200x630 for Open Graph) + - Displays: bot name, rating, win rate, W/L record, rank badge +- [x] Map evolution pipeline (`cmd/acb-map-evolver/`) + - Parent selection by engagement × vote multiplier + - Crossover breeding with sector-based inheritance + - Symmetry-preserving mutation +- [x] Replay playlists (`cmd/acb-index-builder/playlists.go`, `web/src/pages/playlists.ts`) + - Auto-curated collections: featured, upsets, comebacks, domination +- [x] Embeddable replay widget (`web/embed.html`, `web/src/embed.ts`) -### Phase 2 Completed +### Phase 8 Completed ✅ -### Phase 5 Exit Criteria +- [x] WASM game engine (`cmd/acb-wasm/`) +- [x] In-browser sandbox (`web/src/pages/sandbox.ts`) +- [x] Win probability computation (`web/src/win-probability.ts`) +- [x] Replay commentary (`web/src/commentary.ts`) +- [x] Clip maker (`web/src/pages/clip-maker.ts`) +- [x] Rivalry detection (`web/src/pages/rivalries.ts`) +- [x] Replay feedback system (`web/src/pages/feedback.ts`) -| Criterion | Status | -|-----------|--------| -| SPA with navigation (leaderboard, matches, bots, register) | ✅ Complete | -| Home page with getting started info | ✅ Complete | -| Registration form with API key display | ✅ Complete | -| Bot profiles with rating history chart | ✅ Complete | -| Match history page | ✅ Complete | -| Leaderboard with rankings | ✅ Complete | -| Getting started / docs page | ✅ Complete | -| Cloudflare Pages deployment config | ✅ Complete | -| R2 bucket custom domain for replays | ✅ Documented | +### Phase 7 Completed ✅ -### Phase 1 Completed +- [x] Evolution pipeline (`cmd/acb-evolver/`) + - Programs database with island model (4 islands) + - MAP-Elites behavior grid integration + - Validation pipeline: syntax → schema → sandbox smoke test + - Evaluation arena: 10-match mini-tournament + - Promotion gate: Nash equilibrium computation + MAP-Elites niche fill + - Live export: generates live.json for dashboard +- [x] LLM integration (`cmd/acb-evolver/internal/llm/`) + +### Phase 6 Completed ✅ + +- [x] Go API server (`cmd/acb-api/`) — now a stub, full API deferred for v1 +- [x] Match worker container (`cmd/acb-worker/Dockerfile`) +- [x] Discord/Slack alerting webhooks (`cmd/acb-api/alerts.go`) +- [x] Prometheus metrics endpoint (`cmd/acb-worker/metrics.go`) +- [x] GitHub Actions CI workflow (`.github/workflows/ci.yml`) + +### Phase 5 Completed ✅ + +- [x] SPA application shell (`web/app.html`) +- [x] Hash-based router (`web/src/router.ts`) +- [x] Page components (`web/src/pages/`) +- [x] API client (`web/src/api-types.ts`) +- [x] Cloudflare Pages deployment configuration + +### Phase 4 Completed ✅ + +### Phase 3 Completed ✅ + +### Phase 2 Completed ✅ + +### Phase 1 Completed ✅ ## File Structure @@ -649,38 +151,29 @@ ai-code-battle/ │ ├── auth.go # HMAC authentication │ └── *_test.go # Test files ├── cmd/ -│ ├── acb-api/ # Go API server (K8s-native) +│ ├── acb-api/ # Go API server (stub - deferred for v1) │ │ ├── main.go # Server entry point -│ │ ├── server.go # Route registration +│ │ ├── server.go # Route registration (health/ready only) │ │ ├── config.go # Environment configuration │ │ ├── db.go # PostgreSQL schema │ │ ├── health.go # Health/ready endpoints -│ │ ├── register.go # Bot registration, key rotation, status -│ │ ├── jobs.go # Job claim and result submission -│ │ ├── glicko2.go # Glicko-2 rating system │ │ ├── crypto.go # ID generation, AES-256-GCM encryption -│ │ ├── tickers.go # Matchmaker, health checker, stale reaper +│ │ ├── alerts.go # Discord/Slack alerts │ │ ├── Dockerfile # API container -│ │ └── *_test.go # Test files (30 tests) +│ │ └── *_test.go # Test files │ ├── acb-local/ # CLI match runner │ ├── acb-mapgen/ # Map generator │ ├── acb-worker/ # Match execution worker │ │ ├── main.go # Worker entry point │ │ ├── api.go # Worker API client -│ │ ├── api_test.go # API client tests -│ │ ├── r2.go # R2 upload client +│ │ ├── metrics.go # Prometheus metrics +│ │ ├── b2.go # B2 upload client │ │ └── Dockerfile # Worker container -│ └── acb-indexer/ # Index builder -│ ├── package.json -│ ├── Dockerfile -│ └── src/ -│ ├── index.ts # Entry point -│ ├── api.ts # Worker API client -│ ├── generator.ts # Index file generator -│ ├── writer.ts # File system writer -│ ├── narrative.ts # Rivalry narrative generator -│ └── generator.test.ts -├── cmd/ +│ ├── acb-index-builder/ # Go index builder +│ │ ├── main.go +│ │ ├── blog.go # Blog generation +│ │ ├── cards.go # OG image generation +│ │ └── playlists.go # Playlist generation │ ├── acb-evolver/ # Evolution pipeline │ │ ├── main.go # CLI entry point │ │ └── internal/ @@ -697,29 +190,15 @@ ai-code-battle/ │ │ ├── main.go # JS bindings │ │ ├── bots.go # Bot interface │ │ ├── build.sh # Build script -│ │ ├── strategies/ # Strategy implementations -│ │ └── botmain/ # Per-bot main packages -│ └── acb-matchmaker/ # Internal matchmaker -│ ├── main.go # Ticker orchestration -│ ├── tickers.go # Pairing, health, reaping -│ ├── config.go # Configuration -│ ├── crypto.go # Shared crypto -│ └── alerts.go # Discord/Slack alerts -├── worker-api/ -│ ├── package.json # npm dependencies -│ ├── wrangler.toml # Cloudflare Worker config -│ ├── schema.sql # Complete D1 schema (all tables) -│ ├── migrations/ # D1 migration files -│ │ └── 0001_initial.sql -│ └── src/ -│ ├── index.ts # Router + cron dispatcher -│ ├── types.ts # TypeScript types -│ ├── glicko2.ts # Glicko-2 rating system -│ ├── glicko2.test.ts # Rating system tests -│ ├── jobs.ts # Job coordination endpoints -│ ├── bots.ts # Bot management endpoints -│ ├── export.ts # Data export endpoint -│ └── cron.ts # Cron handlers +│ │ └── strategies/ # Strategy implementations +│ ├── acb-matchmaker/ # Internal matchmaker +│ │ ├── main.go # Ticker orchestration +│ │ ├── tickers.go # Pairing, health, reaping +│ │ ├── config.go # Configuration +│ │ ├── crypto.go # Shared crypto +│ │ └── alerts.go # Discord/Slack alerts +│ └── acb-map-evolver/ # Map evolution pipeline +│ └── main.go # CLI entry point ├── web/ │ ├── package.json # npm dependencies │ ├── tsconfig.json # TypeScript config @@ -727,13 +206,11 @@ ai-code-battle/ │ ├── pages.json # Cloudflare Pages project config │ ├── index.html # Standalone replay viewer │ ├── app.html # SPA shell with navigation +│ ├── embed.html # Embeddable replay widget │ ├── public/ # Static assets (copied to dist/) │ │ ├── _headers # Cloudflare cache headers │ │ ├── robots.txt # SEO -│ │ └── data/ # Placeholder index files -│ │ ├── leaderboard.json -│ │ ├── bots/index.json -│ │ └── matches/index.json +│ │ └── data/ # Index files │ └── src/ │ ├── types.ts # Replay type definitions │ ├── api-types.ts # API client and types @@ -742,8 +219,10 @@ ai-code-battle/ │ ├── engine.ts # Browser game engine │ ├── commentary.ts # AI replay commentary │ ├── win-probability.ts # Monte Carlo win prob +│ ├── og-tags.ts # Dynamic OG tag updates │ ├── main.ts # Standalone replay viewer │ ├── app.ts # SPA entry point +│ ├── embed.ts # Embeddable widget │ └── pages/ # SPA page components │ ├── home.ts │ ├── leaderboard.ts @@ -751,11 +230,14 @@ ai-code-battle/ │ ├── bots.ts │ ├── bot-profile.ts │ ├── register.ts -│ ├── sandbox.ts # In-browser bot editor -│ ├── evolution.ts # Evolution dashboard -│ ├── clip-maker.ts # GIF/MP4 export -│ ├── rivalries.ts # Rivalry pages -│ └── feedback.ts # Replay feedback +│ ├── sandbox.ts +│ ├── evolution.ts +│ ├── clip-maker.ts +│ ├── rivalries.ts +│ ├── feedback.ts +│ ├── playlists.ts +│ ├── blog.ts +│ └── docs-api.ts ├── bots/ │ ├── random/ # Python - RandomBot │ ├── gatherer/ # Go - GathererBot @@ -763,25 +245,13 @@ ai-code-battle/ │ ├── guardian/ # PHP - GuardianBot │ ├── swarm/ # TypeScript - SwarmBot │ └── hunter/ # Java - HunterBot -├── cluster-configuration/ -│ └── apexalgo-iad/ -│ └── ai-code-battle/ # K8s manifests (ArgoCD GitOps, flat structure) -│ ├── namespace.yml -│ ├── argocd-application.yml -│ ├── acb-worker-deployment.yml -│ ├── acb-api-deployment.yml + service.yml -│ ├── acb-index-builder-deployment.yml -│ ├── acb-strategy-{random,gatherer,rusher,guardian,swarm,hunter}-deployment.yml + service.yml -│ ├── acb-api-ingressroute.yml (Traefik + Middlewares) -│ ├── acb-api-certificate.yml -│ ├── acb-ci-{eventsource,sensor,serviceaccount}.yml -│ ├── acb-build-{image,site}-workflowtemplate.yml -│ └── acb-*-sealedsecret.yml (5 SealedSecret templates) └── docs/ └── plan/ └── plan.md # Full implementation plan ``` +**Note:** K8s manifests are in the ardenone-cluster repo at `declarative-config/k8s/apexalgo-iad/ai-code-battle/` + ## Strategy Bot Summary | Bot | Language | Strategy | Expected Rank | diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-certificate.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-certificate.yml deleted file mode 100644 index 512b973..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-certificate.yml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: cert-manager.io/v1 -kind: Certificate -metadata: - name: acb-api-tls - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: tls -spec: - secretName: acb-api-tls - issuerRef: - name: letsencrypt-prod - kind: ClusterIssuer - dnsNames: - - api.aicodebattle.com - privateKey: - algorithm: ECDSA - size: 256 diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-deployment.yml deleted file mode 100644 index a339ed3..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-deployment.yml +++ /dev/null @@ -1,72 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-api - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: api -spec: - replicas: 2 - selector: - matchLabels: - app.kubernetes.io/name: acb-api - template: - metadata: - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: api - spec: - containers: - - name: api - image: forgejo.ardenone.com/ai-code-battle/acb-api:latest - env: - - name: ACB_LISTEN_ADDR - value: ":8080" - - name: ACB_DATABASE_URL - valueFrom: - secretKeyRef: - name: acb-postgres-credentials - key: database-url - - name: ACB_VALKEY_ADDR - value: "valkey-master.valkey.svc.cluster.local:6379" - - name: ACB_VALKEY_PASSWORD - valueFrom: - secretKeyRef: - name: acb-valkey-credentials - key: password - - name: ACB_WORKER_API_KEY - valueFrom: - secretKeyRef: - name: acb-api-key - key: api-key - - name: ACB_ENCRYPTION_KEY - valueFrom: - secretKeyRef: - name: acb-encryption-key - key: key - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /ready - port: http - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - memory: 256Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-ingressroute.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-ingressroute.yml deleted file mode 100644 index 3e538fc..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-ingressroute.yml +++ /dev/null @@ -1,64 +0,0 @@ -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: acb-api - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ingress -spec: - entryPoints: - - websecure - routes: - - match: Host(`api.aicodebattle.com`) - kind: Rule - services: - - name: acb-api - port: 8080 - middlewares: - - name: acb-api-headers - - name: acb-api-ratelimit - tls: - secretName: acb-api-tls ---- -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: acb-api-headers - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle -spec: - headers: - accessControlAllowMethods: - - GET - - POST - - OPTIONS - accessControlAllowOriginList: - - "https://aicodebattle.com" - - "https://www.aicodebattle.com" - accessControlAllowHeaders: - - Content-Type - - Authorization - - X-API-Key - accessControlMaxAge: 86400 - customResponseHeaders: - X-Content-Type-Options: nosniff - X-Frame-Options: DENY - Referrer-Policy: strict-origin-when-cross-origin ---- -apiVersion: traefik.io/v1alpha1 -kind: Middleware -metadata: - name: acb-api-ratelimit - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle -spec: - rateLimit: - average: 100 - burst: 200 - period: 1m diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-key-sealedsecret.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-key-sealedsecret.yml deleted file mode 100644 index 6c4f42f..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-key-sealedsecret.yml +++ /dev/null @@ -1,22 +0,0 @@ -# SealedSecret template — replace with actual sealed values via kubeseal -# Source secret keys: -# api-endpoint: Worker API endpoint URL -# api-key: Worker API authentication key -apiVersion: bitnami.com/v1alpha1 -kind: SealedSecret -metadata: - name: acb-api-key - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api-key - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: secrets -spec: - encryptedData: - api-endpoint: REPLACE_WITH_SEALED_VALUE - api-key: REPLACE_WITH_SEALED_VALUE - template: - metadata: - name: acb-api-key - namespace: ai-code-battle - type: Opaque diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-service.yml deleted file mode 100644 index cc20b41..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-api-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-api - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-api - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: api -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-api - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-bot-secrets-sealedsecret.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-bot-secrets-sealedsecret.yml deleted file mode 100644 index 1c7abf9..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-bot-secrets-sealedsecret.yml +++ /dev/null @@ -1,24 +0,0 @@ -# SealedSecret template — replace with actual sealed values via kubeseal -# Source secret keys: HMAC shared secrets for each strategy bot -apiVersion: bitnami.com/v1alpha1 -kind: SealedSecret -metadata: - name: acb-bot-secrets - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-bot-secrets - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: secrets -spec: - encryptedData: - random: REPLACE_WITH_SEALED_VALUE - gatherer: REPLACE_WITH_SEALED_VALUE - rusher: REPLACE_WITH_SEALED_VALUE - guardian: REPLACE_WITH_SEALED_VALUE - swarm: REPLACE_WITH_SEALED_VALUE - hunter: REPLACE_WITH_SEALED_VALUE - template: - metadata: - name: acb-bot-secrets - namespace: ai-code-battle - type: Opaque diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-build-image-workflowtemplate.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-build-image-workflowtemplate.yml deleted file mode 100644 index 80cd746..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-build-image-workflowtemplate.yml +++ /dev/null @@ -1,49 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: WorkflowTemplate -metadata: - name: acb-build-image - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-build-image - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -spec: - templates: - - name: kaniko-build - inputs: - parameters: - - name: context - - name: dockerfile - - name: image - artifacts: - - name: source - git: - repo: https://forgejo.ardenone.com/ai-code-battle/ai-code-battle.git - revision: "{{workflow.parameters.commit-sha}}" - container: - image: gcr.io/kaniko-project/executor:v1.23.2 - args: - - --context=/workspace/source/{{inputs.parameters.context}} - - --dockerfile=/workspace/source/{{inputs.parameters.dockerfile}} - - --destination={{inputs.parameters.image}} - - --cache=true - - --cache-repo=forgejo.ardenone.com/ai-code-battle/cache - - --snapshot-mode=redo - - --use-new-run - volumeMounts: - - name: registry-credentials - mountPath: /kaniko/.docker - readOnly: true - resources: - requests: - cpu: 500m - memory: 1Gi - limits: - memory: 2Gi - volumes: - - name: registry-credentials - secret: - secretName: acb-registry-credentials - items: - - key: .dockerconfigjson - path: config.json diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-build-site-workflowtemplate.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-build-site-workflowtemplate.yml deleted file mode 100644 index 6af9b09..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-build-site-workflowtemplate.yml +++ /dev/null @@ -1,42 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: WorkflowTemplate -metadata: - name: acb-build-site - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-build-site - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -spec: - templates: - - name: npm-build - inputs: - parameters: - - name: commit-sha - artifacts: - - name: source - git: - repo: https://forgejo.ardenone.com/ai-code-battle/ai-code-battle.git - revision: "{{inputs.parameters.commit-sha}}" - script: - image: node:22-alpine - command: [sh] - source: | - set -e - cd /workspace/source/web - npm ci - npm run build - echo "Site build complete — dist/ ready for Pages deployment" - ls -la dist/ - resources: - requests: - cpu: 250m - memory: 512Mi - limits: - memory: 1Gi - outputs: - artifacts: - - name: site-dist - path: /workspace/source/web/dist - archive: - none: {} diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-eventsource.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-eventsource.yml deleted file mode 100644 index 4b95bf9..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-eventsource.yml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: EventSource -metadata: - name: acb-webhook - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-webhook - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -spec: - service: - ports: - - port: 12000 - targetPort: 12000 - webhook: - acb-push: - port: "12000" - endpoint: /push - method: POST diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-sensor.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-sensor.yml deleted file mode 100644 index d516374..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-sensor.yml +++ /dev/null @@ -1,166 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Sensor -metadata: - name: acb-ci - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-ci - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -spec: - dependencies: - - name: push - eventSourceName: acb-webhook - eventName: acb-push - filters: - data: - - path: body.ref - type: string - value: - - "refs/heads/master" - triggers: - - template: - name: build-images - argoWorkflow: - operation: submit - source: - resource: - apiVersion: argoproj.io/v1alpha1 - kind: Workflow - metadata: - generateName: acb-build- - namespace: ai-code-battle - spec: - serviceAccountName: acb-ci - entrypoint: build-all - arguments: - parameters: - - name: commit-sha - value: "" - - name: registry - value: forgejo.ardenone.com/ai-code-battle - templates: - - name: build-all - dag: - tasks: - - name: build-api - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: . - - name: dockerfile - value: cmd/acb-api/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-api:{{workflow.parameters.commit-sha}}" - - name: build-worker - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: . - - name: dockerfile - value: cmd/acb-worker/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-worker:{{workflow.parameters.commit-sha}}" - - name: build-indexer - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: . - - name: dockerfile - value: cmd/acb-indexer/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-indexer:{{workflow.parameters.commit-sha}}" - - name: build-bot-random - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: bots/random - - name: dockerfile - value: bots/random/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-strategy-random:{{workflow.parameters.commit-sha}}" - - name: build-bot-gatherer - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: bots/gatherer - - name: dockerfile - value: bots/gatherer/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-strategy-gatherer:{{workflow.parameters.commit-sha}}" - - name: build-bot-rusher - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: bots/rusher - - name: dockerfile - value: bots/rusher/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-strategy-rusher:{{workflow.parameters.commit-sha}}" - - name: build-bot-guardian - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: bots/guardian - - name: dockerfile - value: bots/guardian/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-strategy-guardian:{{workflow.parameters.commit-sha}}" - - name: build-bot-swarm - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: bots/swarm - - name: dockerfile - value: bots/swarm/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-strategy-swarm:{{workflow.parameters.commit-sha}}" - - name: build-bot-hunter - templateRef: - name: acb-build-image - template: kaniko-build - arguments: - parameters: - - name: context - value: bots/hunter - - name: dockerfile - value: bots/hunter/Dockerfile - - name: image - value: "{{workflow.parameters.registry}}/acb-strategy-hunter:{{workflow.parameters.commit-sha}}" - - name: build-site - templateRef: - name: acb-build-site - template: npm-build - arguments: - parameters: - - name: commit-sha - value: "{{workflow.parameters.commit-sha}}" - parameters: - - src: - dependencyName: push - dataKey: body.after - dest: spec.arguments.parameters.0.value diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-serviceaccount.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-serviceaccount.yml deleted file mode 100644 index 6500400..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-ci-serviceaccount.yml +++ /dev/null @@ -1,44 +0,0 @@ -apiVersion: v1 -kind: ServiceAccount -metadata: - name: acb-ci - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-ci - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: acb-ci-workflow - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-ci - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -rules: - - apiGroups: [""] - resources: ["pods", "pods/log"] - verbs: ["get", "list", "watch"] - - apiGroups: ["argoproj.io"] - resources: ["workflows", "workflowtemplates"] - verbs: ["get", "list", "create"] ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: acb-ci-workflow - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-ci - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -subjects: - - kind: ServiceAccount - name: acb-ci - namespace: ai-code-battle -roleRef: - kind: Role - name: acb-ci-workflow - apiGroup: rbac.authorization.k8s.io diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-cloudflare-api-token-sealedsecret.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-cloudflare-api-token-sealedsecret.yml deleted file mode 100644 index 7ff1dda..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-cloudflare-api-token-sealedsecret.yml +++ /dev/null @@ -1,20 +0,0 @@ -# SealedSecret template — replace with actual sealed values via kubeseal -# Source secret keys: -# token: Cloudflare API token (for wrangler pages deploy by index builder) -apiVersion: bitnami.com/v1alpha1 -kind: SealedSecret -metadata: - name: acb-cloudflare-api-token - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-cloudflare-api-token - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: secrets -spec: - encryptedData: - token: REPLACE_WITH_SEALED_VALUE - template: - metadata: - name: acb-cloudflare-api-token - namespace: ai-code-battle - type: Opaque diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-index-builder-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-index-builder-deployment.yml deleted file mode 100644 index 58ab0fb..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-index-builder-deployment.yml +++ /dev/null @@ -1,61 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-index-builder - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-index-builder - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: index-builder -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-index-builder - strategy: - type: Recreate - template: - metadata: - labels: - app.kubernetes.io/name: acb-index-builder - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: index-builder - spec: - containers: - - name: index-builder - image: forgejo.ardenone.com/ai-code-battle/acb-index-builder:latest - env: - - name: ACB_DATABASE_URL - valueFrom: - secretKeyRef: - name: acb-postgres-credentials - key: database-url - - name: ACB_OUTPUT_DIR - value: "/app/data" - - name: ACB_R2_ENDPOINT - valueFrom: - secretKeyRef: - name: acb-r2-credentials - key: endpoint - - name: ACB_R2_ACCESS_KEY - valueFrom: - secretKeyRef: - name: acb-r2-credentials - key: access-key - - name: ACB_R2_SECRET_KEY - valueFrom: - secretKeyRef: - name: acb-r2-credentials - key: secret-key - - name: ACB_CLOUDFLARE_API_TOKEN - valueFrom: - secretKeyRef: - name: acb-cloudflare-api-token - key: token - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - memory: 256Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-matchmaker-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-matchmaker-deployment.yml deleted file mode 100644 index cdefdcd..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-matchmaker-deployment.yml +++ /dev/null @@ -1,75 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-matchmaker - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-matchmaker - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: matchmaker -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-matchmaker - template: - metadata: - labels: - app.kubernetes.io/name: acb-matchmaker - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: matchmaker - spec: - containers: - - name: matchmaker - image: forgejo.ardenone.com/ai-code-battle/acb-matchmaker:latest - env: - - name: ACB_DATABASE_URL - valueFrom: - secretKeyRef: - name: acb-database-url - key: url - - name: ACB_VALKEY_ADDR - value: "valkey.ai-code-battle.svc:6379" - - name: ACB_VALKEY_PASSWORD - valueFrom: - secretKeyRef: - name: acb-valkey-password - key: password - optional: true - - name: ACB_ENCRYPTION_KEY - valueFrom: - secretKeyRef: - name: acb-api-key - key: encryption-key - optional: true - - name: ACB_DISCORD_WEBHOOK - valueFrom: - secretKeyRef: - name: acb-alert-webhooks - key: discord - optional: true - - name: ACB_SLACK_WEBHOOK - valueFrom: - secretKeyRef: - name: acb-alert-webhooks - key: slack - optional: true - - name: ACB_MATCHMAKER_INTERVAL - value: "60" - - name: ACB_HEALTHCHECK_INTERVAL - value: "900" - - name: ACB_REAPER_INTERVAL - value: "300" - - name: ACB_BOT_TIMEOUT - value: "5" - - name: ACB_STALE_JOB_MINUTES - value: "15" - - name: ACB_MAX_CONSEC_FAILS - value: "3" - resources: - requests: - cpu: 50m - memory: 128Mi - limits: - memory: 256Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-r2-credentials-sealedsecret.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-r2-credentials-sealedsecret.yml deleted file mode 100644 index 209e0e4..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-r2-credentials-sealedsecret.yml +++ /dev/null @@ -1,24 +0,0 @@ -# SealedSecret template — replace with actual sealed values via kubeseal -# Source secret keys: -# endpoint: R2 S3-compatible endpoint URL -# access-key: R2 access key ID -# secret-key: R2 secret access key -apiVersion: bitnami.com/v1alpha1 -kind: SealedSecret -metadata: - name: acb-r2-credentials - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-r2-credentials - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: secrets -spec: - encryptedData: - endpoint: REPLACE_WITH_SEALED_VALUE - access-key: REPLACE_WITH_SEALED_VALUE - secret-key: REPLACE_WITH_SEALED_VALUE - template: - metadata: - name: acb-r2-credentials - namespace: ai-code-battle - type: Opaque diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-registry-credentials-sealedsecret.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-registry-credentials-sealedsecret.yml deleted file mode 100644 index 22bce51..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-registry-credentials-sealedsecret.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Template: replace encryptedData with actual SealedSecret values -# Generate with: -# kubectl create secret docker-registry acb-registry-credentials \ -# --docker-server=forgejo.ardenone.com \ -# --docker-username= \ -# --docker-password= \ -# --dry-run=client -o yaml | kubeseal --format yaml -apiVersion: bitnami.com/v1alpha1 -kind: SealedSecret -metadata: - name: acb-registry-credentials - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-registry-credentials - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: ci -spec: - encryptedData: - .dockerconfigjson: REPLACE_WITH_SEALED_VALUE - template: - metadata: - name: acb-registry-credentials - namespace: ai-code-battle - type: kubernetes.io/dockerconfigjson diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-gatherer-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-gatherer-deployment.yml deleted file mode 100644 index 1bf4154..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-gatherer-deployment.yml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-strategy-gatherer - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-gatherer - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-strategy-gatherer - template: - metadata: - labels: - app.kubernetes.io/name: acb-strategy-gatherer - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot - spec: - containers: - - name: gatherer - image: forgejo.ardenone.com/ai-code-battle/acb-strategy-gatherer:latest - env: - - name: BOT_PORT - value: "8080" - - name: BOT_SECRET - valueFrom: - secretKeyRef: - name: acb-bot-secrets - key: gatherer - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-gatherer-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-gatherer-service.yml deleted file mode 100644 index 70d83b1..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-gatherer-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-strategy-gatherer - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-gatherer - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-strategy-gatherer - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-guardian-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-guardian-deployment.yml deleted file mode 100644 index 1bebf6b..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-guardian-deployment.yml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-strategy-guardian - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-guardian - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-strategy-guardian - template: - metadata: - labels: - app.kubernetes.io/name: acb-strategy-guardian - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot - spec: - containers: - - name: guardian - image: forgejo.ardenone.com/ai-code-battle/acb-strategy-guardian:latest - env: - - name: BOT_PORT - value: "8080" - - name: BOT_SECRET - valueFrom: - secretKeyRef: - name: acb-bot-secrets - key: guardian - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-guardian-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-guardian-service.yml deleted file mode 100644 index 12b856b..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-guardian-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-strategy-guardian - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-guardian - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-strategy-guardian - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-hunter-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-hunter-deployment.yml deleted file mode 100644 index beb8092..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-hunter-deployment.yml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-strategy-hunter - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-hunter - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-strategy-hunter - template: - metadata: - labels: - app.kubernetes.io/name: acb-strategy-hunter - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot - spec: - containers: - - name: hunter - image: forgejo.ardenone.com/ai-code-battle/acb-strategy-hunter:latest - env: - - name: BOT_PORT - value: "8080" - - name: BOT_SECRET - valueFrom: - secretKeyRef: - name: acb-bot-secrets - key: hunter - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-hunter-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-hunter-service.yml deleted file mode 100644 index 2d92bc6..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-hunter-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-strategy-hunter - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-hunter - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-strategy-hunter - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-random-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-random-deployment.yml deleted file mode 100644 index da3ad2f..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-random-deployment.yml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-strategy-random - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-random - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-strategy-random - template: - metadata: - labels: - app.kubernetes.io/name: acb-strategy-random - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot - spec: - containers: - - name: random - image: forgejo.ardenone.com/ai-code-battle/acb-strategy-random:latest - env: - - name: BOT_PORT - value: "8080" - - name: BOT_SECRET - valueFrom: - secretKeyRef: - name: acb-bot-secrets - key: random - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-random-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-random-service.yml deleted file mode 100644 index f63ee40..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-random-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-strategy-random - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-random - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-strategy-random - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-rusher-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-rusher-deployment.yml deleted file mode 100644 index e0db3ee..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-rusher-deployment.yml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-strategy-rusher - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-rusher - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-strategy-rusher - template: - metadata: - labels: - app.kubernetes.io/name: acb-strategy-rusher - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot - spec: - containers: - - name: rusher - image: forgejo.ardenone.com/ai-code-battle/acb-strategy-rusher:latest - env: - - name: BOT_PORT - value: "8080" - - name: BOT_SECRET - valueFrom: - secretKeyRef: - name: acb-bot-secrets - key: rusher - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-rusher-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-rusher-service.yml deleted file mode 100644 index dea69c6..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-rusher-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-strategy-rusher - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-rusher - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-strategy-rusher - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-swarm-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-swarm-deployment.yml deleted file mode 100644 index 720c92c..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-swarm-deployment.yml +++ /dev/null @@ -1,55 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-strategy-swarm - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-swarm - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/name: acb-strategy-swarm - template: - metadata: - labels: - app.kubernetes.io/name: acb-strategy-swarm - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot - spec: - containers: - - name: swarm - image: forgejo.ardenone.com/ai-code-battle/acb-strategy-swarm:latest - env: - - name: BOT_PORT - value: "8080" - - name: BOT_SECRET - valueFrom: - secretKeyRef: - name: acb-bot-secrets - key: swarm - ports: - - name: http - containerPort: 8080 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /health - port: http - initialDelaySeconds: 3 - periodSeconds: 10 - resources: - requests: - cpu: 50m - memory: 64Mi - limits: - memory: 128Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-swarm-service.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-swarm-service.yml deleted file mode 100644 index b503735..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-strategy-swarm-service.yml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: acb-strategy-swarm - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-strategy-swarm - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: strategy-bot -spec: - type: ClusterIP - selector: - app.kubernetes.io/name: acb-strategy-swarm - ports: - - name: http - port: 8080 - targetPort: http - protocol: TCP diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-worker-deployment.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/acb-worker-deployment.yml deleted file mode 100644 index e6e62ae..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/acb-worker-deployment.yml +++ /dev/null @@ -1,79 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: acb-worker - namespace: ai-code-battle - labels: - app.kubernetes.io/name: acb-worker - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: worker -spec: - replicas: 2 - selector: - matchLabels: - app.kubernetes.io/name: acb-worker - template: - metadata: - labels: - app.kubernetes.io/name: acb-worker - app.kubernetes.io/part-of: ai-code-battle - app.kubernetes.io/component: worker - spec: - containers: - - name: worker - image: forgejo.ardenone.com/ai-code-battle/acb-worker:latest - args: - - "-poll=5s" - - "-heartbeat=30s" - - "-timeout=3s" - env: - - name: ACB_API_ENDPOINT - valueFrom: - secretKeyRef: - name: acb-api-key - key: api-endpoint - - name: ACB_API_KEY - valueFrom: - secretKeyRef: - name: acb-api-key - key: api-key - - name: ACB_R2_ENDPOINT - valueFrom: - secretKeyRef: - name: acb-r2-credentials - key: endpoint - - name: ACB_R2_ACCESS_KEY - valueFrom: - secretKeyRef: - name: acb-r2-credentials - key: access-key - - name: ACB_R2_SECRET_KEY - valueFrom: - secretKeyRef: - name: acb-r2-credentials - key: secret-key - - name: ACB_METRICS_ADDR - value: ":9090" - ports: - - name: metrics - containerPort: 9090 - protocol: TCP - livenessProbe: - httpGet: - path: /health - port: metrics - initialDelaySeconds: 5 - periodSeconds: 30 - readinessProbe: - httpGet: - path: /ready - port: metrics - initialDelaySeconds: 5 - periodSeconds: 10 - resources: - requests: - cpu: 100m - memory: 256Mi - limits: - memory: 512Mi - restartPolicy: Always diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/argocd-application.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/argocd-application.yml deleted file mode 100644 index a2dd92e..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/argocd-application.yml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: argoproj.io/v1alpha1 -kind: Application -metadata: - name: ai-code-battle - namespace: argocd -spec: - project: default - source: - repoURL: https://forgejo.ardenone.com/ai-code-battle/ai-code-battle.git - targetRevision: master - path: cluster-configuration/apexalgo-iad/ai-code-battle - directory: - recurse: false - destination: - server: https://kubernetes.default.svc - namespace: ai-code-battle - syncPolicy: - automated: - prune: true - selfHeal: true - syncOptions: - - CreateNamespace=false diff --git a/cluster-configuration/apexalgo-iad/ai-code-battle/namespace.yml b/cluster-configuration/apexalgo-iad/ai-code-battle/namespace.yml deleted file mode 100644 index 9f039fa..0000000 --- a/cluster-configuration/apexalgo-iad/ai-code-battle/namespace.yml +++ /dev/null @@ -1,6 +0,0 @@ -apiVersion: v1 -kind: Namespace -metadata: - name: ai-code-battle - labels: - app.kubernetes.io/name: ai-code-battle diff --git a/cmd/acb-api/glicko2_test.go b/cmd/acb-api/glicko2_test.go deleted file mode 100644 index 26cb661..0000000 --- a/cmd/acb-api/glicko2_test.go +++ /dev/null @@ -1,199 +0,0 @@ -package main - -import ( - "math" - "testing" -) - -func TestGFunc(t *testing.T) { - // g(0) should be 1 - if g := gFunc(0); math.Abs(g-1.0) > 1e-10 { - t.Errorf("g(0) = %f, want 1.0", g) - } - - // g(phi) should be between 0 and 1 for positive phi - if g := gFunc(1.0); g <= 0 || g >= 1.0 { - t.Errorf("g(1.0) = %f, want in (0, 1)", g) - } - - // g should decrease as phi increases - g1 := gFunc(0.5) - g2 := gFunc(1.5) - if g1 <= g2 { - t.Errorf("g should decrease: g(0.5)=%f, g(1.5)=%f", g1, g2) - } -} - -func TestEFunc(t *testing.T) { - // E(mu, mu, phi) should be 0.5 (equal ratings) - e := eFunc(0, 0, 1.0) - if math.Abs(e-0.5) > 1e-10 { - t.Errorf("E(0, 0, 1.0) = %f, want 0.5", e) - } - - // Higher mu should give higher expected score - eHigh := eFunc(1.0, 0, 1.0) - eLow := eFunc(-1.0, 0, 1.0) - if eHigh <= 0.5 || eLow >= 0.5 { - t.Errorf("expected eHigh>0.5, eLow<0.5, got %f, %f", eHigh, eLow) - } -} - -func TestUpdateRatings_TwoPlayers(t *testing.T) { - // Two equally rated players, player 0 wins - r := []Glicko2Rating{ - {Mu: 1500, Phi: 350, Sigma: 0.06}, - {Mu: 1500, Phi: 350, Sigma: 0.06}, - } - scores := []float64{10, 5} // player 0 wins - - newR := updateRatings(r, scores) - - // Winner should gain rating - if newR[0].Mu <= r[0].Mu { - t.Errorf("winner mu should increase: %f -> %f", r[0].Mu, newR[0].Mu) - } - // Loser should lose rating - if newR[1].Mu >= r[1].Mu { - t.Errorf("loser mu should decrease: %f -> %f", r[1].Mu, newR[1].Mu) - } - - // RD should decrease for both (more information) - if newR[0].Phi >= r[0].Phi { - t.Errorf("winner phi should decrease: %f -> %f", r[0].Phi, newR[0].Phi) - } - if newR[1].Phi >= r[1].Phi { - t.Errorf("loser phi should decrease: %f -> %f", r[1].Phi, newR[1].Phi) - } -} - -func TestUpdateRatings_Draw(t *testing.T) { - r := []Glicko2Rating{ - {Mu: 1500, Phi: 350, Sigma: 0.06}, - {Mu: 1500, Phi: 350, Sigma: 0.06}, - } - scores := []float64{5, 5} // draw - - newR := updateRatings(r, scores) - - // Equal ratings + draw = negligible mu change - diff := math.Abs(newR[0].Mu - newR[1].Mu) - if diff > 1.0 { - t.Errorf("draw should keep ratings close: %f vs %f (diff=%f)", newR[0].Mu, newR[1].Mu, diff) - } - - // Both should be close to original - if math.Abs(newR[0].Mu-1500) > 5.0 { - t.Errorf("draw between equals should barely change rating: %f", newR[0].Mu) - } -} - -func TestUpdateRatings_UpsetGivesLargerGain(t *testing.T) { - // Lower-rated player beats higher-rated player - r := []Glicko2Rating{ - {Mu: 1300, Phi: 100, Sigma: 0.06}, // underdog - {Mu: 1700, Phi: 100, Sigma: 0.06}, // favorite - } - scores := []float64{10, 5} // underdog wins - - newR := updateRatings(r, scores) - - underdogGain := newR[0].Mu - r[0].Mu - favoriteGain := r[1].Mu - newR[1].Mu - - if underdogGain <= 0 { - t.Errorf("underdog should gain rating: %f", underdogGain) - } - if favoriteGain <= 0 { - t.Errorf("favorite should lose rating: %f", favoriteGain) - } - - // Now test expected win: higher-rated player beats lower - r2 := []Glicko2Rating{ - {Mu: 1700, Phi: 100, Sigma: 0.06}, // favorite - {Mu: 1300, Phi: 100, Sigma: 0.06}, // underdog - } - scores2 := []float64{10, 5} - newR2 := updateRatings(r2, scores2) - expectedGain := newR2[0].Mu - r2[0].Mu - - // Upset should give larger rating change than expected result - if underdogGain <= expectedGain { - t.Errorf("upset gain (%f) should exceed expected win gain (%f)", underdogGain, expectedGain) - } -} - -func TestUpdateRatings_MultiPlayer(t *testing.T) { - // 4-player match - r := []Glicko2Rating{ - {Mu: 1500, Phi: 200, Sigma: 0.06}, - {Mu: 1500, Phi: 200, Sigma: 0.06}, - {Mu: 1500, Phi: 200, Sigma: 0.06}, - {Mu: 1500, Phi: 200, Sigma: 0.06}, - } - scores := []float64{20, 15, 10, 5} - - newR := updateRatings(r, scores) - - // Ratings should be ordered by score - for i := 0; i < len(newR)-1; i++ { - if newR[i].Mu <= newR[i+1].Mu { - t.Errorf("player %d (score=%0.f, mu=%f) should be rated above player %d (score=%0.f, mu=%f)", - i, scores[i], newR[i].Mu, i+1, scores[i+1], newR[i+1].Mu) - } - } -} - -func TestUpdateRatings_LowRDPlayersChangeMore(t *testing.T) { - // Lower RD (more certain) means the system gives more weight to the result - highRD := Glicko2Rating{Mu: 1500, Phi: 300, Sigma: 0.06} - lowRD := Glicko2Rating{Mu: 1500, Phi: 100, Sigma: 0.06} - - // Both beat a 1500-rated opponent - opp := Glicko2Rating{Mu: 1500, Phi: 200, Sigma: 0.06} - - r1 := updateRatings([]Glicko2Rating{highRD, opp}, []float64{10, 5}) - r2 := updateRatings([]Glicko2Rating{lowRD, opp}, []float64{10, 5}) - - highRDGain := r1[0].Mu - highRD.Mu - lowRDGain := r2[0].Mu - lowRD.Mu - - // High RD player should change more (less certainty = more adjustable) - if highRDGain <= lowRDGain { - t.Errorf("high RD player should gain more: %f vs %f", highRDGain, lowRDGain) - } -} - -func TestUpdateRatings_Determinism(t *testing.T) { - r := []Glicko2Rating{ - {Mu: 1600, Phi: 150, Sigma: 0.06}, - {Mu: 1400, Phi: 250, Sigma: 0.06}, - } - scores := []float64{8, 12} - - r1 := updateRatings(r, scores) - r2 := updateRatings(r, scores) - - for i := range r1 { - if r1[i].Mu != r2[i].Mu || r1[i].Phi != r2[i].Phi || r1[i].Sigma != r2[i].Sigma { - t.Errorf("ratings not deterministic at index %d: %+v vs %+v", i, r1[i], r2[i]) - } - } -} - -func TestDisplayRating(t *testing.T) { - r := Glicko2Rating{Mu: 1500, Phi: 350, Sigma: 0.06} - display := r.DisplayRating() - expected := 1500.0 - 2*350.0 - if math.Abs(display-expected) > 1e-10 { - t.Errorf("DisplayRating() = %f, want %f", display, expected) - } -} - -func TestComputeVolatility_Convergence(t *testing.T) { - // Should not panic or infinite loop - sigma := computeVolatility(0.06, 1.0, 10.0, 5.0) - if sigma <= 0 || math.IsNaN(sigma) || math.IsInf(sigma, 0) { - t.Errorf("volatility should be positive finite: %f", sigma) - } -} diff --git a/cmd/acb-api/jobs.go b/cmd/acb-api/jobs.go deleted file mode 100644 index 963d17f..0000000 --- a/cmd/acb-api/jobs.go +++ /dev/null @@ -1,216 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "net/http" - "time" -) - -const valkeyJobQueue = "acb:jobs:pending" - -type JobClaimRequest struct { - WorkerID string `json:"worker_id"` -} - -type JobClaimResponse struct { - JobID string `json:"job_id"` - MatchID string `json:"match_id"` - ConfigJSON json.RawMessage `json:"config"` -} - -func (s *Server) handleJobClaim(w http.ResponseWriter, r *http.Request) { - // Authenticate worker - if !s.authenticateWorker(r) { - writeError(w, http.StatusUnauthorized, "invalid API key") - return - } - - var req JobClaimRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid JSON body") - return - } - if req.WorkerID == "" { - writeError(w, http.StatusBadRequest, "worker_id is required") - return - } - - ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) - defer cancel() - - // Blocking pop from Valkey queue (short timeout for HTTP context) - result, err := s.rdb.BRPop(ctx, 4*time.Second, valkeyJobQueue).Result() - if err != nil { - // Timeout or empty queue - writeJSON(w, http.StatusNoContent, nil) - return - } - - jobID := result[1] // BRPop returns [key, value] - - // Fetch job details from PostgreSQL and mark as running - var resp JobClaimResponse - var configJSON []byte - err = s.db.QueryRowContext(r.Context(), - `UPDATE jobs SET status = 'running', worker_id = $1, claimed_at = NOW() - WHERE job_id = $2 AND status = 'pending' - RETURNING job_id, match_id, config_json`, - req.WorkerID, jobID, - ).Scan(&resp.JobID, &resp.MatchID, &configJSON) - if err != nil { - // Job was already claimed or doesn't exist; put it back if it was something else - writeJSON(w, http.StatusNoContent, nil) - return - } - resp.ConfigJSON = configJSON - - writeJSON(w, http.StatusOK, resp) -} - -type JobResultRequest struct { - WorkerID string `json:"worker_id"` - Winner *int `json:"winner"` - Condition string `json:"condition"` - TurnCount int `json:"turn_count"` - Scores json.RawMessage `json:"scores"` -} - -func (s *Server) handleJobResult(w http.ResponseWriter, r *http.Request) { - if !s.authenticateWorker(r) { - writeError(w, http.StatusUnauthorized, "invalid API key") - return - } - - jobID := r.PathValue("job_id") - - var req JobResultRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid JSON body") - return - } - - ctx := r.Context() - - // Start transaction - tx, err := s.db.BeginTx(ctx, nil) - if err != nil { - writeError(w, http.StatusInternalServerError, "transaction error") - return - } - defer tx.Rollback() - - // Get match_id from job - var matchID string - err = tx.QueryRowContext(ctx, - `UPDATE jobs SET status = 'completed', completed_at = NOW() - WHERE job_id = $1 AND status = 'running' - RETURNING match_id`, jobID, - ).Scan(&matchID) - if err != nil { - writeError(w, http.StatusNotFound, "job not found or not running") - return - } - - // Update match - _, err = tx.ExecContext(ctx, - `UPDATE matches SET status = 'completed', winner = $1, condition = $2, - turn_count = $3, scores_json = $4, completed_at = NOW() - WHERE match_id = $5`, - req.Winner, req.Condition, req.TurnCount, req.Scores, matchID, - ) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to update match") - return - } - - // Update participant scores - var scores []int - if err := json.Unmarshal(req.Scores, &scores); err == nil { - for slot, score := range scores { - _, _ = tx.ExecContext(ctx, - `UPDATE match_participants SET score = $1, status = 'completed' - WHERE match_id = $2 AND player_slot = $3`, - score, matchID, slot, - ) - } - } - - // Get participants for rating update - rows, err := tx.QueryContext(ctx, - `SELECT mp.bot_id, mp.player_slot, mp.score, - b.rating_mu, b.rating_phi, b.rating_sigma - FROM match_participants mp - JOIN bots b ON b.bot_id = mp.bot_id - WHERE mp.match_id = $1 - ORDER BY mp.player_slot`, matchID, - ) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to fetch participants") - return - } - - type participant struct { - botID string - slot int - score int - mu, phi float64 - sigma float64 - } - var participants []participant - for rows.Next() { - var p participant - if err := rows.Scan(&p.botID, &p.slot, &p.score, &p.mu, &p.phi, &p.sigma); err != nil { - rows.Close() - writeError(w, http.StatusInternalServerError, "scan error") - return - } - participants = append(participants, p) - } - rows.Close() - - // Update Glicko-2 ratings - if len(participants) >= 2 { - ratings := make([]Glicko2Rating, len(participants)) - scores := make([]float64, len(participants)) - for i, p := range participants { - ratings[i] = Glicko2Rating{Mu: p.mu, Phi: p.phi, Sigma: p.sigma} - scores[i] = float64(p.score) - } - - newRatings := updateRatings(ratings, scores) - - for i, p := range participants { - nr := newRatings[i] - _, _ = tx.ExecContext(ctx, - `UPDATE bots SET rating_mu = $1, rating_phi = $2, rating_sigma = $3, last_active = NOW() - WHERE bot_id = $4`, - nr.Mu, nr.Phi, nr.Sigma, p.botID, - ) - displayRating := nr.Mu - 2*nr.Phi - _, _ = tx.ExecContext(ctx, - `INSERT INTO rating_history (bot_id, match_id, rating) - VALUES ($1, $2, $3)`, - p.botID, matchID, displayRating, - ) - } - } - - if err := tx.Commit(); err != nil { - writeError(w, http.StatusInternalServerError, "commit error") - return - } - - writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) -} - -func (s *Server) authenticateWorker(r *http.Request) bool { - if s.cfg.WorkerAPIKey == "" { - return true // no auth configured (dev mode) - } - key := r.Header.Get("Authorization") - if key == "" { - key = r.Header.Get("X-API-Key") - } - return key == "Bearer "+s.cfg.WorkerAPIKey || key == s.cfg.WorkerAPIKey -} diff --git a/cmd/acb-api/predictions.go b/cmd/acb-api/predictions.go deleted file mode 100644 index e589767..0000000 --- a/cmd/acb-api/predictions.go +++ /dev/null @@ -1,236 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "net/http" - "time" -) - -// handleSubmitPrediction handles POST /api/predictions -func (s *Server) handleSubmitPrediction(w http.ResponseWriter, r *http.Request) { - var req struct { - MatchID string `json:"match_id"` - PredictorID string `json:"predictor_id"` - PredictedBot string `json:"predicted_bot"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - if req.MatchID == "" || req.PredictorID == "" || req.PredictedBot == "" { - writeError(w, http.StatusBadRequest, "match_id, predictor_id, and predicted_bot are required") - return - } - - ctx := r.Context() - - // Verify match exists and is pending/active - var matchStatus string - err := s.db.QueryRowContext(ctx, `SELECT status FROM matches WHERE match_id = $1`, req.MatchID).Scan(&matchStatus) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "match not found") - return - } else if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - if matchStatus == "completed" { - writeError(w, http.StatusConflict, "match already completed; predictions closed") - return - } - - // Upsert prediction (one per predictor per match) - _, err = s.db.ExecContext(ctx, ` - INSERT INTO predictions (match_id, predictor_id, predicted_bot) - VALUES ($1, $2, $3) - ON CONFLICT (match_id, predictor_id) - DO UPDATE SET predicted_bot = EXCLUDED.predicted_bot - `, req.MatchID, req.PredictorID, req.PredictedBot) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to store prediction") - return - } - - writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) -} - -// handleResolvePredictions handles POST /api/predictions/{match_id}/resolve -// Called internally (worker or ticker) after a match completes. -func (s *Server) handleResolvePredictions(w http.ResponseWriter, r *http.Request) { - matchID := r.PathValue("match_id") - if matchID == "" { - writeError(w, http.StatusBadRequest, "missing match_id") - return - } - - ctx := r.Context() - - // Get match winner - var winnerID sql.NullString - err := s.db.QueryRowContext(ctx, ` - SELECT mp.bot_id FROM match_participants mp - JOIN matches m ON mp.match_id = m.match_id - WHERE m.match_id = $1 - AND mp.player_slot = m.winner - `, matchID).Scan(&winnerID) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "match not found or has no winner") - return - } else if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - - winner := winnerID.String - - // Get all unresolved predictions for this match - rows, err := s.db.QueryContext(ctx, ` - SELECT id, predictor_id, predicted_bot - FROM predictions - WHERE match_id = $1 AND correct IS NULL - `, matchID) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - type predRow struct { - id int64 - predictorID string - predictedBot string - } - var preds []predRow - for rows.Next() { - var p predRow - if err := rows.Scan(&p.id, &p.predictorID, &p.predictedBot); err != nil { - continue - } - preds = append(preds, p) - } - - now := time.Now().UTC() - resolved := 0 - for _, p := range preds { - correct := p.predictedBot == winner - _, err := s.db.ExecContext(ctx, ` - UPDATE predictions SET correct = $1, resolved_at = $2 WHERE id = $3 - `, correct, now, p.id) - if err != nil { - continue - } - - // Update predictor stats - if correct { - _, _ = s.db.ExecContext(ctx, ` - INSERT INTO predictor_stats (predictor_id, correct, streak, best_streak) - VALUES ($1, 1, 1, 1) - ON CONFLICT (predictor_id) DO UPDATE SET - correct = predictor_stats.correct + 1, - streak = predictor_stats.streak + 1, - best_streak = GREATEST(predictor_stats.best_streak, predictor_stats.streak + 1), - updated_at = NOW() - `, p.predictorID) - } else { - _, _ = s.db.ExecContext(ctx, ` - INSERT INTO predictor_stats (predictor_id, incorrect, streak) - VALUES ($1, 1, 0) - ON CONFLICT (predictor_id) DO UPDATE SET - incorrect = predictor_stats.incorrect + 1, - streak = 0, - updated_at = NOW() - `, p.predictorID) - } - resolved++ - } - - writeJSON(w, http.StatusOK, map[string]int{"resolved": resolved}) -} - -// handlePredictionLeaderboard handles GET /api/predictions/leaderboard -func (s *Server) handlePredictionLeaderboard(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - rows, err := s.db.QueryContext(ctx, ` - SELECT predictor_id, correct, incorrect, - CASE WHEN (correct + incorrect) > 0 - THEN ROUND(100.0 * correct / (correct + incorrect), 1) - ELSE 0 END AS accuracy, - streak, best_streak - FROM predictor_stats - WHERE (correct + incorrect) >= 5 - ORDER BY accuracy DESC, correct DESC - LIMIT 100 - `) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - type entry struct { - PredictorID string `json:"predictor_id"` - Correct int `json:"correct"` - Incorrect int `json:"incorrect"` - Accuracy float64 `json:"accuracy"` - Streak int `json:"streak"` - BestStreak int `json:"best_streak"` - } - entries := make([]entry, 0) - for rows.Next() { - var e entry - if err := rows.Scan(&e.PredictorID, &e.Correct, &e.Incorrect, &e.Accuracy, &e.Streak, &e.BestStreak); err != nil { - continue - } - entries = append(entries, e) - } - - writeJSON(w, http.StatusOK, map[string]any{ - "leaderboard": entries, - "updated_at": time.Now().UTC(), - }) -} - -// handleGetPredictions handles GET /api/predictions/{match_id} -func (s *Server) handleGetPredictions(w http.ResponseWriter, r *http.Request) { - matchID := r.PathValue("match_id") - ctx := r.Context() - - rows, err := s.db.QueryContext(ctx, ` - SELECT predictor_id, predicted_bot, correct - FROM predictions - WHERE match_id = $1 - ORDER BY created_at DESC - `, matchID) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - type pred struct { - PredictorID string `json:"predictor_id"` - PredictedBot string `json:"predicted_bot"` - Correct *bool `json:"correct"` - } - preds := make([]pred, 0) - for rows.Next() { - var p pred - var correct sql.NullBool - if err := rows.Scan(&p.PredictorID, &p.PredictedBot, &correct); err != nil { - continue - } - if correct.Valid { - b := correct.Bool - p.Correct = &b - } - preds = append(preds, p) - } - - writeJSON(w, http.StatusOK, map[string]any{ - "match_id": matchID, - "predictions": preds, - }) -} diff --git a/cmd/acb-api/register.go b/cmd/acb-api/register.go deleted file mode 100644 index 8f92445..0000000 --- a/cmd/acb-api/register.go +++ /dev/null @@ -1,231 +0,0 @@ -package main - -import ( - "encoding/json" - "fmt" - "net/http" - "regexp" - "strings" - "time" -) - -var validBotName = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{1,30}[a-zA-Z0-9]$`) - -type RegisterRequest struct { - Name string `json:"name"` - EndpointURL string `json:"endpoint_url"` - Owner string `json:"owner"` - Description string `json:"description,omitempty"` -} - -type RegisterResponse struct { - BotID string `json:"bot_id"` - SharedSecret string `json:"shared_secret"` - Message string `json:"message"` -} - -func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) { - var req RegisterRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid JSON body") - return - } - - req.Name = strings.TrimSpace(req.Name) - req.Owner = strings.TrimSpace(req.Owner) - req.EndpointURL = strings.TrimSpace(req.EndpointURL) - - if !validBotName.MatchString(req.Name) { - writeError(w, http.StatusBadRequest, "name must be 3-32 alphanumeric/hyphen chars") - return - } - if req.EndpointURL == "" { - writeError(w, http.StatusBadRequest, "endpoint_url is required") - return - } - if req.Owner == "" { - writeError(w, http.StatusBadRequest, "owner is required") - return - } - - // Health check the bot endpoint - if err := s.checkBotHealth(req.EndpointURL); err != nil { - writeError(w, http.StatusBadRequest, fmt.Sprintf("bot health check failed: %v", err)) - return - } - - botID, err := generateID("b_", 4) // b_ + 8 hex chars - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to generate bot ID") - return - } - - secret, err := generateSecret() - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to generate shared secret") - return - } - - // Encrypt secret for storage - encryptedSecret := secret // default: store plaintext if no key - if s.cfg.EncryptionKey != "" { - encryptedSecret, err = encryptSecret(secret, s.cfg.EncryptionKey) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to encrypt secret") - return - } - } - - _, err = s.db.ExecContext(r.Context(), - `INSERT INTO bots (bot_id, name, owner, endpoint_url, shared_secret, status, description, last_active) - VALUES ($1, $2, $3, $4, $5, 'active', $6, NOW())`, - botID, req.Name, req.Owner, req.EndpointURL, encryptedSecret, req.Description, - ) - if err != nil { - if strings.Contains(err.Error(), "duplicate key") || strings.Contains(err.Error(), "unique") { - writeError(w, http.StatusConflict, "bot name already taken") - return - } - writeError(w, http.StatusInternalServerError, "failed to register bot") - return - } - - writeJSON(w, http.StatusCreated, RegisterResponse{ - BotID: botID, - SharedSecret: secret, - Message: "Bot registered. Save the shared_secret — it will not be shown again.", - }) -} - -func (s *Server) checkBotHealth(endpointURL string) error { - url := strings.TrimRight(endpointURL, "/") + "/health" - client := &http.Client{Timeout: time.Duration(s.cfg.BotTimeoutSecs) * time.Second} - resp, err := client.Get(url) - if err != nil { - return fmt.Errorf("connection failed: %w", err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return fmt.Errorf("expected 200, got %d", resp.StatusCode) - } - return nil -} - -type RotateKeyRequest struct { - BotID string `json:"bot_id"` - SharedSecret string `json:"shared_secret"` - Retire bool `json:"retire,omitempty"` -} - -type RotateKeyResponse struct { - NewSecret string `json:"new_secret,omitempty"` - Message string `json:"message"` -} - -func (s *Server) handleRotateKey(w http.ResponseWriter, r *http.Request) { - var req RotateKeyRequest - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid JSON body") - return - } - - // Verify current secret - var storedSecret string - err := s.db.QueryRowContext(r.Context(), - `SELECT shared_secret FROM bots WHERE bot_id = $1`, req.BotID, - ).Scan(&storedSecret) - if err != nil { - writeError(w, http.StatusNotFound, "bot not found") - return - } - - // Decrypt stored secret for comparison - plainSecret := storedSecret - if s.cfg.EncryptionKey != "" { - plainSecret, err = decryptSecret(storedSecret, s.cfg.EncryptionKey) - if err != nil { - writeError(w, http.StatusInternalServerError, "decryption error") - return - } - } - - if plainSecret != req.SharedSecret { - writeError(w, http.StatusUnauthorized, "invalid shared secret") - return - } - - if req.Retire { - _, err = s.db.ExecContext(r.Context(), - `UPDATE bots SET status = 'retired' WHERE bot_id = $1`, req.BotID, - ) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to retire bot") - return - } - writeJSON(w, http.StatusOK, RotateKeyResponse{Message: "Bot retired."}) - return - } - - newSecret, err := generateSecret() - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to generate new secret") - return - } - - encryptedSecret := newSecret - if s.cfg.EncryptionKey != "" { - encryptedSecret, err = encryptSecret(newSecret, s.cfg.EncryptionKey) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to encrypt secret") - return - } - } - - _, err = s.db.ExecContext(r.Context(), - `UPDATE bots SET shared_secret = $1 WHERE bot_id = $2`, - encryptedSecret, req.BotID, - ) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to update secret") - return - } - - writeJSON(w, http.StatusOK, RotateKeyResponse{ - NewSecret: newSecret, - Message: "Secret rotated. Save the new secret — it will not be shown again.", - }) -} - -func (s *Server) handleBotStatus(w http.ResponseWriter, r *http.Request) { - botID := r.PathValue("bot_id") - - var bot struct { - BotID string `json:"bot_id"` - Name string `json:"name"` - Owner string `json:"owner"` - Status string `json:"status"` - Rating float64 `json:"rating"` - RatingMu float64 `json:"rating_mu"` - RatingPhi float64 `json:"rating_phi"` - Description *string `json:"description,omitempty"` - CreatedAt string `json:"created_at"` - LastActive *string `json:"last_active,omitempty"` - } - - var desc, lastActive *string - err := s.db.QueryRowContext(r.Context(), - `SELECT bot_id, name, owner, status, rating_mu, rating_phi, description, created_at, last_active - FROM bots WHERE bot_id = $1`, botID, - ).Scan(&bot.BotID, &bot.Name, &bot.Owner, &bot.Status, &bot.RatingMu, &bot.RatingPhi, - &desc, &bot.CreatedAt, &lastActive) - if err != nil { - writeError(w, http.StatusNotFound, "bot not found") - return - } - - bot.Description = desc - bot.LastActive = lastActive - bot.Rating = bot.RatingMu - 2*bot.RatingPhi // conservative display rating - - writeJSON(w, http.StatusOK, bot) -} diff --git a/cmd/acb-api/seasons.go b/cmd/acb-api/seasons.go deleted file mode 100644 index d02b53d..0000000 --- a/cmd/acb-api/seasons.go +++ /dev/null @@ -1,248 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "net/http" - "time" -) - -// handleListSeasons handles GET /api/seasons -func (s *Server) handleListSeasons(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - rows, err := s.db.QueryContext(ctx, ` - SELECT id, name, theme, rules_version, status, champion_id, starts_at, ends_at, created_at - FROM seasons - ORDER BY created_at DESC - LIMIT 20 - `) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - type seasonEntry struct { - ID int64 `json:"id"` - Name string `json:"name"` - Theme *string `json:"theme"` - RulesVersion string `json:"rules_version"` - Status string `json:"status"` - ChampionID *string `json:"champion_id"` - StartsAt time.Time `json:"starts_at"` - EndsAt *time.Time `json:"ends_at"` - CreatedAt time.Time `json:"created_at"` - } - seasons := make([]seasonEntry, 0) - for rows.Next() { - var se seasonEntry - var theme, championID sql.NullString - var endsAt sql.NullTime - if err := rows.Scan(&se.ID, &se.Name, &theme, &se.RulesVersion, &se.Status, - &championID, &se.StartsAt, &endsAt, &se.CreatedAt); err != nil { - continue - } - if theme.Valid { - se.Theme = &theme.String - } - if championID.Valid { - se.ChampionID = &championID.String - } - if endsAt.Valid { - se.EndsAt = &endsAt.Time - } - seasons = append(seasons, se) - } - - writeJSON(w, http.StatusOK, map[string]any{"seasons": seasons}) -} - -// handleCreateSeason handles POST /api/seasons -func (s *Server) handleCreateSeason(w http.ResponseWriter, r *http.Request) { - var req struct { - Name string `json:"name"` - Theme string `json:"theme"` - RulesVersion string `json:"rules_version"` - EndsAt string `json:"ends_at"` // RFC3339 - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - if req.Name == "" { - writeError(w, http.StatusBadRequest, "name is required") - return - } - if req.RulesVersion == "" { - req.RulesVersion = "1.0" - } - - ctx := r.Context() - - var endsAt sql.NullTime - if req.EndsAt != "" { - t, err := time.Parse(time.RFC3339, req.EndsAt) - if err == nil { - endsAt = sql.NullTime{Time: t, Valid: true} - } - } - - var id int64 - err := s.db.QueryRowContext(ctx, ` - INSERT INTO seasons (name, theme, rules_version, ends_at) - VALUES ($1, $2, $3, $4) - RETURNING id - `, req.Name, req.Theme, req.RulesVersion, endsAt).Scan(&id) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create season") - return - } - - writeJSON(w, http.StatusOK, map[string]any{"season_id": id, "ok": true}) -} - -// handleGetSeason handles GET /api/seasons/{id} -func (s *Server) handleGetSeason(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - ctx := r.Context() - - var se struct { - ID int64 `json:"id"` - Name string `json:"name"` - Theme *string `json:"theme"` - RulesVersion string `json:"rules_version"` - Status string `json:"status"` - ChampionID *string `json:"champion_id"` - StartsAt time.Time `json:"starts_at"` - EndsAt *time.Time `json:"ends_at"` - } - var theme, championID sql.NullString - var endsAt sql.NullTime - err := s.db.QueryRowContext(ctx, ` - SELECT id, name, theme, rules_version, status, champion_id, starts_at, ends_at - FROM seasons WHERE id = $1 - `, id).Scan(&se.ID, &se.Name, &theme, &se.RulesVersion, &se.Status, - &championID, &se.StartsAt, &endsAt) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "season not found") - return - } else if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - if theme.Valid { - se.Theme = &theme.String - } - if championID.Valid { - se.ChampionID = &championID.String - } - if endsAt.Valid { - se.EndsAt = &endsAt.Time - } - - // Get leaderboard snapshot for this season - rows, err := s.db.QueryContext(ctx, ` - SELECT ss.bot_id, b.name, ss.rank, ss.rating, ss.wins, ss.losses, ss.recorded_at - FROM season_snapshots ss - JOIN bots b ON ss.bot_id = b.bot_id - WHERE ss.season_id = $1 - ORDER BY ss.rank - LIMIT 50 - `, id) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - type snap struct { - BotID string `json:"bot_id"` - BotName string `json:"bot_name"` - Rank int `json:"rank"` - Rating float64 `json:"rating"` - Wins int `json:"wins"` - Losses int `json:"losses"` - RecordedAt time.Time `json:"recorded_at"` - } - snapshots := make([]snap, 0) - for rows.Next() { - var sn snap - if err := rows.Scan(&sn.BotID, &sn.BotName, &sn.Rank, &sn.Rating, &sn.Wins, &sn.Losses, &sn.RecordedAt); err != nil { - continue - } - snapshots = append(snapshots, sn) - } - - writeJSON(w, http.StatusOK, map[string]any{ - "season": se, - "standings": snapshots, - }) -} - -// handleSnapshotSeason handles POST /api/seasons/{id}/snapshot -// Takes a snapshot of the current leaderboard for the season archive. -func (s *Server) handleSnapshotSeason(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - ctx := r.Context() - - // Check season exists - var seasonName string - err := s.db.QueryRowContext(ctx, `SELECT name FROM seasons WHERE id = $1`, id).Scan(&seasonName) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "season not found") - return - } - - // Take snapshot of current leaderboard - _, err = s.db.ExecContext(ctx, ` - INSERT INTO season_snapshots (season_id, bot_id, rank, rating, wins, losses) - SELECT $1, bot_id, - ROW_NUMBER() OVER (ORDER BY rating_mu DESC), - rating_mu, - (SELECT COUNT(*) FROM match_participants mp2 - JOIN matches m2 ON mp2.match_id = m2.match_id - WHERE mp2.bot_id = b.bot_id AND m2.status = 'completed' - AND m2.winner = mp2.player_slot), - (SELECT COUNT(*) FROM match_participants mp3 - JOIN matches m3 ON mp3.match_id = m3.match_id - WHERE mp3.bot_id = b.bot_id AND m3.status = 'completed' - AND m3.winner != mp3.player_slot AND m3.winner >= 0) - FROM bots b - WHERE status = 'active' - ORDER BY rating_mu DESC - LIMIT 100 - `, id) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to snapshot season") - return - } - - writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) -} - -// handleCloseSeason handles POST /api/seasons/{id}/close -func (s *Server) handleCloseSeason(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - ctx := r.Context() - - // Find current leader - var championID sql.NullString - _ = s.db.QueryRowContext(ctx, ` - SELECT bot_id FROM season_snapshots - WHERE season_id = $1 - ORDER BY rank ASC LIMIT 1 - `, id).Scan(&championID) - - _, err := s.db.ExecContext(ctx, ` - UPDATE seasons SET status = 'archived', champion_id = $1, ends_at = NOW() - WHERE id = $2 - `, championID, id) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to close season") - return - } - - writeJSON(w, http.StatusOK, map[string]bool{"ok": true}) -} diff --git a/cmd/acb-api/series.go b/cmd/acb-api/series.go deleted file mode 100644 index fc9d4ff..0000000 --- a/cmd/acb-api/series.go +++ /dev/null @@ -1,279 +0,0 @@ -package main - -import ( - "database/sql" - "encoding/json" - "errors" - "net/http" - "time" -) - -// handleCreateSeries handles POST /api/series -func (s *Server) handleCreateSeries(w http.ResponseWriter, r *http.Request) { - var req struct { - BotAID string `json:"bot_a_id"` - BotBID string `json:"bot_b_id"` - Format int `json:"format"` // best of N - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - if req.BotAID == "" || req.BotBID == "" { - writeError(w, http.StatusBadRequest, "bot_a_id and bot_b_id are required") - return - } - if req.Format < 1 { - req.Format = 5 - } - - ctx := r.Context() - - var id int64 - err := s.db.QueryRowContext(ctx, ` - INSERT INTO series (bot_a_id, bot_b_id, format) - VALUES ($1, $2, $3) - RETURNING id - `, req.BotAID, req.BotBID, req.Format).Scan(&id) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to create series") - return - } - - writeJSON(w, http.StatusOK, map[string]any{"series_id": id, "ok": true}) -} - -// handleListSeries handles GET /api/series -func (s *Server) handleListSeries(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - rows, err := s.db.QueryContext(ctx, ` - SELECT s.id, s.bot_a_id, ba.name, s.bot_b_id, bb.name, - s.format, s.a_wins, s.b_wins, s.status, s.winner_id, - s.created_at, s.updated_at - FROM series s - JOIN bots ba ON s.bot_a_id = ba.bot_id - JOIN bots bb ON s.bot_b_id = bb.bot_id - ORDER BY s.updated_at DESC - LIMIT 50 - `) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - type seriesEntry struct { - ID int64 `json:"id"` - BotAID string `json:"bot_a_id"` - BotAName string `json:"bot_a_name"` - BotBID string `json:"bot_b_id"` - BotBName string `json:"bot_b_name"` - Format int `json:"format"` - AWins int `json:"a_wins"` - BWins int `json:"b_wins"` - Status string `json:"status"` - WinnerID *string `json:"winner_id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - } - entries := make([]seriesEntry, 0) - for rows.Next() { - var e seriesEntry - var winnerID sql.NullString - if err := rows.Scan(&e.ID, &e.BotAID, &e.BotAName, &e.BotBID, &e.BotBName, - &e.Format, &e.AWins, &e.BWins, &e.Status, &winnerID, - &e.CreatedAt, &e.UpdatedAt); err != nil { - continue - } - if winnerID.Valid { - e.WinnerID = &winnerID.String - } - entries = append(entries, e) - } - - writeJSON(w, http.StatusOK, map[string]any{"series": entries}) -} - -// handleGetSeries handles GET /api/series/{id} -func (s *Server) handleGetSeries(w http.ResponseWriter, r *http.Request) { - id := r.PathValue("id") - ctx := r.Context() - - type game struct { - MatchID string `json:"match_id"` - GameNum int `json:"game_num"` - WinnerID *string `json:"winner_id"` - CreatedAt time.Time `json:"created_at"` - } - - rows, err := s.db.QueryContext(ctx, ` - SELECT match_id, game_num, winner_id, created_at - FROM series_games - WHERE series_id = $1 - ORDER BY game_num - `, id) - if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - defer rows.Close() - - games := make([]game, 0) - for rows.Next() { - var g game - var winnerID sql.NullString - if err := rows.Scan(&g.MatchID, &g.GameNum, &winnerID, &g.CreatedAt); err != nil { - continue - } - if winnerID.Valid { - g.WinnerID = &winnerID.String - } - games = append(games, g) - } - - // Get series header - var se struct { - ID int64 `json:"id"` - BotAID string `json:"bot_a_id"` - BotAName string `json:"bot_a_name"` - BotBID string `json:"bot_b_id"` - BotBName string `json:"bot_b_name"` - Format int `json:"format"` - AWins int `json:"a_wins"` - BWins int `json:"b_wins"` - Status string `json:"status"` - WinnerID *string `json:"winner_id"` - CreatedAt time.Time `json:"created_at"` - } - var winnerID sql.NullString - err = s.db.QueryRowContext(ctx, ` - SELECT s.id, s.bot_a_id, ba.name, s.bot_b_id, bb.name, - s.format, s.a_wins, s.b_wins, s.status, s.winner_id, s.created_at - FROM series s - JOIN bots ba ON s.bot_a_id = ba.bot_id - JOIN bots bb ON s.bot_b_id = bb.bot_id - WHERE s.id = $1 - `, id).Scan(&se.ID, &se.BotAID, &se.BotAName, &se.BotBID, &se.BotBName, - &se.Format, &se.AWins, &se.BWins, &se.Status, &winnerID, &se.CreatedAt) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "series not found") - return - } else if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - if winnerID.Valid { - se.WinnerID = &winnerID.String - } - - writeJSON(w, http.StatusOK, map[string]any{ - "series": se, - "games": games, - }) -} - -// handleAddSeriesGame handles POST /api/series/{id}/games -func (s *Server) handleAddSeriesGame(w http.ResponseWriter, r *http.Request) { - seriesID := r.PathValue("id") - var req struct { - MatchID string `json:"match_id"` - } - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - writeError(w, http.StatusBadRequest, "invalid request body") - return - } - - ctx := r.Context() - - // Get series info - var botAID, botBID string - var format, aWins, bWins int - var status string - err := s.db.QueryRowContext(ctx, ` - SELECT bot_a_id, bot_b_id, format, a_wins, b_wins, status - FROM series WHERE id = $1 - `, seriesID).Scan(&botAID, &botBID, &format, &aWins, &bWins, &status) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "series not found") - return - } else if err != nil { - writeError(w, http.StatusInternalServerError, "database error") - return - } - if status != "active" { - writeError(w, http.StatusConflict, "series is not active") - return - } - - // Get match winner - var matchWinnerSlot sql.NullInt64 - err = s.db.QueryRowContext(ctx, `SELECT winner FROM matches WHERE match_id = $1`, req.MatchID).Scan(&matchWinnerSlot) - if errors.Is(err, sql.ErrNoRows) { - writeError(w, http.StatusNotFound, "match not found") - return - } - - // Determine which bot won - var winnerBotID sql.NullString - if matchWinnerSlot.Valid { - slot := int(matchWinnerSlot.Int64) - if slot == 0 { - winnerBotID.String = botAID - winnerBotID.Valid = true - } else if slot == 1 { - winnerBotID.String = botBID - winnerBotID.Valid = true - } - } - - // Get next game number - var gameNum int - _ = s.db.QueryRowContext(ctx, ` - SELECT COALESCE(MAX(game_num), 0) + 1 FROM series_games WHERE series_id = $1 - `, seriesID).Scan(&gameNum) - - // Insert game - _, err = s.db.ExecContext(ctx, ` - INSERT INTO series_games (series_id, match_id, game_num, winner_id) - VALUES ($1, $2, $3, $4) - `, seriesID, req.MatchID, gameNum, winnerBotID) - if err != nil { - writeError(w, http.StatusInternalServerError, "failed to add game") - return - } - - // Update win counts and check if series is decided - if winnerBotID.Valid { - if winnerBotID.String == botAID { - aWins++ - } else { - bWins++ - } - } - - toWin := (format / 2) + 1 - newStatus := "active" - var seriesWinner sql.NullString - if aWins >= toWin { - newStatus = "completed" - seriesWinner.String = botAID - seriesWinner.Valid = true - } else if bWins >= toWin { - newStatus = "completed" - seriesWinner.String = botBID - seriesWinner.Valid = true - } - - _, _ = s.db.ExecContext(ctx, ` - UPDATE series SET a_wins=$1, b_wins=$2, status=$3, winner_id=$4, updated_at=NOW() - WHERE id = $5 - `, aWins, bWins, newStatus, seriesWinner, seriesID) - - writeJSON(w, http.StatusOK, map[string]any{ - "game_num": gameNum, - "a_wins": aWins, - "b_wins": bWins, - "status": newStatus, - }) -} diff --git a/cmd/acb-api/server.go b/cmd/acb-api/server.go index 9f1a6ab..60d14ac 100644 --- a/cmd/acb-api/server.go +++ b/cmd/acb-api/server.go @@ -8,21 +8,18 @@ import ( "github.com/redis/go-redis/v9" ) +// Server is a stub for the v1 API. +// The full API (registration, job claim/result, ratings) is deferred. +// Matchmaking is handled by acb-matchmaker; workers communicate directly with PostgreSQL. type Server struct { - cfg Config - db *sql.DB - rdb *redis.Client - // Note: alerter removed - alerting now handled by acb-matchmaker deployment + cfg Config + db *sql.DB + rdb *redis.Client } func (s *Server) RegisterRoutes(mux *http.ServeMux) { mux.HandleFunc("GET /health", s.handleHealth) mux.HandleFunc("GET /ready", s.handleReady) - mux.HandleFunc("POST /api/register", s.handleRegister) - mux.HandleFunc("POST /api/rotate-key", s.handleRotateKey) - mux.HandleFunc("GET /api/status/{bot_id}", s.handleBotStatus) - mux.HandleFunc("POST /api/jobs/claim", s.handleJobClaim) - mux.HandleFunc("POST /api/jobs/{job_id}/result", s.handleJobResult) } func writeJSON(w http.ResponseWriter, status int, v any) { diff --git a/cmd/acb-api/server_test.go b/cmd/acb-api/server_test.go index c266961..d85c63d 100644 --- a/cmd/acb-api/server_test.go +++ b/cmd/acb-api/server_test.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "net/http" "net/http/httptest" @@ -9,8 +8,7 @@ import ( ) // newTestServer creates a Server with no database or redis (for unit tests -// that don't need them). For handler tests that need DB, use the integration -// tests pattern with a test database. +// that don't need them). func newTestServer() *Server { return &Server{ cfg: Config{ @@ -41,123 +39,6 @@ func TestHealthEndpoint(t *testing.T) { } } -func TestAuthenticateWorker(t *testing.T) { - srv := newTestServer() - - tests := []struct { - name string - header string - value string - want bool - }{ - {"bearer", "Authorization", "Bearer test-key", true}, - {"x-api-key", "X-API-Key", "test-key", true}, - {"wrong key", "Authorization", "Bearer wrong", false}, - {"no header", "", "", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - req := httptest.NewRequest("GET", "/", nil) - if tt.header != "" { - req.Header.Set(tt.header, tt.value) - } - got := srv.authenticateWorker(req) - if got != tt.want { - t.Errorf("authenticateWorker() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestAuthenticateWorker_NoKeyConfigured(t *testing.T) { - srv := &Server{cfg: Config{WorkerAPIKey: ""}} - req := httptest.NewRequest("GET", "/", nil) - if !srv.authenticateWorker(req) { - t.Error("with no key configured, all requests should be authenticated") - } -} - -func TestRegisterValidation(t *testing.T) { - srv := newTestServer() - mux := http.NewServeMux() - srv.RegisterRoutes(mux) - - tests := []struct { - name string - body RegisterRequest - wantCode int - }{ - { - name: "missing name", - body: RegisterRequest{Name: "", EndpointURL: "http://example.com", Owner: "alice"}, - wantCode: http.StatusBadRequest, - }, - { - name: "name too short", - body: RegisterRequest{Name: "ab", EndpointURL: "http://example.com", Owner: "alice"}, - wantCode: http.StatusBadRequest, - }, - { - name: "name with spaces", - body: RegisterRequest{Name: "my bot", EndpointURL: "http://example.com", Owner: "alice"}, - wantCode: http.StatusBadRequest, - }, - { - name: "missing endpoint", - body: RegisterRequest{Name: "valid-bot", EndpointURL: "", Owner: "alice"}, - wantCode: http.StatusBadRequest, - }, - { - name: "missing owner", - body: RegisterRequest{Name: "valid-bot", EndpointURL: "http://example.com", Owner: ""}, - wantCode: http.StatusBadRequest, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - body, _ := json.Marshal(tt.body) - req := httptest.NewRequest("POST", "/api/register", bytes.NewReader(body)) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != tt.wantCode { - t.Errorf("status = %d, want %d; body: %s", w.Code, tt.wantCode, w.Body.String()) - } - }) - } -} - -func TestValidBotName(t *testing.T) { - tests := []struct { - name string - input string - valid bool - }{ - {"simple", "mybot", true}, - {"with-hyphen", "my-bot", true}, - {"with-numbers", "bot123", true}, - {"mixed", "My-Bot-42", true}, - {"three-chars", "abc", true}, - {"too-short", "ab", false}, - {"starts-with-hyphen", "-bot", false}, - {"ends-with-hyphen", "bot-", false}, - {"spaces", "my bot", false}, - {"special", "bot@123", false}, - {"empty", "", false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := validBotName.MatchString(tt.input) - if got != tt.valid { - t.Errorf("validBotName(%q) = %v, want %v", tt.input, got, tt.valid) - } - }) - } -} - func TestWriteJSON(t *testing.T) { w := httptest.NewRecorder() writeJSON(w, http.StatusCreated, map[string]string{"key": "value"}) @@ -190,33 +71,3 @@ func TestWriteError(t *testing.T) { t.Errorf("body = %v, want error=test error", body) } } - -func TestJobClaimRequiresAuth(t *testing.T) { - srv := newTestServer() - mux := http.NewServeMux() - srv.RegisterRoutes(mux) - - body, _ := json.Marshal(JobClaimRequest{WorkerID: "w1"}) - req := httptest.NewRequest("POST", "/api/jobs/claim", bytes.NewReader(body)) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusUnauthorized { - t.Errorf("job claim without auth: status = %d, want 401", w.Code) - } -} - -func TestJobResultRequiresAuth(t *testing.T) { - srv := newTestServer() - mux := http.NewServeMux() - srv.RegisterRoutes(mux) - - body, _ := json.Marshal(JobResultRequest{WorkerID: "w1", Condition: "score", TurnCount: 100}) - req := httptest.NewRequest("POST", "/api/jobs/j_12345678/result", bytes.NewReader(body)) - w := httptest.NewRecorder() - mux.ServeHTTP(w, req) - - if w.Code != http.StatusUnauthorized { - t.Errorf("job result without auth: status = %d, want 401", w.Code) - } -} diff --git a/cmd/acb-indexer/.gitignore b/cmd/acb-indexer/.gitignore deleted file mode 100644 index 1d98640..0000000 --- a/cmd/acb-indexer/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -dist/ -.env -data/ -*.log diff --git a/cmd/acb-indexer/Dockerfile b/cmd/acb-indexer/Dockerfile deleted file mode 100644 index b3444df..0000000 --- a/cmd/acb-indexer/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# AI Code Battle Index Builder Container -# Generates static JSON index files and deploys to Cloudflare Pages - -FROM node:20-alpine - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install dependencies -RUN npm ci --only=production - -# Copy source code -COPY tsconfig.json ./ -COPY src/ ./src/ - -# Build TypeScript -RUN npm run build - -# Create output directory -RUN mkdir -p /app/data - -# Environment variables (set at runtime) -# API_URL - Worker API URL (e.g., https://api.aicodebattle.com) -# API_KEY - Worker API key -# OUTPUT_DIR - Output directory (default: /app/data) -# DEPLOY_COMMAND - Optional deploy command (e.g., wrangler pages deploy) - -ENV OUTPUT_DIR=/app/data - -# Run the index builder -CMD ["node", "dist/index.js"] diff --git a/cmd/acb-indexer/package-lock.json b/cmd/acb-indexer/package-lock.json deleted file mode 100644 index ecb3832..0000000 --- a/cmd/acb-indexer/package-lock.json +++ /dev/null @@ -1,2232 +0,0 @@ -{ - "name": "acb-indexer", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "acb-indexer", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^20.11.24", - "tsx": "^4.7.1", - "typescript": "^5.3.3", - "vitest": "^1.3.1" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.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 - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "dev": true - }, - "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 - }, - "node_modules/@types/node": { - "version": "20.19.37", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", - "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", - "dev": true, - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true - }, - "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, - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dotenv": { - "version": "16.6.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", - "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://dotenvx.com" - } - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-tsconfig": { - "version": "4.13.7", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", - "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", - "dev": true, - "dependencies": { - "resolve-pkg-maps": "^1.0.0" - }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" - } - }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "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, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "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 - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true - }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, - "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "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" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, - "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, - "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" - } - }, - "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", - "fsevents": "~2.3.2" - } - }, - "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, - "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, - "engines": { - "node": ">=8" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", - "dev": true, - "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" - }, - "bin": { - "tsx": "dist/cli.mjs" - }, - "engines": { - "node": ">=18.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - } - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", - "dev": true - }, - "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 - }, - "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", - "dev": true, - "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/cmd/acb-indexer/package.json b/cmd/acb-indexer/package.json deleted file mode 100644 index d6aae1f..0000000 --- a/cmd/acb-indexer/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "acb-indexer", - "version": "1.0.0", - "description": "AI Code Battle Index Builder - Generates static JSON index files for Cloudflare Pages", - "type": "module", - "main": "dist/index.js", - "scripts": { - "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts", - "test": "vitest" - }, - "keywords": ["aicodebattle", "indexer", "cloudflare-pages"], - "license": "MIT", - "dependencies": { - "dotenv": "^16.4.5" - }, - "devDependencies": { - "@types/node": "^20.11.24", - "tsx": "^4.7.1", - "typescript": "^5.3.3", - "vitest": "^1.3.1" - } -} diff --git a/cmd/acb-indexer/src/api.ts b/cmd/acb-indexer/src/api.ts deleted file mode 100644 index 56c0905..0000000 --- a/cmd/acb-indexer/src/api.ts +++ /dev/null @@ -1,42 +0,0 @@ -// API Client for fetching data from Worker API - -import type { ApiClientConfig, ExportData } from './types.js'; - -export class ApiClient { - private apiUrl: string; - private apiKey: string; - - constructor(config: ApiClientConfig) { - this.apiUrl = config.apiUrl.replace(/\/$/, ''); - this.apiKey = config.apiKey; - } - - /** - * Fetch all data needed for index building - */ - async fetchExportData(): Promise { - const response = await fetch(`${this.apiUrl}/api/data/export`, { - headers: { - 'X-API-Key': this.apiKey, - 'Accept': 'application/json', - }, - }); - - if (!response.ok) { - const text = await response.text(); - throw new Error(`API request failed: ${response.status} - ${text}`); - } - - const result = await response.json() as { success: boolean; data?: ExportData; error?: string }; - - if (!result.success) { - throw new Error(`API returned error: ${result.error}`); - } - - if (!result.data) { - throw new Error('API returned no data'); - } - - return result.data; - } -} diff --git a/cmd/acb-indexer/src/generator.test.ts b/cmd/acb-indexer/src/generator.test.ts deleted file mode 100644 index 4a40ded..0000000 --- a/cmd/acb-indexer/src/generator.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -// Index Generator Tests - -import { describe, it, expect } from 'vitest'; -import { IndexGenerator } from './generator.js'; -import type { ExportData, ExportBot, ExportMatch } from './types.js'; - -function createMockData(): ExportData { - const bots: ExportBot[] = [ - { - id: 'bot-1', - name: 'TestBot1', - owner_id: 'owner-1', - rating: 1500, - rating_deviation: 50, - rating_volatility: 0.06, - matches_played: 10, - matches_won: 7, - created_at: '2026-01-01T00:00:00Z', - updated_at: '2026-03-01T00:00:00Z', - health_status: 'healthy', - }, - { - id: 'bot-2', - name: 'TestBot2', - owner_id: 'owner-2', - rating: 1450, - rating_deviation: 60, - rating_volatility: 0.07, - matches_played: 5, - matches_won: 2, - created_at: '2026-01-15T00:00:00Z', - updated_at: '2026-03-01T00:00:00Z', - health_status: 'healthy', - }, - { - id: 'bot-3', - name: 'UnrankedBot', - owner_id: 'owner-3', - rating: 1200, - rating_deviation: 350, - rating_volatility: 0.06, - matches_played: 0, - matches_won: 0, - created_at: '2026-02-01T00:00:00Z', - updated_at: '2026-02-01T00:00:00Z', - health_status: 'unknown', - }, - ]; - - const matches: ExportMatch[] = [ - { - id: 'match-1', - status: 'completed', - winner_id: 'bot-1', - turns: 50, - end_reason: 'domination', - map_id: 'map-1', - created_at: '2026-03-01T10:00:00Z', - completed_at: '2026-03-01T10:05:00Z', - participants: [ - { - bot_id: 'bot-1', - player_index: 0, - score: 100, - rating_before: 1480, - rating_after: 1500, - }, - { - bot_id: 'bot-2', - player_index: 1, - score: 50, - rating_before: 1470, - rating_after: 1450, - }, - ], - }, - ]; - - return { - bots, - matches, - rating_history: [ - { - bot_id: 'bot-1', - rating: 1480, - rating_deviation: 55, - recorded_at: '2026-02-15T00:00:00Z', - }, - { - bot_id: 'bot-1', - rating: 1500, - rating_deviation: 50, - recorded_at: '2026-03-01T00:00:00Z', - }, - ], - generated_at: '2026-03-24T08:00:00Z', - }; -} - -describe('IndexGenerator', () => { - it('generates leaderboard with correct rankings', () => { - const generator = new IndexGenerator(createMockData()); - const leaderboard = generator.generateLeaderboard(); - - expect(leaderboard.updated_at).toBe('2026-03-24T08:00:00Z'); - expect(leaderboard.entries).toHaveLength(2); // Only bots with matches - expect(leaderboard.entries[0].bot_id).toBe('bot-1'); - expect(leaderboard.entries[0].rank).toBe(1); - expect(leaderboard.entries[0].rating).toBe(1500); - expect(leaderboard.entries[0].win_rate).toBe(70); // 7/10 * 100 - }); - - it('generates bot directory', () => { - const generator = new IndexGenerator(createMockData()); - const directory = generator.generateBotDirectory(); - - expect(directory.bots).toHaveLength(3); - expect(directory.bots[0].id).toBe('bot-1'); - expect(directory.bots[0].name).toBe('TestBot1'); - }); - - it('generates bot profile with rating history', () => { - const generator = new IndexGenerator(createMockData()); - const profile = generator.generateBotProfile('bot-1'); - - expect(profile).not.toBeNull(); - expect(profile!.id).toBe('bot-1'); - expect(profile!.name).toBe('TestBot1'); - expect(profile!.rating_history).toHaveLength(2); - expect(profile!.recent_matches).toHaveLength(1); - expect(profile!.recent_matches[0].participants[0].won).toBe(true); - }); - - it('returns null for non-existent bot profile', () => { - const generator = new IndexGenerator(createMockData()); - const profile = generator.generateBotProfile('non-existent'); - - expect(profile).toBeNull(); - }); - - it('generates match index', () => { - const generator = new IndexGenerator(createMockData()); - const matchIndex = generator.generateMatchIndex(); - - expect(matchIndex.matches).toHaveLength(1); - expect(matchIndex.matches[0].id).toBe('match-1'); - expect(matchIndex.matches[0].winner_id).toBe('bot-1'); - expect(matchIndex.matches[0].participants).toHaveLength(2); - }); - - it('generates all indexes at once', () => { - const generator = new IndexGenerator(createMockData()); - const all = generator.generateAll(); - - expect(all.leaderboard.entries).toHaveLength(2); - expect(all.botDirectory.bots).toHaveLength(3); - expect(all.botProfiles.size).toBe(3); - expect(all.matchIndex.matches).toHaveLength(1); - }); -}); diff --git a/cmd/acb-indexer/src/generator.ts b/cmd/acb-indexer/src/generator.ts deleted file mode 100644 index 410be0b..0000000 --- a/cmd/acb-indexer/src/generator.ts +++ /dev/null @@ -1,166 +0,0 @@ -// Index Generator - Creates static JSON index files - -import type { - ExportData, - ExportBot, - ExportMatch, - LeaderboardIndex, - LeaderboardEntry, - BotDirectory, - BotDirectoryEntry, - BotProfile, - MatchIndex, - MatchSummary, -} from './types.js'; - -export class IndexGenerator { - private data: ExportData; - private botNameMap: Map; - - constructor(data: ExportData) { - this.data = data; - this.botNameMap = new Map(data.bots.map(b => [b.id, b.name])); - } - - /** - * Generate leaderboard.json - */ - generateLeaderboard(): LeaderboardIndex { - const entries: LeaderboardEntry[] = this.data.bots - .filter(bot => bot.matches_played > 0) - .map((bot, index) => ({ - rank: index + 1, - bot_id: bot.id, - name: bot.name, - owner_id: bot.owner_id, - rating: Math.round(bot.rating), - rating_deviation: Math.round(bot.rating_deviation * 10) / 10, - matches_played: bot.matches_played, - matches_won: bot.matches_won, - win_rate: bot.matches_played > 0 - ? Math.round((bot.matches_won / bot.matches_played) * 1000) / 10 - : 0, - health_status: bot.health_status, - })); - - return { - updated_at: this.data.generated_at, - entries, - }; - } - - /** - * Generate bots/index.json - bot directory - */ - generateBotDirectory(): BotDirectory { - const bots: BotDirectoryEntry[] = this.data.bots.map(bot => ({ - id: bot.id, - name: bot.name, - rating: Math.round(bot.rating), - matches_played: bot.matches_played, - win_rate: bot.matches_played > 0 - ? Math.round((bot.matches_won / bot.matches_played) * 1000) / 10 - : 0, - })); - - return { - updated_at: this.data.generated_at, - bots, - }; - } - - /** - * Generate individual bot profile - */ - generateBotProfile(botId: string): BotProfile | null { - const bot = this.data.bots.find(b => b.id === botId); - if (!bot) return null; - - // Get rating history for this bot - const ratingHistory = this.data.rating_history - .filter(h => h.bot_id === botId) - .sort((a, b) => a.recorded_at.localeCompare(b.recorded_at)); - - // Get recent matches for this bot (last 20) - const recentMatches = this.data.matches - .filter(m => m.participants.some(p => p.bot_id === botId)) - .slice(0, 20) - .map(m => this.generateMatchSummary(m)); - - return { - id: bot.id, - name: bot.name, - owner_id: bot.owner_id, - rating: Math.round(bot.rating), - rating_deviation: Math.round(bot.rating_deviation * 10) / 10, - rating_volatility: Math.round(bot.rating_volatility * 10000) / 10000, - matches_played: bot.matches_played, - matches_won: bot.matches_won, - win_rate: bot.matches_played > 0 - ? Math.round((bot.matches_won / bot.matches_played) * 1000) / 10 - : 0, - health_status: bot.health_status, - created_at: bot.created_at, - updated_at: bot.updated_at, - rating_history: ratingHistory, - recent_matches: recentMatches, - }; - } - - /** - * Generate matches/index.json - recent match list - */ - generateMatchIndex(): MatchIndex { - const matches = this.data.matches.map(m => this.generateMatchSummary(m)); - - return { - updated_at: this.data.generated_at, - matches, - }; - } - - /** - * Generate match summary for a single match - */ - private generateMatchSummary(match: ExportMatch): MatchSummary { - return { - id: match.id, - completed_at: match.completed_at, - participants: match.participants.map(p => ({ - bot_id: p.bot_id, - name: this.botNameMap.get(p.bot_id) || 'Unknown', - score: p.score, - won: p.bot_id === match.winner_id, - })), - winner_id: match.winner_id, - turns: match.turns, - end_reason: match.end_reason, - }; - } - - /** - * Generate all index files - */ - generateAll(): { - leaderboard: LeaderboardIndex; - botDirectory: BotDirectory; - botProfiles: Map; - matchIndex: MatchIndex; - } { - const botProfiles = new Map(); - - for (const bot of this.data.bots) { - const profile = this.generateBotProfile(bot.id); - if (profile) { - botProfiles.set(bot.id, profile); - } - } - - return { - leaderboard: this.generateLeaderboard(), - botDirectory: this.generateBotDirectory(), - botProfiles, - matchIndex: this.generateMatchIndex(), - }; - } -} diff --git a/cmd/acb-indexer/src/index.ts b/cmd/acb-indexer/src/index.ts deleted file mode 100644 index 58f15f3..0000000 --- a/cmd/acb-indexer/src/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env node -// AI Code Battle Index Builder -// Fetches data from Worker API and generates static JSON index files - -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { exec } from 'child_process'; -import { promisify } from 'util'; - -import 'dotenv/config'; -import { ApiClient } from './api.js'; -import { IndexGenerator } from './generator.js'; -import { FileWriter } from './writer.js'; -import type { EvolutionLiveData } from './types.js'; - -const execAsync = promisify(exec); - -interface Config { - apiUrl: string; - apiKey: string; - outputDir: string; - deployCommand?: string; - evolutionDataPath?: string; -} - -function getConfig(): Config { - const apiUrl = process.env.API_URL; - const apiKey = process.env.API_KEY; - const outputDir = process.env.OUTPUT_DIR || './data'; - const deployCommand = process.env.DEPLOY_COMMAND; - const evolutionDataPath = process.env.EVOLUTION_DATA_PATH; - - if (!apiUrl) { - console.error('ERROR: API_URL environment variable is required'); - process.exit(1); - } - - if (!apiKey) { - console.error('ERROR: API_KEY environment variable is required'); - process.exit(1); - } - - return { - apiUrl, - apiKey, - outputDir, - deployCommand, - evolutionDataPath, - }; -} - -async function runIndexBuilder(config: Config): Promise { - console.log('AI Code Battle Index Builder'); - console.log('============================'); - console.log(`API URL: ${config.apiUrl}`); - console.log(`Output directory: ${config.outputDir}`); - console.log(''); - - // Initialize components - const apiClient = new ApiClient({ - apiUrl: config.apiUrl, - apiKey: config.apiKey, - }); - - const fileWriter = new FileWriter(config.outputDir); - - // Step 1: Fetch data from API - console.log('Fetching data from Worker API...'); - const data = await apiClient.fetchExportData(); - console.log(` - ${data.bots.length} bots`); - console.log(` - ${data.matches.length} matches`); - console.log(` - ${data.rating_history.length} rating history entries`); - console.log(''); - - // Step 2: Generate index files - console.log('Generating index files...'); - const generator = new IndexGenerator(data); - const indexes = generator.generateAll(); - - // Step 3: Write files to disk - console.log('Writing index files...'); - await fileWriter.writeAll(indexes); - - // Step 4: Deploy (optional) - if (config.deployCommand) { - console.log('\nDeploying to Cloudflare Pages...'); - try { - const { stdout, stderr } = await execAsync(config.deployCommand, { - cwd: config.outputDir, - }); - if (stdout) console.log(stdout); - if (stderr) console.error(stderr); - console.log('Deploy complete!'); - } catch (error) { - console.error('Deploy failed:', error); - process.exit(1); - } - } -} - -async function main(): Promise { - const config = getConfig(); - - try { - await runIndexBuilder(config); - } catch (error) { - console.error('Index builder failed:', error); - process.exit(1); - } -} - -// Run if executed directly -main(); diff --git a/cmd/acb-indexer/src/narrative.ts b/cmd/acb-indexer/src/narrative.ts deleted file mode 100644 index f435d59..0000000 --- a/cmd/acb-indexer/src/narrative.ts +++ /dev/null @@ -1,299 +0,0 @@ -// Narrative Engine - generates weekly meta report blog posts from match data. -// Optionally enhances prose via the Anthropic API when ANTHROPIC_API_KEY is set. - -import type { - ExportData, - ExportMatch, - ExportBot, - BlogPost, - BlogWeekStats, - BlogIndex, - EvolutionLiveData, -} from './types.js'; - -// --------------------------------------------------------------------------- -// Week helpers -// --------------------------------------------------------------------------- - -function startOfWeek(d: Date): Date { - const day = d.getUTCDay(); // 0=Sun - const diff = (day === 0 ? -6 : 1 - day); // Monday - const out = new Date(d); - out.setUTCDate(d.getUTCDate() + diff); - out.setUTCHours(0, 0, 0, 0); - return out; -} - -function isoDate(d: Date): string { - return d.toISOString().slice(0, 10); -} - -function weekSlug(weekStart: Date): string { - return `week-${isoDate(weekStart)}`; -} - -// --------------------------------------------------------------------------- -// Stats extraction -// --------------------------------------------------------------------------- - -function matchesInWeek(matches: ExportMatch[], weekStart: Date): ExportMatch[] { - const start = weekStart.getTime(); - const end = start + 7 * 24 * 60 * 60 * 1000; - return matches.filter(m => { - if (!m.completed_at) return false; - const t = new Date(m.completed_at).getTime(); - return t >= start && t < end; - }); -} - -function computeWeekStats( - weekMatches: ExportMatch[], - bots: ExportBot[], - evo: EvolutionLiveData | null, -): BlogWeekStats { - const botMap = new Map(bots.map(b => [b.id, b])); - - // Top bot by rating - const sorted = [...bots].sort((a, b) => b.rating - a.rating); - const topBot = sorted[0]; - - // Match activity per bot - const activityCount = new Map(); - for (const m of weekMatches) { - for (const p of m.participants) { - activityCount.set(p.bot_id, (activityCount.get(p.bot_id) ?? 0) + 1); - } - } - let mostActiveBot = topBot?.name ?? 'N/A'; - let mostActiveBotMatches = 0; - for (const [id, count] of activityCount) { - if (count > mostActiveBotMatches) { - mostActiveBotMatches = count; - mostActiveBot = botMap.get(id)?.name ?? id; - } - } - - // Biggest upset: lower-rated bot beats higher-rated by the largest margin - let biggestUpset: string | null = null; - let maxUpsetMargin = 0; - for (const m of weekMatches) { - if (!m.winner_id || m.participants.length < 2) continue; - const winner = m.participants.find(p => p.bot_id === m.winner_id); - if (!winner) continue; - const loser = m.participants.find(p => p.bot_id !== m.winner_id); - if (!loser) continue; - const winnerBot = botMap.get(winner.bot_id); - const loserBot = botMap.get(loser.bot_id); - if (!winnerBot || !loserBot) continue; - const margin = loserBot.rating - winnerBot.rating; - if (margin > maxUpsetMargin) { - maxUpsetMargin = margin; - biggestUpset = `${winnerBot.name} defeated ${loserBot.name} (+${Math.round(margin)} rating gap)`; - } - } - - // Island leader from evolution data - let islandLeader: string | null = null; - if (evo) { - let bestFitness = -Infinity; - for (const [island, stat] of Object.entries(evo.islands)) { - if (stat.best_fitness > bestFitness) { - bestFitness = stat.best_fitness; - islandLeader = island; - } - } - } - - return { - matches_played: weekMatches.length, - top_bot: topBot?.name ?? 'N/A', - top_bot_rating: Math.round(topBot?.rating ?? 0), - biggest_upset: biggestUpset, - most_active_bot: mostActiveBot, - most_active_bot_matches: mostActiveBotMatches, - island_leader: islandLeader, - }; -} - -// --------------------------------------------------------------------------- -// Template-based narrative (used when no LLM key is available) -// --------------------------------------------------------------------------- - -function templateNarrative(weekStart: Date, stats: BlogWeekStats): { title: string; summary: string; body_html: string } { - const weekLabel = isoDate(weekStart); - const title = `Meta Report: Week of ${weekLabel}`; - - const summary = - `This week ${stats.matches_played} matches were played. ` + - `${stats.top_bot} leads the leaderboard at ${stats.top_bot_rating} rating. ` + - (stats.biggest_upset - ? `The biggest upset saw ${stats.biggest_upset}. ` - : '') + - `${stats.most_active_bot} was the most active with ${stats.most_active_bot_matches} matches.`; - - const upsetSection = stats.biggest_upset - ? `

Biggest Upset

-

${stats.biggest_upset}.

` - : ''; - - const evoSection = stats.island_leader - ? `

Evolution Observatory

-

Island ${stats.island_leader} leads the evolution pipeline this week.

` - : ''; - - const body_html = ` -

Overview

-

- The week of ${weekLabel} produced ${stats.matches_played} completed matches - on the AI Code Battle platform. -

- -

Leaderboard Snapshot

-

- ${stats.top_bot} holds the top position with a rating of - ${stats.top_bot_rating}. The competition remains fierce as bots jockey - for position in the weekly rankings. -

- -

Most Active Competitor

-

- ${stats.most_active_bot} played the most matches this week - (${stats.most_active_bot_matches} games), demonstrating consistent - availability and aggressive scheduling. -

- -${upsetSection} - -${evoSection} - -

What to Watch

-

- With the meta always shifting, next week promises fresh rivalries and strategy evolution. - Keep an eye on the Evolution Dashboard for emerging program - lineages and the Rivalries page for head-to-head trends. -

-`.trim(); - - return { title, summary, body_html }; -} - -// --------------------------------------------------------------------------- -// LLM-enhanced narrative (Anthropic API) -// --------------------------------------------------------------------------- - -async function llmNarrative( - weekStart: Date, - stats: BlogWeekStats, - templateResult: { title: string; summary: string; body_html: string }, -): Promise<{ title: string; summary: string; body_html: string }> { - const apiKey = process.env.ANTHROPIC_API_KEY; - if (!apiKey) return templateResult; - - const prompt = `You are a sports journalist covering an AI bot programming competition. -Write a short, engaging weekly meta report for the week of ${isoDate(weekStart)}. - -Statistics: -- Matches played: ${stats.matches_played} -- Top bot: ${stats.top_bot} (rating: ${stats.top_bot_rating}) -- Most active bot: ${stats.most_active_bot} (${stats.most_active_bot_matches} matches) -- Biggest upset: ${stats.biggest_upset ?? 'none this week'} -- Evolution island leader: ${stats.island_leader ?? 'data not available'} - -Write: -1. A catchy title (one line, no markdown) -2. A one-paragraph summary (plain text, 2-3 sentences) -3. Full HTML body content (use

,

,

tags; no //) - -Format your response as JSON with keys: title, summary, body_html`; - - try { - const res = await fetch('https://api.anthropic.com/v1/messages', { - method: 'POST', - headers: { - 'x-api-key': apiKey, - 'anthropic-version': '2023-06-01', - 'content-type': 'application/json', - }, - body: JSON.stringify({ - model: 'claude-haiku-4-5-20251001', - max_tokens: 1024, - messages: [{ role: 'user', content: prompt }], - }), - }); - - if (!res.ok) { - console.warn(`LLM API returned ${res.status}, falling back to template narrative`); - return templateResult; - } - - const json = await res.json() as { content: Array<{ text: string }> }; - const text = json.content[0]?.text ?? ''; - - // Extract JSON from response (may be wrapped in markdown code fences) - const jsonMatch = text.match(/\{[\s\S]*\}/); - if (!jsonMatch) { - console.warn('LLM response did not contain JSON, using template'); - return templateResult; - } - - const parsed = JSON.parse(jsonMatch[0]) as { title?: string; summary?: string; body_html?: string }; - return { - title: parsed.title ?? templateResult.title, - summary: parsed.summary ?? templateResult.summary, - body_html: parsed.body_html ?? templateResult.body_html, - }; - } catch (err) { - console.warn('LLM narrative failed, using template:', err); - return templateResult; - } -} - -// --------------------------------------------------------------------------- -// Public API -// --------------------------------------------------------------------------- - -export async function generateWeeklyPost( - data: ExportData, - evo: EvolutionLiveData | null, - weekStart?: Date, -): Promise { - const now = new Date(); - const week = weekStart ?? startOfWeek(now); - - const weekMatches = matchesInWeek(data.matches, week); - const stats = computeWeekStats(weekMatches, data.bots, evo); - - const template = templateNarrative(week, stats); - const narrative = await llmNarrative(week, stats, template); - - return { - slug: weekSlug(week), - title: narrative.title, - published_at: now.toISOString(), - week_start: isoDate(week), - summary: narrative.summary, - body_html: narrative.body_html, - stats, - }; -} - -export function buildBlogIndex(posts: BlogPost[]): BlogIndex { - return { - updated_at: new Date().toISOString(), - posts: posts.sort((a, b) => b.week_start.localeCompare(a.week_start)), - }; -} - -/** - * Compute the start-of-week dates for the last N weeks. - */ -export function lastNWeekStarts(n: number, from?: Date): Date[] { - const base = startOfWeek(from ?? new Date()); - const weeks: Date[] = []; - for (let i = 0; i < n; i++) { - const d = new Date(base); - d.setUTCDate(base.getUTCDate() - i * 7); - weeks.push(d); - } - return weeks; -} diff --git a/cmd/acb-indexer/src/playlists.ts b/cmd/acb-indexer/src/playlists.ts deleted file mode 100644 index c64c5e4..0000000 --- a/cmd/acb-indexer/src/playlists.ts +++ /dev/null @@ -1,288 +0,0 @@ -// Playlist Generator - Auto-curated replay collections -import type { - ExportMatch, - ExportBot, - Playlist, - PlaylistCategory, - PlaylistMatch, - PlaylistSummary, - PlaylistIndex, -} from './types.js'; - -export class PlaylistGenerator { - private matches: ExportMatch[]; - private bots: ExportBot[]; - private botNameMap: Map; - private now: string; - - constructor(matches: ExportMatch[], bots: ExportBot[]) { - this.matches = matches.filter(m => m.status === 'completed'); - this.bots = bots; - this.botNameMap = new Map(bots.map(b => [b.id, b.name])); - this.now = new Date().toISOString(); - } - - /** - * Generate all playlists - */ - generateAll(): Playlist[] { - return [ - this.generateFeaturedPlaylist(), - this.generateUpsetsPlaylist(), - this.generateComebacksPlaylist(), - this.generateDominationPlaylist(), - this.generateCloseGamesPlaylist(), - this.generateLongGamesPlaylist(), - this.generateWeeklyBestPlaylist(), - ].filter((p): p is Playlist => p !== null && p.matches.length > 0); - } - - /** - * Generate playlist index - */ - generateIndex(playlists: Playlist[]): PlaylistIndex { - return { - updated_at: this.now, - playlists: playlists.map(p => ({ - slug: p.slug, - title: p.title, - description: p.description, - category: p.category, - match_count: p.match_count, - thumbnail_match_id: p.matches[0]?.match_id, - })), - }; - } - - /** - * Featured matches - high-rated bot confrontations - */ - private generateFeaturedPlaylist(): Playlist { - const botRatingMap = new Map(this.bots.map(b => [b.id, b.rating])); - const featured = this.matches - .filter(m => { - // Only 2-player matches between high-rated bots - if (m.participants.length !== 2) return false; - const ratings = m.participants.map(p => botRatingMap.get(p.bot_id) || 0); - return ratings.every(r => r > 1600); - }) - .sort((a, b) => (b.completed_at || '').localeCompare(a.completed_at || '')) - .slice(0, 10) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - return { - slug: 'featured', - title: 'Featured Matches', - description: 'High-level confrontations between top-rated bots', - category: 'featured', - match_count: featured.length, - created_at: this.now, - updated_at: this.now, - matches: featured, - }; - } - - /** - * Upsets - lower-rated bot beats higher-rated opponent - */ - private generateUpsetsPlaylist(): Playlist { - const botRatingMap = new Map(this.bots.map(b => [b.id, b.rating])); - const upsets = this.matches - .filter(m => { - if (m.participants.length !== 2 || !m.winner_id) return false; - const winnerRating = botRatingMap.get(m.winner_id) || 1500; - const loserId = m.participants.find(p => p.bot_id !== m.winner_id)?.bot_id; - if (!loserId) return false; - const loserRating = botRatingMap.get(loserId) || 1500; - // Upset: winner was at least 100 points lower rated - return winnerRating < loserRating - 100; - }) - .sort((a, b) => { - // Sort by upset magnitude (largest first) - const aMag = this.getUpsetMagnitude(a, botRatingMap); - const bMag = this.getUpsetMagnitude(b, botRatingMap); - return bMag - aMag; - }) - .slice(0, 10) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - return { - slug: 'upsets', - title: 'Epic Upsets', - description: 'Unexpected victories where underdogs triumphed', - category: 'upsets', - match_count: upsets.length, - created_at: this.now, - updated_at: this.now, - matches: upsets, - }; - } - - private getUpsetMagnitude(match: ExportMatch, ratingMap: Map): number { - if (!match.winner_id) return 0; - const winnerRating = ratingMap.get(match.winner_id) || 1500; - const loserId = match.participants.find(p => p.bot_id !== match.winner_id)?.bot_id; - if (!loserId) return 0; - const loserRating = ratingMap.get(loserId) || 1500; - return loserRating - winnerRating; - } - - /** - * Comebacks - matches with large score swings - */ - private generateComebacksPlaylist(): Playlist { - // Comebacks are hard to detect without turn-by-turn data - // For now, use close final scores as a proxy - const closeMatches = this.matches - .filter(m => m.participants.length === 2) - .filter(m => { - const scores = m.participants.map(p => p.score); - const diff = Math.abs(scores[0] - scores[1]); - // Close game: score difference <= 2 - return diff <= 2 && diff > 0; - }) - .sort((a, b) => (b.completed_at || '').localeCompare(a.completed_at || '')) - .slice(0, 10) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - return { - slug: 'comebacks', - title: 'Epic Comebacks', - description: 'Matches where fortunes shifted dramatically', - category: 'comebacks', - match_count: closeMatches.length, - created_at: this.now, - updated_at: this.now, - matches: closeMatches, - }; - } - - /** - * Domination - massive score differences - */ - private generateDominationPlaylist(): Playlist { - const dominated = this.matches - .filter(m => m.participants.length === 2) - .filter(m => { - const scores = m.participants.map(p => p.score); - const diff = Math.abs(scores[0] - scores[1]); - // Domination: score difference >= 5 - return diff >= 5; - }) - .sort((a, b) => { - // Sort by domination magnitude - const aDiff = Math.abs(a.participants[0].score - a.participants[1].score); - const bDiff = Math.abs(b.participants[0].score - b.participants[1].score); - return bDiff - aDiff; - }) - .slice(0, 10) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - return { - slug: 'domination', - title: 'Total Domination', - description: 'One-sided victories with massive score differences', - category: 'domination', - match_count: dominated.length, - created_at: this.now, - updated_at: this.now, - matches: dominated, - }; - } - - /** - * Close games - decided by a single point - */ - private generateCloseGamesPlaylist(): Playlist { - const close = this.matches - .filter(m => m.participants.length === 2) - .filter(m => { - const scores = m.participants.map(p => p.score); - const diff = Math.abs(scores[0] - scores[1]); - return diff === 1; - }) - .sort((a, b) => (b.completed_at || '').localeCompare(a.completed_at || '')) - .slice(0, 10) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - return { - slug: 'close-games', - title: 'Photo Finishes', - description: 'Matches decided by the thinnest of margins', - category: 'close_games', - match_count: close.length, - created_at: this.now, - updated_at: this.now, - matches: close, - }; - } - - /** - * Long games - high turn counts - */ - private generateLongGamesPlaylist(): Playlist { - const longGames = this.matches - .filter(m => (m.turns || 0) >= 300) - .sort((a, b) => (b.turns || 0) - (a.turns || 0)) - .slice(0, 10) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - return { - slug: 'long-games', - title: 'Marathon Matches', - description: 'Extended battles that went the distance', - category: 'long_games', - match_count: longGames.length, - created_at: this.now, - updated_at: this.now, - matches: longGames, - }; - } - - /** - * Weekly best - most recent week's top matches - */ - private generateWeeklyBestPlaylist(): Playlist { - const oneWeekAgo = new Date(); - oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); - const weekStart = oneWeekAgo.toISOString().split('T')[0]; - - const weeklyMatches = this.matches - .filter(m => (m.completed_at || '') >= weekStart) - .sort((a, b) => (b.completed_at || '').localeCompare(a.completed_at || '')) - .slice(0, 15) - .map((m, i) => this.matchToPlaylistEntry(m, i)); - - // Generate title with date range - const now = new Date(); - const weekEndStr = now.toISOString().split('T')[0]; - - return { - slug: 'weekly-best', - title: `Best of the Week (${weekStart} to ${weekEndStr})`, - description: 'Top matches from the past 7 days', - category: 'weekly', - match_count: weeklyMatches.length, - created_at: this.now, - updated_at: this.now, - matches: weeklyMatches, - }; - } - - /** - * Convert a match to a playlist entry - */ - private matchToPlaylistEntry(match: ExportMatch, order: number): PlaylistMatch { - const winnerName = match.winner_id ? this.botNameMap.get(match.winner_id) : 'Draw'; - const participants = match.participants - .map(p => this.botNameMap.get(p.bot_id) || 'Unknown') - .join(' vs '); - - return { - match_id: match.id, - order, - title: `${participants} - ${winnerName} wins`, - thumbnail_url: `https://r2.aicodebattle.com/thumbnails/${match.id}.png`, - }; - } -} diff --git a/cmd/acb-indexer/src/types.ts b/cmd/acb-indexer/src/types.ts deleted file mode 100644 index 6d246d5..0000000 --- a/cmd/acb-indexer/src/types.ts +++ /dev/null @@ -1,242 +0,0 @@ -// Index Builder Types - -export interface ApiClientConfig { - apiUrl: string; - apiKey: string; -} - -export interface ExportBot { - id: string; - name: string; - owner_id: string; - rating: number; - rating_deviation: number; - rating_volatility: number; - matches_played: number; - matches_won: number; - created_at: string; - updated_at: string; - health_status: string; -} - -export interface ExportMatch { - id: string; - status: string; - winner_id: string | null; - turns: number | null; - end_reason: string | null; - map_id: string; - created_at: string; - completed_at: string | null; - participants: ExportMatchParticipant[]; -} - -export interface ExportMatchParticipant { - bot_id: string; - player_index: number; - score: number; - rating_before: number; - rating_after: number | null; -} - -export interface RatingHistoryEntry { - bot_id: string; - rating: number; - rating_deviation: number; - recorded_at: string; -} - -export interface ExportData { - bots: ExportBot[]; - matches: ExportMatch[]; - rating_history: RatingHistoryEntry[]; - generated_at: string; -} - -// Generated Index Types - -export interface LeaderboardEntry { - rank: number; - bot_id: string; - name: string; - owner_id: string; - rating: number; - rating_deviation: number; - matches_played: number; - matches_won: number; - win_rate: number; - health_status: string; -} - -export interface LeaderboardIndex { - updated_at: string; - entries: LeaderboardEntry[]; -} - -export interface BotProfile { - id: string; - name: string; - owner_id: string; - rating: number; - rating_deviation: number; - rating_volatility: number; - matches_played: number; - matches_won: number; - win_rate: number; - health_status: string; - created_at: string; - updated_at: string; - rating_history: RatingHistoryEntry[]; - recent_matches: MatchSummary[]; -} - -export interface BotDirectoryEntry { - id: string; - name: string; - rating: number; - matches_played: number; - win_rate: number; -} - -export interface BotDirectory { - updated_at: string; - bots: BotDirectoryEntry[]; -} - -export interface MatchSummary { - id: string; - completed_at: string | null; - participants: { - bot_id: string; - name: string; - score: number; - won: boolean; - }[]; - winner_id: string | null; - turns: number | null; - end_reason: string | null; -} - -export interface MatchIndex { - updated_at: string; - matches: MatchSummary[]; -} - -// Blog / Narrative Engine types - -export interface BlogPost { - slug: string; - title: string; - published_at: string; // ISO 8601 date - week_start: string; // ISO 8601 date (Monday of the covered week) - summary: string; // one-paragraph plain-text teaser - body_html: string; // full HTML narrative content - stats: BlogWeekStats; -} - -export interface BlogWeekStats { - matches_played: number; - top_bot: string; - top_bot_rating: number; - biggest_upset: string | null; // "BotA defeated BotB" or null - most_active_bot: string; - most_active_bot_matches: number; - island_leader: string | null; // leading evolution island -} - -export interface BlogIndex { - updated_at: string; - posts: BlogPost[]; -} - -// Evolution dashboard types (written by acb-evolver live-export) -export interface EvolutionIslandStat { - count: number; - best_fitness: number; - avg_fitness: number; - diversity: number; - promoted_count: number; -} - -export interface EvolutionGenerationEntry { - generation: number; - island: string; - evaluated_at: string; - count: number; - promoted: number; - best_fitness: number; - avg_fitness: number; -} - -export interface EvolutionLineageNode { - id: number; - parent_ids: number[]; - generation: number; - island: string; - fitness: number; - promoted: boolean; - language: string; - created_at: string; -} - -export interface EvolutionMetaSnapshot { - generation: number; - island_counts: Record; - island_best_fitness: Record; -} - -export interface EvolutionLiveData { - updated_at: string; - total_programs: number; - promoted_count: number; - islands: Record; - generation_log: EvolutionGenerationEntry[]; - lineage: EvolutionLineageNode[]; - meta_snapshots: EvolutionMetaSnapshot[]; -} - -// Replay Playlist types - -export interface PlaylistMatch { - match_id: string; - order: number; // Position in playlist - title?: string; // Optional custom title (e.g., "The Upset") - thumbnail_url?: string; -} - -export interface Playlist { - slug: string; - title: string; - description: string; - category: PlaylistCategory; - match_count: number; - created_at: string; - updated_at: string; - matches: PlaylistMatch[]; -} - -export type PlaylistCategory = - | 'featured' // Curated featured matches - | 'rivalry' // Matches between specific rivals - | 'upsets' // Unexpected outcomes - | 'comebacks' // Big turnarounds - | 'domination' // One-sided victories - | 'close_games' // Narrow wins - | 'long_games' // High turn counts - | 'tutorial' // Tutorial/example matches - | 'season' // Season highlights - | 'weekly'; // Weekly best - -export interface PlaylistIndex { - updated_at: string; - playlists: PlaylistSummary[]; -} - -export interface PlaylistSummary { - slug: string; - title: string; - description: string; - category: PlaylistCategory; - match_count: number; - thumbnail_match_id?: string; -} diff --git a/cmd/acb-indexer/src/writer.ts b/cmd/acb-indexer/src/writer.ts deleted file mode 100644 index 7b5ca05..0000000 --- a/cmd/acb-indexer/src/writer.ts +++ /dev/null @@ -1,128 +0,0 @@ -// File Writer - Writes generated index files to disk - -import * as fs from 'fs/promises'; -import * as path from 'path'; - -import type { LeaderboardIndex, BotDirectory, BotProfile, MatchIndex, EvolutionLiveData } from './types.js'; - -export class FileWriter { - private outputDir: string; - - constructor(outputDir: string) { - this.outputDir = outputDir; - } - - /** - * Ensure output directory structure exists - */ - async ensureDirectories(): Promise { - const dirs = [ - this.outputDir, - path.join(this.outputDir, 'bots'), - path.join(this.outputDir, 'matches'), - path.join(this.outputDir, 'evolution'), - ]; - - for (const dir of dirs) { - try { - await fs.mkdir(dir, { recursive: true }); - } catch (error) { - if ((error as NodeJS.ErrnoException).code !== 'EEXIST') { - throw error; - } - } - } - } - - /** - * Write JSON file - */ - private async writeJson(filePath: string, data: unknown): Promise { - const content = JSON.stringify(data, null, 2); - await fs.writeFile(filePath, content, 'utf-8'); - console.log(`Wrote: ${filePath}`); - } - - /** - * Write leaderboard.json - */ - async writeLeaderboard(leaderboard: LeaderboardIndex): Promise { - const filePath = path.join(this.outputDir, 'leaderboard.json'); - await this.writeJson(filePath, leaderboard); - } - - /** - * Write bots/index.json - */ - async writeBotDirectory(directory: BotDirectory): Promise { - const filePath = path.join(this.outputDir, 'bots', 'index.json'); - await this.writeJson(filePath, directory); - } - - /** - * Write individual bot profile - */ - async writeBotProfile(botId: string, profile: BotProfile): Promise { - const filePath = path.join(this.outputDir, 'bots', `${botId}.json`); - await this.writeJson(filePath, profile); - } - - /** - * Write all bot profiles - */ - async writeBotProfiles(profiles: Map): Promise { - const writePromises: Promise[] = []; - - for (const [botId, profile] of profiles) { - writePromises.push(this.writeBotProfile(botId, profile)); - } - - await Promise.all(writePromises); - } - - /** - * Write matches/index.json - */ - async writeMatchIndex(matchIndex: MatchIndex): Promise { - const filePath = path.join(this.outputDir, 'matches', 'index.json'); - await this.writeJson(filePath, matchIndex); - } - - /** - * Write evolution/live.json - */ - async writeEvolutionLive(data: EvolutionLiveData): Promise { - const filePath = path.join(this.outputDir, 'evolution', 'live.json'); - await this.writeJson(filePath, data); - } - - /** - * Write all index files - */ - async writeAll(data: { - leaderboard: LeaderboardIndex; - botDirectory: BotDirectory; - botProfiles: Map; - matchIndex: MatchIndex; - evolutionLive?: EvolutionLiveData; - }): Promise { - await this.ensureDirectories(); - - await this.writeLeaderboard(data.leaderboard); - await this.writeBotDirectory(data.botDirectory); - await this.writeBotProfiles(data.botProfiles); - await this.writeMatchIndex(data.matchIndex); - - if (data.evolutionLive) { - await this.writeEvolutionLive(data.evolutionLive); - } - - console.log(`\nIndex generation complete!`); - console.log(` - ${data.leaderboard.entries.length} leaderboard entries`); - console.log(` - ${data.botProfiles.size} bot profiles`); - console.log(` - ${data.matchIndex.matches.length} matches`); - if (data.evolutionLive) { - console.log(` - ${data.evolutionLive.total_programs} evolution programs`); - } - } -} diff --git a/cmd/acb-indexer/tsconfig.json b/cmd/acb-indexer/tsconfig.json deleted file mode 100644 index ddbc383..0000000 --- a/cmd/acb-indexer/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "outDir": "./dist", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "declaration": true, - "resolveJsonModule": true - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "**/*.test.ts"] -} diff --git a/cmd/acb-worker/api.go b/cmd/acb-worker/api.go index 2c65f99..b23411c 100644 --- a/cmd/acb-worker/api.go +++ b/cmd/acb-worker/api.go @@ -1,37 +1,12 @@ -// API client for Worker API communication +// API types for acb-worker +// HTTP API client removed - worker now uses direct PostgreSQL writes package main import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" "time" ) -// APIClient communicates with the Worker API. -type APIClient struct { - endpoint string - apiKey string - httpClient *http.Client - maxRetries int -} - -// NewAPIClient creates a new API client. -func NewAPIClient(cfg *Config) *APIClient { - return &APIClient{ - endpoint: cfg.APIEndpoint, - apiKey: cfg.APIKey, - httpClient: &http.Client{ - Timeout: 30 * time.Second, - }, - maxRetries: cfg.MaxRetries, - } -} - -// Job represents a pending job from the API. +// Job represents a pending job (kept for compatibility). type Job struct { ID string `json:"id"` MatchID string `json:"match_id"` @@ -43,6 +18,7 @@ type Job struct { } // JobClaimResponse contains the data needed to execute a match. +// This maps to JobClaimData from db.go for compatibility. type JobClaimResponse struct { Job Job `json:"job"` Match Match `json:"match"` @@ -100,175 +76,97 @@ type BotSecret struct { Secret string `json:"secret"` } -// APIResponse is a generic API response. -type APIResponse struct { - Success bool `json:"success"` - Data json.RawMessage `json:"data,omitempty"` - Error string `json:"error,omitempty"` +// MatchResult represents the result of a match for submission. +type MatchResult struct { + WinnerID string `json:"winner_id"` + Turns int `json:"turns"` + EndReason string `json:"end_reason"` + Scores map[string]int `json:"scores"` } -// GetNextJob fetches the next pending job. -func (c *APIClient) GetNextJob(ctx context.Context) (*Job, error) { - resp, err := c.doRequest(ctx, "GET", "/api/jobs/next", nil) - if err != nil { - return nil, err +// ConvertDBJobToJob converts a DBJob to Job type. +func ConvertDBJobToJob(dbJob *DBJob) *Job { + if dbJob == nil { + return nil } - - var apiResp APIResponse - if err := json.Unmarshal(resp, &apiResp); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) + return &Job{ + ID: dbJob.ID, + MatchID: dbJob.MatchID, + Status: dbJob.Status, + WorkerID: dbJob.WorkerID, + ClaimedAt: dbJob.ClaimedAt, + HeartbeatAt: dbJob.HeartbeatAt, + CreatedAt: dbJob.CreatedAt, } - - if !apiResp.Success { - return nil, fmt.Errorf("API error: %s", apiResp.Error) - } - - if apiResp.Data == nil { - return nil, nil // No pending jobs - } - - var job Job - if err := json.Unmarshal(apiResp.Data, &job); err != nil { - return nil, fmt.Errorf("failed to parse job: %w", err) - } - - return &job, nil } -// ClaimJob claims a job for execution. -func (c *APIClient) ClaimJob(ctx context.Context, jobID string, workerID string) (*JobClaimResponse, error) { - body := map[string]string{"worker_id": workerID} - - resp, err := c.doRequest(ctx, "POST", "/api/jobs/"+jobID+"/claim", body) - if err != nil { - return nil, err +// ConvertDBClaimToResponse converts JobClaimData to JobClaimResponse. +func ConvertDBClaimToResponse(data *JobClaimData) *JobClaimResponse { + if data == nil { + return nil } - var apiResp APIResponse - if err := json.Unmarshal(resp, &apiResp); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) - } + // Convert participants + participants := make([]Participant, len(data.Participants)) + botSecrets := make([]BotSecret, len(data.Participants)) + bots := make([]BotInfo, len(data.Bots)) - if !apiResp.Success { - return nil, fmt.Errorf("API error: %s", apiResp.Error) - } - - var claimResp JobClaimResponse - if err := json.Unmarshal(apiResp.Data, &claimResp); err != nil { - return nil, fmt.Errorf("failed to parse claim response: %w", err) - } - - return &claimResp, nil -} - -// Heartbeat sends a heartbeat for a claimed job. -func (c *APIClient) Heartbeat(ctx context.Context, jobID string, workerID string) error { - body := map[string]string{"worker_id": workerID} - - _, err := c.doRequest(ctx, "POST", "/api/jobs/"+jobID+"/heartbeat", body) - return err -} - -// SubmitResult submits the result of a completed match. -func (c *APIClient) SubmitResult(ctx context.Context, jobID string, result *MatchResult, replayURL string) error { - body := map[string]interface{}{ - "winner_id": result.WinnerID, - "turns": result.Turns, - "end_reason": result.EndReason, - "replay_url": replayURL, - "scores": result.Scores, - } - - _, err := c.doRequest(ctx, "POST", "/api/jobs/"+jobID+"/result", body) - return err -} - -// FailJob marks a job as failed. -func (c *APIClient) FailJob(ctx context.Context, jobID string, workerID string, errorMessage string) error { - body := map[string]string{ - "worker_id": workerID, - "error_message": errorMessage, - } - - _, err := c.doRequest(ctx, "POST", "/api/jobs/"+jobID+"/fail", body) - return err -} - -// doRequest makes an HTTP request with retries. -func (c *APIClient) doRequest(ctx context.Context, method string, path string, body interface{}) ([]byte, error) { - var lastErr error - - for attempt := 0; attempt <= c.maxRetries; attempt++ { - if attempt > 0 { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(time.Second * time.Duration(attempt)): - } + for i, p := range data.Participants { + participants[i] = Participant{ + ID: p.MatchID + "-" + p.BotID, + MatchID: p.MatchID, + BotID: p.BotID, + PlayerIndex: p.PlayerSlot, + Score: p.Score, + RatingBefore: int(p.RatingMuBefore), + RatingDeviationBefore: int(p.RatingPhiBefore), } - - resp, err := c.doSingleRequest(ctx, method, path, body) - if err != nil { - lastErr = err - // Check if it's a client error (don't retry) - if httpErr, ok := err.(*HTTPError); ok && httpErr.StatusCode >= 400 && httpErr.StatusCode < 500 { - return nil, err - } - continue - } - - return resp, nil - } - - return nil, fmt.Errorf("request failed after %d retries: %w", c.maxRetries, lastErr) -} - -// doSingleRequest makes a single HTTP request. -func (c *APIClient) doSingleRequest(ctx context.Context, method string, path string, body interface{}) ([]byte, error) { - var reqBody io.Reader - if body != nil { - data, err := json.Marshal(body) - if err != nil { - return nil, fmt.Errorf("failed to marshal request body: %w", err) - } - reqBody = bytes.NewReader(data) - } - - req, err := http.NewRequestWithContext(ctx, method, c.endpoint+path, reqBody) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+c.apiKey) - - resp, err := c.httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("request failed: %w", err) - } - defer resp.Body.Close() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode >= 400 { - return nil, &HTTPError{ - StatusCode: resp.StatusCode, - Body: string(respBody), + botSecrets[i] = BotSecret{ + BotID: p.BotID, + Secret: "", // Will be filled from bots lookup } } - return respBody, nil -} + // Convert bots and match secrets + botSecretMap := make(map[string]string) + for i, b := range data.Bots { + bots[i] = BotInfo{ + ID: b.ID, + EndpointURL: b.EndpointURL, + } + botSecretMap[b.ID] = b.Secret + } -// HTTPError represents an HTTP error response. -type HTTPError struct { - StatusCode int - Body string -} + // Fill in secrets + for i, p := range data.Participants { + botSecrets[i].Secret = botSecretMap[p.BotID] + } -func (e *HTTPError) Error() string { - return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Body) + return &JobClaimResponse{ + Job: Job{ + ID: data.Job.ID, + MatchID: data.Job.MatchID, + Status: data.Job.Status, + WorkerID: data.Job.WorkerID, + ClaimedAt: data.Job.ClaimedAt, + CreatedAt: data.Job.CreatedAt, + }, + Match: Match{ + ID: data.Match.ID, + Status: data.Match.Status, + MapID: data.Match.MapID, + CreatedAt: data.Match.CreatedAt, + }, + Participants: participants, + Map: MapData{ + ID: data.Map.ID, + Width: data.Map.Width, + Height: data.Map.Height, + Walls: data.Map.Walls, + Spawns: data.Map.Spawns, + Cores: data.Map.Cores, + }, + Bots: bots, + BotSecrets: botSecrets, + } } diff --git a/cmd/acb-worker/r2.go b/cmd/acb-worker/b2.go similarity index 68% rename from cmd/acb-worker/r2.go rename to cmd/acb-worker/b2.go index 4707fd8..30a95a7 100644 --- a/cmd/acb-worker/r2.go +++ b/cmd/acb-worker/b2.go @@ -1,4 +1,4 @@ -// R2 client for uploading replays +// B2 client for uploading replays to Backblaze B2 (cold archive) package main import ( @@ -12,28 +12,28 @@ import ( "github.com/aws/aws-sdk-go-v2/service/s3" ) -// R2Client handles R2 bucket operations. -type R2Client struct { +// B2Client handles B2 bucket operations (S3-compatible). +type B2Client struct { client *s3.Client bucket string endpoint string } -// NewR2Client creates a new R2 client. -func NewR2Client(cfg *Config) *R2Client { - // Create custom endpoint resolver for R2 +// NewB2Client creates a new B2 client. +func NewB2Client(cfg *Config) *B2Client { + // Create custom endpoint resolver for B2 (S3-compatible) customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { return aws.Endpoint{ - URL: cfg.R2Endpoint, - SigningRegion: "auto", + URL: cfg.B2Endpoint, + SigningRegion: cfg.B2Region, }, nil }) - // Load AWS config with R2 credentials + // Load AWS config with B2 credentials awsCfg, err := config.LoadDefaultConfig(context.TODO(), config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( - cfg.R2AccessKey, - cfg.R2SecretKey, + cfg.B2AccessKey, + cfg.B2SecretKey, "", )), config.WithEndpointResolverWithOptions(customResolver), @@ -42,15 +42,15 @@ func NewR2Client(cfg *Config) *R2Client { panic(fmt.Sprintf("failed to load AWS config: %v", err)) } - return &R2Client{ + return &B2Client{ client: s3.NewFromConfig(awsCfg), - bucket: cfg.R2Bucket, - endpoint: cfg.R2Endpoint, + bucket: cfg.B2Bucket, + endpoint: cfg.B2Endpoint, } } -// Upload uploads data to R2. -func (c *R2Client) Upload(ctx context.Context, key string, data []byte, contentType string) error { +// Upload uploads data to B2. +func (c *B2Client) Upload(ctx context.Context, key string, data []byte, contentType string) error { _, err := c.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(key), @@ -61,8 +61,8 @@ func (c *R2Client) Upload(ctx context.Context, key string, data []byte, contentT return err } -// Download downloads data from R2. -func (c *R2Client) Download(ctx context.Context, key string) ([]byte, error) { +// Download downloads data from B2. +func (c *B2Client) Download(ctx context.Context, key string) ([]byte, error) { resp, err := c.client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(key), @@ -80,8 +80,8 @@ func (c *R2Client) Download(ctx context.Context, key string) ([]byte, error) { return buf.Bytes(), nil } -// Delete deletes an object from R2. -func (c *R2Client) Delete(ctx context.Context, key string) error { +// Delete deletes an object from B2. +func (c *B2Client) Delete(ctx context.Context, key string) error { _, err := c.client.DeleteObject(ctx, &s3.DeleteObjectInput{ Bucket: aws.String(c.bucket), Key: aws.String(key), @@ -90,7 +90,7 @@ func (c *R2Client) Delete(ctx context.Context, key string) error { } // List lists objects with a prefix. -func (c *R2Client) List(ctx context.Context, prefix string) ([]string, error) { +func (c *B2Client) List(ctx context.Context, prefix string) ([]string, error) { var keys []string paginator := s3.NewListObjectsV2Paginator(c.client, &s3.ListObjectsV2Input{ @@ -114,7 +114,7 @@ func (c *R2Client) List(ctx context.Context, prefix string) ([]string, error) { return keys, nil } -// Endpoint returns the R2 endpoint URL. -func (c *R2Client) Endpoint() string { +// Endpoint returns the B2 endpoint URL. +func (c *B2Client) Endpoint() string { return c.endpoint } diff --git a/cmd/acb-worker/db.go b/cmd/acb-worker/db.go new file mode 100644 index 0000000..799f73d --- /dev/null +++ b/cmd/acb-worker/db.go @@ -0,0 +1,469 @@ +// PostgreSQL database client for match results and job coordination +package main + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "time" + + _ "github.com/lib/pq" +) + +// DBClient handles PostgreSQL operations. +type DBClient struct { + db *sql.DB +} + +// NewDBClient creates a new database client. +func NewDBClient(databaseURL string) (*DBClient, error) { + db, err := sql.Open("postgres", databaseURL) + if err != nil { + return nil, fmt.Errorf("failed to open database: %w", err) + } + + // Configure connection pool + db.SetMaxOpenConns(10) + db.SetMaxIdleConns(5) + db.SetConnMaxLifetime(5 * time.Minute) + + // Verify connection + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := db.PingContext(ctx); err != nil { + return nil, fmt.Errorf("failed to connect to database: %w", err) + } + + return &DBClient{db: db}, nil +} + +// Close closes the database connection. +func (c *DBClient) Close() error { + return c.db.Close() +} + +// DBJob represents a pending job from the database. +type DBJob struct { + ID string `json:"id"` + MatchID string `json:"match_id"` + Status string `json:"status"` + WorkerID *string `json:"worker_id"` + ClaimedAt *time.Time `json:"claimed_at"` + HeartbeatAt *time.Time `json:"heartbeat_at"` + CreatedAt time.Time `json:"created_at"` +} + +// DBMatch represents match metadata from the database. +type DBMatch struct { + ID string `json:"id"` + Status string `json:"status"` + Winner *int `json:"winner"` // player index + MapID string `json:"map_id"` + CreatedAt time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + CompletedAt *time.Time `json:"completed_at"` +} + +// DBMatch represents match metadata. +type DBMatch struct { + ID string `json:"id"` + Status string `json:"status"` + Winner *int `json:"winner"` // player index + MapID string `json:"map_id"` + CreatedAt time.Time `json:"created_at"` + StartedAt *time.Time `json:"started_at"` + CompletedAt *time.Time `json:"completed_at"` +} + +// DBParticipant represents a match participant. +type DBParticipant struct { + MatchID string `json:"match_id"` + BotID string `json:"bot_id"` + PlayerSlot int `json:"player_slot"` + Score int `json:"score"` + RatingMuBefore float64 + RatingPhiBefore float64 + RatingSigmaBefore float64 + RatingMuAfter *float64 + RatingPhiAfter *float64 + RatingSigmaAfter *float64 +} + +// DBBotInfo contains bot endpoint and secret information. +type DBBotInfo struct { + ID string + EndpointURL string + Secret string +} + +// DBMapData represents map configuration. +type DBMapData struct { + ID string `json:"id"` + Width int `json:"width"` + Height int `json:"height"` + Walls string `json:"walls"` + Spawns string `json:"spawns"` + Cores string `json:"cores"` +} + +// JobClaimData contains all data needed to execute a match. +type JobClaimData struct { + Job DBJob + Match DBMatch + Participants []DBParticipant + Map DBMapData + Bots []DBBotInfo +} + +// GetNextJob fetches the next pending job from the database. +func (c *DBClient) GetNextJob(ctx context.Context) (*DBJob, error) { + query := ` + SELECT job_id, match_id, status, worker_id, claimed_at, heartbeat_at, created_at + FROM jobs + WHERE status = 'pending' + ORDER BY created_at ASC + LIMIT 1 + FOR UPDATE SKIP LOCKED + ` + + var job DBJob + err := c.db.QueryRowContext(ctx, query).Scan( + &job.ID, &job.MatchID, &job.Status, &job.WorkerID, + &job.ClaimedAt, &job.HeartbeatAt, &job.CreatedAt, + ) + if err == sql.ErrNoRows { + return nil, nil // No pending jobs + } + if err != nil { + return nil, fmt.Errorf("failed to get next job: %w", err) + } + + return &job, nil +} + +// ClaimJob claims a job and returns all data needed to execute the match. +func (c *DBClient) ClaimJob(ctx context.Context, jobID string, workerID string) (*JobClaimData, error) { + tx, err := c.db.BeginTx(ctx, nil) + if err != nil { + return nil, fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + // Update job status + now := time.Now().UTC() + _, err = tx.ExecContext(ctx, ` + UPDATE jobs + SET status = 'claimed', worker_id = $1, claimed_at = $2 + WHERE job_id = $3 AND status = 'pending' + `, workerID, now, jobID) + if err != nil { + return nil, fmt.Errorf("failed to claim job: %w", err) + } + + // Get job details + var job DBJob + err = tx.QueryRowContext(ctx, ` + SELECT job_id, match_id, status, worker_id, claimed_at, heartbeat_at, created_at + FROM jobs WHERE job_id = $1 + `, jobID).Scan( + &job.ID, &job.MatchID, &job.Status, &job.WorkerID, + &job.ClaimedAt, &job.HeartbeatAt, &job.CreatedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to get job: %w", err) + } + + // Get match details + var match DBMatch + err = tx.QueryRowContext(ctx, ` + SELECT match_id, status, winner, map_id, created_at, completed_at + FROM matches WHERE match_id = $1 + `, job.MatchID).Scan( + &match.ID, &match.Status, &match.Winner, &match.MapID, + &match.CreatedAt, &match.CompletedAt, + ) + if err != nil { + return nil, fmt.Errorf("failed to get match: %w", err) + } + + // Get map data + var mapData DBMapData + err = tx.QueryRowContext(ctx, ` + SELECT map_id, grid_width, grid_height, map_json->>'walls' as walls, + map_json->>'spawns' as spawns, map_json->>'cores' as cores + FROM maps WHERE map_id = $1 + `, match.MapID).Scan( + &mapData.ID, &mapData.Width, &mapData.Height, + &mapData.Walls, &mapData.Spawns, &mapData.Cores, + ) + if err != nil { + return nil, fmt.Errorf("failed to get map: %w", err) + } + + // Get participants + participantRows, err := tx.QueryContext(ctx, ` + SELECT mp.match_id, mp.bot_id, mp.player_slot, mp.score, + b.rating_mu, b.rating_phi, b.rating_sigma + FROM match_participants mp + JOIN bots b ON mp.bot_id = b.bot_id + WHERE mp.match_id = $1 + ORDER BY mp.player_slot + `, job.MatchID) + if err != nil { + return nil, fmt.Errorf("failed to get participants: %w", err) + } + defer participantRows.Close() + + var participants []DBParticipant + var botIDs []string + for participantRows.Next() { + var p DBParticipant + err := participantRows.Scan( + &p.MatchID, &p.BotID, &p.PlayerSlot, &p.Score, + &p.RatingMuBefore, &p.RatingPhiBefore, &p.RatingSigmaBefore, + ) + if err != nil { + return nil, fmt.Errorf("failed to scan participant: %w", err) + } + participants = append(participants, p) + botIDs = append(botIDs, p.BotID) + } + + // Get bot endpoints and secrets + botRows, err := tx.QueryContext(ctx, ` + SELECT bot_id, endpoint_url, shared_secret + FROM bots WHERE bot_id = ANY($1) + `, botIDs) + if err != nil { + return nil, fmt.Errorf("failed to get bots: %w", err) + } + defer botRows.Close() + + var bots []DBBotInfo + for botRows.Next() { + var b DBBotInfo + if err := botRows.Scan(&b.ID, &b.EndpointURL, &b.Secret); err != nil { + return nil, fmt.Errorf("failed to scan bot: %w", err) + } + bots = append(bots, b) + } + + // Update match status to running + _, err = tx.ExecContext(ctx, ` + UPDATE matches SET status = 'running' WHERE match_id = $1 + `, job.MatchID) + if err != nil { + return nil, fmt.Errorf("failed to update match status: %w", err) + } + + if err := tx.Commit(); err != nil { + return nil, fmt.Errorf("failed to commit transaction: %w", err) + } + + return &JobClaimData{ + Job: job, + Match: match, + Participants: participants, + Map: mapData, + Bots: bots, + }, nil +} + +// Heartbeat updates the heartbeat timestamp for a job. +func (c *DBClient) Heartbeat(ctx context.Context, jobID string, workerID string) error { + result, err := c.db.ExecContext(ctx, ` + UPDATE jobs + SET heartbeat_at = NOW() + WHERE job_id = $1 AND worker_id = $2 AND status = 'claimed' + `, jobID, workerID) + if err != nil { + return fmt.Errorf("failed to send heartbeat: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to check rows affected: %w", err) + } + if rows == 0 { + return fmt.Errorf("job not found or not claimed by this worker") + } + + return nil +} + +// SubmitMatchResult writes the match result to the database and updates ratings. +func (c *DBClient) SubmitMatchResult(ctx context.Context, jobID string, result *MatchResult, replayURL string, ratingUpdates []RatingUpdate) error { + tx, err := c.db.BeginTx(ctx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + defer tx.Rollback() + + now := time.Now().UTC() + + // Determine winner player index from result + var winnerIndex *int + if result.WinnerID != "" { + // Look up player slot for winner + var idx int + err := tx.QueryRowContext(ctx, ` + SELECT player_slot FROM match_participants WHERE match_id = ( + SELECT match_id FROM jobs WHERE job_id = $1 + ) AND bot_id = $2 + `, jobID, result.WinnerID).Scan(&idx) + if err == nil { + winnerIndex = &idx + } + } + + // Update job status + _, err = tx.ExecContext(ctx, ` + UPDATE jobs + SET status = 'completed', completed_at = $1 + WHERE job_id = $2 + `, now, jobID) + if err != nil { + return fmt.Errorf("failed to update job: %w", err) + } + + // Get match ID + var matchID string + err = tx.QueryRowContext(ctx, ` + SELECT match_id FROM jobs WHERE job_id = $1 + `, jobID).Scan(&matchID) + if err != nil { + return fmt.Errorf("failed to get match ID: %w", err) + } + + // Update match status and result + scoresJSON, _ := json.Marshal(result.Scores) + _, err = tx.ExecContext(ctx, ` + UPDATE matches + SET status = 'completed', winner = $1, condition = $2, + turn_count = $3, scores_json = $4, completed_at = $5 + WHERE match_id = $6 + `, winnerIndex, result.EndReason, result.Turns, scoresJSON, now, matchID) + if err != nil { + return fmt.Errorf("failed to update match: %w", err) + } + + // Update participant scores + for botID, score := range result.Scores { + _, err = tx.ExecContext(ctx, ` + UPDATE match_participants + SET score = $1 + WHERE match_id = $2 AND bot_id = $3 + `, score, matchID, botID) + if err != nil { + return fmt.Errorf("failed to update participant score: %w", err) + } + } + + // Apply rating updates (Glicko-2) + for _, update := range ratingUpdates { + // Update bot rating + _, err = tx.ExecContext(ctx, ` + UPDATE bots + SET rating_mu = $1, rating_phi = $2, rating_sigma = $3, last_active = $4 + WHERE bot_id = $5 + `, update.Mu, update.Phi, update.Sigma, now, update.BotID) + if err != nil { + return fmt.Errorf("failed to update bot rating: %w", err) + } + + // Record rating history + _, err = tx.ExecContext(ctx, ` + INSERT INTO rating_history (bot_id, match_id, rating, recorded_at) + VALUES ($1, $2, $3, $4) + ON CONFLICT (bot_id, match_id) DO UPDATE SET rating = $3, recorded_at = $4 + `, update.BotID, matchID, update.DisplayRating, now) + if err != nil { + return fmt.Errorf("failed to record rating history: %w", err) + } + + // Update participant with rating after + _, err = tx.ExecContext(ctx, ` + UPDATE match_participants + SET rating_mu_after = $1, rating_phi_after = $2, rating_sigma_after = $3 + WHERE match_id = $4 AND bot_id = $5 + `, update.Mu, update.Phi, update.Sigma, matchID, update.BotID) + if err != nil { + return fmt.Errorf("failed to update participant rating after: %w", err) + } + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + + return nil +} + +// FailJob marks a job as failed. +func (c *DBClient) FailJob(ctx context.Context, jobID string, workerID string, errorMessage string) error { + result, err := c.db.ExecContext(ctx, ` + UPDATE jobs + SET status = 'failed', completed_at = NOW() + WHERE job_id = $1 AND worker_id = $2 AND status = 'claimed' + `, jobID, workerID) + if err != nil { + return fmt.Errorf("failed to fail job: %w", err) + } + + rows, err := result.RowsAffected() + if err != nil { + return fmt.Errorf("failed to check rows affected: %w", err) + } + if rows == 0 { + return fmt.Errorf("job not found or not claimed by this worker") + } + + // Also update match status + _, err = c.db.ExecContext(ctx, ` + UPDATE matches + SET status = 'failed', completed_at = NOW() + WHERE match_id = (SELECT match_id FROM jobs WHERE job_id = $1) + `, jobID) + if err != nil { + return fmt.Errorf("failed to update match status: %w", err) + } + + return nil +} + +// RatingUpdate represents a Glicko-2 rating update for a bot. +type RatingUpdate struct { + BotID string + Mu float64 + Phi float64 + Sigma float64 + DisplayRating float64 + RatingMuBefore float64 + RatingPhiBefore float64 + RatingDeviationChange float64 +} + +// GetBotRatings retrieves current ratings for a list of bots. +func (c *DBClient) GetBotRatings(ctx context.Context, botIDs []string) (map[string]Glicko2Rating, error) { + rows, err := c.db.QueryContext(ctx, ` + SELECT bot_id, rating_mu, rating_phi, rating_sigma + FROM bots WHERE bot_id = ANY($1) + `, botIDs) + if err != nil { + return nil, fmt.Errorf("failed to get bot ratings: %w", err) + } + defer rows.Close() + + ratings := make(map[string]Glicko2Rating) + for rows.Next() { + var botID string + var r Glicko2Rating + if err := rows.Scan(&botID, &r.Mu, &r.Phi, &r.Sigma); err != nil { + return nil, fmt.Errorf("failed to scan rating: %w", err) + } + ratings[botID] = r + } + + return ratings, nil +} diff --git a/cmd/acb-api/glicko2.go b/cmd/acb-worker/glicko2.go similarity index 78% rename from cmd/acb-api/glicko2.go rename to cmd/acb-worker/glicko2.go index a3534b0..63b3075 100644 --- a/cmd/acb-api/glicko2.go +++ b/cmd/acb-worker/glicko2.go @@ -1,10 +1,9 @@ +// Glicko-2 Rating System Implementation for acb-worker +// Based on: http://www.glicko.net/glicko/glicko2.pdf package main import "math" -// Glicko-2 Rating System Implementation -// Based on: http://www.glicko.net/glicko/glicko2.pdf - const ( glicko2Scale = 173.7178 glicko2Tau = 0.5 @@ -13,6 +12,7 @@ const ( glicko2Epsilon = 1e-6 ) +// Glicko2Rating represents a Glicko-2 rating. type Glicko2Rating struct { Mu float64 `json:"mu"` Phi float64 `json:"phi"` @@ -144,12 +144,12 @@ func updateSingleRating(r Glicko2Rating, opps []opponent) Glicko2Rating { } } -// updateRatings computes new ratings for all participants in a multi-player match. +// UpdateRatings computes new ratings for all participants in a multi-player match. // Scores are used pairwise: for each pair (i, j), player i gets: // - 1.0 if scores[i] > scores[j] // - 0.5 if scores[i] == scores[j] // - 0.0 if scores[i] < scores[j] -func updateRatings(ratings []Glicko2Rating, scores []float64) []Glicko2Rating { +func UpdateRatings(ratings []Glicko2Rating, scores []float64) []Glicko2Rating { n := len(ratings) if n < 2 { return ratings @@ -183,3 +183,29 @@ func updateRatings(ratings []Glicko2Rating, scores []float64) []Glicko2Rating { return result } + +// ComputeRatingUpdates computes rating updates for match participants. +// botIDs, currentRatings, and scores must all have the same length. +func ComputeRatingUpdates(botIDs []string, currentRatings []Glicko2Rating, scores []float64) []RatingUpdate { + if len(botIDs) != len(currentRatings) || len(botIDs) != len(scores) { + return nil + } + + newRatings := UpdateRatings(currentRatings, scores) + updates := make([]RatingUpdate, len(botIDs)) + + for i := range botIDs { + updates[i] = RatingUpdate{ + BotID: botIDs[i], + Mu: newRatings[i].Mu, + Phi: newRatings[i].Phi, + Sigma: newRatings[i].Sigma, + DisplayRating: newRatings[i].DisplayRating(), + RatingMuBefore: currentRatings[i].Mu, + RatingPhiBefore: currentRatings[i].Phi, + RatingDeviationChange: newRatings[i].Phi - currentRatings[i].Phi, + } + } + + return updates +} diff --git a/worker-api/migrations/0001_initial.sql b/worker-api/migrations/0001_initial.sql deleted file mode 100644 index ae14c25..0000000 --- a/worker-api/migrations/0001_initial.sql +++ /dev/null @@ -1,264 +0,0 @@ --- Migration: 0001_initial --- Description: Initial database schema for AI Code Battle --- Created: 2025-03-24 - --- ============================================ --- Core Tables --- ============================================ - --- Bots table: stores registered bots -CREATE TABLE IF NOT EXISTS bots ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - owner_id TEXT NOT NULL, - endpoint_url TEXT NOT NULL, - api_key_hash TEXT NOT NULL, - rating REAL NOT NULL DEFAULT 1500.0, - rating_deviation REAL NOT NULL DEFAULT 350.0, - rating_volatility REAL NOT NULL DEFAULT 0.06, - evolved INTEGER NOT NULL DEFAULT 0, - island TEXT, - generation INTEGER, - parent_ids TEXT, - description TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - last_health_check TEXT, - health_status TEXT DEFAULT 'unknown', - matches_played INTEGER NOT NULL DEFAULT 0, - matches_won INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS idx_bots_owner ON bots(owner_id); -CREATE INDEX IF NOT EXISTS idx_bots_rating ON bots(rating DESC); -CREATE INDEX IF NOT EXISTS idx_bots_evolved ON bots(evolved); - --- Matches table: stores match metadata -CREATE TABLE IF NOT EXISTS matches ( - id TEXT PRIMARY KEY, - status TEXT NOT NULL DEFAULT 'pending', - winner_id TEXT, - turns INTEGER, - end_reason TEXT, - map_id TEXT NOT NULL, - scores_json TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - started_at TEXT, - completed_at TEXT -); - -CREATE INDEX IF NOT EXISTS idx_matches_status ON matches(status); -CREATE INDEX IF NOT EXISTS idx_matches_created ON matches(created_at DESC); -CREATE INDEX IF NOT EXISTS idx_matches_map ON matches(map_id); - --- Match participants: links bots to matches -CREATE TABLE IF NOT EXISTS match_participants ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - bot_id TEXT NOT NULL, - player_index INTEGER NOT NULL, - score INTEGER NOT NULL DEFAULT 0, - status TEXT, - rating_before REAL NOT NULL, - rating_after REAL, - rating_deviation_before REAL NOT NULL, - rating_deviation_after REAL, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, - FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE, - UNIQUE(match_id, bot_id), - UNIQUE(match_id, player_index) -); - -CREATE INDEX IF NOT EXISTS idx_match_participants_match ON match_participants(match_id); -CREATE INDEX IF NOT EXISTS idx_match_participants_bot ON match_participants(bot_id); - --- Jobs table: match execution jobs for workers -CREATE TABLE IF NOT EXISTS jobs ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - worker_id TEXT, - claimed_at TEXT, - heartbeat_at TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - completed_at TEXT, - error_message TEXT, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); -CREATE INDEX IF NOT EXISTS idx_jobs_worker ON jobs(worker_id); -CREATE INDEX IF NOT EXISTS idx_jobs_heartbeat ON jobs(heartbeat_at); - --- Rating history: tracks rating changes over time -CREATE TABLE IF NOT EXISTS rating_history ( - id TEXT PRIMARY KEY, - bot_id TEXT NOT NULL, - match_id TEXT NOT NULL, - rating_before REAL NOT NULL, - rating_after REAL NOT NULL, - rating_deviation REAL NOT NULL, - recorded_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_rating_history_bot ON rating_history(bot_id); -CREATE INDEX IF NOT EXISTS idx_rating_history_time ON rating_history(recorded_at DESC); - --- Maps table: stores generated maps -CREATE TABLE IF NOT EXISTS maps ( - id TEXT PRIMARY KEY, - width INTEGER NOT NULL, - height INTEGER NOT NULL, - player_count INTEGER NOT NULL DEFAULT 2, - walls TEXT NOT NULL, - spawns TEXT NOT NULL, - cores TEXT NOT NULL, - energy_nodes TEXT NOT NULL, - wall_density REAL NOT NULL DEFAULT 0.15, - status TEXT NOT NULL DEFAULT 'active', - engagement_score REAL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_maps_status ON maps(status); -CREATE INDEX IF NOT EXISTS idx_maps_player_count ON maps(player_count); -CREATE INDEX IF NOT EXISTS idx_maps_engagement ON maps(engagement_score DESC); - --- Bot secrets: stores API keys for bots (separate for security) -CREATE TABLE IF NOT EXISTS bot_secrets ( - bot_id TEXT PRIMARY KEY, - api_key_hash TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE -); - --- ============================================ --- Prediction System --- ============================================ - --- Predictions: visitor predictions on match outcomes -CREATE TABLE IF NOT EXISTS predictions ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - predictor_id TEXT NOT NULL, - predictor_name TEXT, - predicted_bot_id TEXT NOT NULL, - correct INTEGER, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, - FOREIGN KEY (predicted_bot_id) REFERENCES bots(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_predictions_match ON predictions(match_id); -CREATE INDEX IF NOT EXISTS idx_predictions_predictor ON predictions(predictor_id); - --- Predictor stats: aggregate prediction accuracy -CREATE TABLE IF NOT EXISTS predictor_stats ( - predictor_id TEXT PRIMARY KEY, - predictor_name TEXT, - correct INTEGER NOT NULL DEFAULT 0, - incorrect INTEGER NOT NULL DEFAULT 0, - streak INTEGER NOT NULL DEFAULT 0, - best_streak INTEGER NOT NULL DEFAULT 0, - rating REAL NOT NULL DEFAULT 1000.0 -); - -CREATE INDEX IF NOT EXISTS idx_predictor_stats_rating ON predictor_stats(rating DESC); - --- ============================================ --- Map Voting --- ============================================ - --- Map votes: community voting on map quality -CREATE TABLE IF NOT EXISTS map_votes ( - id TEXT PRIMARY KEY, - map_id TEXT NOT NULL, - voter_id TEXT NOT NULL, - vote INTEGER NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE, - UNIQUE(map_id, voter_id) -); - -CREATE INDEX IF NOT EXISTS idx_map_votes_map ON map_votes(map_id); - --- ============================================ --- Replay Feedback --- ============================================ - --- Replay feedback: community annotations on replays -CREATE TABLE IF NOT EXISTS replay_feedback ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - turn INTEGER NOT NULL, - type TEXT NOT NULL, - body TEXT NOT NULL, - author TEXT NOT NULL, - upvotes INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_feedback_match ON replay_feedback(match_id, turn); -CREATE INDEX IF NOT EXISTS idx_feedback_type ON replay_feedback(type); -CREATE INDEX IF NOT EXISTS idx_feedback_upvotes ON replay_feedback(upvotes DESC); - --- ============================================ --- Multi-Game Series --- ============================================ - --- Series: best-of-N match series between two bots -CREATE TABLE IF NOT EXISTS series ( - id TEXT PRIMARY KEY, - bot_a_id TEXT NOT NULL, - bot_b_id TEXT NOT NULL, - format INTEGER NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - a_wins INTEGER NOT NULL DEFAULT 0, - b_wins INTEGER NOT NULL DEFAULT 0, - season_id TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - completed_at TEXT, - FOREIGN KEY (bot_a_id) REFERENCES bots(id) ON DELETE CASCADE, - FOREIGN KEY (bot_b_id) REFERENCES bots(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_series_status ON series(status); -CREATE INDEX IF NOT EXISTS idx_series_bots ON series(bot_a_id, bot_b_id); -CREATE INDEX IF NOT EXISTS idx_series_season ON series(season_id); - --- Series games: individual games within a series -CREATE TABLE IF NOT EXISTS series_games ( - series_id TEXT NOT NULL, - game_number INTEGER NOT NULL, - match_id TEXT, - map_id TEXT NOT NULL, - winner INTEGER, - PRIMARY KEY (series_id, game_number), - FOREIGN KEY (series_id) REFERENCES series(id) ON DELETE CASCADE, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE SET NULL -); - -CREATE INDEX IF NOT EXISTS idx_series_games_match ON series_games(match_id); - --- ============================================ --- Seasonal Rotations --- ============================================ - --- Seasons: seasonal leaderboards with rule variations -CREATE TABLE IF NOT EXISTS seasons ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - theme TEXT NOT NULL, - rules_version INTEGER NOT NULL DEFAULT 1, - started_at TEXT NOT NULL, - ended_at TEXT, - champion_id TEXT, - status TEXT NOT NULL DEFAULT 'active', - FOREIGN KEY (champion_id) REFERENCES bots(id) ON DELETE SET NULL -); - -CREATE INDEX IF NOT EXISTS idx_seasons_status ON seasons(status); -CREATE INDEX IF NOT EXISTS idx_seasons_dates ON seasons(started_at, ended_at); diff --git a/worker-api/package-lock.json b/worker-api/package-lock.json deleted file mode 100644 index 1c3277c..0000000 --- a/worker-api/package-lock.json +++ /dev/null @@ -1,2881 +0,0 @@ -{ - "name": "acb-api", - "version": "0.1.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "acb-api", - "version": "0.1.0", - "devDependencies": { - "@cloudflare/workers-types": "^4.20250310.0", - "typescript": "^5.8.2", - "vitest": "^3.0.9", - "wrangler": "^4.4.0" - } - }, - "node_modules/@cloudflare/kv-asset-handler": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", - "integrity": "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ==", - "dev": true, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@cloudflare/unenv-preset": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.0.tgz", - "integrity": "sha512-8ovsRpwzPoEqPUzoErAYVv8l3FMZNeBVQfJTvtzP4AgLSRGZISRfuChFxHWUQd3n6cnrwkuTGxT+2cGo8EsyYg==", - "dev": true, - "peerDependencies": { - "unenv": "2.0.0-rc.24", - "workerd": "1.20260301.1 || ~1.20260302.1 || ~1.20260303.1 || ~1.20260304.1 || >1.20260305.0 <2.0.0-0" - }, - "peerDependenciesMeta": { - "workerd": { - "optional": true - } - } - }, - "node_modules/@cloudflare/workerd-darwin-64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260317.1.tgz", - "integrity": "sha512-8hjh3sPMwY8M/zedq3/sXoA2Q4BedlGufn3KOOleIG+5a4ReQKLlUah140D7J6zlKmYZAFMJ4tWC7hCuI/s79g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-darwin-arm64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260317.1.tgz", - "integrity": "sha512-M/MnNyvO5HMgoIdr3QHjdCj2T1ki9gt0vIUnxYxBu9ISXS/jgtMl6chUVPJ7zHYBn9MyYr8ByeN6frjYxj0MGg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260317.1.tgz", - "integrity": "sha512-1ltuEjkRcS3fsVF7CxsKlWiRmzq2ZqMfqDN0qUOgbUwkpXsLVJsXmoblaLf5OP00ELlcgF0QsN0p2xPEua4Uug==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-linux-arm64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260317.1.tgz", - "integrity": "sha512-3QrNnPF1xlaNwkHpasvRvAMidOvQs2NhXQmALJrEfpIJ/IDL2la8g499yXp3eqhG3hVMCB07XVY149GTs42Xtw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workerd-windows-64": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260317.1.tgz", - "integrity": "sha512-MfZTz+7LfuIpMGTa3RLXHX8Z/pnycZLItn94WRdHr8LPVet+C5/1Nzei399w/jr3+kzT4pDKk26JF/tlI5elpQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@cloudflare/workers-types": { - "version": "4.20260317.1", - "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260317.1.tgz", - "integrity": "sha512-+G4eVwyCpm8Au1ex8vQBCuA9wnwqetz4tPNRoB/53qvktERWBRMQnrtvC1k584yRE3emMThtuY0gWshvSJ++PQ==", - "dev": true - }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "dev": true, - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@img/colour": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", - "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", - "dev": 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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "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" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "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, - "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 - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "node_modules/@poppinss/colors": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", - "dev": true, - "dependencies": { - "kleur": "^4.1.5" - } - }, - "node_modules/@poppinss/dumper": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", - "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", - "dev": true, - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@sindresorhus/is": "^7.0.2", - "supports-color": "^10.0.0" - } - }, - "node_modules/@poppinss/exception": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", - "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", - "dev": true - }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.15.tgz", - "integrity": "sha512-BMq1K3DsElxDWawkX6eLg9+CKJrTVGCBAWVuHXVUV2u0s2711qiChLSId6ikYPfxhdYocLNt3wWwSvDiTvFabw==", - "dev": true - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true - }, - "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 - }, - "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", - "dev": true, - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", - "dev": true, - "dependencies": { - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", - "dev": true, - "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", - "dev": true, - "dependencies": { - "tinyspy": "^4.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", - "dev": true, - "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/blake3-wasm": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", - "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", - "dev": true - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", - "dev": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", - "dev": true, - "engines": { - "node": ">= 16" - } - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "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, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true - }, - "node_modules/kleur": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true - }, - "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, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/miniflare": { - "version": "4.20260317.2", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260317.2.tgz", - "integrity": "sha512-qNL+yWAFMX6fr0pWU6Lx1vNpPobpnDSF1V8eunIckWvoIQl8y1oBjL2RJFEGY3un+l3f9gwW9dirDPP26usYJQ==", - "dev": true, - "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "^0.34.5", - "undici": "7.24.4", - "workerd": "1.20260317.1", - "ws": "8.18.0", - "youch": "4.1.0-beta.10" - }, - "bin": { - "miniflare": "bootstrap.js" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "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 - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", - "dev": true - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true - }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "engines": { - "node": ">= 14.16" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true - }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", - "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" - } - ], - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", - "dev": true, - "dependencies": { - "@types/estree": "1.0.8" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", - "fsevents": "~2.3.2" - } - }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, - "hasInstallScript": 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/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true - }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "dev": true - }, - "node_modules/strip-literal": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", - "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/supports-color": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", - "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true - }, - "node_modules/tinyexec": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", - "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", - "dev": true - }, - "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", - "dev": true, - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", - "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", - "dev": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "optional": true - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici": { - "version": "7.24.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", - "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", - "dev": true, - "engines": { - "node": ">=20.18.1" - } - }, - "node_modules/unenv": { - "version": "2.0.0-rc.24", - "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", - "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", - "dev": true, - "dependencies": { - "pathe": "^2.0.3" - } - }, - "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", - "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", - "happy-dom": "*", - "jsdom": "*" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@types/debug": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - } - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/workerd": { - "version": "1.20260317.1", - "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260317.1.tgz", - "integrity": "sha512-ZuEq1OdrJBS+NV+L5HMYPCzVn49a2O60slQiiLpG44jqtlOo+S167fWC76kEXteXLLLydeuRrluRel7WdOUa4g==", - "dev": true, - "hasInstallScript": true, - "bin": { - "workerd": "bin/workerd" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@cloudflare/workerd-darwin-64": "1.20260317.1", - "@cloudflare/workerd-darwin-arm64": "1.20260317.1", - "@cloudflare/workerd-linux-64": "1.20260317.1", - "@cloudflare/workerd-linux-arm64": "1.20260317.1", - "@cloudflare/workerd-windows-64": "1.20260317.1" - } - }, - "node_modules/wrangler": { - "version": "4.77.0", - "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.77.0.tgz", - "integrity": "sha512-E2Gm69+K++BFd3QvoWjC290RPQj1vDOUotA++sNHmtKPb7EP6C8Qv+1D5Ii73tfZtyNgakpqHlh8lBBbVWTKAQ==", - "dev": true, - "dependencies": { - "@cloudflare/kv-asset-handler": "0.4.2", - "@cloudflare/unenv-preset": "2.16.0", - "blake3-wasm": "2.1.5", - "esbuild": "0.27.3", - "miniflare": "4.20260317.2", - "path-to-regexp": "6.3.0", - "unenv": "2.0.0-rc.24", - "workerd": "1.20260317.1" - }, - "bin": { - "wrangler": "bin/wrangler.js", - "wrangler2": "bin/wrangler.js" - }, - "engines": { - "node": ">=20.3.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.20260317.1" - }, - "peerDependenciesMeta": { - "@cloudflare/workers-types": { - "optional": true - } - } - }, - "node_modules/wrangler/node_modules/@esbuild/aix-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", - "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", - "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", - "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/android-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", - "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/darwin-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", - "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/darwin-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", - "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", - "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/freebsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", - "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", - "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", - "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", - "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-loong64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", - "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-mips64el": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", - "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-ppc64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", - "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-riscv64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", - "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-s390x": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", - "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/linux-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", - "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", - "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/netbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", - "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", - "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/openbsd-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", - "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", - "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/sunos-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", - "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-arm64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", - "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-ia32": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", - "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/@esbuild/win32-x64": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", - "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/wrangler/node_modules/esbuild": { - "version": "0.27.3", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", - "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.3", - "@esbuild/android-arm": "0.27.3", - "@esbuild/android-arm64": "0.27.3", - "@esbuild/android-x64": "0.27.3", - "@esbuild/darwin-arm64": "0.27.3", - "@esbuild/darwin-x64": "0.27.3", - "@esbuild/freebsd-arm64": "0.27.3", - "@esbuild/freebsd-x64": "0.27.3", - "@esbuild/linux-arm": "0.27.3", - "@esbuild/linux-arm64": "0.27.3", - "@esbuild/linux-ia32": "0.27.3", - "@esbuild/linux-loong64": "0.27.3", - "@esbuild/linux-mips64el": "0.27.3", - "@esbuild/linux-ppc64": "0.27.3", - "@esbuild/linux-riscv64": "0.27.3", - "@esbuild/linux-s390x": "0.27.3", - "@esbuild/linux-x64": "0.27.3", - "@esbuild/netbsd-arm64": "0.27.3", - "@esbuild/netbsd-x64": "0.27.3", - "@esbuild/openbsd-arm64": "0.27.3", - "@esbuild/openbsd-x64": "0.27.3", - "@esbuild/openharmony-arm64": "0.27.3", - "@esbuild/sunos-x64": "0.27.3", - "@esbuild/win32-arm64": "0.27.3", - "@esbuild/win32-ia32": "0.27.3", - "@esbuild/win32-x64": "0.27.3" - } - }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/youch": { - "version": "4.1.0-beta.10", - "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", - "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", - "dev": true, - "dependencies": { - "@poppinss/colors": "^4.1.5", - "@poppinss/dumper": "^0.6.4", - "@speed-highlight/core": "^1.2.7", - "cookie": "^1.0.2", - "youch-core": "^0.3.3" - } - }, - "node_modules/youch-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", - "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", - "dev": true, - "dependencies": { - "@poppinss/exception": "^1.2.2", - "error-stack-parser-es": "^1.0.5" - } - } - } -} diff --git a/worker-api/package.json b/worker-api/package.json deleted file mode 100644 index 8320a45..0000000 --- a/worker-api/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "acb-api", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "wrangler dev", - "deploy": "wrangler deploy", - "cf-typegen": "wrangler types", - "test": "vitest" - }, - "devDependencies": { - "@cloudflare/workers-types": "^4.20250310.0", - "typescript": "^5.8.2", - "vitest": "^3.0.9", - "wrangler": "^4.4.0" - } -} diff --git a/worker-api/schema.sql b/worker-api/schema.sql deleted file mode 100644 index 96176db..0000000 --- a/worker-api/schema.sql +++ /dev/null @@ -1,263 +0,0 @@ --- AI Code Battle D1 Schema --- Complete schema with all tables from the implementation plan - --- ============================================ --- Core Tables (Phase 4) --- ============================================ - --- Bots table: stores registered bots -CREATE TABLE IF NOT EXISTS bots ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL UNIQUE, - owner_id TEXT NOT NULL, - endpoint_url TEXT NOT NULL, - api_key_hash TEXT NOT NULL, - rating REAL NOT NULL DEFAULT 1500.0, - rating_deviation REAL NOT NULL DEFAULT 350.0, - rating_volatility REAL NOT NULL DEFAULT 0.06, - evolved INTEGER NOT NULL DEFAULT 0, - island TEXT, - generation INTEGER, - parent_ids TEXT, - description TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - updated_at TEXT NOT NULL DEFAULT (datetime('now')), - last_health_check TEXT, - health_status TEXT DEFAULT 'unknown', - matches_played INTEGER NOT NULL DEFAULT 0, - matches_won INTEGER NOT NULL DEFAULT 0 -); - -CREATE INDEX IF NOT EXISTS idx_bots_owner ON bots(owner_id); -CREATE INDEX IF NOT EXISTS idx_bots_rating ON bots(rating DESC); -CREATE INDEX IF NOT EXISTS idx_bots_evolved ON bots(evolved); - --- Matches table: stores match metadata -CREATE TABLE IF NOT EXISTS matches ( - id TEXT PRIMARY KEY, - status TEXT NOT NULL DEFAULT 'pending', - winner_id TEXT, - turns INTEGER, - end_reason TEXT, - map_id TEXT NOT NULL, - scores_json TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - started_at TEXT, - completed_at TEXT -); - -CREATE INDEX IF NOT EXISTS idx_matches_status ON matches(status); -CREATE INDEX IF NOT EXISTS idx_matches_created ON matches(created_at DESC); -CREATE INDEX IF NOT EXISTS idx_matches_map ON matches(map_id); - --- Match participants: links bots to matches -CREATE TABLE IF NOT EXISTS match_participants ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - bot_id TEXT NOT NULL, - player_index INTEGER NOT NULL, - score INTEGER NOT NULL DEFAULT 0, - status TEXT, - rating_before REAL NOT NULL, - rating_after REAL, - rating_deviation_before REAL NOT NULL, - rating_deviation_after REAL, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, - FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE, - UNIQUE(match_id, bot_id), - UNIQUE(match_id, player_index) -); - -CREATE INDEX IF NOT EXISTS idx_match_participants_match ON match_participants(match_id); -CREATE INDEX IF NOT EXISTS idx_match_participants_bot ON match_participants(bot_id); - --- Jobs table: match execution jobs for workers -CREATE TABLE IF NOT EXISTS jobs ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - worker_id TEXT, - claimed_at TEXT, - heartbeat_at TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - completed_at TEXT, - error_message TEXT, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); -CREATE INDEX IF NOT EXISTS idx_jobs_worker ON jobs(worker_id); -CREATE INDEX IF NOT EXISTS idx_jobs_heartbeat ON jobs(heartbeat_at); - --- Rating history: tracks rating changes over time -CREATE TABLE IF NOT EXISTS rating_history ( - id TEXT PRIMARY KEY, - bot_id TEXT NOT NULL, - match_id TEXT NOT NULL, - rating_before REAL NOT NULL, - rating_after REAL NOT NULL, - rating_deviation REAL NOT NULL, - recorded_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_rating_history_bot ON rating_history(bot_id); -CREATE INDEX IF NOT EXISTS idx_rating_history_time ON rating_history(recorded_at DESC); - --- Maps table: stores generated maps -CREATE TABLE IF NOT EXISTS maps ( - id TEXT PRIMARY KEY, - width INTEGER NOT NULL, - height INTEGER NOT NULL, - player_count INTEGER NOT NULL DEFAULT 2, - walls TEXT NOT NULL, - spawns TEXT NOT NULL, - cores TEXT NOT NULL, - energy_nodes TEXT NOT NULL, - wall_density REAL NOT NULL DEFAULT 0.15, - status TEXT NOT NULL DEFAULT 'active', - engagement_score REAL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')) -); - -CREATE INDEX IF NOT EXISTS idx_maps_status ON maps(status); -CREATE INDEX IF NOT EXISTS idx_maps_player_count ON maps(player_count); -CREATE INDEX IF NOT EXISTS idx_maps_engagement ON maps(engagement_score DESC); - --- Bot secrets: stores API keys for bots (separate for security) -CREATE TABLE IF NOT EXISTS bot_secrets ( - bot_id TEXT PRIMARY KEY, - api_key_hash TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (bot_id) REFERENCES bots(id) ON DELETE CASCADE -); - --- ============================================ --- Prediction System (Section 13.5) --- ============================================ - --- Predictions: visitor predictions on match outcomes -CREATE TABLE IF NOT EXISTS predictions ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - predictor_id TEXT NOT NULL, - predictor_name TEXT, - predicted_bot_id TEXT NOT NULL, - correct INTEGER, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE, - FOREIGN KEY (predicted_bot_id) REFERENCES bots(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_predictions_match ON predictions(match_id); -CREATE INDEX IF NOT EXISTS idx_predictions_predictor ON predictions(predictor_id); - --- Predictor stats: aggregate prediction accuracy -CREATE TABLE IF NOT EXISTS predictor_stats ( - predictor_id TEXT PRIMARY KEY, - predictor_name TEXT, - correct INTEGER NOT NULL DEFAULT 0, - incorrect INTEGER NOT NULL DEFAULT 0, - streak INTEGER NOT NULL DEFAULT 0, - best_streak INTEGER NOT NULL DEFAULT 0, - rating REAL NOT NULL DEFAULT 1000.0 -); - -CREATE INDEX IF NOT EXISTS idx_predictor_stats_rating ON predictor_stats(rating DESC); - --- ============================================ --- Map Voting (Section 13.6) --- ============================================ - --- Map votes: community voting on map quality -CREATE TABLE IF NOT EXISTS map_votes ( - id TEXT PRIMARY KEY, - map_id TEXT NOT NULL, - voter_id TEXT NOT NULL, - vote INTEGER NOT NULL, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (map_id) REFERENCES maps(id) ON DELETE CASCADE, - UNIQUE(map_id, voter_id) -); - -CREATE INDEX IF NOT EXISTS idx_map_votes_map ON map_votes(map_id); - --- ============================================ --- Replay Feedback (Section 13.6) --- ============================================ - --- Replay feedback: community annotations on replays -CREATE TABLE IF NOT EXISTS replay_feedback ( - id TEXT PRIMARY KEY, - match_id TEXT NOT NULL, - turn INTEGER NOT NULL, - type TEXT NOT NULL, - body TEXT NOT NULL, - author TEXT NOT NULL, - upvotes INTEGER NOT NULL DEFAULT 0, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_feedback_match ON replay_feedback(match_id, turn); -CREATE INDEX IF NOT EXISTS idx_feedback_type ON replay_feedback(type); -CREATE INDEX IF NOT EXISTS idx_feedback_upvotes ON replay_feedback(upvotes DESC); - --- ============================================ --- Multi-Game Series (Section 14.7) --- ============================================ - --- Series: best-of-N match series between two bots -CREATE TABLE IF NOT EXISTS series ( - id TEXT PRIMARY KEY, - bot_a_id TEXT NOT NULL, - bot_b_id TEXT NOT NULL, - format INTEGER NOT NULL, - status TEXT NOT NULL DEFAULT 'pending', - a_wins INTEGER NOT NULL DEFAULT 0, - b_wins INTEGER NOT NULL DEFAULT 0, - season_id TEXT, - created_at TEXT NOT NULL DEFAULT (datetime('now')), - completed_at TEXT, - FOREIGN KEY (bot_a_id) REFERENCES bots(id) ON DELETE CASCADE, - FOREIGN KEY (bot_b_id) REFERENCES bots(id) ON DELETE CASCADE -); - -CREATE INDEX IF NOT EXISTS idx_series_status ON series(status); -CREATE INDEX IF NOT EXISTS idx_series_bots ON series(bot_a_id, bot_b_id); -CREATE INDEX IF NOT EXISTS idx_series_season ON series(season_id); - --- Series games: individual games within a series -CREATE TABLE IF NOT EXISTS series_games ( - series_id TEXT NOT NULL, - game_number INTEGER NOT NULL, - match_id TEXT, - map_id TEXT NOT NULL, - winner INTEGER, - PRIMARY KEY (series_id, game_number), - FOREIGN KEY (series_id) REFERENCES series(id) ON DELETE CASCADE, - FOREIGN KEY (match_id) REFERENCES matches(id) ON DELETE SET NULL -); - -CREATE INDEX IF NOT EXISTS idx_series_games_match ON series_games(match_id); - --- ============================================ --- Seasonal Rotations (Section 14.9) --- ============================================ - --- Seasons: seasonal leaderboards with rule variations -CREATE TABLE IF NOT EXISTS seasons ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - theme TEXT NOT NULL, - rules_version INTEGER NOT NULL DEFAULT 1, - started_at TEXT NOT NULL, - ended_at TEXT, - champion_id TEXT, - status TEXT NOT NULL DEFAULT 'active', - FOREIGN KEY (champion_id) REFERENCES bots(id) ON DELETE SET NULL -); - -CREATE INDEX IF NOT EXISTS idx_seasons_status ON seasons(status); -CREATE INDEX IF NOT EXISTS idx_seasons_dates ON seasons(started_at, ended_at); diff --git a/worker-api/src/bots.ts b/worker-api/src/bots.ts deleted file mode 100644 index a08858d..0000000 --- a/worker-api/src/bots.ts +++ /dev/null @@ -1,240 +0,0 @@ -// Bot Management Endpoints - -import type { Env, Bot, CreateBotRequest, ApiResponse } from './types'; - -/** - * Generate a random API key (256-bit, hex-encoded) - */ -function generateApiKey(): string { - const bytes = new Uint8Array(32); - crypto.getRandomValues(bytes); - return Array.from(bytes) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); -} - -/** - * Hash an API key for storage - */ -async function hashApiKey(key: string): Promise { - const encoder = new TextEncoder(); - const data = encoder.encode(key); - const hash = await crypto.subtle.digest('SHA-256', data); - return Array.from(new Uint8Array(hash)) - .map((b) => b.toString(16).padStart(2, '0')) - .join(''); -} - -/** - * POST /api/register - Register a new bot - */ -export async function registerBot( - env: Env, - request: CreateBotRequest -): Promise> { - // Validate request - if (!request.name || !request.owner_id || !request.endpoint_url) { - return { success: false, error: 'Missing required fields' }; - } - - // Validate endpoint URL - try { - new URL(request.endpoint_url); - } catch { - return { success: false, error: 'Invalid endpoint URL' }; - } - - const botId = crypto.randomUUID(); - const apiKey = generateApiKey(); - const apiKeyHash = await hashApiKey(apiKey); - const now = new Date().toISOString(); - - // Check if owner already has a bot with this name - const existing = await env.DB.prepare( - 'SELECT id FROM bots WHERE owner_id = ? AND name = ?' - ) - .bind(request.owner_id, request.name) - .first(); - - if (existing) { - return { success: false, error: 'Bot with this name already exists for this owner' }; - } - - // Create bot - await env.DB.prepare( - `INSERT INTO bots (id, name, owner_id, endpoint_url, api_key_hash, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?)` - ) - .bind( - botId, - request.name, - request.owner_id, - request.endpoint_url, - apiKeyHash, - now, - now - ) - .run(); - - // Store API key hash separately - await env.DB.prepare( - `INSERT INTO bot_secrets (bot_id, api_key_hash, created_at) - VALUES (?, ?, ?)` - ) - .bind(botId, apiKeyHash, now) - .run(); - - return { - success: true, - data: { - id: botId, - api_key: apiKey, // Return the plain key only on creation - }, - }; -} - -/** - * GET /api/bots - List all bots - */ -export async function listBots(env: Env): Promise> { - const result = await env.DB.prepare( - `SELECT - id, name, owner_id, endpoint_url, rating, rating_deviation, rating_volatility, - created_at, updated_at, last_health_check, health_status, matches_played, matches_won - FROM bots - ORDER BY rating DESC` - ).all(); - - // Remove sensitive fields - const bots = (result.results || []).map((bot) => ({ - ...bot, - api_key_hash: '', - })); - - return { success: true, data: bots }; -} - -/** - * GET /api/bots/:id - Get bot details - */ -export async function getBot(env: Env, botId: string): Promise> { - const bot = await env.DB.prepare( - `SELECT - id, name, owner_id, endpoint_url, rating, rating_deviation, rating_volatility, - created_at, updated_at, last_health_check, health_status, matches_played, matches_won - FROM bots - WHERE id = ?` - ) - .bind(botId) - .first(); - - if (!bot) { - return { success: false, error: 'Bot not found' }; - } - - return { success: true, data: { ...bot, api_key_hash: '' } }; -} - -/** - * PUT /api/bots/:id - Update bot details - */ -export async function updateBot( - env: Env, - botId: string, - updates: { name?: string; endpoint_url?: string } -): Promise> { - const now = new Date().toISOString(); - - const setClauses: string[] = []; - const values: unknown[] = []; - - if (updates.name) { - setClauses.push('name = ?'); - values.push(updates.name); - } - - if (updates.endpoint_url) { - try { - new URL(updates.endpoint_url); - setClauses.push('endpoint_url = ?'); - values.push(updates.endpoint_url); - } catch { - return { success: false, error: 'Invalid endpoint URL' }; - } - } - - if (setClauses.length === 0) { - return { success: false, error: 'No valid updates provided' }; - } - - setClauses.push('updated_at = ?'); - values.push(now); - values.push(botId); - - const result = await env.DB.prepare( - `UPDATE bots SET ${setClauses.join(', ')} WHERE id = ?` - ) - .bind(...values) - .run(); - - if (result.meta.changes === 0) { - return { success: false, error: 'Bot not found' }; - } - - return { success: true }; -} - -/** - * POST /api/rotate-key - Rotate bot API key - */ -export async function rotateApiKey( - env: Env, - botId: string, - ownerId: string -): Promise> { - // Verify ownership - const bot = await env.DB.prepare('SELECT owner_id FROM bots WHERE id = ?') - .bind(botId) - .first<{ owner_id: string }>(); - - if (!bot) { - return { success: false, error: 'Bot not found' }; - } - - if (bot.owner_id !== ownerId) { - return { success: false, error: 'Not authorized' }; - } - - const newApiKey = generateApiKey(); - const apiKeyHash = await hashApiKey(newApiKey); - const now = new Date().toISOString(); - - // Update bot - await env.DB.prepare('UPDATE bots SET api_key_hash = ?, updated_at = ? WHERE id = ?') - .bind(apiKeyHash, now, botId) - .run(); - - // Update secret - await env.DB.prepare('UPDATE bot_secrets SET api_key_hash = ? WHERE bot_id = ?') - .bind(apiKeyHash, botId) - .run(); - - return { success: true, data: { api_key: newApiKey } }; -} - -/** - * GET /api/leaderboard - Get current leaderboard - */ -export async function getLeaderboard(env: Env): Promise> { - const result = await env.DB.prepare( - `SELECT - id, name, owner_id, rating, rating_deviation, matches_played, matches_won, - created_at, updated_at, health_status - FROM bots - WHERE matches_played > 0 - ORDER BY rating DESC - LIMIT 100` - ).all(); - - return { success: true, data: result.results || [] }; -} diff --git a/worker-api/src/cron.ts b/worker-api/src/cron.ts deleted file mode 100644 index ea88fd9..0000000 --- a/worker-api/src/cron.ts +++ /dev/null @@ -1,228 +0,0 @@ -// Cron Job Handlers - -import type { Env, Bot } from './types'; - -/** - * Matchmaker cron: Create match jobs for bots that need games - * Runs every minute - */ -export async function runMatchmaker(env: Env): Promise<{ created: number }> { - const now = new Date().toISOString(); - - // Get bots that are healthy and have played fewer than 10 matches today - // For simplicity, we'll just pair bots randomly for now - // A more sophisticated system would consider rating proximity - - // Get active bots (healthy, played at least one match or registered recently) - const bots = await env.DB.prepare( - `SELECT id, rating, matches_played FROM bots - WHERE health_status = 'healthy' - ORDER BY RANDOM() - LIMIT 10` - ).all(); - - if (!bots.results || bots.results.length < 2) { - return { created: 0 }; - } - - // Get a random map - const map = await env.DB.prepare( - 'SELECT id FROM maps ORDER BY RANDOM() LIMIT 1' - ).first<{ id: string }>(); - - if (!map) { - return { created: 0 }; - } - - let created = 0; - - // Create matches in pairs - for (let i = 0; i < bots.results.length - 1; i += 2) { - const bot1 = bots.results[i]; - const bot2 = bots.results[i + 1]; - - // Check if these bots already have a pending match together - const existingMatch = await env.DB.prepare( - `SELECT m.id FROM matches m - JOIN match_participants mp1 ON m.id = mp1.match_id - JOIN match_participants mp2 ON m.id = mp2.match_id - WHERE m.status = 'pending' - AND mp1.bot_id = ? AND mp2.bot_id = ?` - ) - .bind(bot1.id, bot2.id) - .first(); - - if (existingMatch) { - continue; // Skip this pair - } - - // Create match - const matchId = crypto.randomUUID(); - await env.DB.prepare( - `INSERT INTO matches (id, status, map_id, created_at) - VALUES (?, 'pending', ?, ?)` - ) - .bind(matchId, map.id, now) - .run(); - - // Get bot ratings for participants - const bot1Data = await env.DB.prepare( - 'SELECT rating, rating_deviation FROM bots WHERE id = ?' - ) - .bind(bot1.id) - .first<{ rating: number; rating_deviation: number }>(); - - const bot2Data = await env.DB.prepare( - 'SELECT rating, rating_deviation FROM bots WHERE id = ?' - ) - .bind(bot2.id) - .first<{ rating: number; rating_deviation: number }>(); - - if (!bot1Data || !bot2Data) continue; - - // Create participants (player_index 0 and 1) - await env.DB.prepare( - `INSERT INTO match_participants (id, match_id, bot_id, player_index, score, rating_before, rating_deviation_before) - VALUES (?, ?, ?, 0, 0, ?, ?)` - ) - .bind(crypto.randomUUID(), matchId, bot1.id, bot1Data.rating, bot1Data.rating_deviation) - .run(); - - await env.DB.prepare( - `INSERT INTO match_participants (id, match_id, bot_id, player_index, score, rating_before, rating_deviation_before) - VALUES (?, ?, ?, 1, 0, ?, ?)` - ) - .bind(crypto.randomUUID(), matchId, bot2.id, bot2Data.rating, bot2Data.rating_deviation) - .run(); - - // Create job - await env.DB.prepare( - `INSERT INTO jobs (id, match_id, status, created_at) - VALUES (?, ?, 'pending', ?)` - ) - .bind(crypto.randomUUID(), matchId, now) - .run(); - - created++; - } - - return { created }; -} - -/** - * Health checker cron: Ping bot endpoints to check health - * Runs every 15 minutes - */ -export async function runHealthChecker(env: Env): Promise<{ checked: number }> { - const bots = await env.DB.prepare( - `SELECT id, endpoint_url FROM bots WHERE health_status != 'unhealthy' OR last_health_check IS NULL` - ).all<{ id: string; endpoint_url: string }>(); - - let checked = 0; - const now = new Date().toISOString(); - - for (const bot of bots.results || []) { - try { - // Simple health check - just try to connect - const response = await fetch(bot.endpoint_url, { - method: 'GET', - signal: AbortSignal.timeout(5000), // 5 second timeout - }); - - const status = response.ok ? 'healthy' : 'unhealthy'; - - await env.DB.prepare( - `UPDATE bots SET health_status = ?, last_health_check = ? WHERE id = ?` - ) - .bind(status, now, bot.id) - .run(); - - checked++; - } catch { - // Connection failed - await env.DB.prepare( - `UPDATE bots SET health_status = 'unhealthy', last_health_check = ? WHERE id = ?` - ) - .bind(now, bot.id) - .run(); - - checked++; - } - } - - return { checked }; -} - -/** - * Stale job reaper: Reclaim jobs that have timed out - * Runs every 5 minutes - */ -export async function runStaleJobReaper(env: Env): Promise<{ reclaimed: number }> { - const now = new Date(); - const staleThreshold = new Date(now.getTime() - 5 * 60 * 1000); // 5 minutes ago - const staleThresholdStr = staleThreshold.toISOString(); - - // Find jobs that have been claimed but haven't had a heartbeat in 5 minutes - const staleJobs = await env.DB.prepare( - `SELECT id, match_id FROM jobs - WHERE status = 'claimed' - AND heartbeat_at < ?` - ) - .bind(staleThresholdStr) - .all<{ id: string; match_id: string }>(); - - let reclaimed = 0; - - for (const job of staleJobs.results || []) { - // Reset the job to pending so another worker can claim it - await env.DB.prepare( - `UPDATE jobs SET - status = 'pending', - worker_id = NULL, - claimed_at = NULL, - heartbeat_at = NULL - WHERE id = ?` - ) - .bind(job.id) - .run(); - - // Reset match status to pending - await env.DB.prepare( - `UPDATE matches SET status = 'pending', started_at = NULL WHERE id = ?` - ) - .bind(job.match_id) - .run(); - - reclaimed++; - } - - return { reclaimed }; -} - -/** - * Dispatch cron handler based on event type - */ -export async function handleCron( - env: Env, - cron: string -): Promise<{ success: boolean; result: unknown }> { - // Parse cron expression to determine which handler to run - // */1 * * * * = matchmaker (every minute) - // */5 * * * * = stale job reaper (every 5 minutes) - // */15 * * * * = health checker (every 15 minutes) - - // The cron expression is passed, but we need to determine the type - // For simplicity, we'll check the pattern - if (cron === '*/1 * * * *' || cron.includes('*/1')) { - const result = await runMatchmaker(env); - return { success: true, result }; - } else if (cron === '*/5 * * * *' || cron.includes('*/5')) { - const result = await runStaleJobReaper(env); - return { success: true, result }; - } else if (cron === '*/15 * * * *' || cron.includes('*/15')) { - const result = await runHealthChecker(env); - return { success: true, result }; - } - - return { success: false, result: 'Unknown cron pattern' }; -} diff --git a/worker-api/src/export.ts b/worker-api/src/export.ts deleted file mode 100644 index b04a43a..0000000 --- a/worker-api/src/export.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Data Export Endpoint for Index Builder - -import type { Env, Bot, Match, MatchParticipant, ApiResponse } from './types'; - -/** - * Export data for index building. - * This endpoint is called by the Rackspace index builder every ~90 minutes. - * It returns all data needed to generate the index JSON files. - */ -export interface ExportData { - bots: ExportBot[]; - matches: ExportMatch[]; - rating_history: RatingHistoryEntry[]; - generated_at: string; -} - -export interface ExportBot { - id: string; - name: string; - owner_id: string; - rating: number; - rating_deviation: number; - rating_volatility: number; - matches_played: number; - matches_won: number; - created_at: string; - updated_at: string; - health_status: string; -} - -export interface ExportMatch { - id: string; - status: string; - winner_id: string | null; - turns: number | null; - end_reason: string | null; - map_id: string; - created_at: string; - completed_at: string | null; - participants: ExportMatchParticipant[]; -} - -export interface ExportMatchParticipant { - bot_id: string; - player_index: number; - score: number; - rating_before: number; - rating_after: number | null; -} - -export interface RatingHistoryEntry { - bot_id: string; - rating: number; - rating_deviation: number; - recorded_at: string; -} - -/** - * GET /api/data/export - Export all data for index building - */ -export async function exportData(env: Env): Promise> { - const now = new Date().toISOString(); - - // Fetch all bots - const botsResult = await env.DB.prepare( - `SELECT - id, name, owner_id, rating, rating_deviation, rating_volatility, - matches_played, matches_won, created_at, updated_at, health_status - FROM bots - ORDER BY rating DESC` - ).all(); - - // Fetch recent matches (last 1000 completed) - const matchesResult = await env.DB.prepare( - `SELECT id, status, winner_id, turns, end_reason, map_id, created_at, completed_at - FROM matches - WHERE status = 'completed' - ORDER BY completed_at DESC - LIMIT 1000` - ).all(); - - // Fetch match participants for all matches - const matchIds = matchesResult.results.map(m => m.id); - let participants: MatchParticipant[] = []; - - if (matchIds.length > 0) { - // Build query with proper parameter binding - const placeholders = matchIds.map(() => '?').join(','); - const participantsResult = await env.DB.prepare( - `SELECT bot_id, match_id, player_index, score, rating_before, rating_after - FROM match_participants - WHERE match_id IN (${placeholders})` - ).bind(...matchIds).all(); - - participants = participantsResult.results || []; - } - - // Group participants by match_id - const participantsByMatch = new Map(); - for (const p of participants) { - if (!participantsByMatch.has(p.match_id)) { - participantsByMatch.set(p.match_id, []); - } - participantsByMatch.get(p.match_id)!.push(p); - } - - // Build export matches with embedded participants - const exportMatches: ExportMatch[] = matchesResult.results.map(m => ({ - id: m.id, - status: m.status, - winner_id: m.winner_id, - turns: m.turns, - end_reason: m.end_reason, - map_id: m.map_id, - created_at: m.created_at, - completed_at: m.completed_at, - participants: (participantsByMatch.get(m.id) || []).map(p => ({ - bot_id: p.bot_id, - player_index: p.player_index, - score: p.score, - rating_before: p.rating_before, - rating_after: p.rating_after, - })), - })); - - // Fetch rating history (last 30 days) - const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(); - const ratingHistoryResult = await env.DB.prepare( - `SELECT bot_id, rating, rating_deviation, recorded_at - FROM rating_history - WHERE recorded_at >= ? - ORDER BY bot_id, recorded_at ASC` - ) - .bind(thirtyDaysAgo) - .all(); - - return { - success: true, - data: { - bots: botsResult.results || [], - matches: exportMatches, - rating_history: ratingHistoryResult.results || [], - generated_at: now, - }, - }; -} diff --git a/worker-api/src/glicko2.test.ts b/worker-api/src/glicko2.test.ts deleted file mode 100644 index da0074d..0000000 --- a/worker-api/src/glicko2.test.ts +++ /dev/null @@ -1,292 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { - toGlicko2, - fromGlicko2, - updateRating, - g, - E, -} from './glicko2'; - -describe('Glicko-2 Rating System', () => { - describe('Scale Conversion', () => { - it('converts rating to Glicko-2 scale correctly', () => { - // Default rating 1500 should map to mu=0 - const result = toGlicko2(1500, 350); - expect(result.mu).toBe(0); - expect(result.phi).toBeCloseTo(350 / 173.7178, 10); - }); - - it('converts rating above default correctly', () => { - const result = toGlicko2(1900, 100); - expect(result.mu).toBeCloseTo(400 / 173.7178, 10); - expect(result.phi).toBeCloseTo(100 / 173.7178, 10); - }); - - it('converts rating below default correctly', () => { - const result = toGlicko2(1300, 200); - expect(result.mu).toBeCloseTo(-200 / 173.7178, 10); - expect(result.phi).toBeCloseTo(200 / 173.7178, 10); - }); - - it('round-trips correctly', () => { - const originalRating = 1650; - const originalRd = 150; - - const g2 = toGlicko2(originalRating, originalRd); - const result = fromGlicko2(g2); - - expect(result.rating).toBeCloseTo(originalRating, 10); - expect(result.rd).toBeCloseTo(originalRd, 10); - }); - }); - - describe('g function', () => { - it('returns 1 when phi is 0', () => { - expect(g(0)).toBe(1); - }); - - it('decreases as phi increases', () => { - const g1 = g(0.1); - const g2 = g(0.5); - const g3 = g(1.0); - - expect(g1).toBeGreaterThan(g2); - expect(g2).toBeGreaterThan(g3); - }); - - it('returns correct values for known inputs', () => { - // g(0.2) = 1/sqrt(1 + 3*0.04/pi^2) ≈ 0.993976 - expect(g(0.2)).toBeCloseTo(0.993976, 4); - }); - }); - - describe('E function', () => { - it('returns 0.5 when ratings are equal', () => { - const e = E(0, 0, 0.2); - expect(e).toBeCloseTo(0.5, 10); - }); - - it('returns > 0.5 when player rating is higher', () => { - const e = E(0.5, 0, 0.2); // Player rated higher - expect(e).toBeGreaterThan(0.5); - }); - - it('returns < 0.5 when opponent rating is higher', () => { - const e = E(0, 0.5, 0.2); // Opponent rated higher - expect(e).toBeLessThan(0.5); - }); - }); - - describe('Rating Updates', () => { - it('increases rating after win against equal opponent', () => { - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 200, - rating_volatility: 0.06, - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const opponents = [ - { rating: 1500, rd: 200, score: 1 }, // Win - ]; - - const result = updateRating(bot, opponents); - - // Rating should increase after winning - expect(result.rating).toBeGreaterThan(1500); - // RD should decrease after playing - expect(result.rd).toBeLessThan(200); - }); - - it('decreases rating after loss against equal opponent', () => { - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 200, - rating_volatility: 0.06, - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const opponents = [ - { rating: 1500, rd: 200, score: 0 }, // Loss - ]; - - const result = updateRating(bot, opponents); - - // Rating should decrease after losing - expect(result.rating).toBeLessThan(1500); - // RD should decrease after playing - expect(result.rd).toBeLessThan(200); - }); - - it('handles draw correctly', () => { - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 200, - rating_volatility: 0.06, - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const opponents = [ - { rating: 1500, rd: 200, score: 0.5 }, // Draw - ]; - - const result = updateRating(bot, opponents); - - // Rating should stay roughly the same against equal opponent - expect(result.rating).toBeCloseTo(1500, 1); - // RD should decrease after playing - expect(result.rd).toBeLessThan(200); - }); - - it('handles multiple opponents', () => { - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 200, - rating_volatility: 0.06, - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const opponents = [ - { rating: 1600, rd: 150, score: 1 }, // Win vs higher rated - { rating: 1400, rd: 150, score: 0 }, // Loss vs lower rated - ]; - - const result = updateRating(bot, opponents); - - // Both rating and RD should be updated - expect(result.rating).toBeGreaterThan(0); - expect(result.rd).toBeLessThan(200); - }); - - it('increases RD when no games played (rating decay)', () => { - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 100, - rating_volatility: 0.06, - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const result = updateRating(bot, []); - - // Rating should stay the same - expect(result.rating).toBe(1500); - // RD should increase (rating decay) - expect(result.rd).toBeGreaterThan(100); - }); - - it('constrains RD to maximum', () => { - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 340, - rating_volatility: 0.5, // High volatility - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const result = updateRating(bot, []); - - // RD should not exceed 350 - expect(result.rd).toBeLessThanOrEqual(350); - }); - }); - - describe('Real-world scenarios', () => { - it('matches expected rating change from Glicko-2 paper example', () => { - // This is a simplified test based on the Glicko-2 paper - // Player with rating 1500, RD 200 playing against: - // - Opponent 1: 1400, 30, win (score=1) - // - Opponent 2: 1550, 100, loss (score=0) - // - Opponent 3: 1700, 300, loss (score=0) - - const bot = { - id: 'test', - name: 'Test', - owner_id: 'owner', - endpoint_url: 'http://example.com', - api_key_hash: 'hash', - rating: 1500, - rating_deviation: 200, - rating_volatility: 0.06, - created_at: '2024-01-01', - updated_at: '2024-01-01', - last_health_check: null, - health_status: 'healthy' as const, - matches_played: 0, - matches_won: 0, - }; - - const opponents = [ - { rating: 1400, rd: 30, score: 1 }, - { rating: 1550, rd: 100, score: 0 }, - { rating: 1700, rd: 300, score: 0 }, - ]; - - const result = updateRating(bot, opponents); - - // The new rating should be in a reasonable range - // Based on the paper, expected new rating is approximately 1464 - expect(result.rating).toBeGreaterThan(1400); - expect(result.rating).toBeLessThan(1550); - expect(result.rd).toBeLessThan(200); - }); - }); -}); diff --git a/worker-api/src/glicko2.ts b/worker-api/src/glicko2.ts deleted file mode 100644 index 6353a55..0000000 --- a/worker-api/src/glicko2.ts +++ /dev/null @@ -1,309 +0,0 @@ -// Glicko-2 Rating System Implementation -// Based on: http://www.glicko.net/glicko/glicko2.pdf - -import type { Env, Bot, MatchParticipant } from './types'; - -// Glicko-2 constants -const SCALE = 173.7178; // Rating scale conversion factor -const TAU = 0.5; // System constant (constrains volatility change) -const DEFAULT_RATING = 1500; -const DEFAULT_RD = 350; -const DEFAULT_VOLATILITY = 0.06; - -export interface Glicko2Rating { - mu: number; // Mean rating (Glicko-2 scale) - phi: number; // Rating deviation (Glicko-2 scale) - sigma: number; // Volatility -} - -/** - * Convert rating to Glicko-2 scale - */ -export function toGlicko2(rating: number, rd: number): Glicko2Rating { - return { - mu: (rating - DEFAULT_RATING) / SCALE, - phi: rd / SCALE, - sigma: DEFAULT_VOLATILITY, - }; -} - -/** - * Convert from Glicko-2 scale to original scale - */ -export function fromGlicko2(g2: Glicko2Rating): { rating: number; rd: number } { - return { - rating: g2.mu * SCALE + DEFAULT_RATING, - rd: g2.phi * SCALE, - }; -} - -/** - * Compute g(phi) function - */ -export function g(phi: number): number { - return 1 / Math.sqrt(1 + (3 * phi * phi) / (Math.PI * Math.PI)); -} - -/** - * Compute E(mu, mu_j, phi_j) function - */ -export function E(mu: number, mu_j: number, phi_j: number): number { - return 1 / (1 + Math.exp(-g(phi_j) * (mu - mu_j))); -} - -/** - * Compute new rating deviation (Step 5/6) - */ -function computeNewPhi(phi: number, v: number): number { - const phiSquared = phi * phi; - const vInverse = 1 / v; - return 1 / Math.sqrt(1 / phiSquared + vInverse); -} - -/** - * Iterative algorithm to compute new volatility (Step 5.4) - */ -function computeNewVolatility( - sigma: number, - phi: number, - v: number, - delta: number, - tau: number = TAU -): number { - let a = Math.log(sigma * sigma); - const epsilon = 0.000001; - - const f = (x: number): number => { - const expX = Math.exp(x); - const tmp = phi * phi + v + expX; - return ( - (expX * (delta * delta - phi * phi - v - expX)) / (2 * tmp * tmp) - - (x - a) / (tau * tau) - ); - }; - - // Set initial bounds - let A = a; - let B: number; - if (delta * delta > phi * phi + v) { - B = Math.log(delta * delta - phi * phi - v); - } else { - let k = 1; - while (f(a - k * tau) < 0) { - k++; - } - B = a - k * tau; - } - - // Illinois algorithm - let fA = f(A); - let fB = f(B); - - while (Math.abs(B - A) > epsilon) { - const C = A + ((A - B) * fA) / (fB - fA); - const fC = f(C); - - if (fC * fB <= 0) { - A = B; - fA = fB; - } else { - fA = fA / 2; - } - - B = C; - fB = fC; - } - - return Math.exp(A / 2); -} - -/** - * Calculate rating updates for a bot after a match - * @param bot The bot whose rating to update - * @param opponents Array of opponent ratings and game outcomes (1=win, 0.5=draw, 0=loss) - * @returns New rating values - */ -export function updateRating( - bot: Bot, - opponents: Array<{ - rating: number; - rd: number; - score: number; - }> -): { rating: number; rd: number; volatility: number } { - if (opponents.length === 0) { - // No games played - increase RD over time (rating decay) - const phi = bot.rating_deviation / SCALE; - const newPhi = Math.min(Math.sqrt(phi * phi + bot.rating_volatility * bot.rating_volatility), 350 / SCALE); - return { - rating: bot.rating, - rd: newPhi * SCALE, - volatility: bot.rating_volatility, - }; - } - - // Convert to Glicko-2 scale - const g2 = toGlicko2(bot.rating, bot.rating_deviation); - g2.sigma = bot.rating_volatility; - - // Step 3: Compute v (variance of game outcomes) - let vInverse = 0; - for (const opp of opponents) { - const oppG2 = toGlicko2(opp.rating, opp.rd); - const gPhi = g(oppG2.phi); - const eValue = E(g2.mu, oppG2.mu, oppG2.phi); - vInverse += gPhi * gPhi * eValue * (1 - eValue); - } - const v = 1 / vInverse; - - // Step 4: Compute delta (rating improvement) - let deltaSum = 0; - for (const opp of opponents) { - const oppG2 = toGlicko2(opp.rating, opp.rd); - const gPhi = g(oppG2.phi); - const eValue = E(g2.mu, oppG2.mu, oppG2.phi); - deltaSum += gPhi * (opp.score - eValue); - } - const delta = v * deltaSum; - - // Step 5: Compute new volatility - const newSigma = computeNewVolatility(g2.sigma, g2.phi, v, delta); - - // Step 6: Update phi - const phiStar = Math.sqrt(g2.phi * g2.phi + newSigma * newSigma); - - // Step 7: Update phi and mu - const newPhi = 1 / Math.sqrt(1 / (phiStar * phiStar) + 1 / v); - const newMu = g2.mu + newPhi * newPhi * deltaSum; - - // Convert back - const result = fromGlicko2({ mu: newMu, phi: newPhi, sigma: newSigma }); - - return { - rating: result.rating, - rd: result.rd, - volatility: newSigma, - }; -} - -/** - * Update ratings for all participants in a completed match - */ -export async function updateMatchRatings( - env: Env, - matchId: string, - participants: MatchParticipant[], - winnerId: string | null -): Promise { - // Get all bots involved - const botIds = participants.map((p) => p.bot_id); - const placeholders = botIds.map(() => '?').join(','); - - const bots = await env.DB.prepare( - `SELECT * FROM bots WHERE id IN (${placeholders})` - ) - .bind(...botIds) - .all(); - - if (!bots.results || bots.results.length !== participants.length) { - throw new Error('Could not find all participant bots'); - } - - const botMap = new Map(bots.results.map((b) => [b.id, b])); - - // Calculate new ratings for each participant - const updates: Array<{ - botId: string; - rating: number; - rd: number; - volatility: number; - won: boolean; - }> = []; - - for (const participant of participants) { - const bot = botMap.get(participant.bot_id); - if (!bot) continue; - - // Build opponent list - const opponents = participants - .filter((p) => p.bot_id !== participant.bot_id) - .map((opp) => { - const oppBot = botMap.get(opp.bot_id)!; - // Score: 1 for win, 0.5 for draw (if no winner), 0 for loss - let score = 0.5; - if (winnerId === participant.bot_id) { - score = 1; - } else if (winnerId === opp.bot_id) { - score = 0; - } - return { - rating: oppBot.rating, - rd: oppBot.rating_deviation, - score, - }; - }); - - const newRating = updateRating(bot, opponents); - const won = winnerId === participant.bot_id; - - updates.push({ - botId: participant.bot_id, - rating: newRating.rating, - rd: newRating.rd, - volatility: newRating.volatility, - won, - }); - } - - // Apply updates in a batch - const now = new Date().toISOString(); - - for (const update of updates) { - // Update bot rating - await env.DB.prepare( - `UPDATE bots SET - rating = ?, - rating_deviation = ?, - rating_volatility = ?, - matches_played = matches_played + 1, - matches_won = matches_won + ?, - updated_at = ? - WHERE id = ?` - ) - .bind( - update.rating, - update.rd, - update.volatility, - update.won ? 1 : 0, - now, - update.botId - ) - .run(); - - // Update participant with rating change - await env.DB.prepare( - `UPDATE match_participants SET - rating_after = ?, - rating_deviation_after = ? - WHERE match_id = ? AND bot_id = ?` - ) - .bind(update.rating, update.rd, matchId, update.botId) - .run(); - - // Record rating history - await env.DB.prepare( - `INSERT INTO rating_history (id, bot_id, match_id, rating_before, rating_after, rating_deviation, recorded_at) - VALUES (?, ?, ?, ?, ?, ?, ?)` - ) - .bind( - crypto.randomUUID(), - update.botId, - matchId, - botMap.get(update.botId)!.rating, - update.rating, - update.rd, - now - ) - .run(); - } -} diff --git a/worker-api/src/index.ts b/worker-api/src/index.ts deleted file mode 100644 index 8b23a69..0000000 --- a/worker-api/src/index.ts +++ /dev/null @@ -1,259 +0,0 @@ -// AI Code Battle Worker API -// Phase 4: Match Orchestration - -import type { Env, ApiResponse, ClaimJobRequest, SubmitResultRequest, CreateBotRequest } from './types'; -import { handleCron } from './cron'; -import { - getNextJob, - claimJob, - heartbeatJob, - submitResult, - failJob, -} from './jobs'; -import { - registerBot, - listBots, - getBot, - updateBot, - rotateApiKey, - getLeaderboard, -} from './bots'; -import { exportData } from './export'; - -export default { - async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise { - const url = new URL(request.url); - const path = url.pathname; - const method = request.method; - - // CORS headers - const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key', - }; - - // Handle preflight - if (method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - // Helper for JSON responses - const json = (data: ApiResponse, status = 200): Response => { - return new Response(JSON.stringify(data), { - status, - headers: { - 'Content-Type': 'application/json', - ...corsHeaders, - }, - }); - }; - - // Helper to verify API key - const verifyApiKey = async (): Promise => { - const apiKey = request.headers.get('X-API-Key'); - if (!apiKey) return false; - return apiKey === env.API_KEY; - }; - - // Helper to parse JSON body - const parseBody = async (): Promise => { - try { - return await request.json(); - } catch { - return null; - } - }; - - try { - // Health check (liveness probe - always returns 200 if process is running) - if (path === '/health' || path === '/api/health') { - return json({ - success: true, - data: { - status: 'healthy', - timestamp: new Date().toISOString(), - }, - }); - } - - // Readiness check (checks if service can handle requests) - if (path === '/ready' || path === '/api/ready') { - try { - // Test database connectivity - const dbResult = await env.DB.prepare('SELECT 1 as ok').first(); - const dbHealthy = dbResult?.ok === 1; - - if (!dbHealthy) { - return json({ - success: false, - data: { - status: 'not_ready', - database: 'error', - timestamp: new Date().toISOString(), - }, - }, 503); - } - - return json({ - success: true, - data: { - status: 'ready', - database: 'connected', - timestamp: new Date().toISOString(), - }, - }); - } catch (error) { - return json({ - success: false, - data: { - status: 'not_ready', - database: 'error', - error: String(error), - timestamp: new Date().toISOString(), - }, - }, 503); - } - } - - // ============ Job Endpoints (require API key) ============ - - if (path === '/api/jobs/next' && method === 'GET') { - if (!(await verifyApiKey())) { - return json({ success: false, error: 'Unauthorized' }, 401); - } - const result = await getNextJob(env); - return json(result); - } - - if (path.match(/^\/api\/jobs\/[^/]+\/claim$/) && method === 'POST') { - if (!(await verifyApiKey())) { - return json({ success: false, error: 'Unauthorized' }, 401); - } - const jobId = path.split('/')[3]; - const body = await parseBody(); - if (!body?.worker_id) { - return json({ success: false, error: 'Missing worker_id' }, 400); - } - const result = await claimJob(env, jobId, body.worker_id); - return json(result, result.success ? 200 : 400); - } - - if (path.match(/^\/api\/jobs\/[^/]+\/heartbeat$/) && method === 'POST') { - if (!(await verifyApiKey())) { - return json({ success: false, error: 'Unauthorized' }, 401); - } - const jobId = path.split('/')[3]; - const body = await parseBody<{ worker_id: string }>(); - if (!body?.worker_id) { - return json({ success: false, error: 'Missing worker_id' }, 400); - } - const result = await heartbeatJob(env, jobId, body.worker_id); - return json(result, result.success ? 200 : 400); - } - - if (path.match(/^\/api\/jobs\/[^/]+\/result$/) && method === 'POST') { - if (!(await verifyApiKey())) { - return json({ success: false, error: 'Unauthorized' }, 401); - } - const jobId = path.split('/')[3]; - const body = await parseBody(); - if (!body) { - return json({ success: false, error: 'Invalid request body' }, 400); - } - const result = await submitResult(env, jobId, body); - return json(result, result.success ? 200 : 400); - } - - if (path.match(/^\/api\/jobs\/[^/]+\/fail$/) && method === 'POST') { - if (!(await verifyApiKey())) { - return json({ success: false, error: 'Unauthorized' }, 401); - } - const jobId = path.split('/')[3]; - const body = await parseBody<{ worker_id: string; error_message: string }>(); - if (!body?.worker_id || !body?.error_message) { - return json({ success: false, error: 'Missing required fields' }, 400); - } - const result = await failJob(env, jobId, body.worker_id, body.error_message); - return json(result, result.success ? 200 : 400); - } - - // ============ Bot Endpoints (public or owner-verified) ============ - - if (path === '/api/register' && method === 'POST') { - const body = await parseBody(); - if (!body) { - return json({ success: false, error: 'Invalid request body' }, 400); - } - const result = await registerBot(env, body); - return json(result, result.success ? 201 : 400); - } - - if (path === '/api/bots' && method === 'GET') { - const result = await listBots(env); - return json(result); - } - - if (path.match(/^\/api\/bots\/[^/]+$/) && method === 'GET') { - const botId = path.split('/')[3]; - const result = await getBot(env, botId); - return json(result, result.success ? 200 : 404); - } - - if (path.match(/^\/api\/bots\/[^/]+$/) && method === 'PUT') { - const botId = path.split('/')[3]; - const body = await parseBody<{ name?: string; endpoint_url?: string }>(); - if (!body) { - return json({ success: false, error: 'Invalid request body' }, 400); - } - const result = await updateBot(env, botId, body); - return json(result, result.success ? 200 : 400); - } - - if (path === '/api/rotate-key' && method === 'POST') { - const body = await parseBody<{ bot_id: string; owner_id: string }>(); - if (!body?.bot_id || !body?.owner_id) { - return json({ success: false, error: 'Missing required fields' }, 400); - } - const result = await rotateApiKey(env, body.bot_id, body.owner_id); - return json(result, result.success ? 200 : 400); - } - - if (path === '/api/leaderboard' && method === 'GET') { - const result = await getLeaderboard(env); - return json(result); - } - - // ============ Data Export Endpoint (for index builder) ============ - - if (path === '/api/data/export' && method === 'GET') { - if (!(await verifyApiKey())) { - return json({ success: false, error: 'Unauthorized' }, 401); - } - const result = await exportData(env); - return json(result); - } - - // 404 for unmatched routes - return json({ success: false, error: 'Not found' }, 404); - } catch (error) { - console.error('Worker error:', error); - return json( - { success: false, error: 'Internal server error' }, - 500 - ); - } - }, - - async scheduled(event: ScheduledEvent, env: Env, ctx: ExecutionContext): Promise { - const cron = event.cron; - console.log(`Running scheduled task: ${cron}`); - - try { - const result = await handleCron(env, cron); - console.log(`Cron result:`, result); - } catch (error) { - console.error(`Cron error:`, error); - } - }, -}; diff --git a/worker-api/src/jobs.ts b/worker-api/src/jobs.ts deleted file mode 100644 index 655703f..0000000 --- a/worker-api/src/jobs.ts +++ /dev/null @@ -1,244 +0,0 @@ -// Job Coordination Endpoints - -import type { Env, Job, Match, MatchParticipant, JobClaimResponse, ApiResponse, SubmitResultRequest } from './types'; -import { updateMatchRatings } from './glicko2'; - -/** - * GET /api/jobs/next - Get next available job for worker - */ -export async function getNextJob(env: Env): Promise> { - // Find a pending job, ordered by creation time - const result = await env.DB.prepare( - `SELECT * FROM jobs - WHERE status = 'pending' - ORDER BY created_at ASC - LIMIT 1` - ).first(); - - return { success: true, data: result || null }; -} - -/** - * POST /api/jobs/:id/claim - Claim a job for execution - */ -export async function claimJob( - env: Env, - jobId: string, - workerId: string -): Promise> { - const now = new Date().toISOString(); - - // Try to claim the job atomically - const result = await env.DB.prepare( - `UPDATE jobs SET - status = 'claimed', - worker_id = ?, - claimed_at = ?, - heartbeat_at = ? - WHERE id = ? AND status = 'pending'` - ) - .bind(workerId, now, now, jobId) - .run(); - - if (result.meta.changes === 0) { - return { success: false, error: 'Job not found or already claimed' }; - } - - // Get the job details - const job = await env.DB.prepare('SELECT * FROM jobs WHERE id = ?') - .bind(jobId) - .first(); - - if (!job) { - return { success: false, error: 'Job not found' }; - } - - // Get match details - const match = await env.DB.prepare('SELECT * FROM matches WHERE id = ?') - .bind(job.match_id) - .first(); - - if (!match) { - return { success: false, error: 'Match not found' }; - } - - // Update match status to running - await env.DB.prepare( - `UPDATE matches SET status = 'running', started_at = ? WHERE id = ?` - ) - .bind(now, match.id) - .run(); - - // Get participants with their ratings - const participants = await env.DB.prepare( - `SELECT * FROM match_participants WHERE match_id = ?` - ) - .bind(match.id) - .all(); - - // Get bot details (endpoint URLs) - const botIds = participants.results.map((p) => p.bot_id); - const placeholders = botIds.map(() => '?').join(','); - const bots = await env.DB.prepare( - `SELECT id, endpoint_url FROM bots WHERE id IN (${placeholders})` - ) - .bind(...botIds) - .all<{ id: string; endpoint_url: string }>(); - - // Get bot secrets (API keys for HMAC auth) - const secrets = await env.DB.prepare( - `SELECT bot_id, api_key_hash as secret FROM bot_secrets WHERE bot_id IN (${placeholders})` - ) - .bind(...botIds) - .all<{ bot_id: string; secret: string }>(); - - // Get map details - const map = await env.DB.prepare('SELECT * FROM maps WHERE id = ?') - .bind(match.map_id) - .first<{ id: string; width: number; height: number; walls: string; spawns: string; cores: string }>(); - - if (!map) { - return { success: false, error: 'Map not found' }; - } - - return { - success: true, - data: { - job: job, - match: match, - participants: participants.results, - map: map, - bots: bots.results, - bot_secrets: secrets.results, - }, - }; -} - -/** - * POST /api/jobs/:id/heartbeat - Update job heartbeat - */ -export async function heartbeatJob( - env: Env, - jobId: string, - workerId: string -): Promise> { - const now = new Date().toISOString(); - - const result = await env.DB.prepare( - `UPDATE jobs SET heartbeat_at = ? WHERE id = ? AND worker_id = ?` - ) - .bind(now, jobId, workerId) - .run(); - - if (result.meta.changes === 0) { - return { success: false, error: 'Job not found or not owned by worker' }; - } - - return { success: true }; -} - -/** - * POST /api/jobs/:id/result - Submit job result - */ -export async function submitResult( - env: Env, - jobId: string, - result: SubmitResultRequest -): Promise> { - const now = new Date().toISOString(); - - // Get the job - const job = await env.DB.prepare('SELECT * FROM jobs WHERE id = ?') - .bind(jobId) - .first(); - - if (!job) { - return { success: false, error: 'Job not found' }; - } - - if (job.status !== 'claimed' && job.status !== 'running') { - return { success: false, error: 'Job not in a valid state for result submission' }; - } - - // Get participants - const participants = await env.DB.prepare( - 'SELECT * FROM match_participants WHERE match_id = ?' - ) - .bind(job.match_id) - .all(); - - // Update scores - for (const [botId, score] of Object.entries(result.scores)) { - await env.DB.prepare( - `UPDATE match_participants SET score = ? WHERE match_id = ? AND bot_id = ?` - ) - .bind(score, job.match_id, botId) - .run(); - } - - // Update ratings using Glicko-2 - await updateMatchRatings(env, job.match_id, participants.results, result.winner_id); - - // Update job status - await env.DB.prepare( - `UPDATE jobs SET status = 'completed', completed_at = ? WHERE id = ?` - ) - .bind(now, jobId) - .run(); - - // Update match status - await env.DB.prepare( - `UPDATE matches SET - status = 'completed', - winner_id = ?, - turns = ?, - end_reason = ?, - completed_at = ? - WHERE id = ?` - ) - .bind(result.winner_id, result.turns, result.end_reason, now, job.match_id) - .run(); - - return { success: true }; -} - -/** - * POST /api/jobs/:id/fail - Mark job as failed - */ -export async function failJob( - env: Env, - jobId: string, - workerId: string, - errorMessage: string -): Promise> { - const now = new Date().toISOString(); - - const result = await env.DB.prepare( - `UPDATE jobs SET - status = 'failed', - completed_at = ?, - error_message = ? - WHERE id = ? AND worker_id = ?` - ) - .bind(now, errorMessage, jobId, workerId) - .run(); - - if (result.meta.changes === 0) { - return { success: false, error: 'Job not found or not owned by worker' }; - } - - // Also update match status - const job = await env.DB.prepare('SELECT match_id FROM jobs WHERE id = ?') - .bind(jobId) - .first<{ match_id: string }>(); - - if (job) { - await env.DB.prepare( - `UPDATE matches SET status = 'failed', completed_at = ? WHERE id = ?` - ) - .bind(now, job.match_id) - .run(); - } - - return { success: true }; -} diff --git a/worker-api/src/types.ts b/worker-api/src/types.ts deleted file mode 100644 index 7560b67..0000000 --- a/worker-api/src/types.ts +++ /dev/null @@ -1,122 +0,0 @@ -// AI Code Battle Worker Types - -export interface Env { - DB: D1Database; - API_KEY: string; - ENVIRONMENT: string; -} - -// Bot types -export interface Bot { - id: string; - name: string; - owner_id: string; - endpoint_url: string; - api_key_hash: string; - rating: number; - rating_deviation: number; - rating_volatility: number; - created_at: string; - updated_at: string; - last_health_check: string | null; - health_status: 'healthy' | 'unhealthy' | 'unknown'; - matches_played: number; - matches_won: number; -} - -export interface CreateBotRequest { - name: string; - owner_id: string; - endpoint_url: string; -} - -// Match types -export type MatchStatus = 'pending' | 'running' | 'completed' | 'failed'; - -export interface Match { - id: string; - status: MatchStatus; - winner_id: string | null; - turns: number | null; - end_reason: string | null; - map_id: string; - created_at: string; - started_at: string | null; - completed_at: string | null; -} - -export interface MatchParticipant { - id: string; - match_id: string; - bot_id: string; - player_index: number; - score: number; - rating_before: number; - rating_after: number | null; - rating_deviation_before: number; - rating_deviation_after: number | null; -} - -// Job types -export type JobStatus = 'pending' | 'claimed' | 'running' | 'completed' | 'failed' | 'timeout'; - -export interface Job { - id: string; - match_id: string; - status: JobStatus; - worker_id: string | null; - claimed_at: string | null; - heartbeat_at: string | null; - created_at: string; - completed_at: string | null; - error_message: string | null; -} - -export interface ClaimJobRequest { - worker_id: string; -} - -export interface SubmitResultRequest { - winner_id: string; - turns: number; - end_reason: string; - replay_url: string; - scores: Record; -} - -// Rating types -export interface RatingChange { - bot_id: string; - rating_before: number; - rating_after: number; - rating_deviation: number; -} - -// API Response types -export interface ApiResponse { - success: boolean; - data?: T; - error?: string; -} - -export interface JobClaimResponse { - job: Job; - match: Match; - participants: MatchParticipant[]; - map: { - id: string; - width: number; - height: number; - walls: string; - spawns: string; - cores: string; - }; - bots: Array<{ - id: string; - endpoint_url: string; - }>; - bot_secrets: Array<{ - bot_id: string; - secret: string; - }>; -} diff --git a/worker-api/tsconfig.json b/worker-api/tsconfig.json deleted file mode 100644 index faa3811..0000000 --- a/worker-api/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "ES2022", - "moduleResolution": "bundler", - "lib": ["ES2022"], - "types": ["@cloudflare/workers-types"], - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "esModuleInterop": true, - "resolveJsonModule": true, - "isolatedModules": true, - "forceConsistentCasingInFileNames": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/worker-api/vitest.config.ts b/worker-api/vitest.config.ts deleted file mode 100644 index ba7eef0..0000000 --- a/worker-api/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - environment: 'node', - include: ['src/**/*.test.ts'], - globals: true, - }, -}); diff --git a/worker-api/wrangler.toml b/worker-api/wrangler.toml deleted file mode 100644 index bffb02a..0000000 --- a/worker-api/wrangler.toml +++ /dev/null @@ -1,24 +0,0 @@ -name = "acb-api" -main = "src/index.ts" -compatibility_date = "2025-03-10" -compatibility_flags = ["nodejs_compat"] - -[[d1_databases]] -binding = "DB" -database_name = "acb-db" -database_id = "placeholder-will-be-set-on-deploy" -migrations_dir = "migrations" - -[vars] -ENVIRONMENT = "development" - -[triggers] -crons = [ - "*/1 * * * *", # Matchmaker: every minute - "*/5 * * * *", # Stale job reaper: every 5 minutes - "*/15 * * * *" # Health checker: every 15 minutes -] - -# API key for worker authentication (set via wrangler secret put API_KEY) -# [secrets] -# API_KEY