diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c30e..acf7ad6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,14 @@ jobs: working-directory: agent-002-governance-analyst run: npx tsc --noEmit + - name: Install agent-004-validator-monitor/ dependencies + working-directory: agent-004-validator-monitor + run: npm ci + + - name: Typecheck agent-004-validator-monitor/ + working-directory: agent-004-validator-monitor + run: npx tsc --noEmit + # ── Spec verification: schemas, reference impls, datasets ─────────── verify-specs: name: Verify Specs diff --git a/agent-004-validator-monitor/.env.example b/agent-004-validator-monitor/.env.example new file mode 100644 index 0000000..c052d2b --- /dev/null +++ b/agent-004-validator-monitor/.env.example @@ -0,0 +1,14 @@ +# Required: Anthropic API key for Claude-powered narrative reports +ANTHROPIC_API_KEY=sk-ant-... + +# Regen LCD endpoint (defaults to public endpoint if not set) +REGEN_LCD_URL=https://regen.api.chandrastation.com + +# Optional: Discord webhook for posting validator reports and alerts +DISCORD_WEBHOOK_URL= + +# Optional: polling interval in seconds (default: 900 = 15 minutes) +POLL_INTERVAL_SECONDS=900 + +# Optional: Claude model to use (default: claude-sonnet-4-5-20250929) +ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 diff --git a/agent-004-validator-monitor/.gitignore b/agent-004-validator-monitor/.gitignore new file mode 100644 index 0000000..467cd39 --- /dev/null +++ b/agent-004-validator-monitor/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.db +*.db-shm +*.db-wal +.env diff --git a/agent-004-validator-monitor/README.md b/agent-004-validator-monitor/README.md new file mode 100644 index 0000000..5a860f9 --- /dev/null +++ b/agent-004-validator-monitor/README.md @@ -0,0 +1,116 @@ +# AGENT-004: Regen Validator Monitor + +**Layer 1 (fully automated, read-only, informational) agent that watches the Regen Network validator set, scores per-validator performance against the M014 methodology, tracks delegation flows, and monitors network decentralization.** + +Mirrors the AGENT-002 Governance Analyst / AGENT-003 Market Monitor structure: the same OODA executor, the same standalone Node.js process shape, the same SQLite-backed local state. Scope is validator infrastructure intelligence rather than governance or marketplace. + +## What it does + +| Workflow | Trigger | Output | +|----------|---------|--------| +| **WF-VM-01** Performance Tracking | Periodic (each poll cycle) | Per-validator scorecards, composite 0-1000, PoA eligibility, performance alerts | +| **WF-VM-02** Delegation Flow Analysis | Periodic (delta against previous snapshot) | Net inflow/outflow, whale detection, top movers | +| **WF-VM-03** Decentralization Monitoring | Periodic (each poll cycle) | Nakamoto coefficient, Gini index, top-N share, health tier, trend vs previous | + +Each workflow is an **OODA loop** (Observe → Orient → Decide → Act). Numeric decisions (scoring, Nakamoto, Gini, health tier, whale detection) are computed **deterministically**; Claude is only used for the narrative layer. + +## Architecture + +``` +Regen Ledger (LCD REST API) + ↓ observe +AGENT-004 (OODA engine) + ↓ orient (deterministic) + ↓ decide (deterministic) +Local SQLite (state) + ↓ act (narrative via Claude) +Console / Discord webhook +``` + +**No MCP dependency.** Talks directly to any Cosmos LCD endpoint. When Ledger MCP becomes available, the `LedgerClient` can be swapped behind the same interface. + +**No ElizaOS dependency.** Standalone Node.js process. The ElizaOS character for AGENT-004 lives at `agents/packages/agents/src/characters/validator-monitor.ts`; this package shares the same system prompt text and the same threshold constants so downstream tooling has a single source of truth. + +## Quick start + +```bash +# 1. Install +cd agent-004-validator-monitor +npm install + +# 2. Configure +cp .env.example .env +# Edit .env — at minimum set ANTHROPIC_API_KEY + +# 3. Run (single cycle) +npm run analyze + +# 4. Run (continuous polling) +npm start + +# 5. Run (dev mode with auto-reload) +npm run dev +``` + +## Configuration + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `ANTHROPIC_API_KEY` | Yes | — | Claude API key | +| `REGEN_LCD_URL` | No | `https://regen.api.chandrastation.com` | Cosmos LCD endpoint | +| `DISCORD_WEBHOOK_URL` | No | — | Discord webhook for posting reports | +| `POLL_INTERVAL_SECONDS` | No | `900` (15 min) | Polling interval (seconds) | +| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-5-20250929` | Claude model to use | + +Thresholds live in `src/config.ts` under `validator.*` and mirror the character definition at `agents/packages/agents/src/characters/validator-monitor.ts`. + +## How it maps to the framework specs + +| Framework Spec | Implementation | +|----------------|---------------| +| Phase 2.2 WF-VM-01 | `src/workflows/performance-tracking.ts` | +| Phase 2.2 WF-VM-02 | `src/workflows/delegation-flow-analysis.ts` | +| Phase 2.2 WF-VM-03 | `src/workflows/decentralization-monitor.ts` | +| Phase 2.4 OODA executor | `src/ooda.ts` | +| Phase 2.4 Agent character | `agents/packages/agents/src/characters/validator-monitor.ts` | +| Phase 2.5 Workflow executions table | `src/store.ts` (SQLite) | +| Phase 3.2 Ledger MCP client | `src/ledger.ts` (direct LCD) | +| M014 scoring methodology | `src/workflows/performance-tracking.ts` | + +## Scoring methodology (M014) + +Composite score is **0-1000**, computed deterministically in `performance-tracking.ts`: + +| Component | Weight | Source | +|---|---|---| +| Uptime | 400 | `signed_blocks_window − missed_blocks_counter` from slashing signing info | +| Governance participation | 350 | Votes cast over recent finalized proposals (MVP keeps this at 0; see below) | +| Stability | 250 | 250 − (jailings × 100) − (commission changes in window × 40), floored at 0 | + +**PoA eligibility:** composite >= `config.validator.poaEligibilityScore` (default 800). + +## Design decisions + +1. **Deterministic numbers, narrative-only LLM calls.** All scoring, Nakamoto and Gini computation, health classification, and whale detection happen locally in plain TypeScript. Claude is only invoked to write the report. Keeps the agent cheap, reproducible, and auditable. + +2. **Real MsgDelegate / MsgUndelegate / MsgBeginRedelegate tx-stream.** WF-VM-02 reads recent staking events from the Cosmos LCD `tx-search` endpoint, filtered by message type URL, and parses them into `DelegationEvent` records. Each tx can carry multiple events (batched operations); the parser walks `logs[].events[]` and the flattened `tx.events[]` for cross-SDK compatibility. The aggregator groups events per-validator: delegate → inflow, undelegate → outflow, redelegate → source outflow + destination inflow. Earlier drafts used a token-delta proxy against `validator.tokens` snapshots; the current implementation produces events with delegator identity and bit-exact amounts rather than synthesized deltas. + +3. **Governance participation scoring is MVP-zero.** The operator→delegator bech32 conversion (required to fetch per-validator votes via `/proposals/{id}/votes/{voter}`) is deferred to a follow-up PR. All validators currently receive a 0 governance score, so relative composite scores still surface real differences in uptime and stability without punishing anyone asymmetrically. A separate PR will add the bech32 conversion and full vote fetching. + +4. **Nakamoto coefficient uses the 33.4% halt-threshold convention.** Defined as the smallest number of validators whose cumulative voting power strictly exceeds 33.4% of total bonded stake — matches the standard Cosmos-era definition. + +5. **Gini index uses the textbook formula** operating on ascending-sorted token amounts, normalized to uregen. 0 = perfect equality, 1 = perfect inequality. + +6. **Standalone over ElizaOS.** Matches AGENT-002 and AGENT-003. ElizaOS plugin API may change; a standalone process proves the workflow logic works independently of any runtime framework. + +## Governance layer + +This agent operates at **Layer 1 only**: + +- Read-only access to on-chain state +- Cannot delegate, undelegate, or redelegate +- Cannot submit proposals or votes +- Cannot execute transactions +- Informational output only + +Matches the framework's principle of starting with the lowest-risk, highest-value capability. Raising the automation layer (e.g., automated delegation rebalancing to under-concentrated validators) is a separate governance decision. diff --git a/agent-004-validator-monitor/package-lock.json b/agent-004-validator-monitor/package-lock.json new file mode 100644 index 0000000..6bbafda --- /dev/null +++ b/agent-004-validator-monitor/package-lock.json @@ -0,0 +1,2867 @@ +{ + "name": "@regen/agent-004-validator-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@regen/agent-004-validator-monitor", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "bech32": "^2.0.0", + "better-sqlite3": "^11.7.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz", + "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==", + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", + "integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bech32": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz", + "integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg==", + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-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, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "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.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "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": "2.1.9", + "@vitest/ui": "2.1.9", + "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/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/agent-004-validator-monitor/package.json b/agent-004-validator-monitor/package.json new file mode 100644 index 0000000..522244c --- /dev/null +++ b/agent-004-validator-monitor/package.json @@ -0,0 +1,31 @@ +{ + "name": "@regen/agent-004-validator-monitor", + "version": "0.1.0", + "description": "Regen Network Validator Monitor Agent — Layer 1 read-only validator performance tracking, delegation flow analysis, and network decentralization monitoring", + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc", + "start": "node --import tsx src/index.ts", + "dev": "node --import tsx --watch src/index.ts", + "analyze": "node --import tsx src/index.ts --once", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@anthropic-ai/sdk": "^0.39.0", + "better-sqlite3": "^11.7.0", + "bech32": "^2.0.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.0.0", + "tsx": "^4.19.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } +} diff --git a/agent-004-validator-monitor/src/bech32.ts b/agent-004-validator-monitor/src/bech32.ts new file mode 100644 index 0000000..af68209 --- /dev/null +++ b/agent-004-validator-monitor/src/bech32.ts @@ -0,0 +1,73 @@ +import { createHash } from "node:crypto"; +import { bech32 } from "bech32"; + +/** + * Cosmos bech32 derivation helpers. + * + * These are the minimum set of conversions the validator monitor + * needs to join across the three address spaces that Cosmos uses: + * + * - operator bech32 (`regenvaloper1…`) — governs the validator + * - delegator bech32 (`regen1…`) — votes in governance + * - consensus bech32 (`regenvalcons1…`) — signs blocks + * + * The agent cannot reliably match a validator against its slashing + * signing info or its governance votes without these conversions — + * that mismatch was the bug behind AGENT-004's uptime always showing + * 100% and governance participation always showing 0. + */ + +const HRP_OPERATOR = "regenvaloper"; +const HRP_ACCOUNT = "regen"; +const HRP_CONS = "regenvalcons"; + +/** Re-encode a bech32 address under a different human-readable prefix. */ +function reencodeBech32(addr: string, newHrp: string): string | null { + try { + const decoded = bech32.decode(addr); + return bech32.encode(newHrp, decoded.words); + } catch { + return null; + } +} + +/** Convert `regenvaloper1…` to the underlying `regen1…` delegator bech32. */ +export function operatorToDelegator(operatorAddress: string): string | null { + return reencodeBech32(operatorAddress, HRP_ACCOUNT); +} + +/** Convert `regen1…` delegator bech32 back to the operator form. */ +export function delegatorToOperator(delegatorAddress: string): string | null { + return reencodeBech32(delegatorAddress, HRP_OPERATOR); +} + +/** + * Derive the `regenvalcons1…` consensus address from a validator's + * ed25519 consensus public key. The pubkey bytes are SHA256-hashed + * and the first 20 bytes of the digest are bech32-encoded under the + * consensus HRP. This matches the standard Cosmos derivation in + * `tmcrypto.PubKey.Address()`. + * + * `pubkeyBase64` is the base64 string that Cosmos LCDs expose under + * `validator.consensus_pubkey.key`. + */ +export function consensusPubkeyToConsAddress( + pubkeyBase64: string +): string | null { + if (!pubkeyBase64) return null; + let pubkeyBytes: Buffer; + try { + pubkeyBytes = Buffer.from(pubkeyBase64, "base64"); + } catch { + return null; + } + if (pubkeyBytes.length === 0) return null; + + const digest = createHash("sha256").update(pubkeyBytes).digest(); + const addressBytes = digest.subarray(0, 20); + try { + return bech32.encode(HRP_CONS, bech32.toWords(addressBytes)); + } catch { + return null; + } +} diff --git a/agent-004-validator-monitor/src/config.ts b/agent-004-validator-monitor/src/config.ts new file mode 100644 index 0000000..1092304 --- /dev/null +++ b/agent-004-validator-monitor/src/config.ts @@ -0,0 +1,62 @@ +export const config = { + // Regen LCD endpoint + lcdUrl: process.env.REGEN_LCD_URL || "https://regen.api.chandrastation.com", + + // Anthropic + anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", + model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-5-20250929", + + // Discord webhook (optional) + discordWebhookUrl: process.env.DISCORD_WEBHOOK_URL || "", + + // Polling — validator state moves slowly; 15 min is plenty and + // keeps the agent off the LCD's rate-limit radar. + pollIntervalMs: (parseInt(process.env.POLL_INTERVAL_SECONDS || "900", 10)) * 1000, + + // Validator monitor thresholds. These mirror the character + // thresholds in + // agents/packages/agents/src/characters/validator-monitor.ts so + // downstream tooling has a single source of truth. Keep them in + // sync if either file changes. + validator: { + /** Concentration % (of a single validator) that triggers CRITICAL */ + criticalConcentrationPct: 33, + /** Concentration % that triggers WARNING */ + warningConcentrationPct: 20, + /** Minimum composite score (0-1000) for M014 PoA eligibility */ + poaEligibilityScore: 800, + /** Uptime component weight (of 1000) */ + scoreWeightUptime: 400, + /** Governance participation component weight (of 1000) */ + scoreWeightGovernance: 350, + /** Stability component weight (of 1000) */ + scoreWeightStability: 250, + /** Trailing window for uptime scoring (days) — used for the narrative, not the math */ + uptimeTrailingDays: 30, + /** Per-commission-change stability penalty */ + stabilityPenaltyCommissionChange: 40, + /** Per-jailing stability penalty */ + stabilityPenaltyJailing: 100, + /** Whale movement threshold (REGEN, in uregen) — flags delegation deltas */ + whaleDelegationUregen: "100000000000", // 100,000 REGEN + /** Nakamoto floor below which WF-VM-03 warns */ + nakamotoWarningFloor: 8, + /** Nakamoto floor below which WF-VM-03 escalates */ + nakamotoCriticalFloor: 5, + /** Gini ceiling above which WF-VM-03 warns */ + giniWarningCeiling: 0.65, + }, + + // Agent identity + agentId: "AGENT-004", + agentName: "RegenValidatorMonitor", + governanceLayer: 1 as const, +} as const; + +export function validateConfig(): void { + if (!config.anthropicApiKey) { + throw new Error( + "ANTHROPIC_API_KEY is required. Copy .env.example to .env and set it." + ); + } +} diff --git a/agent-004-validator-monitor/src/index.ts b/agent-004-validator-monitor/src/index.ts new file mode 100644 index 0000000..6c95317 --- /dev/null +++ b/agent-004-validator-monitor/src/index.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env node +import { config, validateConfig } from "./config.js"; +import { LedgerClient } from "./ledger.js"; +import { executeOODA } from "./ooda.js"; +import { store } from "./store.js"; +import { createPerformanceTrackingWorkflow } from "./workflows/performance-tracking.js"; +import { createDelegationFlowAnalysisWorkflow } from "./workflows/delegation-flow-analysis.js"; +import { createDecentralizationMonitorWorkflow } from "./workflows/decentralization-monitor.js"; + +// ── Banner ──────────────────────────────────────────────────── + +function banner() { + console.log(` + ╔══════════════════════════════════════════════════════════════╗ + ║ REGEN VALIDATOR MONITOR (AGENT-004) ║ + ║ ║ + ║ Layer 1 — Fully Automated, Informational Only ║ + ║ Workflows: WF-VM-01, WF-VM-02, WF-VM-03 ║ + ║ ║ + ║ Regen Agentic Tokenomics Framework ║ + ╚══════════════════════════════════════════════════════════════╝ +`); +} + +// ── Main loop ───────────────────────────────────────────────── + +async function runCycle(ledger: LedgerClient): Promise { + const ts = new Date().toISOString(); + console.log(`\n[${ts}] ═══ Starting validator monitor cycle ═══\n`); + + // WF-VM-01: Performance tracking + alerts + const wf01 = createPerformanceTrackingWorkflow(ledger); + await executeOODA(wf01); + + // WF-VM-02: Delegation flow analysis + const wf02 = createDelegationFlowAnalysisWorkflow(ledger); + await executeOODA(wf02); + + // WF-VM-03: Decentralization monitoring + const wf03 = createDecentralizationMonitorWorkflow(ledger); + await executeOODA(wf03); + + const execCount = store.getExecutionCount(); + console.log( + `[${new Date().toISOString()}] ═══ Cycle complete (${execCount} total executions logged) ═══\n` + ); +} + +async function main() { + banner(); + validateConfig(); + + const runOnce = process.argv.includes("--once"); + const ledger = new LedgerClient(); + + console.log(`Configuration:`); + console.log(` LCD endpoint: ${config.lcdUrl}`); + console.log(` LLM model: ${config.model}`); + console.log(` Discord: ${config.discordWebhookUrl ? "configured" : "not configured"}`); + console.log(` Mode: ${runOnce ? "single run" : `polling every ${config.pollIntervalMs / 1000}s`}`); + console.log(); + + try { + const { blockHeight } = await ledger.checkConnection(); + console.log(`Connected to Regen Ledger at block ${blockHeight}\n`); + } catch (err) { + console.error( + `Failed to connect to Regen Ledger at ${config.lcdUrl}:`, + err + ); + process.exit(1); + } + + if (runOnce) { + await runCycle(ledger); + } else { + // Recursive setTimeout rather than setInterval so a slow cycle + // never overlaps with the next tick. If a cycle takes longer + // than pollIntervalMs the next tick simply starts late — we + // never have two runCycle invocations sharing the SQLite + // connection in parallel. + let timeoutId: NodeJS.Timeout | null = null; + let stopping = false; + + const runNext = () => { + runCycle(ledger) + .catch((err) => console.error(`Cycle failed:`, err)) + .finally(() => { + if (!stopping) { + timeoutId = setTimeout(runNext, config.pollIntervalMs); + } + }); + }; + + runNext(); + + const shutdown = () => { + console.log("\nShutting down gracefully..."); + stopping = true; + if (timeoutId !== null) clearTimeout(timeoutId); + store.close(); + process.exit(0); + }; + + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + + console.log("Agent running. Press Ctrl+C to stop.\n"); + } +} + +main().catch((err) => { + console.error("Fatal error:", err); + process.exit(1); +}); diff --git a/agent-004-validator-monitor/src/ledger.test.ts b/agent-004-validator-monitor/src/ledger.test.ts new file mode 100644 index 0000000..ee93311 --- /dev/null +++ b/agent-004-validator-monitor/src/ledger.test.ts @@ -0,0 +1,317 @@ +import { describe, it, expect } from "vitest"; +import { LedgerClient } from "./ledger.js"; + +// ============================================================ +// parseDelegationEventsFromTx — staking event extraction +// ============================================================ + +describe("LedgerClient.parseDelegationEventsFromTx", () => { + const client = new LedgerClient(); + + it("returns empty array for a tx with no events", () => { + const tx = { txhash: "tx-empty", timestamp: "2026-02-18T12:00:00Z", logs: [] }; + expect(client.parseDelegationEventsFromTx(tx)).toEqual([]); + }); + + it("extracts a MsgDelegate event with the sender as delegator", () => { + const tx = { + txhash: "tx-delegate", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [ + { key: "action", value: "/cosmos.staking.v1beta1.MsgDelegate" }, + { key: "sender", value: "regen1delegator1" }, + ], + }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1alpha" }, + { key: "amount", value: "1000uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ + txHash: "tx-delegate", + eventType: "delegate", + delegator: "regen1delegator1", + validator: "regenvaloper1alpha", + sourceValidator: null, + amountUregen: "1000", + }); + }); + + it("extracts a MsgUndelegate event from the `unbond` type", () => { + const tx = { + txhash: "tx-undelegate", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [ + { key: "action", value: "/cosmos.staking.v1beta1.MsgUndelegate" }, + { key: "sender", value: "regen1delegator2" }, + ], + }, + { + type: "unbond", + attributes: [ + { key: "validator", value: "regenvaloper1beta" }, + { key: "amount", value: "500uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.eventType).toBe("undelegate"); + expect(out[0]!.validator).toBe("regenvaloper1beta"); + expect(out[0]!.amountUregen).toBe("500"); + }); + + it("extracts a MsgBeginRedelegate event with source + destination", () => { + const tx = { + txhash: "tx-redelegate", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [ + { key: "action", value: "/cosmos.staking.v1beta1.MsgBeginRedelegate" }, + { key: "sender", value: "regen1delegator3" }, + ], + }, + { + type: "redelegate", + attributes: [ + { key: "source_validator", value: "regenvaloper1src" }, + { key: "destination_validator", value: "regenvaloper1dst" }, + { key: "amount", value: "2000uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.eventType).toBe("redelegate"); + expect(out[0]!.sourceValidator).toBe("regenvaloper1src"); + expect(out[0]!.validator).toBe("regenvaloper1dst"); + expect(out[0]!.amountUregen).toBe("2000"); + }); + + it("parses multiple staking events in a single batched tx", () => { + // A Cosmos SDK tx with three messages surfaces them as three + // separate `logs[]` entries, each carrying its own message event + // with its own sender. We iterate log-by-log so each staking + // event is attributed to its own delegator rather than + // positionally mapping against a flattened sender array (which + // would mis-attribute if a tx mixed staking with non-staking + // messages). + const tx = { + txhash: "tx-batched", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d1" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1a" }, + { key: "amount", value: "100uregen" }, + ], + }, + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d2" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1b" }, + { key: "amount", value: "200uregen" }, + ], + }, + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d3" }] }, + { + type: "unbond", + attributes: [ + { key: "validator", value: "regenvaloper1c" }, + { key: "amount", value: "50uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(3); + expect(out[0]!.delegator).toBe("regen1d1"); + expect(out[1]!.delegator).toBe("regen1d2"); + expect(out[2]!.delegator).toBe("regen1d3"); + }); + + it("attributes staking events through interleaved non-staking messages", () => { + // The old positional-match code would have attributed the + // undelegate to the MsgSend sender here, because the senders + // array would have been [d1, sender-of-send, d3] and the third + // staking event (there's only one) would fall through to the + // wrong slot. Iterating log-by-log fixes that. + const tx = { + txhash: "tx-interleaved", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1delegator" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1a" }, + { key: "amount", value: "100uregen" }, + ], + }, + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1sendsomething" }] }, + // No staking event in this message. + ], + }, + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1undelegator" }] }, + { + type: "unbond", + attributes: [ + { key: "validator", value: "regenvaloper1a" }, + { key: "amount", value: "50uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(2); + expect(out[0]!.eventType).toBe("delegate"); + expect(out[0]!.delegator).toBe("regen1delegator"); + expect(out[1]!.eventType).toBe("undelegate"); + expect(out[1]!.delegator).toBe("regen1undelegator"); + }); + + it("ignores delegate events with missing validator attribute", () => { + const tx = { + txhash: "tx-missing-validator", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [{ key: "sender", value: "regen1d" }], + }, + { + type: "delegate", + attributes: [{ key: "amount", value: "1000uregen" }], + }, + ], + }, + ], + }; + expect(client.parseDelegationEventsFromTx(tx)).toEqual([]); + }); + + it("ignores delegate events with malformed amount attribute", () => { + const tx = { + txhash: "tx-bad-amount", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "message", + attributes: [{ key: "sender", value: "regen1d" }], + }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1x" }, + { key: "amount", value: "no-numbers-here" }, + ], + }, + ], + }, + ], + }; + expect(client.parseDelegationEventsFromTx(tx)).toEqual([]); + }); + + it("parses the amount prefix when the denom follows the number", () => { + // The Cosmos SDK coin-amount format is "uregen". The parser + // extracts the numeric prefix and drops the denom. + const tx = { + txhash: "tx-coin-format", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1x" }, + { key: "amount", value: "12345678uregen" }, + ], + }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out[0]!.amountUregen).toBe("12345678"); + }); + + it("reads events from tx.events[] alongside logs[].events[]", () => { + const tx = { + txhash: "tx-flat", + timestamp: "2026-02-18T12:00:00Z", + events: [ + { type: "message", attributes: [{ key: "sender", value: "regen1d" }] }, + { + type: "delegate", + attributes: [ + { key: "validator", value: "regenvaloper1y" }, + { key: "amount", value: "42uregen" }, + ], + }, + ], + }; + const out = client.parseDelegationEventsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.amountUregen).toBe("42"); + }); +}); diff --git a/agent-004-validator-monitor/src/ledger.ts b/agent-004-validator-monitor/src/ledger.ts new file mode 100644 index 0000000..08f01b9 --- /dev/null +++ b/agent-004-validator-monitor/src/ledger.ts @@ -0,0 +1,339 @@ +import { config } from "./config.js"; +import type { + Validator, + SigningInfo, + SlashingParams, + StakingPool, + Proposal, + Vote, + DelegationEvent, +} from "./types.js"; + +/** + * Parse a Cosmos SDK coin-amount attribute into a uregen string. + * The staking event attributes carry amounts like `"1000uregen"` — + * the numeric part followed by the denom. Strip the denom and + * return the numeric prefix as a string so BigInt downstream can + * consume it losslessly. Returns null on bad input. + */ +function parseCoinAmount(raw: string | null): string | null { + if (!raw) return null; + const m = raw.match(/^(\d+)/); + if (!m) return null; + return m[1] ?? null; +} + +/** + * Regen Ledger LCD (REST) client — staking, slashing, gov endpoints. + * + * Talks directly to a Cosmos LCD endpoint — no MCP dependency. Matches + * the pattern used by AGENT-002 and AGENT-003. When Ledger MCP becomes + * available, this can be swapped behind the same interface. + */ +export class LedgerClient { + private baseUrl: string; + + constructor(baseUrl?: string) { + this.baseUrl = (baseUrl || config.lcdUrl).replace(/\/$/, ""); + } + + // ── Staking ────────────────────────────────────────────────── + + async getValidators(status = "BOND_STATUS_BONDED"): Promise { + const params = new URLSearchParams(); + params.set("status", status); + params.set("pagination.limit", "500"); + + const data = await this.get( + `/cosmos/staking/v1beta1/validators?${params.toString()}` + ); + return (data.validators || []) as Validator[]; + } + + async getValidator(operatorAddress: string): Promise { + try { + const data = await this.get( + `/cosmos/staking/v1beta1/validators/${operatorAddress}` + ); + return (data.validator || null) as Validator | null; + } catch { + return null; + } + } + + async getStakingPool(): Promise { + const data = await this.get(`/cosmos/staking/v1beta1/pool`); + return data.pool as StakingPool; + } + + // ── Slashing ───────────────────────────────────────────────── + + async getSigningInfos(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "500"); + + const data = await this.get( + `/cosmos/slashing/v1beta1/signing_infos?${params.toString()}` + ); + return (data.info || []) as SigningInfo[]; + } + + async getSlashingParams(): Promise { + try { + const data = await this.get(`/cosmos/slashing/v1beta1/params`); + return (data.params || null) as SlashingParams | null; + } catch { + return null; + } + } + + // ── Governance (for participation scoring) ─────────────────── + + async getRecentFinalizedProposals(limit = 20): Promise { + // Pull the most recent passed + rejected proposals. Passed = "3", + // Rejected = "4" in the Cosmos gov v1beta1 status enum. + const params = new URLSearchParams(); + params.set("pagination.limit", String(limit)); + params.set("pagination.reverse", "true"); + + const results: Proposal[] = []; + for (const status of ["3", "4"]) { + params.set("proposal_status", status); + try { + const data = await this.get( + `/cosmos/gov/v1beta1/proposals?${params.toString()}` + ); + const list = (data.proposals || []) as Proposal[]; + results.push(...list); + } catch { + // ignore; partial proposal history still produces a useful score + } + } + + // Newest first, deduped by id, capped at `limit` + const seen = new Set(); + const ordered = results + .slice() + .sort((a, b) => Number(b.id) - Number(a.id)) + .filter((p) => { + if (seen.has(p.id)) return false; + seen.add(p.id); + return true; + }) + .slice(0, limit); + return ordered; + } + + async getVoteForVoter( + proposalId: string, + voter: string + ): Promise { + try { + const data = await this.get( + `/cosmos/gov/v1beta1/proposals/${proposalId}/votes/${voter}` + ); + return (data.vote || null) as Vote | null; + } catch { + return null; + } + } + + // ── Tx-search: staking delegation events ──────────────────── + // + // Pulls recent tx responses for each of the three staking message + // types and parses them into DelegationEvent records. Each call + // hits a different tx-search event filter because the LCD only + // supports one `events=` filter per request. + + async getRecentDelegationTxs(limit = 100): Promise { + const typeUrls = [ + "/cosmos.staking.v1beta1.MsgDelegate", + "/cosmos.staking.v1beta1.MsgUndelegate", + "/cosmos.staking.v1beta1.MsgBeginRedelegate", + ]; + + // Dedup transactions by hash so a tx containing more than one of + // the three message types (e.g. an atomic `MsgDelegate` + + // `MsgUndelegate` in the same tx) is only parsed once, no matter + // how many of the per-type queries return it. Without this dedup + // every event in that tx would get aggregated multiple times, + // inflating the per-cycle delegation deltas. + const seenHashes = new Set(); + const results: DelegationEvent[] = []; + + for (const typeUrl of typeUrls) { + try { + const params = new URLSearchParams(); + params.set("events", `message.action='${typeUrl}'`); + params.set("pagination.limit", String(limit)); + params.set("pagination.reverse", "true"); + params.set("order_by", "ORDER_BY_DESC"); + + const data = await this.get(`/cosmos/tx/v1beta1/txs?${params.toString()}`); + const txResponses = (data.tx_responses || []) as Array>; + + for (const tx of txResponses) { + const hash = typeof tx.txhash === "string" ? tx.txhash : ""; + if (hash && seenHashes.has(hash)) continue; + if (hash) seenHashes.add(hash); + results.push(...this.parseDelegationEventsFromTx(tx)); + } + } catch (err) { + // Per-type-url failure is isolated — the other two still run. + // Log rather than swallow so transient LCD issues are visible + // instead of silently falling back to empty delegation data. + console.error( + `LedgerClient.getRecentDelegationTxs(${typeUrl}) failed:`, + err + ); + } + } + + return results; + } + + /** + * Parse a single Cosmos tx_response into zero or more + * DelegationEvent records. Public so unit tests can feed it + * synthetic inputs. Matches three Cosmos SDK event types: + * + * - `delegate` (MsgDelegate) + * - `unbond` (MsgUndelegate) + * - `redelegate` (MsgBeginRedelegate) + * + * Walks `tx.logs` message-by-message so each staking event is + * attributed to the delegator (`sender`) of its own message. A + * positional mapping against a flattened senders array would + * mis-attribute staking events whenever a tx mixes staking with + * non-staking messages — for example, + * `[MsgDelegate, MsgSend, MsgUndelegate]` would attribute the + * undelegate to the MsgSend sender. + * + * Falls back to `tx.events` only when `tx.logs` is empty (old LCD + * builds). Even in the fallback case we preserve the "scope senders + * to a single message" semantics by treating the flat list as one + * scope, which is an imperfect match but strictly no worse than + * the walk we replaced. + */ + parseDelegationEventsFromTx(tx: Record): DelegationEvent[] { + const txHash = typeof tx.txhash === "string" ? tx.txhash : ""; + const timestamp = + typeof tx.timestamp === "string" ? tx.timestamp : new Date().toISOString(); + + const logs = (tx.logs || []) as Array>; + const eventScopes: Array>> = []; + if (logs.length > 0) { + for (const log of logs) { + eventScopes.push( + (log.events || []) as Array> + ); + } + } else { + // No per-message logs — very old LCD build. Fall back to the + // flattened `tx.events` list as a single scope. + eventScopes.push( + (tx.events || []) as Array> + ); + } + + const results: DelegationEvent[] = []; + + for (const events of eventScopes) { + // Within one message's events there is at most one relevant + // `message.sender` attribute — capture it once and use it for + // every staking event in the same scope. + let sender = ""; + for (const ev of events) { + if (typeof ev.type === "string" && ev.type === "message") { + const attributes = + (ev.attributes || []) as Array<{ key: string; value: string }>; + const hit = attributes.find((a) => a.key === "sender"); + if (hit) { + sender = hit.value; + break; + } + } + } + + for (const ev of events) { + const type = typeof ev.type === "string" ? ev.type : ""; + const attributes = + (ev.attributes || []) as Array<{ key: string; value: string }>; + const attr = (k: string): string | null => { + const hit = attributes.find((a) => a.key === k); + return hit ? hit.value : null; + }; + + if (type === "delegate") { + const validator = attr("validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!validator || amount === null) continue; + results.push({ + txHash, + eventType: "delegate", + delegator: sender, + validator, + sourceValidator: null, + amountUregen: amount, + occurredAt: timestamp, + }); + } else if (type === "unbond") { + const validator = attr("validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!validator || amount === null) continue; + results.push({ + txHash, + eventType: "undelegate", + delegator: sender, + validator, + sourceValidator: null, + amountUregen: amount, + occurredAt: timestamp, + }); + } else if (type === "redelegate") { + const src = attr("source_validator") ?? ""; + const dst = attr("destination_validator") ?? ""; + const amount = parseCoinAmount(attr("amount")); + if (!src || !dst || amount === null) continue; + results.push({ + txHash, + eventType: "redelegate", + delegator: sender, + validator: dst, + sourceValidator: src, + amountUregen: amount, + occurredAt: timestamp, + }); + } + } + } + + return results; + } + + // ── Connectivity check ────────────────────────────────────── + + async checkConnection(): Promise<{ blockHeight: string }> { + const data = await this.get(`/cosmos/base/tendermint/v1beta1/blocks/latest`); + const block = data.block as Record | undefined; + const header = block?.header as Record | undefined; + const height = (header?.height as string) || "unknown"; + return { blockHeight: height }; + } + + // ── HTTP ───────────────────────────────────────────────────── + + private async get(path: string): Promise> { + const url = `${this.baseUrl}${path}`; + const res = await fetch(url, { + headers: { Accept: "application/json" }, + signal: AbortSignal.timeout(15_000), + }); + + if (!res.ok) { + throw new Error(`LCD ${res.status}: ${res.statusText} — ${url}`); + } + + return (await res.json()) as Record; + } +} diff --git a/agent-004-validator-monitor/src/monitor.ts b/agent-004-validator-monitor/src/monitor.ts new file mode 100644 index 0000000..17b2758 --- /dev/null +++ b/agent-004-validator-monitor/src/monitor.ts @@ -0,0 +1,201 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { config } from "./config.js"; +import type { + ValidatorScorecard, + DelegationFlowSummary, + DecentralizationSnapshot, + PerformanceAlert, +} from "./types.js"; + +const client = new Anthropic({ apiKey: config.anthropicApiKey }); + +/** + * System prompt mirrors the AGENT-004 character definition at + * agents/packages/agents/src/characters/validator-monitor.ts. All + * thresholds are injected from config so the deterministic pipeline + * and the narrative layer cannot drift. + */ +const SYSTEM_PROMPT = `You are the Regen Validator Monitor Agent (AGENT-004). + +Your responsibilities: +1. Tracking validator uptime and block production performance +2. Monitoring governance voting participation +3. Analyzing delegation flows, whale movements, and concentration +4. Computing M014 performance scores +5. Assessing network decentralization and PoA transition readiness + +Core Principles: +- Prioritize network security and decentralization above all else +- Present performance data objectively +- Track trends over time, not just point-in-time snapshots +- Never recommend delegation choices; surface data for delegators to decide + +Scoring Methodology (M014): +- Uptime weight: ${config.validator.scoreWeightUptime} / 1000 +- Governance weight: ${config.validator.scoreWeightGovernance} / 1000 +- Stability weight: ${config.validator.scoreWeightStability} / 1000 +- PoA eligibility: composite >= ${config.validator.poaEligibilityScore} + +Alert Levels: +- NORMAL: Metrics within healthy bounds +- WARNING / HIGH: Degradation detected +- CRITICAL: Immediate risk to network health (e.g., single validator > ${config.validator.criticalConcentrationPct}% of stake) + +Output Format: +- Use markdown tables with units +- Include timeframes for all metrics +- Cite the deterministic numbers passed to you; do not invent values`; + +// ============================================================ +// WF-VM-01: Performance narrative +// ============================================================ + +export async function describePerformanceReport( + scorecards: ValidatorScorecard[], + alerts: PerformanceAlert[], + bondedUregen: string +): Promise { + const poaEligible = scorecards.filter((s) => s.poaEligible).length; + const top = scorecards.slice(0, 10); + + const prompt = `Generate a Validator Performance Report for the Regen Network active validator set. + +## Deterministic Pipeline Output +- Active set: ${scorecards.length} validators +- Bonded: ${bondedUregen} uregen +- PoA eligible (composite >= ${config.validator.poaEligibilityScore}): ${poaEligible}/${scorecards.length} + +## Top 10 Validators by Composite Score +${top + .map( + (s, i) => + `${i + 1}. ${s.moniker} (${s.operatorAddress.slice(0, 14)}…) — composite ${s.compositeScore} (uptime ${s.uptimeScore}, gov ${s.governanceScore}, stability ${s.stabilityScore}) — stake ${s.stakePct.toFixed(2)}%` + ) + .join("\n")} + +## Alerts (${alerts.length}) +${ + alerts.length === 0 + ? "- None" + : alerts.map((a) => `- ${a.severity}: ${a.moniker} — ${a.reason}`).join("\n") +} + +Generate a structured Markdown report with: +1. Header "Validator Performance Report" with active set size and PoA eligibility ratio +2. "Top 10 Validators" table with: rank, moniker, uptime ratio, gov participation, stability, composite +3. "Scoring Methodology (M014)" block using ONLY the weights passed in the prompt +4. "Alerts This Period" bullet list using ONLY the alerts passed in +5. "PoA Transition Readiness" section — ratio of validators meeting the threshold, no speculation about when PoA will activate + +Use ONLY the numbers provided. Do not recommend delegation choices.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1500, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-VM-02: Delegation flow narrative +// ============================================================ + +export async function describeDelegationFlows( + summary: DelegationFlowSummary +): Promise { + const prompt = `Generate a Delegation Flow Report for the Regen staking set. + +## Deterministic Pipeline Output +- Window: ${summary.windowLabel} +- Validators with any flow: ${summary.validatorsWithFlow} +- Total inflow: ${summary.totalInflowUregen} uregen +- Total outflow: ${summary.totalOutflowUregen} uregen +- Net flow (signed): ${summary.netFlowUregen} uregen +- Whale-sized flows (>= ${config.validator.whaleDelegationUregen} uregen): ${summary.whaleFlowCount} +- Top inflow validator: ${summary.topInflow ? `${summary.topInflow.moniker} (+${summary.topInflow.deltaAbsUregen} uregen)` : "(none)"} +- Top outflow validator: ${summary.topOutflow ? `${summary.topOutflow.moniker} (-${summary.topOutflow.deltaAbsUregen} uregen)` : "(none)"} +- Captured: ${summary.capturedAt} + +Generate a structured Markdown report with: +1. Header "Delegation Flow Report" with severity chosen from the whale count and net flow sign +2. "Flow Summary" table with inflow / outflow / net / whale count +3. "Notable Movements" section listing top inflow and top outflow (or a note if none) +4. "Assessment" section — one or two sentences noting whether the net flow direction warrants continued monitoring + +Use ONLY the numbers provided. Do not predict future delegation behavior.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-VM-03: Decentralization narrative +// ============================================================ + +export async function describeDecentralizationSnapshot( + snapshot: DecentralizationSnapshot, + previous: DecentralizationSnapshot | null +): Promise { + const prev = previous + ? ` +## Previous Snapshot (${previous.capturedAt}) +- Validator count: ${previous.validatorCount} +- Nakamoto: ${previous.nakamotoCoefficient} +- Gini: ${previous.giniIndex.toFixed(3)} +- Top-10 share: ${previous.top10SharePct.toFixed(2)}% +- Health: ${previous.health} +` + : "## Previous Snapshot\nNone — this is the first decentralization snapshot."; + + const prompt = `Generate a Network Decentralization Report for Regen Network. + +## Deterministic Pipeline Output +- Active validator count: ${snapshot.validatorCount} +- Bonded: ${snapshot.bondedUregen} uregen +- Nakamoto coefficient: ${snapshot.nakamotoCoefficient} +- Gini index: ${snapshot.giniIndex.toFixed(3)} +- Top-10 share: ${snapshot.top10SharePct.toFixed(2)}% +- Top-20 share: ${snapshot.top20SharePct.toFixed(2)}% +- Largest validator: ${snapshot.largestShareValidator} (${snapshot.largestSharePct.toFixed(2)}%) +- Health: ${snapshot.health} +- Captured: ${snapshot.capturedAt} + +${prev} + +Generate a structured Markdown report with: +1. Header "Network Decentralization Report" with health badge +2. "Metrics" table with current snapshot values +3. "Trend" section comparing to the previous snapshot (or noting this is the first) +4. "Assessment" — one paragraph interpreting the Nakamoto coefficient and Gini. If health is WARNING or CRITICAL, explain what triggered it using the thresholds in config (nakamoto < ${config.validator.nakamotoWarningFloor}, gini > ${config.validator.giniWarningCeiling}, single validator > ${config.validator.criticalConcentrationPct}%) + +Use ONLY the numbers provided. Do not speculate about future decentralization.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1200, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// Helpers +// ============================================================ + +function extractText(response: Anthropic.Message): string { + return response.content + .filter((b): b is Anthropic.TextBlock => b.type === "text") + .map((b) => b.text) + .join("\n"); +} diff --git a/agent-004-validator-monitor/src/ooda.ts b/agent-004-validator-monitor/src/ooda.ts new file mode 100644 index 0000000..a05e332 --- /dev/null +++ b/agent-004-validator-monitor/src/ooda.ts @@ -0,0 +1,83 @@ +import { config } from "./config.js"; +import { store } from "./store.js"; +import type { OODAExecution } from "./types.js"; + +/** + * Generic OODA loop executor. + * + * Same shape as agent-002 and agent-003. Kept locally so this agent + * can run as a standalone Node process. If the agents converge on a + * common runtime later, this is a candidate to lift into a shared + * package. + */ +export interface OODAWorkflow { + id: string; + name: string; + observe: () => Promise; + orient: (observations: TObserve) => Promise; + decide: (orientation: TOrient) => Promise; + act: (decision: TDecide) => Promise; +} + +export async function executeOODA( + workflow: OODAWorkflow +): Promise> { + const executionId = crypto.randomUUID(); + const execution: OODAExecution = { + executionId, + workflowId: workflow.id, + status: "running", + observations: null as unknown as TObserve, + orientation: null, + decision: null, + actions: null, + startedAt: new Date(), + completedAt: null, + error: null, + }; + + const log = (phase: string, msg: string) => + console.log( + `[${new Date().toISOString()}] [${workflow.id}] [${phase}] ${msg}` + ); + + try { + log("OBSERVE", "Gathering data..."); + execution.observations = await workflow.observe(); + + log("ORIENT", "Analyzing context..."); + execution.orientation = await workflow.orient(execution.observations); + + log("DECIDE", "Making decision..."); + execution.decision = await workflow.decide(execution.orientation); + + log("ACT", "Executing actions..."); + execution.actions = await workflow.act(execution.decision); + + execution.status = "completed"; + log("DONE", "Workflow completed successfully."); + } catch (err) { + execution.status = "failed"; + execution.error = err instanceof Error ? err.message : String(err); + log("ERROR", execution.error); + } + + execution.completedAt = new Date(); + + store.logExecution({ + executionId: execution.executionId, + workflowId: execution.workflowId, + agentId: config.agentId, + status: execution.status, + startedAt: execution.startedAt.toISOString(), + completedAt: execution.completedAt.toISOString(), + result: JSON.stringify({ + orientation: execution.orientation, + decision: execution.decision, + actions: execution.actions, + error: execution.error, + }), + }); + + return execution; +} diff --git a/agent-004-validator-monitor/src/output.ts b/agent-004-validator-monitor/src/output.ts new file mode 100644 index 0000000..64f3ee9 --- /dev/null +++ b/agent-004-validator-monitor/src/output.ts @@ -0,0 +1,58 @@ +import { config } from "./config.js"; +import type { OutputMessage } from "./types.js"; + +/** + * Output dispatcher. + * + * Sends agent outputs to configured channels: + * - Console (always) + * - Discord webhook (if configured) + * + * Mirrors the AGENT-002 and AGENT-003 dispatcher shape. + */ +export async function output(msg: OutputMessage): Promise { + logToConsole(msg); + + if (config.discordWebhookUrl) { + await postToDiscord(msg).catch((err) => + console.error(` Discord post failed: ${err}`) + ); + } +} + +function logToConsole(msg: OutputMessage): void { + const prefix = + msg.alertLevel === "CRITICAL" + ? "!!! CRITICAL" + : msg.alertLevel === "HIGH" + ? "!! HIGH" + : "--"; + + console.log(`\n${"=".repeat(72)}`); + console.log( + `${prefix} [${msg.workflow}] ${msg.subjectId}: ${msg.title}` + ); + console.log(`${"=".repeat(72)}`); + console.log(msg.content); + console.log(`${"=".repeat(72)}\n`); +} + +async function postToDiscord(msg: OutputMessage): Promise { + const maxLen = 1900; + let content = `**[${msg.workflow}]** ${msg.subjectId}: **${msg.title}**\n\n`; + const remaining = maxLen - content.length; + content += + msg.content.length > remaining + ? msg.content.slice(0, remaining - 20) + "\n\n*...truncated*" + : msg.content; + + await fetch(config.discordWebhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + username: config.agentName, + content, + }), + signal: AbortSignal.timeout(10_000), + }); +} diff --git a/agent-004-validator-monitor/src/store.ts b/agent-004-validator-monitor/src/store.ts new file mode 100644 index 0000000..ef6ad65 --- /dev/null +++ b/agent-004-validator-monitor/src/store.ts @@ -0,0 +1,335 @@ +import Database from "better-sqlite3"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const DB_PATH = path.join(__dirname, "..", "agent-004.db"); + +/** + * Local SQLite store for AGENT-004 state. + * + * Tracks per-validator token snapshots (used as the delta source for + * WF-VM-02 until a real tx-stream lands), scorecards, decentralization + * snapshots, and workflow execution history. + */ +export class Store { + private db: Database.Database; + + /** + * `dbPath` defaults to the per-agent DB file on disk. Tests can + * pass `":memory:"` (or a temp file) to avoid clobbering the shared + * DB file and to run test suites in parallel without hitting the + * `database is locked` failure mode. + */ + constructor(dbPath: string = DB_PATH) { + this.db = new Database(dbPath); + this.db.pragma("journal_mode = WAL"); + this.migrate(); + } + + private migrate() { + this.db.exec(` + CREATE TABLE IF NOT EXISTS workflow_executions ( + execution_id TEXT PRIMARY KEY, + workflow_id TEXT NOT NULL, + agent_id TEXT NOT NULL, + status TEXT NOT NULL, + started_at TEXT NOT NULL, + completed_at TEXT, + result TEXT + ); + + -- Snapshot of a validator's staked tokens at a point in time. + -- WF-VM-02 reads the most recent previous row per operator to + -- derive the per-cycle delegation delta. + CREATE TABLE IF NOT EXISTS validator_token_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL, + moniker TEXT NOT NULL, + tokens TEXT NOT NULL, + commission_rate TEXT NOT NULL, + jailed INTEGER NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Snapshot of the commission rate timeline per validator. WF-VM-01 + -- uses this to count commission changes in the stability score. + CREATE TABLE IF NOT EXISTS commission_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL, + commission_rate TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Cached scorecards keyed by operator + captured_at so the + -- narrative layer can look up the previous scorecard for a + -- validator when reporting a change. + CREATE TABLE IF NOT EXISTS scorecards ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + operator_address TEXT NOT NULL, + composite_score INTEGER NOT NULL, + scorecard TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Decentralization snapshots (WF-VM-03) + CREATE TABLE IF NOT EXISTS decentralization_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nakamoto INTEGER NOT NULL, + gini REAL NOT NULL, + top10_pct REAL NOT NULL, + health TEXT NOT NULL, + snapshot TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_tokens_operator ON validator_token_snapshots(operator_address); + CREATE INDEX IF NOT EXISTS idx_commission_operator ON commission_history(operator_address); + CREATE INDEX IF NOT EXISTS idx_scorecards_operator ON scorecards(operator_address); + `); + } + + // ── Token snapshots (for WF-VM-02 delta source) ──────────── + + recordTokenSnapshot(row: { + operatorAddress: string; + moniker: string; + tokens: string; + commissionRate: string; + jailed: boolean; + }): void { + this.db + .prepare( + `INSERT INTO validator_token_snapshots + (operator_address, moniker, tokens, commission_rate, jailed, captured_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + row.operatorAddress, + row.moniker, + row.tokens, + row.commissionRate, + row.jailed ? 1 : 0, + new Date().toISOString() + ); + } + + getPreviousTokenSnapshot( + operatorAddress: string, + excludeLatest: boolean + ): { tokens: string; captured_at: string } | null { + // When excludeLatest is true, skip the most recent row (the one we + // just wrote this cycle) and return the one before it. + const offset = excludeLatest ? 1 : 0; + const row = this.db + .prepare( + `SELECT tokens, captured_at FROM validator_token_snapshots + WHERE operator_address = ? ORDER BY id DESC LIMIT 1 OFFSET ?` + ) + .get(operatorAddress, offset) as + | { tokens: string; captured_at: string } + | undefined; + return row || null; + } + + // ── Commission history ───────────────────────────────────── + + recordCommissionIfChanged( + operatorAddress: string, + commissionRate: string + ): boolean { + const latest = this.db + .prepare( + `SELECT commission_rate FROM commission_history + WHERE operator_address = ? ORDER BY id DESC LIMIT 1` + ) + .get(operatorAddress) as { commission_rate: string } | undefined; + if (latest && latest.commission_rate === commissionRate) return false; + + this.db + .prepare( + `INSERT INTO commission_history + (operator_address, commission_rate, captured_at) + VALUES (?, ?, ?)` + ) + .run(operatorAddress, commissionRate, new Date().toISOString()); + return true; + } + + countCommissionChangesSince( + operatorAddress: string, + sinceIso: string + ): number { + // Count every distinct commission-rate row recorded in the + // trailing window. The trick is the "is the first row in the + // window actually a change" question: if there's an older row + // from *before* the window, the first in-window row *is* a change + // (the rate transitioned from the older one). If there is no + // older row, the first in-window row is the baseline read and + // should not be counted. + // + // The previous implementation counted `cnt - 1`, which dropped a + // real commission change whenever the baseline read fell outside + // the window — e.g. "validator changed commission once in the + // last 30 days but last recorded 40 days ago" returned 0. + const inWindow = this.db + .prepare( + `SELECT COUNT(*) AS cnt FROM commission_history + WHERE operator_address = ? AND captured_at >= ?` + ) + .get(operatorAddress, sinceIso) as { cnt: number }; + + if (inWindow.cnt === 0) return 0; + + const hasBaseline = this.db + .prepare( + `SELECT 1 FROM commission_history + WHERE operator_address = ? AND captured_at < ? LIMIT 1` + ) + .get(operatorAddress, sinceIso) as { 1: number } | undefined; + + // When a baseline row exists outside the window, every in-window + // row is a change. Otherwise the first in-window row is the + // baseline and the remaining rows are changes. + return hasBaseline ? inWindow.cnt : Math.max(0, inWindow.cnt - 1); + } + + // ── Scorecards ───────────────────────────────────────────── + + saveScorecard(row: { + operatorAddress: string; + compositeScore: number; + scorecard: string; + }): void { + this.db + .prepare( + `INSERT INTO scorecards + (operator_address, composite_score, scorecard, captured_at) + VALUES (?, ?, ?, ?)` + ) + .run( + row.operatorAddress, + row.compositeScore, + row.scorecard, + new Date().toISOString() + ); + } + + getLatestScorecard( + operatorAddress: string + ): { compositeScore: number; scorecard: string; capturedAt: string } | null { + const row = this.db + .prepare( + `SELECT composite_score, scorecard, captured_at FROM scorecards + WHERE operator_address = ? ORDER BY id DESC LIMIT 1` + ) + .get(operatorAddress) as + | { composite_score: number; scorecard: string; captured_at: string } + | undefined; + if (!row) return null; + return { + compositeScore: row.composite_score, + scorecard: row.scorecard, + capturedAt: row.captured_at, + }; + } + + // ── Decentralization snapshots ───────────────────────────── + + saveDecentralizationSnapshot(row: { + nakamoto: number; + gini: number; + top10Pct: number; + health: string; + snapshot: string; + }): void { + this.db + .prepare( + `INSERT INTO decentralization_snapshots + (nakamoto, gini, top10_pct, health, snapshot, captured_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + row.nakamoto, + row.gini, + row.top10Pct, + row.health, + row.snapshot, + new Date().toISOString() + ); + } + + getLatestDecentralizationSnapshot(): { + nakamoto: number; + gini: number; + top10_pct: number; + health: string; + snapshot: string; + captured_at: string; + } | null { + // `LIMIT 1 OFFSET 0` intentionally — the caller invokes this in + // the `orient` phase before the current cycle's snapshot has + // been written, so the newest row in the table is the actual + // "previous" cycle's snapshot. An `OFFSET 1` here would skip + // that row and compare against the cycle before last, which + // shifts the trend analysis by one full cycle and returns null + // on the agent's second run (when only one row exists). + const row = this.db + .prepare( + `SELECT nakamoto, gini, top10_pct, health, snapshot, captured_at + FROM decentralization_snapshots ORDER BY id DESC LIMIT 1` + ) + .get() as + | { + nakamoto: number; + gini: number; + top10_pct: number; + health: string; + snapshot: string; + captured_at: string; + } + | undefined; + return row || null; + } + + // ── Workflow executions ──────────────────────────────────── + + logExecution(exec: { + executionId: string; + workflowId: string; + agentId: string; + status: string; + startedAt: string; + completedAt: string; + result: string; + }): void { + this.db + .prepare( + `INSERT INTO workflow_executions + (execution_id, workflow_id, agent_id, status, started_at, completed_at, result) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ) + .run( + exec.executionId, + exec.workflowId, + exec.agentId, + exec.status, + exec.startedAt, + exec.completedAt, + exec.result + ); + } + + getExecutionCount(): number { + const row = this.db + .prepare("SELECT COUNT(*) as cnt FROM workflow_executions") + .get() as { cnt: number }; + return row.cnt; + } + + close(): void { + this.db.close(); + } +} + +export const store = new Store(); diff --git a/agent-004-validator-monitor/src/types.ts b/agent-004-validator-monitor/src/types.ts new file mode 100644 index 0000000..4fd38f4 --- /dev/null +++ b/agent-004-validator-monitor/src/types.ts @@ -0,0 +1,188 @@ +// ============================================================ +// Cosmos SDK staking + slashing + gov types +// ============================================================ + +export interface Validator { + operator_address: string; + consensus_pubkey: { "@type": string; key: string }; + jailed: boolean; + status: string; + tokens: string; + delegator_shares: string; + description: { + moniker: string; + identity: string; + website: string; + security_contact: string; + details: string; + }; + unbonding_height: string; + unbonding_time: string; + commission: { + commission_rates: { + rate: string; + max_rate: string; + max_change_rate: string; + }; + update_time: string; + }; + min_self_delegation: string; +} + +export interface SigningInfo { + address: string; + start_height: string; + index_offset: string; + jailed_until: string; + tombstoned: boolean; + missed_blocks_counter: string; +} + +export interface SlashingParams { + signed_blocks_window: string; + min_signed_per_window: string; + downtime_jail_duration: string; + slash_fraction_double_sign: string; + slash_fraction_downtime: string; +} + +export interface StakingPool { + bonded_tokens: string; + not_bonded_tokens: string; +} + +export interface Proposal { + id: string; + status: string; + voting_start_time: string; + voting_end_time: string; + content: { "@type": string; title: string; description: string }; +} + +export interface Vote { + proposal_id: string; + voter: string; + option: string; + options: { option: string; weight: string }[]; +} + +// ============================================================ +// OODA loop types +// ============================================================ + +export interface OODAExecution { + executionId: string; + workflowId: string; + status: "running" | "completed" | "failed" | "escalated"; + observations: TObserve; + orientation: TOrient | null; + decision: TDecide | null; + actions: TAct | null; + startedAt: Date; + completedAt: Date | null; + error: string | null; +} + +// ============================================================ +// Workflow-specific types +// ============================================================ + +export type AlertLevel = "NORMAL" | "HIGH" | "CRITICAL"; + +/** Validator performance scorecard (WF-VM-01) */ +export interface ValidatorScorecard { + operatorAddress: string; + moniker: string; + jailed: boolean; + tokens: string; + stakePct: number; + commissionRate: number; + uptimeScore: number; // 0..scoreWeightUptime + uptimeRatio: number; // 0..1 + missedBlocks: number; + signedBlocksWindow: number; + governanceScore: number; // 0..scoreWeightGovernance + governanceParticipation: number; // 0..1 + proposalsConsidered: number; + votesCast: number; + stabilityScore: number; // 0..scoreWeightStability + compositeScore: number; // 0..1000 + poaEligible: boolean; +} + +export interface PerformanceAlert { + operatorAddress: string; + moniker: string; + severity: AlertLevel; + reason: string; +} + +/** A single on-chain staking event parsed from a MsgDelegate / + * MsgUndelegate / MsgBeginRedelegate transaction. WF-VM-02's + * observe phase harvests these directly from the LCD tx-search + * endpoint; the old token-delta proxy is no longer used. */ +export interface DelegationEvent { + txHash: string; + eventType: "delegate" | "undelegate" | "redelegate"; + delegator: string; + /** For delegate/undelegate, the operator address of the validator. + * For redelegate, the destination_validator (where stake moves TO). */ + validator: string; + /** Only set for redelegate. Otherwise null. */ + sourceValidator: string | null; + /** Amount in uregen as a string to preserve BigInt precision. */ + amountUregen: string; + occurredAt: string; +} + +/** Delegation flow analysis (WF-VM-02) */ +export interface DelegationFlow { + operatorAddress: string; + moniker: string; + previousTokens: string; + currentTokens: string; + deltaUregen: string; // signed + deltaAbsUregen: string; + isWhale: boolean; + flowDirection: "INFLOW" | "OUTFLOW" | "FLAT"; + capturedAt: string; +} + +export interface DelegationFlowSummary { + windowLabel: string; + totalInflowUregen: string; + totalOutflowUregen: string; + netFlowUregen: string; // signed + validatorsWithFlow: number; + whaleFlowCount: number; + topInflow: DelegationFlow | null; + topOutflow: DelegationFlow | null; + capturedAt: string; +} + +/** Network decentralization (WF-VM-03) */ +export interface DecentralizationSnapshot { + validatorCount: number; + bondedUregen: string; + nakamotoCoefficient: number; + giniIndex: number; + top10SharePct: number; + top20SharePct: number; + largestShareValidator: string; + largestSharePct: number; + health: "HEALTHY" | "WARNING" | "CRITICAL"; + capturedAt: string; +} + +// ============================================================ +// Output types +// ============================================================ + +export interface OutputMessage { + workflow: string; + subjectId: string; + title: string; + content: string; + alertLevel: AlertLevel; + timestamp: Date; +} diff --git a/agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts b/agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts new file mode 100644 index 0000000..537b430 --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/decentralization-monitor.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect } from "vitest"; +import { + nakamotoCoefficient, + giniIndex, + topNSharePct, + classifyHealth, +} from "./decentralization-monitor.js"; +import { config } from "../config.js"; + +// ============================================================ +// nakamotoCoefficient — smallest set of validators whose cumulative +// share strictly exceeds 33.4% of the total bonded stake. +// ============================================================ + +describe("nakamotoCoefficient", () => { + it("returns 0 for empty input", () => { + expect(nakamotoCoefficient([], 0n)).toBe(0); + expect(nakamotoCoefficient([], 100n)).toBe(0); + }); + + it("returns 0 when total is zero", () => { + expect(nakamotoCoefficient([10n, 20n], 0n)).toBe(0); + }); + + it("returns 1 when a single validator holds the entire stake", () => { + expect(nakamotoCoefficient([1000n], 1000n)).toBe(1); + }); + + it("returns 1 when a single validator holds more than 33.4%", () => { + // Top validator has 40% (400 out of 1000) — a single validator can halt. + expect(nakamotoCoefficient([400n, 300n, 200n, 100n], 1000n)).toBe(1); + }); + + it("returns 2 when the top validator holds exactly 33.4%", () => { + // 334/1000 = 33.4% — the strict-inequality predicate `> threshold` + // means the top validator alone does not clear the line. Need a + // second validator. The SPEC defines Nakamoto as the SMALLEST n + // whose cumulative share STRICTLY EXCEEDS 33.4%. + expect(nakamotoCoefficient([334n, 333n, 333n], 1000n)).toBe(2); + }); + + it("returns 2 when the top two combined exceed 33.4%", () => { + // Top two: 200+180 = 380, exceeds 334 (threshold for 33.4% of 1000) + expect(nakamotoCoefficient([200n, 180n, 170n, 150n, 150n, 150n], 1000n)).toBe(2); + }); + + it("returns a larger N when stake is distributed evenly", () => { + // 10 validators, each with 100 out of 1000 total. + // Cumulative share reaches 33.4% between the 3rd and 4th validator + // (3*100 = 300 < 334, 4*100 = 400 > 334). So Nakamoto = 4. + const tokens = Array(10).fill(100n); + expect(nakamotoCoefficient(tokens, 1000n)).toBe(4); + }); + + it("returns the length when no subset exceeds the threshold", () => { + // Degenerate: total is larger than the sum of the list (would not + // happen in practice, but the guard clamps to the array length). + expect(nakamotoCoefficient([10n, 10n], 1000n)).toBe(2); + }); +}); + +// ============================================================ +// giniIndex — textbook Gini on an ascending-sorted array +// ============================================================ + +describe("giniIndex", () => { + it("returns 0 for empty input", () => { + expect(giniIndex([])).toBe(0); + }); + + it("returns 0 for a single-element distribution", () => { + expect(giniIndex([100])).toBe(0); + }); + + it("returns 0 for perfect equality", () => { + expect(giniIndex([100, 100, 100, 100])).toBe(0); + expect(giniIndex([5, 5, 5, 5, 5, 5])).toBe(0); + }); + + it("returns a positive number for unequal distribution", () => { + expect(giniIndex([10, 20, 30, 40])).toBeGreaterThan(0); + }); + + it("approaches 1 for maximally unequal distribution", () => { + // One player has everything, others have nothing + const gini = giniIndex([0, 0, 0, 0, 100]); + expect(gini).toBeGreaterThan(0.6); + expect(gini).toBeLessThan(1); + }); + + it("returns 0 when the cumulative sum is zero (all zeros)", () => { + expect(giniIndex([0, 0, 0])).toBe(0); + }); + + it("is monotonic — more inequality, higher Gini", () => { + const nearEqual = giniIndex([95, 96, 97, 98, 99, 100]); + const moreUnequal = giniIndex([10, 20, 30, 40, 50, 60]); + expect(moreUnequal).toBeGreaterThan(nearEqual); + }); +}); + +// ============================================================ +// topNSharePct — cumulative share of the top N +// ============================================================ + +describe("topNSharePct", () => { + it("returns 0 when total is zero", () => { + expect(topNSharePct([10n, 20n], 0n, 2)).toBe(0); + }); + + it("returns the correct share for the single largest validator", () => { + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 1)).toBe(40); + }); + + it("returns the correct share for the top 3", () => { + // 400 + 300 + 200 = 900 of 1000 = 90% + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 3)).toBe(90); + }); + + it("returns 100% when n exceeds the array length", () => { + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 100)).toBe(100); + }); + + it("returns 100% when n equals the array length", () => { + expect(topNSharePct([400n, 300n, 200n, 100n], 1000n, 4)).toBe(100); + }); + + it("has two-decimal precision on the returned percentage", () => { + // 333 / 1000 = 33.3% + expect(topNSharePct([333n, 333n, 334n], 1000n, 1)).toBe(33.3); + }); +}); + +// ============================================================ +// classifyHealth — composite health tier +// ============================================================ + +describe("classifyHealth", () => { + const v = config.validator; + + it("returns HEALTHY for high Nakamoto, low Gini, low concentration", () => { + expect(classifyHealth(20, 0.3, 10)).toBe("HEALTHY"); + }); + + it("returns CRITICAL when Nakamoto is at or below the critical floor", () => { + expect(classifyHealth(v.nakamotoCriticalFloor, 0.3, 10)).toBe("CRITICAL"); + expect(classifyHealth(v.nakamotoCriticalFloor - 1, 0.3, 10)).toBe("CRITICAL"); + }); + + it("returns CRITICAL when a single validator exceeds the critical concentration", () => { + expect(classifyHealth(20, 0.3, v.criticalConcentrationPct)).toBe("CRITICAL"); + expect(classifyHealth(20, 0.3, v.criticalConcentrationPct + 5)).toBe("CRITICAL"); + }); + + it("returns WARNING when Nakamoto is at the warning floor but above critical", () => { + expect(classifyHealth(v.nakamotoWarningFloor, 0.3, 10)).toBe("WARNING"); + }); + + it("returns WARNING when Gini exceeds the warning ceiling", () => { + expect(classifyHealth(20, v.giniWarningCeiling, 10)).toBe("WARNING"); + expect(classifyHealth(20, v.giniWarningCeiling + 0.05, 10)).toBe("WARNING"); + }); + + it("returns WARNING when a single validator is at the warning concentration", () => { + expect(classifyHealth(20, 0.3, v.warningConcentrationPct)).toBe("WARNING"); + }); + + it("CRITICAL wins over WARNING when multiple thresholds trip", () => { + expect( + classifyHealth(v.nakamotoCriticalFloor, v.giniWarningCeiling, v.warningConcentrationPct) + ).toBe("CRITICAL"); + }); +}); diff --git a/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts new file mode 100644 index 0000000..157e4f5 --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/decentralization-monitor.ts @@ -0,0 +1,229 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeDecentralizationSnapshot } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Validator, + DecentralizationSnapshot, + AlertLevel, +} from "../types.js"; + +/** + * WF-VM-03: Network Decentralization Monitoring + * + * Trigger: periodic (every cycle) OR validator set change + * Layer: 1 (alerts) / Layer 3 (if action needed) + * + * OODA: + * Observe — Fetch active validator set. + * Orient — Compute Nakamoto coefficient, Gini index, top-N shares. + * Classify health tier. + * Decide — Generate narrative via Claude. + * Act — Persist snapshot, output, alert on threshold breach. + */ + +interface Observations { + validators: Validator[]; +} + +interface Orientation { + snapshot: DecentralizationSnapshot; + previous: DecentralizationSnapshot | null; +} + +interface Decision { + snapshot: DecentralizationSnapshot; + previous: DecentralizationSnapshot | null; + report: string; + alertLevel: AlertLevel; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +/** Smallest number of validators whose cumulative share of the total + * bonded stake strictly exceeds 33.4%. Consensus breaks at 2/3+1, so + * the canonical Cosmos-era Nakamoto coefficient uses 33% as the + * halt-threshold proxy. */ +export function nakamotoCoefficient(sortedDescTokens: bigint[], total: bigint): number { + if (total === 0n || sortedDescTokens.length === 0) return 0; + const threshold = (total * 334n) / 1000n; // 33.4% + let cumulative = 0n; + for (let i = 0; i < sortedDescTokens.length; i++) { + cumulative += sortedDescTokens[i]!; + if (cumulative > threshold) return i + 1; + } + return sortedDescTokens.length; +} + +/** Gini index of a distribution. 0 = perfect equality, 1 = perfect + * inequality. Operates on ascending-sorted numbers and uses the + * textbook formula. */ +export function giniIndex(sortedAscTokens: number[]): number { + const n = sortedAscTokens.length; + if (n === 0) return 0; + let sum = 0; + let cumulative = 0; + for (let i = 0; i < n; i++) { + cumulative += sortedAscTokens[i]!; + sum += (2 * (i + 1) - n - 1) * sortedAscTokens[i]!; + } + if (cumulative === 0) return 0; + return Math.abs(sum / (n * cumulative)); +} + +export function topNSharePct( + sortedDescTokens: bigint[], + total: bigint, + n: number +): number { + if (total === 0n) return 0; + let acc = 0n; + for (let i = 0; i < Math.min(n, sortedDescTokens.length); i++) { + acc += sortedDescTokens[i]!; + } + return Number((acc * 10000n) / total) / 100; +} + +export function classifyHealth( + nakamoto: number, + gini: number, + largestSharePct: number +): DecentralizationSnapshot["health"] { + if ( + nakamoto <= config.validator.nakamotoCriticalFloor || + largestSharePct >= config.validator.criticalConcentrationPct + ) { + return "CRITICAL"; + } + if ( + nakamoto <= config.validator.nakamotoWarningFloor || + gini >= config.validator.giniWarningCeiling || + largestSharePct >= config.validator.warningConcentrationPct + ) { + return "WARNING"; + } + return "HEALTHY"; +} + +export function createDecentralizationMonitorWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-VM-03", + name: "Network Decentralization Monitoring", + + async observe(): Promise { + const validators = await ledger.getValidators(); + return { validators }; + }, + + async orient(obs: Observations): Promise { + const capturedAt = new Date().toISOString(); + const active = obs.validators.filter((v) => !v.jailed); + + const tokensBig = active.map((v) => BigInt(v.tokens || "0")); + const total = tokensBig.reduce((acc, t) => acc + t, 0n); + const sortedDesc = [...tokensBig].sort((a, b) => (a > b ? -1 : a < b ? 1 : 0)); + + // Convert uregen → JS number without pre-dividing: integer + // division by 1_000_000n floors validators with less than 1 + // REGEN to zero and loses every fractional REGEN for larger + // stakes, which is load-bearing for Gini. Number.MAX_SAFE_INTEGER + // is 2^53-1 ≈ 9e15 uregen ≈ 9 trillion REGEN, so the whole + // uregen value fits safely — no pre-scaling needed. + const tokensNum = active.map((v) => Number(BigInt(v.tokens || "0"))); + const sortedAscNum = [...tokensNum].sort((a, b) => a - b); + + const nakamoto = nakamotoCoefficient(sortedDesc, total); + const gini = giniIndex(sortedAscNum); + const top10 = topNSharePct(sortedDesc, total, 10); + const top20 = topNSharePct(sortedDesc, total, 20); + + let largest = 0n; + let largestMoniker = "(unknown)"; + let largestPct = 0; + for (const v of active) { + const t = BigInt(v.tokens || "0"); + if (t > largest) { + largest = t; + largestMoniker = v.description.moniker; + largestPct = total > 0n ? Number((t * 10000n) / total) / 100 : 0; + } + } + + const health = classifyHealth(nakamoto, gini, largestPct); + + const snapshot: DecentralizationSnapshot = { + validatorCount: active.length, + bondedUregen: total.toString(), + nakamotoCoefficient: nakamoto, + giniIndex: gini, + top10SharePct: top10, + top20SharePct: top20, + largestShareValidator: largestMoniker, + largestSharePct: largestPct, + health, + capturedAt, + }; + + // Read the PREVIOUS snapshot BEFORE this cycle's save so the + // narrative layer has a real trend reference. + const prevRow = store.getLatestDecentralizationSnapshot(); + let previous: DecentralizationSnapshot | null = null; + if (prevRow) { + try { + previous = JSON.parse(prevRow.snapshot) as DecentralizationSnapshot; + } catch { + previous = null; + } + } + + return { snapshot, previous }; + }, + + async decide(orientation: Orientation): Promise { + const { snapshot, previous } = orientation; + const report = await describeDecentralizationSnapshot(snapshot, previous); + + const alertLevel: AlertLevel = + snapshot.health === "CRITICAL" + ? "CRITICAL" + : snapshot.health === "WARNING" + ? "HIGH" + : "NORMAL"; + + return { snapshot, previous, report, alertLevel }; + }, + + async act(decision: Decision): Promise { + const { snapshot, report, alertLevel } = decision; + + store.saveDecentralizationSnapshot({ + nakamoto: snapshot.nakamotoCoefficient, + gini: snapshot.giniIndex, + top10Pct: snapshot.top10SharePct, + health: snapshot.health, + snapshot: JSON.stringify(snapshot), + }); + + await output({ + workflow: "WF-VM-03", + subjectId: "network", + title: `${snapshot.health} — nakamoto ${snapshot.nakamotoCoefficient}, gini ${snapshot.giniIndex.toFixed(3)}`, + content: report, + alertLevel, + timestamp: new Date(), + }); + + return { + saved: 1, + alertsSent: alertLevel !== "NORMAL" ? 1 : 0, + }; + }, + }; +} diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts new file mode 100644 index 0000000..19def0c --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.test.ts @@ -0,0 +1,240 @@ +import { describe, it, expect } from "vitest"; +import { + absBig, + aggregateEventsToFlows, + summarizeFlows, +} from "./delegation-flow-analysis.js"; +import type { DelegationEvent, DelegationFlow } from "../types.js"; + +// ============================================================ +// absBig — BigInt absolute value +// ============================================================ + +describe("absBig", () => { + it("returns 0 for 0n", () => { + expect(absBig(0n)).toBe(0n); + }); + + it("returns the value for positive inputs", () => { + expect(absBig(1n)).toBe(1n); + expect(absBig(1_000_000_000_000_000n)).toBe(1_000_000_000_000_000n); + }); + + it("returns the negated value for negative inputs", () => { + expect(absBig(-1n)).toBe(1n); + expect(absBig(-1_000_000_000_000_000n)).toBe(1_000_000_000_000_000n); + }); + + it("handles values beyond JS number safe integer range", () => { + const huge = 9_007_199_254_740_993n; + expect(absBig(huge)).toBe(huge); + expect(absBig(-huge)).toBe(huge); + }); + + it("is idempotent on positive inputs", () => { + expect(absBig(absBig(42n))).toBe(42n); + expect(absBig(absBig(-42n))).toBe(42n); + }); +}); + +// ============================================================ +// aggregateEventsToFlows — group events into per-validator flows +// ============================================================ + +function mkEvent(overrides: Partial = {}): DelegationEvent { + return { + txHash: "tx-test", + eventType: "delegate", + delegator: "regen1delegator", + validator: "regenvaloper1alpha", + sourceValidator: null, + amountUregen: "100", + occurredAt: "2026-02-18T12:00:00Z", + ...overrides, + }; +} + +describe("aggregateEventsToFlows", () => { + it("returns empty for empty input", () => { + expect(aggregateEventsToFlows([])).toEqual([]); + }); + + it("aggregates a single delegate event into one inflow", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ amountUregen: "500", validator: "regenvaloper1alpha" }), + ]); + expect(flows).toHaveLength(1); + expect(flows[0]!.operatorAddress).toBe("regenvaloper1alpha"); + expect(flows[0]!.flowDirection).toBe("INFLOW"); + expect(flows[0]!.deltaUregen).toBe("500"); + }); + + it("aggregates a single undelegate event into one outflow", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ + eventType: "undelegate", + amountUregen: "500", + validator: "regenvaloper1alpha", + }), + ]); + expect(flows).toHaveLength(1); + expect(flows[0]!.flowDirection).toBe("OUTFLOW"); + expect(flows[0]!.deltaUregen).toBe("-500"); + expect(flows[0]!.deltaAbsUregen).toBe("500"); + }); + + it("splits a redelegate event into source outflow + destination inflow", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ + eventType: "redelegate", + amountUregen: "1000", + validator: "regenvaloper1dest", + sourceValidator: "regenvaloper1src", + }), + ]); + expect(flows).toHaveLength(2); + const src = flows.find((f) => f.operatorAddress === "regenvaloper1src")!; + const dst = flows.find((f) => f.operatorAddress === "regenvaloper1dest")!; + expect(src.flowDirection).toBe("OUTFLOW"); + expect(src.deltaUregen).toBe("-1000"); + expect(dst.flowDirection).toBe("INFLOW"); + expect(dst.deltaUregen).toBe("1000"); + }); + + it("nets delegate and undelegate on the same validator", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ validator: "regenvaloper1alpha", amountUregen: "1000" }), + mkEvent({ + eventType: "undelegate", + validator: "regenvaloper1alpha", + amountUregen: "300", + }), + ]); + expect(flows).toHaveLength(1); + expect(flows[0]!.deltaUregen).toBe("700"); + expect(flows[0]!.flowDirection).toBe("INFLOW"); + }); + + it("skips validators whose net delta is exactly zero", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ validator: "regenvaloper1alpha", amountUregen: "500" }), + mkEvent({ + eventType: "undelegate", + validator: "regenvaloper1alpha", + amountUregen: "500", + }), + ]); + expect(flows).toEqual([]); + }); + + it("flags whale-sized deltas above the configured threshold", () => { + // whaleDelegationUregen default is 100_000_000_000 (100K REGEN). + const flows = aggregateEventsToFlows([ + mkEvent({ + validator: "regenvaloper1whale", + amountUregen: "200000000000", + }), + mkEvent({ + validator: "regenvaloper1minnow", + amountUregen: "1000", + }), + ]); + const whale = flows.find((f) => f.operatorAddress === "regenvaloper1whale")!; + const minnow = flows.find((f) => f.operatorAddress === "regenvaloper1minnow")!; + expect(whale.isWhale).toBe(true); + expect(minnow.isWhale).toBe(false); + }); + + it("skips events with zero or negative amountUregen", () => { + const flows = aggregateEventsToFlows([ + mkEvent({ amountUregen: "0" }), + mkEvent({ amountUregen: "-100" }), + ]); + expect(flows).toEqual([]); + }); + + it("skips events with a non-numeric amountUregen", () => { + const flows = aggregateEventsToFlows([mkEvent({ amountUregen: "not-a-number" })]); + expect(flows).toEqual([]); + }); +}); + +// ============================================================ +// summarizeFlows — summary math (inflow/outflow/net/whale/top) +// ============================================================ + +function mkFlow(overrides: Partial = {}): DelegationFlow { + return { + operatorAddress: "regenvaloper1test", + moniker: "Test", + previousTokens: "0", + currentTokens: "0", + deltaUregen: "0", + deltaAbsUregen: "0", + isWhale: false, + flowDirection: "FLAT", + capturedAt: "2026-02-18T12:00:00Z", + ...overrides, + }; +} + +describe("summarizeFlows", () => { + it("returns zero totals for empty input", () => { + const summary = summarizeFlows([]); + expect(summary.totalInflowUregen).toBe("0"); + expect(summary.totalOutflowUregen).toBe("0"); + expect(summary.netFlowUregen).toBe("0"); + expect(summary.validatorsWithFlow).toBe(0); + expect(summary.whaleFlowCount).toBe(0); + expect(summary.topInflow).toBeNull(); + expect(summary.topOutflow).toBeNull(); + }); + + it("sums inflows and outflows correctly", () => { + const summary = summarizeFlows([ + mkFlow({ deltaUregen: "1000", flowDirection: "INFLOW" }), + mkFlow({ deltaUregen: "500", flowDirection: "INFLOW" }), + mkFlow({ + deltaUregen: "-300", + deltaAbsUregen: "300", + flowDirection: "OUTFLOW", + }), + ]); + expect(summary.totalInflowUregen).toBe("1500"); + expect(summary.totalOutflowUregen).toBe("300"); + expect(summary.netFlowUregen).toBe("1200"); + expect(summary.validatorsWithFlow).toBe(3); + }); + + it("identifies the top inflow and top outflow", () => { + const summary = summarizeFlows([ + mkFlow({ + operatorAddress: "regenvaloper1big", + deltaUregen: "10000", + flowDirection: "INFLOW", + }), + mkFlow({ + operatorAddress: "regenvaloper1small", + deltaUregen: "100", + flowDirection: "INFLOW", + }), + mkFlow({ + operatorAddress: "regenvaloper1bigout", + deltaUregen: "-5000", + deltaAbsUregen: "5000", + flowDirection: "OUTFLOW", + }), + ]); + expect(summary.topInflow?.operatorAddress).toBe("regenvaloper1big"); + expect(summary.topOutflow?.operatorAddress).toBe("regenvaloper1bigout"); + }); + + it("counts whale flows separately from the total", () => { + const summary = summarizeFlows([ + mkFlow({ deltaUregen: "500000000000", isWhale: true, flowDirection: "INFLOW" }), + mkFlow({ deltaUregen: "300", isWhale: false, flowDirection: "INFLOW" }), + ]); + expect(summary.whaleFlowCount).toBe(1); + expect(summary.validatorsWithFlow).toBe(2); + }); +}); diff --git a/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts new file mode 100644 index 0000000..8746c69 --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/delegation-flow-analysis.ts @@ -0,0 +1,253 @@ +import { LedgerClient } from "../ledger.js"; +import { config } from "../config.js"; +import { describeDelegationFlows } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + DelegationEvent, + DelegationFlow, + DelegationFlowSummary, + AlertLevel, +} from "../types.js"; + +/** + * WF-VM-02: Delegation Flow Analysis + * + * Trigger: MsgDelegate / MsgUndelegate / MsgBeginRedelegate (real + * on-chain events via the LCD tx-search endpoint) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch recent delegation events from the LCD tx-search + * endpoint for all three staking message types. + * Orient — Aggregate events per validator into DelegationFlow + * records. Compute inflow/outflow/net across the whole + * set. Tag whale-sized movements. Pick top inflow and + * outflow. + * Decide — Generate flow summary via Claude. + * Act — Persist, output, alert on whale activity. + * + * Previous versions of this workflow used a token-delta MVP proxy + * — snapshotting `validator.tokens` per cycle and computing the + * delta against the previous snapshot. That proxy worked but + * could not distinguish delegate vs undelegate vs redelegate, did + * not carry delegator identity, and missed all the intra-cycle + * movements. The current implementation uses the real MsgDelegate + * / MsgUndelegate / MsgBeginRedelegate tx stream, so every flow + * in the summary is backed by a real on-chain transaction. + */ + +interface Observations { + events: DelegationEvent[]; +} + +interface Orientation { + flows: DelegationFlow[]; + summary: DelegationFlowSummary; +} + +interface Decision { + summary: DelegationFlowSummary; + report: string; + alertLevel: AlertLevel; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +export function absBig(x: bigint): bigint { + return x < 0n ? -x : x; +} + +/** + * Aggregate a flat list of DelegationEvent records into + * per-validator DelegationFlow records. Exported so unit tests + * can feed it synthetic input without going through the observe + * phase. + * + * Rules: + * - `delegate` → inflow to `event.validator` + * - `undelegate` → outflow from `event.validator` + * - `redelegate` → outflow from `event.sourceValidator`, + * inflow to `event.validator` (destination) + */ +export function aggregateEventsToFlows( + events: DelegationEvent[], + capturedAt: string = new Date().toISOString() +): DelegationFlow[] { + // Per-validator running deltas in uregen, signed. + const deltaByValidator = new Map(); + + const addDelta = (validator: string, delta: bigint) => { + const prev = deltaByValidator.get(validator) ?? 0n; + deltaByValidator.set(validator, prev + delta); + }; + + for (const ev of events) { + let amt: bigint; + try { + amt = BigInt(ev.amountUregen); + } catch { + continue; + } + if (amt <= 0n) continue; + + if (ev.eventType === "delegate") { + addDelta(ev.validator, amt); + } else if (ev.eventType === "undelegate") { + addDelta(ev.validator, -amt); + } else if (ev.eventType === "redelegate") { + if (ev.sourceValidator) addDelta(ev.sourceValidator, -amt); + addDelta(ev.validator, amt); + } + } + + const whaleThreshold = BigInt(config.validator.whaleDelegationUregen); + const flows: DelegationFlow[] = []; + + for (const [operatorAddress, delta] of deltaByValidator) { + if (delta === 0n) continue; + const deltaAbs = absBig(delta); + flows.push({ + operatorAddress, + // DelegationFlow carries a moniker for the narrative layer, but + // the event stream only has validator operator addresses. The + // orient phase inside the workflow resolves monikers by looking + // up the validator set once per cycle; the aggregator here + // leaves moniker as the operator address and expects the caller + // to backfill. Callers that only care about the aggregate math + // can use this field unchanged. + moniker: operatorAddress, + // Previous/current token fields are preserved for backward + // compatibility with the DelegationFlow shape, but they no + // longer correspond to validator.tokens snapshots — they + // describe the delta window instead. + previousTokens: "0", + currentTokens: delta.toString(), + deltaUregen: delta.toString(), + deltaAbsUregen: deltaAbs.toString(), + isWhale: deltaAbs >= whaleThreshold, + flowDirection: delta > 0n ? "INFLOW" : "OUTFLOW", + capturedAt, + }); + } + + return flows; +} + +/** + * Summarize a list of DelegationFlow records into a + * DelegationFlowSummary. Exported so unit tests can pin the + * summary math independently of the aggregator. + */ +export function summarizeFlows( + flows: DelegationFlow[], + capturedAt: string = new Date().toISOString() +): DelegationFlowSummary { + let totalInflow = 0n; + let totalOutflow = 0n; + let whaleFlowCount = 0; + let topInflow: DelegationFlow | null = null; + let topOutflow: DelegationFlow | null = null; + + for (const f of flows) { + const delta = BigInt(f.deltaUregen); + if (delta > 0n) { + totalInflow += delta; + if (!topInflow || delta > BigInt(topInflow.deltaUregen)) { + topInflow = f; + } + } else if (delta < 0n) { + totalOutflow += -delta; + if (!topOutflow || -delta > BigInt(topOutflow.deltaAbsUregen)) { + topOutflow = f; + } + } + if (f.isWhale) whaleFlowCount++; + } + + return { + windowLabel: "recent tx-search window", + totalInflowUregen: totalInflow.toString(), + totalOutflowUregen: totalOutflow.toString(), + netFlowUregen: (totalInflow - totalOutflow).toString(), + validatorsWithFlow: flows.length, + whaleFlowCount, + topInflow, + topOutflow, + capturedAt, + }; +} + +export function createDelegationFlowAnalysisWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-VM-02", + name: "Delegation Flow Analysis", + + async observe(): Promise { + // Pull recent delegation tx events via the LCD tx-search + // endpoint. The ledger client queries each of the three + // staking message types separately and flattens the results + // into a single DelegationEvent list. + const events = await ledger.getRecentDelegationTxs(200); + return { events }; + }, + + async orient(obs: Observations): Promise { + const capturedAt = new Date().toISOString(); + const flows = aggregateEventsToFlows(obs.events, capturedAt); + + // Backfill monikers from the current validator set. This is a + // single extra LCD call per cycle — cheap — and it lets the + // narrative layer show operator names instead of opaque + // bech32 addresses. + const validators = await ledger.getValidators(); + const monikers = new Map(); + for (const v of validators) { + monikers.set(v.operator_address, v.description.moniker); + } + for (const f of flows) { + const moniker = monikers.get(f.operatorAddress); + if (moniker) f.moniker = moniker; + } + + const summary = summarizeFlows(flows, capturedAt); + return { flows, summary }; + }, + + async decide(orientation: Orientation): Promise { + const report = await describeDelegationFlows(orientation.summary); + + const alertLevel: AlertLevel = + orientation.summary.whaleFlowCount > 0 ? "HIGH" : "NORMAL"; + + return { + summary: orientation.summary, + report, + alertLevel, + }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + await output({ + workflow: "WF-VM-02", + subjectId: "delegation-flows", + title: `Net ${decision.summary.netFlowUregen} uregen (${decision.summary.whaleFlowCount} whale)`, + content: decision.report, + alertLevel: decision.alertLevel, + timestamp: new Date(), + }); + saved++; + if (decision.alertLevel !== "NORMAL") alertsSent++; + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-004-validator-monitor/src/workflows/performance-tracking.ts b/agent-004-validator-monitor/src/workflows/performance-tracking.ts new file mode 100644 index 0000000..b191c2d --- /dev/null +++ b/agent-004-validator-monitor/src/workflows/performance-tracking.ts @@ -0,0 +1,313 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describePerformanceReport } from "../monitor.js"; +import { output } from "../output.js"; +import { + operatorToDelegator, + consensusPubkeyToConsAddress, +} from "../bech32.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Validator, + SigningInfo, + SlashingParams, + StakingPool, + Proposal, + ValidatorScorecard, + PerformanceAlert, +} from "../types.js"; + +/** + * WF-VM-01: Validator Performance Tracking + * + * Trigger: periodic OR slash event + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch validator set, signing info, slashing params, + * staking pool, and recent finalized proposals (for + * governance participation scoring). + * Orient — For each validator: compute uptime ratio, governance + * participation ratio, stability score. Combine into a + * composite 0-1000. + * Decide — Flag performance alerts (uptime drop, jailed, high + * concentration). Persist scorecards. + * Act — Generate narrative report via Claude, output. + * + * Determinism: all scoring is local TypeScript. Claude only writes + * the narrative report. + */ + +interface Observations { + validators: Validator[]; + signingByConsAddrLike: Map; + slashingParams: SlashingParams | null; + pool: StakingPool; + finalizedProposals: Proposal[]; + votesCastByOperator: Map; +} + +interface Orientation { + scorecards: ValidatorScorecard[]; + alerts: PerformanceAlert[]; + bondedUregen: string; +} + +interface Decision { + report: string; + alertsToPublish: PerformanceAlert[]; + scorecards: ValidatorScorecard[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + + +export function createPerformanceTrackingWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-VM-01", + name: "Validator Performance Tracking", + + async observe(): Promise { + const [validators, signingInfos, slashingParams, pool, finalizedProposals] = + await Promise.all([ + ledger.getValidators(), + ledger.getSigningInfos(), + ledger.getSlashingParams(), + ledger.getStakingPool(), + ledger.getRecentFinalizedProposals(20), + ]); + + // Index signing info by the consensus address field so we can + // match validators by their consensus-derived key. Signing info + // uses the valcons bech32 form; the validator object exposes + // `consensus_pubkey.key`. The MVP joins on the raw `address` + // field and falls back to `missed_blocks_counter = 0` when we + // can't resolve a match. + const signingByConsAddrLike = new Map(); + for (const info of signingInfos) { + signingByConsAddrLike.set(info.address, info); + } + + // Votes cast per operator — computed lazily here so the orient + // phase can compute the ratio without hitting the LCD again. + // Future work: convert the operator's valoper bech32 to the + // delegator bech32 and query /proposals/{id}/votes/{voter}. + const votesCastByOperator = new Map(); + + return { + validators, + signingByConsAddrLike, + slashingParams, + pool, + finalizedProposals, + votesCastByOperator, + }; + }, + + async orient(obs: Observations): Promise { + const { validators, signingByConsAddrLike, slashingParams, pool } = obs; + + const signedBlocksWindow = slashingParams + ? Number(slashingParams.signed_blocks_window) + : 10_000; + + const bonded = BigInt(pool.bonded_tokens || "0"); + const scorecards: ValidatorScorecard[] = []; + const alerts: PerformanceAlert[] = []; + + // Compute the trailing-window cutoff once — it does not depend + // on the validator, so there is no point recomputing it per + // iteration the way the original loop did. + const sinceIso = new Date( + Date.now() - config.validator.uptimeTrailingDays * 86_400_000 + ).toISOString(); + + // Record a commission baseline for every validator we see this + // cycle; the commission history drives the stability penalty. + for (const v of validators) { + store.recordCommissionIfChanged( + v.operator_address, + v.commission.commission_rates.rate + ); + } + + for (const v of validators) { + const tokens = BigInt(v.tokens || "0"); + const stakePct = + bonded > 0n ? Number((tokens * 10000n) / bonded) / 100 : 0; + + // ── Uptime component ───────────────────────────────── + // Derive the regenvalcons1… address from the validator's + // consensus pubkey so it matches the keys in + // signingByConsAddrLike (which are `info.address`, i.e. the + // valcons bech32). Previously we were joining a base64 + // pubkey string against a bech32 address and the lookup + // always missed — every validator was reporting 0 missed + // blocks, which masked real downtime. When the derivation or + // the lookup still fails we fall back to 100% uptime rather + // than penalizing a validator we cannot classify. + const pubkeyBase64 = v.consensus_pubkey?.key || ""; + const consAddr = consensusPubkeyToConsAddress(pubkeyBase64); + const signing = consAddr + ? signingByConsAddrLike.get(consAddr) + : undefined; + const missedBlocks = signing + ? Number(signing.missed_blocks_counter || "0") + : 0; + const signedBlocks = Math.max(1, signedBlocksWindow - missedBlocks); + const uptimeRatio = Math.max( + 0, + Math.min(1, signedBlocks / signedBlocksWindow) + ); + const uptimeScore = Math.round( + uptimeRatio * config.validator.scoreWeightUptime + ); + + // ── Governance component ───────────────────────────── + // Derive the delegator bech32 from the operator address so we + // could plug in real per-validator vote records downstream. + // The observe phase still leaves `votesCastByOperator` empty + // in this PR (governance scoring is batched in WF-VM-03), so + // every validator is treated as 0-votes relative to the same + // denominator until that wiring lands. + void operatorToDelegator(v.operator_address); + const proposalsConsidered = obs.finalizedProposals.length; + const votesCast = obs.votesCastByOperator.get(v.operator_address) ?? 0; + const governanceParticipation = + proposalsConsidered > 0 ? votesCast / proposalsConsidered : 0; + const governanceScore = Math.round( + governanceParticipation * config.validator.scoreWeightGovernance + ); + + // ── Stability component ────────────────────────────── + // Start at full stability weight; subtract penalties for + // jailing and commission changes in the trailing window. + // Explicit `number` type so we can clamp to 0 below — `as + // const` in config.ts otherwise infers a literal type. + let stabilityScore: number = config.validator.scoreWeightStability; + if (v.jailed) stabilityScore -= config.validator.stabilityPenaltyJailing; + const commissionChanges = store.countCommissionChangesSince( + v.operator_address, + sinceIso + ); + stabilityScore -= + commissionChanges * config.validator.stabilityPenaltyCommissionChange; + if (stabilityScore < 0) stabilityScore = 0; + + const compositeScore = + uptimeScore + governanceScore + stabilityScore; + const poaEligible = + compositeScore >= config.validator.poaEligibilityScore; + + const card: ValidatorScorecard = { + operatorAddress: v.operator_address, + moniker: v.description.moniker, + jailed: v.jailed, + tokens: v.tokens, + stakePct, + commissionRate: Number(v.commission.commission_rates.rate), + uptimeScore, + uptimeRatio, + missedBlocks, + signedBlocksWindow, + governanceScore, + governanceParticipation, + proposalsConsidered, + votesCast, + stabilityScore, + compositeScore, + poaEligible, + }; + scorecards.push(card); + + if (v.jailed) { + alerts.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + severity: "CRITICAL", + reason: "Validator is currently jailed", + }); + } else if ( + missedBlocks > 0 && + missedBlocks / signedBlocksWindow > 0.05 + ) { + alerts.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + severity: "HIGH", + reason: `Missed ${missedBlocks}/${signedBlocksWindow} blocks (${((missedBlocks / signedBlocksWindow) * 100).toFixed(2)}%)`, + }); + } + if (stakePct >= config.validator.criticalConcentrationPct) { + alerts.push({ + operatorAddress: v.operator_address, + moniker: v.description.moniker, + severity: "CRITICAL", + reason: `Single validator holds ${stakePct.toFixed(2)}% of bonded stake`, + }); + } + } + + scorecards.sort((a, b) => b.compositeScore - a.compositeScore); + + return { + scorecards, + alerts, + bondedUregen: pool.bonded_tokens, + }; + }, + + async decide(orientation: Orientation): Promise { + const report = await describePerformanceReport( + orientation.scorecards, + orientation.alerts, + orientation.bondedUregen + ); + + return { + report, + alertsToPublish: orientation.alerts, + scorecards: orientation.scorecards, + }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + // Persist every scorecard so the historical timeline is usable + // for future PoA transition assessments. + for (const card of decision.scorecards) { + store.saveScorecard({ + operatorAddress: card.operatorAddress, + compositeScore: card.compositeScore, + scorecard: JSON.stringify(card), + }); + saved++; + } + + await output({ + workflow: "WF-VM-01", + subjectId: "validator-set", + title: "Performance report", + content: decision.report, + alertLevel: + decision.alertsToPublish.some((a) => a.severity === "CRITICAL") + ? "CRITICAL" + : decision.alertsToPublish.length > 0 + ? "HIGH" + : "NORMAL", + timestamp: new Date(), + }); + if (decision.alertsToPublish.length > 0) alertsSent++; + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-004-validator-monitor/tsconfig.json b/agent-004-validator-monitor/tsconfig.json new file mode 100644 index 0000000..e6a11e8 --- /dev/null +++ b/agent-004-validator-monitor/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts", "vitest.config.ts"] +} diff --git a/agent-004-validator-monitor/vitest.config.ts b/agent-004-validator-monitor/vitest.config.ts new file mode 100644 index 0000000..ce36a74 --- /dev/null +++ b/agent-004-validator-monitor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + }, +});