diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b09c30e..7a9c03f 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-003-market-monitor/ dependencies + working-directory: agent-003-market-monitor + run: npm ci + + - name: Typecheck agent-003-market-monitor/ + working-directory: agent-003-market-monitor + run: npx tsc --noEmit + # ── Spec verification: schemas, reference impls, datasets ─────────── verify-specs: name: Verify Specs diff --git a/agent-003-market-monitor/.env.example b/agent-003-market-monitor/.env.example new file mode 100644 index 0000000..f7da725 --- /dev/null +++ b/agent-003-market-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 market reports and alerts +DISCORD_WEBHOOK_URL= + +# Optional: polling interval in seconds (default: 300 = 5 minutes) +POLL_INTERVAL_SECONDS=300 + +# Optional: Claude model to use (default: claude-sonnet-4-5-20250929) +ANTHROPIC_MODEL=claude-sonnet-4-5-20250929 diff --git a/agent-003-market-monitor/.gitignore b/agent-003-market-monitor/.gitignore new file mode 100644 index 0000000..467cd39 --- /dev/null +++ b/agent-003-market-monitor/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +*.db +*.db-shm +*.db-wal +.env diff --git a/agent-003-market-monitor/README.md b/agent-003-market-monitor/README.md new file mode 100644 index 0000000..d885276 --- /dev/null +++ b/agent-003-market-monitor/README.md @@ -0,0 +1,111 @@ +# AGENT-003: Regen Market Monitor + +**Layer 1 (fully automated, read-only, informational) agent that watches the Regen ecocredit marketplace, detects price anomalies, tracks liquidity health, and summarizes retirement demand.** + +Mirrors the AGENT-002 Governance Analyst structure: the same OODA executor, the same standalone Node.js process shape, the same SQLite-backed local state. The scope is marketplace intelligence rather than governance. + +## What it does + +| Workflow | Trigger | Output | +|----------|---------|--------| +| **WF-MM-01** Price Anomaly Detection | New sell orders (SellOrderCreated / SellOrderFilled) | z-score anomaly alerts per severity, deduped | +| **WF-MM-02** Liquidity Monitoring | Periodic (every poll cycle) | Per-class liquidity health snapshot + trend vs previous | +| **WF-MM-03** Retirement Pattern Analysis | Periodic (every poll cycle) | Retirement volume, demand index, compliance metadata | + +Each workflow is an **OODA loop** (Observe → Orient → Decide → Act) and persists both executions and domain state to SQLite. Numeric decisions (median, z-score, severity classification, health tier) are computed **deterministically**; Claude is only used for the narrative layer. + +## Architecture + +``` +Regen Ledger (LCD REST API) + ↓ observe +AGENT-003 (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-003 lives at `agents/packages/agents/src/characters/market-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-003-market-monitor +npm install + +# 2. Configure +cp .env.example .env +# Edit .env — at minimum set ANTHROPIC_API_KEY + +# 3. Run (single analysis pass) +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 | `300` | Polling interval (seconds) | +| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-5-20250929` | Claude model to use | + +Thresholds live in `src/config.ts` under `market.*` and mirror the character definition at `agents/packages/agents/src/characters/market-monitor.ts`. + +## How it maps to the framework specs + +| Framework Spec | Implementation | +|----------------|---------------| +| Phase 2.2 WF-MM-01 | `src/workflows/price-anomaly-detection.ts` | +| Phase 2.2 WF-MM-02 | `src/workflows/liquidity-monitor.ts` | +| Phase 2.2 WF-MM-03 | `src/workflows/retirement-tracking.ts` | +| Phase 2.4 OODA executor | `src/ooda.ts` | +| Phase 2.4 Agent character | `agents/packages/agents/src/characters/market-monitor.ts` | +| Phase 2.5 Workflow executions table | `src/store.ts` (SQLite) | +| Phase 3.2 Ledger MCP client | `src/ledger.ts` (direct LCD) | + +## Design decisions + +1. **Deterministic numbers, narrative-only LLM calls.** Anomaly classification, liquidity health scoring, and demand index computation are all deterministic. Claude is only invoked to write the narrative report from those numbers. This keeps the agent cheap, reproducible, and auditable. + +2. **Sell-order-as-trade proxy (MVP).** Until Ledger MCP exposes filled-trade events, the agent treats open sell orders as trade observations for the z-score baseline. An unusually high or low ask price is still a market structure signal worth surfacing, and the code path swaps cleanly when real trade events become available. + +3. **MsgRetire tx-search as retirement source.** WF-MM-03 reads + recent retirement transactions from the Cosmos LCD `tx-search` + endpoint filtered on `message.action='/regen.ecocredit.v1.MsgRetire'`. + Each tx response is parsed into zero or more Retirement records + by harvesting `EventRetire` attributes (owner, batch_denom, amount, + jurisdiction, reason) from either `logs[].events[]` or the + flattened top-level `events[]` — the parser accepts both shapes + for cross-SDK compatibility. Earlier drafts used a batch-supply + delta as an MVP proxy; the current implementation produces + richer Retirement records with retiree identity and jurisdiction + metadata that the supply-delta proxy could not carry. + +4. **Dedupe by trade + severity.** WF-MM-01 only alerts once per `(trade_id, severity)` tuple. A trade that later escalates from WARNING to CRITICAL still fires a new alert; a trade that stays at the same severity does not. + +5. **Standalone over ElizaOS.** ElizaOS plugin API may change. A standalone process proves the workflow logic works independently of any runtime framework, matching AGENT-002's approach. + +## Governance layer + +This agent operates at **Layer 1 only**: + +- Read-only access to on-chain state +- Cannot submit proposals +- Cannot create, modify, or cancel sell orders +- Cannot execute transactions +- Informational output only + +This matches the framework's principle of starting with the lowest-risk, highest-value capability. Raising the automation layer is a separate governance decision. diff --git a/agent-003-market-monitor/package-lock.json b/agent-003-market-monitor/package-lock.json new file mode 100644 index 0000000..fda4fd4 --- /dev/null +++ b/agent-003-market-monitor/package-lock.json @@ -0,0 +1,2860 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "dependencies": { + "@anthropic-ai/sdk": "^0.39.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/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-003-market-monitor/package.json b/agent-003-market-monitor/package.json new file mode 100644 index 0000000..6f5a4af --- /dev/null +++ b/agent-003-market-monitor/package.json @@ -0,0 +1,30 @@ +{ + "name": "@regen/agent-003-market-monitor", + "version": "0.1.0", + "description": "Regen Network Market Monitor Agent — Layer 1 read-only price anomaly detection, liquidity monitoring, and retirement tracking", + "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" + }, + "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-003-market-monitor/src/config.ts b/agent-003-market-monitor/src/config.ts new file mode 100644 index 0000000..b65c526 --- /dev/null +++ b/agent-003-market-monitor/src/config.ts @@ -0,0 +1,51 @@ +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 — market monitor runs faster than governance analyst + // because trade/retirement events move quickly. + pollIntervalMs: (parseInt(process.env.POLL_INTERVAL_SECONDS || "300", 10)) * 1000, + + // Market monitor thresholds. These mirror the character thresholds in + // agents/packages/agents/src/characters/market-monitor.ts so downstream + // tooling has a single source of truth. Keep them in sync if either + // file changes. + market: { + /** z-score at or above which anomaly severity becomes CRITICAL */ + criticalZScore: 3.5, + /** z-score at which anomaly is flagged for watchlist (WARNING) */ + warningZScore: 2.0, + /** minimum sample size before computing a z-score */ + minSamples: 5, + /** trailing window for class median (days) */ + classMedianWindowDays: 30, + /** trailing window for batch median (days) */ + batchMedianWindowDays: 30, + /** liquidity depth threshold (USD) below which health is DEGRADED */ + liquidityDepthFloor: 5_000, + /** large trade volume (USD) that triggers an off-cycle liquidity report */ + largeTradeVolumeUsd: 10_000, + /** retirement tx window (hours) for demand signal extraction */ + retirementWindowHours: 168, // 7 days + }, + + // Agent identity + agentId: "AGENT-003", + agentName: "RegenMarketMonitor", + 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-003-market-monitor/src/index.ts b/agent-003-market-monitor/src/index.ts new file mode 100644 index 0000000..675b3ae --- /dev/null +++ b/agent-003-market-monitor/src/index.ts @@ -0,0 +1,114 @@ +#!/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 { createPriceAnomalyDetectionWorkflow } from "./workflows/price-anomaly-detection.js"; +import { createLiquidityMonitorWorkflow } from "./workflows/liquidity-monitor.js"; +import { createRetirementTrackingWorkflow } from "./workflows/retirement-tracking.js"; + +// ── Banner ──────────────────────────────────────────────────── + +function banner() { + console.log(` + ╔══════════════════════════════════════════════════════════════╗ + ║ REGEN MARKET MONITOR (AGENT-003) ║ + ║ ║ + ║ Layer 1 — Fully Automated, Informational Only ║ + ║ Workflows: WF-MM-01, WF-MM-02, WF-MM-03 ║ + ║ ║ + ║ Regen Agentic Tokenomics Framework ║ + ╚══════════════════════════════════════════════════════════════╝ +`); +} + +// ── Main loop ───────────────────────────────────────────────── + +async function runCycle(ledger: LedgerClient): Promise { + const ts = new Date().toISOString(); + console.log(`\n[${ts}] ═══ Starting market monitor cycle ═══\n`); + + // WF-MM-01: Detect price anomalies in open sell orders + const wf01 = createPriceAnomalyDetectionWorkflow(ledger); + await executeOODA(wf01); + + // WF-MM-02: Snapshot liquidity health per credit class + const wf02 = createLiquidityMonitorWorkflow(ledger); + await executeOODA(wf02); + + // WF-MM-03: Summarize retirement activity + demand signal + const wf03 = createRetirementTrackingWorkflow(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 database 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-003-market-monitor/src/ledger.test.ts b/agent-003-market-monitor/src/ledger.test.ts new file mode 100644 index 0000000..4b33ba4 --- /dev/null +++ b/agent-003-market-monitor/src/ledger.test.ts @@ -0,0 +1,241 @@ +import { describe, it, expect } from "vitest"; +import { LedgerClient } from "./ledger.js"; + +// ============================================================ +// parseRetirementsFromTx — event extraction from tx response +// ============================================================ + +describe("LedgerClient.parseRetirementsFromTx", () => { + 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: [] }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("extracts a single retirement from an EventRetire attribute set", () => { + const tx = { + txhash: "tx-single", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree1" }, + { key: "batch_denom", value: "C01-001-20240101-20241231-001" }, + { key: "amount", value: "1000" }, + { key: "jurisdiction", value: "US-CA" }, + { key: "reason", value: "voluntary offset" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]).toMatchObject({ + txHash: "tx-single", + batchDenom: "C01-001-20240101-20241231-001", + classId: "C01", + retiree: "regen1retiree1", + quantity: 1000, + jurisdiction: "US-CA", + reason: "voluntary offset", + }); + }); + + it("accepts the v1beta1 event type as a fallback", () => { + const tx = { + txhash: "tx-beta", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1beta1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree2" }, + { key: "batch_denom", value: "BT-001-20240101-20241231-001" }, + { key: "amount", value: "500" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.classId).toBe("BT"); + expect(out[0]!.quantity).toBe(500); + expect(out[0]!.jurisdiction).toBeNull(); + expect(out[0]!.reason).toBeNull(); + }); + + it("ignores events that are not EventRetire", () => { + const tx = { + txhash: "tx-other", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "coin_spent", + attributes: [{ key: "amount", value: "100uregen" }], + }, + { + type: "transfer", + attributes: [{ key: "recipient", value: "regen1other" }], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("ignores EventRetire with missing batch_denom", () => { + const tx = { + txhash: "tx-missing-denom", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "amount", value: "1000" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("ignores EventRetire with non-finite or non-positive amount", () => { + const tx = { + txhash: "tx-bad-amount", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-001" }, + { key: "amount", value: "not-a-number" }, + ], + }, + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-002" }, + { key: "amount", value: "0" }, + ], + }, + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-003" }, + { key: "amount", value: "-500" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toEqual([]); + }); + + it("extracts multiple retirements from a batched tx", () => { + const tx = { + txhash: "tx-batched", + timestamp: "2026-02-18T12:00:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree1" }, + { key: "batch_denom", value: "C01-001-2024-2024-001" }, + { key: "amount", value: "100" }, + ], + }, + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree2" }, + { key: "batch_denom", value: "C01-002-2024-2024-001" }, + { key: "amount", value: "200" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(2); + expect(out[0]!.quantity).toBe(100); + expect(out[1]!.quantity).toBe(200); + }); + + it("reads events from tx.events[] as well as logs[].events[]", () => { + // Some LCD versions flatten the events onto the top-level tx + // rather than nesting them under logs[].events. The parser + // harvests both paths. + const tx = { + txhash: "tx-flat", + timestamp: "2026-02-18T12:00:00Z", + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1retiree" }, + { key: "batch_denom", value: "C01-001-2024-2024-001" }, + { key: "amount", value: "42" }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out).toHaveLength(1); + expect(out[0]!.quantity).toBe(42); + }); + + it("carries the tx hash and timestamp through to the Retirement record", () => { + const tx = { + txhash: "ABCDEF1234567890", + timestamp: "2026-03-01T08:30:00Z", + logs: [ + { + events: [ + { + type: "regen.ecocredit.v1.EventRetire", + attributes: [ + { key: "owner", value: "regen1r" }, + { key: "batch_denom", value: "C-1-2024-2024-1" }, + { key: "amount", value: "10" }, + ], + }, + ], + }, + ], + }; + const out = client.parseRetirementsFromTx(tx); + expect(out[0]!.txHash).toBe("ABCDEF1234567890"); + expect(out[0]!.retiredAt).toBe("2026-03-01T08:30:00Z"); + }); +}); diff --git a/agent-003-market-monitor/src/ledger.ts b/agent-003-market-monitor/src/ledger.ts new file mode 100644 index 0000000..2568608 --- /dev/null +++ b/agent-003-market-monitor/src/ledger.ts @@ -0,0 +1,336 @@ +import { config } from "./config.js"; +import type { + CreditClass, + CreditBatch, + BatchSupply, + SellOrder, + Retirement, +} from "./types.js"; + +/** Extract the class id prefix from a batch denom (shared helper). */ +function classIdFromBatchDenomHelper(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +/** + * Regen Ledger LCD (REST) client — ecocredit marketplace endpoints. + * + * Talks directly to a Cosmos LCD endpoint — no MCP dependency. + * When Ledger MCP becomes available, this can be swapped out behind + * the same interface. Matches the pattern used by AGENT-001 + * (registry-reviewer) and AGENT-002 (governance-analyst). + */ +export class LedgerClient { + private baseUrl: string; + + constructor(baseUrl?: string) { + this.baseUrl = (baseUrl || config.lcdUrl).replace(/\/$/, ""); + } + + // ── Credit Classes ───────────────────────────────────────── + + async getCreditClasses(): Promise { + // Walk every page of the `/regen/ecocredit/v1/classes` endpoint so + // the agent keeps monitoring new credit classes as the registry + // grows. A hardcoded limit of 200 would silently drop classes + // created after that ceiling, which is exactly the kind of blind + // spot this agent is supposed to prevent. + const pageSize = 200; + const classes: CreditClass[] = []; + let nextKey: string | null = null; + // Cap total pages as a safety belt against runaway pagination. + const MAX_PAGES = 25; + for (let i = 0; i < MAX_PAGES; i++) { + const params = new URLSearchParams(); + params.set("pagination.limit", String(pageSize)); + if (nextKey) params.set("pagination.key", nextKey); + + const data = await this.get( + `/regen/ecocredit/v1/classes?${params.toString()}` + ); + const pageClasses = (data.classes || []) as CreditClass[]; + classes.push(...pageClasses); + + const pagination = data.pagination as + | { next_key?: string | null } + | undefined; + const rawKey = pagination?.next_key; + if (!rawKey) break; + nextKey = rawKey; + } + return classes; + } + + async getCreditClass(classId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/classes/${classId}` + ); + return (data.class || null) as CreditClass | null; + } catch (err) { + console.error(`LedgerClient.getCreditClass(${classId}) failed:`, err); + return null; + } + } + + // ── Credit Batches ───────────────────────────────────────── + + async getCreditBatches(): Promise { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + params.set("pagination.reverse", "true"); + + const data = await this.get( + `/regen/ecocredit/v1/batches?${params.toString()}` + ); + return (data.batches || []) as CreditBatch[]; + } + + async getCreditBatch(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}` + ); + return (data.batch || null) as CreditBatch | null; + } catch (err) { + console.error(`LedgerClient.getCreditBatch(${denom}) failed:`, err); + return null; + } + } + + async getBatchSupply(denom: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/v1/batches/${denom}/supply` + ); + return (data.supply || null) as BatchSupply | null; + } catch (err) { + console.error(`LedgerClient.getBatchSupply(${denom}) failed:`, err); + return null; + } + } + + // ── Marketplace: Sell Orders ─────────────────────────────── + + async getSellOrders(limit = 200): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", String(limit)); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch (err) { + console.error(`LedgerClient.getSellOrders(limit=${limit}) failed:`, err); + return []; + } + } + + async getSellOrdersByBatch(denom: string): Promise { + try { + const params = new URLSearchParams(); + params.set("pagination.limit", "200"); + + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/batch/${denom}?${params.toString()}` + ); + return (data.sell_orders || []) as SellOrder[]; + } catch (err) { + console.error( + `LedgerClient.getSellOrdersByBatch(${denom}) failed:`, + err + ); + return []; + } + } + + async getSellOrder(orderId: string): Promise { + try { + const data = await this.get( + `/regen/ecocredit/marketplace/v1/sell-orders/${orderId}` + ); + return (data.sell_order || null) as SellOrder | null; + } catch (err) { + console.error(`LedgerClient.getSellOrder(${orderId}) failed:`, err); + return null; + } + } + + // ── Tx-search: MsgRetire events ───────────────────────────── + // + // Pulls recent tx responses filtered by `message.action` matching + // the Regen ecocredit MsgRetire type URL. Each matching response + // is parsed into zero or more Retirement records by walking the + // `events` list and extracting the EventRetire attributes. + // + // A single transaction can contain multiple MsgRetire messages + // (batched retirements), so each tx can emit more than one + // Retirement record. + // + // Returns an empty array on any error — the workflow treats + // "no tx results" identically to "no recent retirements", so + // transient LCD failures degrade to zero retirement activity + // rather than crashing the cycle. + + async getRecentRetirementTxs(limit = 100): Promise { + // Query both the v1 and v1beta1 MsgRetire type URLs — the SDK + // has carried both for years and registries still emit v1beta1 + // for older code paths. The parser below accepts either event + // type. Dedup by tx hash so the same tx appearing under both + // queries is only processed once. + const typeUrls = [ + "/regen.ecocredit.v1.MsgRetire", + "/regen.ecocredit.v1beta1.MsgRetire", + ]; + + const seenHashes = new Set(); + const retirements: Retirement[] = []; + + 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); + retirements.push(...this.parseRetirementsFromTx(tx)); + } + } catch (err) { + console.error( + `LedgerClient.getRecentRetirementTxs(${typeUrl}) failed:`, + err + ); + } + } + + return retirements; + } + + /** + * Parse a single Cosmos tx_response into zero or more Retirement + * records. Public so the unit tests can feed it synthetic + * responses without hitting a real LCD. + */ + parseRetirementsFromTx(tx: Record): Retirement[] { + 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 events: Array> = []; + + // Cosmos LCD responses can carry events at two levels: per-msg + // inside `logs[i].events[]`, and flattened at `tx.events[]`. + // In modern Cosmos SDK versions the flat list is a superset of + // everything in the logs, so walking both paths double-counts + // every event. Prefer the flat list if it's present and non-empty, + // and fall back to the per-log list only when the flat list is + // missing or empty (very old LCD builds). + const flatEvents = (tx.events || []) as Array>; + if (flatEvents.length > 0) { + events.push(...flatEvents); + } else { + for (const log of logs) { + const logEvents = (log.events || []) as Array>; + events.push(...logEvents); + } + } + + const results: Retirement[] = []; + for (const ev of events) { + const type = typeof ev.type === "string" ? ev.type : ""; + // Match the Regen v1 EventRetire type or the fallback SDK + // message event. Either path produces the same Retirement + // shape for the downstream workflow. + if ( + type !== "regen.ecocredit.v1.EventRetire" && + type !== "regen.ecocredit.v1beta1.EventRetire" + ) { + continue; + } + + 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; + }; + + const batchDenom = attr("batch_denom") ?? attr("batchDenom") ?? ""; + if (!batchDenom) continue; + + // Parse raw integer units as BigInt first to preserve precision + // beyond Number.MAX_SAFE_INTEGER. Regen batches are 6-decimal + // today (capped well below 2^53), but higher-precision credit + // types (18-decimal biodiversity tokens etc.) are on the roadmap + // and the parser should not lose precision at the boundary. + const quantityRaw = (attr("amount") ?? attr("quantity") ?? "0").trim(); + let quantityBig: bigint; + try { + // Strip any decimal suffix — the on-chain event always emits + // the smallest-unit integer, but some forwarders prettify it. + const intPart = quantityRaw.split(".")[0] || "0"; + quantityBig = BigInt(intPart); + } catch { + continue; + } + if (quantityBig <= 0n) continue; + // The downstream Retirement record uses `number` for ergonomics. + // For current Regen credit precision this is a safe downcast; + // future higher-precision assets should lift the whole field to + // string/bigint in types.ts. + const quantity = Number(quantityBig); + if (!Number.isFinite(quantity)) continue; + + const retiree = attr("owner") ?? attr("retirer") ?? ""; + const jurisdiction = attr("jurisdiction"); + const reason = attr("reason"); + const classId = classIdFromBatchDenomHelper(batchDenom); + + results.push({ + txHash, + batchDenom, + classId, + retiree, + quantity, + jurisdiction, + reason, + retiredAt: 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-003-market-monitor/src/monitor.ts b/agent-003-market-monitor/src/monitor.ts new file mode 100644 index 0000000..e91d2b2 --- /dev/null +++ b/agent-003-market-monitor/src/monitor.ts @@ -0,0 +1,205 @@ +import Anthropic from "@anthropic-ai/sdk"; +import { config } from "./config.js"; +import type { + PriceAnomaly, + LiquiditySnapshot, + RetirementSummary, +} from "./types.js"; + +const client = new Anthropic({ apiKey: config.anthropicApiKey }); + +/** + * System prompt mirrors the AGENT-003 character definition at + * agents/packages/agents/src/characters/market-monitor.ts. Thresholds + * are injected from config so a single source of truth drives both the + * deterministic pipeline AND the narrative layer. + */ +const SYSTEM_PROMPT = `You are the Regen Market Monitor Agent (AGENT-003). + +Your responsibilities: +1. Monitoring ecological credit prices across all credit classes +2. Tracking marketplace liquidity and order book health +3. Analyzing retirement patterns and demand signals +4. Detecting price anomalies and potential manipulation + +Core Principles: +- Prioritize market integrity above all else +- Minimize false positives — verify anomalies against multiple data sources +- Present data with precision — include units, timeframes, and confidence intervals +- Never provide trading advice or price predictions +- Cite the deterministic numbers passed to you; do not invent values + +Workflows: +- WF-MM-01 (Price Impact Alert): z-score analysis (CRITICAL >= ${config.market.criticalZScore}, WARNING >= ${config.market.warningZScore}) +- WF-MM-02 (Liquidity Monitor): hourly liquidity health checks +- WF-MM-03 (Retirement Tracking): demand signals and impact quantification + +Alert Severity Levels: +- INFO: Normal activity, logged for trend analysis +- WARNING: z-score between ${config.market.warningZScore} and ${config.market.criticalZScore}, added to watchlist +- CRITICAL: z-score >= ${config.market.criticalZScore}, escalate for investigation + +Output Format: +- Use markdown with explicit tables +- Include severity level in the title of every alert +- Include units, timeframes, and sample sizes +- Quantify confidence when relevant`; + +// ============================================================ +// WF-MM-01: Price anomaly narrative +// ============================================================ + +export async function describePriceAnomaly( + anomaly: PriceAnomaly +): Promise { + const prompt = `Generate a Price Impact Alert report for this anomaly detection. + +## Deterministic Pipeline Output +- Trade ID: ${anomaly.tradeId} +- Credit Class: ${anomaly.classId} +- Batch: ${anomaly.batchDenom} +- Seller: ${anomaly.seller} +- Quantity: ${anomaly.quantity} +- Trade Price: $${anomaly.pricePerCredit.toFixed(4)}/credit +- Class Median (${config.market.classMedianWindowDays}d): $${anomaly.classMedian.toFixed(4)}/credit (n=${anomaly.sampleSizeClass}) +- Batch Median (${config.market.batchMedianWindowDays}d): $${anomaly.batchMedian.toFixed(4)}/credit (n=${anomaly.sampleSizeBatch}) +- Z-score vs class: ${anomaly.zScoreVsClass.toFixed(2)} +- Z-score vs batch: ${anomaly.zScoreVsBatch.toFixed(2)} +- Severity: ${anomaly.severity} +- Confidence: ${anomaly.confidence.toFixed(2)} +- Detected: ${anomaly.detectedAt} + +Generate a structured Markdown report with: +1. A header "Price Impact Alert" followed by severity, class, batch, timestamp +2. An "Anomaly Details" table using ONLY the numbers above +3. A "Context" section (bullet points) — note the relative deviation, sample size adequacy, and any concerns about class vs batch z-score divergence +4. An "Action" section — what the agent has done (watchlist add / escalation) and what the next review is + +Do not invent any numbers beyond what's provided. Do not recommend trading positions.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-MM-02: Liquidity narrative +// ============================================================ + +export async function describeLiquidity( + snapshot: LiquiditySnapshot, + previousSnapshot: LiquiditySnapshot | null +): Promise { + const prev = previousSnapshot + ? ` +## Previous Snapshot (${previousSnapshot.capturedAt}) +- Listed value: $${previousSnapshot.totalListedValueUsd.toFixed(2)} +- Median ask: $${previousSnapshot.medianAskUsd.toFixed(4)} +- Depth (top-10): $${previousSnapshot.depthUsd.toFixed(2)} +- Health: ${previousSnapshot.health} (${previousSnapshot.healthScore.toFixed(2)}) +` + : "## Previous Snapshot\nNone — this is the first snapshot for this class."; + + const prompt = `Generate a Liquidity Report for the Regen ecocredit marketplace. + +## Deterministic Pipeline Output +- Class: ${snapshot.classId} +- Sell orders: ${snapshot.sellOrderCount} +- Listed quantity: ${snapshot.totalListedQuantity} +- Listed value: $${snapshot.totalListedValueUsd.toFixed(2)} +- Lowest ask: $${snapshot.lowestAskUsd.toFixed(4)} +- Highest ask: $${snapshot.highestAskUsd.toFixed(4)} +- Median ask: $${snapshot.medianAskUsd.toFixed(4)} +- Mean ask: $${snapshot.meanAskUsd.toFixed(4)} +- Depth (top-10 sum): $${snapshot.depthUsd.toFixed(2)} +- Health score: ${snapshot.healthScore.toFixed(2)} +- Health: ${snapshot.health} +- Captured: ${snapshot.capturedAt} + +${prev} + +Generate a structured Markdown report with: +1. Header "Liquidity Report — ${snapshot.classId}" with health badge +2. A "Current Snapshot" table with the numbers above +3. A "Trend" section comparing to the previous snapshot (or noting this is the first) +4. A "Health Assessment" section — interpret the depth floor vs the configured liquidity_depth_floor +5. A one-line "Alert" line if health is DEGRADED or CRITICAL + +Use ONLY the numbers provided. Do not invent trade volumes, slippage estimates, or predictions.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + system: SYSTEM_PROMPT, + messages: [{ role: "user", content: prompt }], + }); + + return extractText(response); +} + +// ============================================================ +// WF-MM-03: Retirement narrative +// ============================================================ + +export async function describeRetirementSummary( + summary: RetirementSummary, + baselineDemand: number +): Promise { + const trend = + baselineDemand === 0 + ? "no prior baseline" + : summary.demandIndex > baselineDemand + ? `up from ${baselineDemand.toFixed(0)}` + : summary.demandIndex < baselineDemand + ? `down from ${baselineDemand.toFixed(0)}` + : `flat vs ${baselineDemand.toFixed(0)}`; + + const prompt = `Generate a Retirement Pattern Summary for the Regen ecocredit marketplace. + +## Deterministic Pipeline Output +- Class: ${summary.classId} +- Window: trailing ${summary.windowHours} hours +- Retirements (count): ${summary.retirementCount} +- Total quantity: ${summary.totalQuantity} +- Total value: $${summary.totalValueUsd.toFixed(2)} +- Unique retirees: ${summary.uniqueRetirees} +- Top retiree: ${summary.topRetiree ?? "(none)"} +- Top retiree quantity: ${summary.topRetireeQuantity} +- % with jurisdiction metadata: ${summary.pctWithJurisdiction.toFixed(1)}% +- Demand index (0-100): ${summary.demandIndex.toFixed(0)} (${trend}) +- Captured: ${summary.capturedAt} + +Generate a structured Markdown report with: +1. Header "Retirement Summary — ${summary.classId}" +2. "Volume" table with counts, quantity, value +3. "Demand Signals" section — interpret the demand index and trend +4. "Concentration" section — note if one retiree accounts for an unusual share +5. "Metadata Compliance" note — if % with jurisdiction is high, that usually implies compliance-driven demand + +Use ONLY the numbers provided. Do not extrapolate future demand.`; + + const response = await client.messages.create({ + model: config.model, + max_tokens: 1000, + 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-003-market-monitor/src/ooda.ts b/agent-003-market-monitor/src/ooda.ts new file mode 100644 index 0000000..e7f3219 --- /dev/null +++ b/agent-003-market-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. + * + * Identical shape to the AGENT-002 executor — kept locally so that each + * agent process can run as a standalone Node program without a shared + * package. 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-003-market-monitor/src/output.ts b/agent-003-market-monitor/src/output.ts new file mode 100644 index 0000000..6b3b790 --- /dev/null +++ b/agent-003-market-monitor/src/output.ts @@ -0,0 +1,59 @@ +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) + * + * Extend this to add Telegram, Twitter, KOI object creation, etc. + * Mirrors the AGENT-002 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-003-market-monitor/src/store.ts b/agent-003-market-monitor/src/store.ts new file mode 100644 index 0000000..e52d6a0 --- /dev/null +++ b/agent-003-market-monitor/src/store.ts @@ -0,0 +1,309 @@ +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-003.db"); + +/** + * Local SQLite store for AGENT-003 state. + * + * Tracks trade observations (for trailing median + z-score), liquidity + * snapshots, retirement summaries, and workflow execution history. + * Intentionally lightweight — can be replaced with PostgreSQL later. + */ +class Store { + private db: Database.Database; + + constructor() { + this.db = new Database(DB_PATH); + 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 + ); + + -- Trailing trade observations, used as the sample set for + -- class/batch median + z-score in WF-MM-01. Each row is one + -- observed trade; the analyzer reads rows matching the batch or + -- class within the trailing window. + CREATE TABLE IF NOT EXISTS trade_observations ( + trade_id TEXT PRIMARY KEY, + class_id TEXT NOT NULL, + batch_denom TEXT NOT NULL, + price_per_credit REAL NOT NULL, + quantity REAL NOT NULL, + executed_at TEXT NOT NULL + ); + + -- Deduplication record for WF-MM-01 — we only alert once per + -- (trade_id, severity). Re-alerts only escalate to a higher + -- severity (enforced in the workflow). + CREATE TABLE IF NOT EXISTS anomaly_alerts ( + trade_id TEXT PRIMARY KEY, + severity TEXT NOT NULL, + z_class REAL NOT NULL, + z_batch REAL NOT NULL, + detected_at TEXT NOT NULL, + report TEXT NOT NULL + ); + + -- Snapshots of liquidity health per class, used by WF-MM-02 + -- to compare against the previous snapshot and surface trends. + CREATE TABLE IF NOT EXISTS liquidity_snapshots ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id TEXT NOT NULL, + snapshot TEXT NOT NULL, + health TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + -- Summaries of retirement activity per class, used by WF-MM-03 + -- to compute a rolling demand index. + CREATE TABLE IF NOT EXISTS retirement_summaries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + class_id TEXT NOT NULL, + window_hours INTEGER NOT NULL, + total_quantity REAL NOT NULL, + total_value_usd REAL NOT NULL, + retirement_count INTEGER NOT NULL, + demand_index REAL NOT NULL, + summary TEXT NOT NULL, + captured_at TEXT NOT NULL + ); + + CREATE INDEX IF NOT EXISTS idx_trade_class ON trade_observations(class_id); + CREATE INDEX IF NOT EXISTS idx_trade_batch ON trade_observations(batch_denom); + CREATE INDEX IF NOT EXISTS idx_trade_executed ON trade_observations(executed_at); + CREATE INDEX IF NOT EXISTS idx_liquidity_class ON liquidity_snapshots(class_id); + CREATE INDEX IF NOT EXISTS idx_retirement_class ON retirement_summaries(class_id); + `); + } + + // ── Trade observations (for z-score baseline) ────────────── + + recordTrade(obs: { + tradeId: string; + classId: string; + batchDenom: string; + pricePerCredit: number; + quantity: number; + executedAt: string; + }): void { + this.db + .prepare( + `INSERT OR IGNORE INTO trade_observations + (trade_id, class_id, batch_denom, price_per_credit, quantity, executed_at) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + obs.tradeId, + obs.classId, + obs.batchDenom, + obs.pricePerCredit, + obs.quantity, + obs.executedAt + ); + } + + getTradesByClass(classId: string, windowDays: number): number[] { + const cutoff = new Date(Date.now() - windowDays * 86_400_000).toISOString(); + const rows = this.db + .prepare( + `SELECT price_per_credit as p FROM trade_observations + WHERE class_id = ? AND executed_at >= ?` + ) + .all(classId, cutoff) as { p: number }[]; + return rows.map((r) => r.p); + } + + getTradesByBatch(batchDenom: string, windowDays: number): number[] { + const cutoff = new Date(Date.now() - windowDays * 86_400_000).toISOString(); + const rows = this.db + .prepare( + `SELECT price_per_credit as p FROM trade_observations + WHERE batch_denom = ? AND executed_at >= ?` + ) + .all(batchDenom, cutoff) as { p: number }[]; + return rows.map((r) => r.p); + } + + // ── Anomaly alerts ───────────────────────────────────────── + + hasAnomalyAlert(tradeId: string): boolean { + const row = this.db + .prepare("SELECT severity FROM anomaly_alerts WHERE trade_id = ?") + .get(tradeId) as { severity: string } | undefined; + return !!row; + } + + getAnomalyAlertSeverity(tradeId: string): string | null { + const row = this.db + .prepare("SELECT severity FROM anomaly_alerts WHERE trade_id = ?") + .get(tradeId) as { severity: string } | undefined; + return row?.severity || null; + } + + saveAnomalyAlert(alert: { + tradeId: string; + severity: string; + zClass: number; + zBatch: number; + report: string; + }): void { + this.db + .prepare( + `INSERT OR REPLACE INTO anomaly_alerts + (trade_id, severity, z_class, z_batch, detected_at, report) + VALUES (?, ?, ?, ?, ?, ?)` + ) + .run( + alert.tradeId, + alert.severity, + alert.zClass, + alert.zBatch, + new Date().toISOString(), + alert.report + ); + } + + // ── Liquidity snapshots ──────────────────────────────────── + + saveLiquiditySnapshot(classId: string, snapshot: string, health: string): void { + this.db + .prepare( + `INSERT INTO liquidity_snapshots (class_id, snapshot, health, captured_at) + VALUES (?, ?, ?, ?)` + ) + .run(classId, snapshot, health, new Date().toISOString()); + } + + getLatestLiquiditySnapshot( + classId: string + ): { snapshot: string; health: string; captured_at: string } | null { + const row = this.db + .prepare( + `SELECT snapshot, health, captured_at FROM liquidity_snapshots + WHERE class_id = ? ORDER BY id DESC LIMIT 1` + ) + .get(classId) as + | { snapshot: string; health: string; captured_at: string } + | undefined; + return row || null; + } + + /** Latest median ask price for a class, pulled from the most recent + * liquidity snapshot. Used by WF-MM-03 to value retirement volume + * in USD rather than treating quantity as 1:1 with USD. Returns + * null when no snapshot exists yet. */ + getLatestMedianAsk(classId: string): number | null { + const row = this.db + .prepare( + `SELECT snapshot FROM liquidity_snapshots + WHERE class_id = ? ORDER BY id DESC LIMIT 1` + ) + .get(classId) as { snapshot: string } | undefined; + if (!row) return null; + try { + const parsed = JSON.parse(row.snapshot) as { medianAskUsd?: number }; + const m = parsed.medianAskUsd; + return typeof m === "number" && m > 0 ? m : null; + } catch { + return null; + } + } + + // ── Retirement summaries ─────────────────────────────────── + + saveRetirementSummary(row: { + classId: string; + windowHours: number; + totalQuantity: number; + totalValueUsd: number; + retirementCount: number; + demandIndex: number; + summary: string; + }): void { + this.db + .prepare( + `INSERT INTO retirement_summaries + (class_id, window_hours, total_quantity, total_value_usd, + retirement_count, demand_index, summary, captured_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + row.classId, + row.windowHours, + row.totalQuantity, + row.totalValueUsd, + row.retirementCount, + row.demandIndex, + row.summary, + new Date().toISOString() + ); + } + + getBaselineDemand(classId: string, lookbackDays: number): number { + // Average of the demand_index values captured in the prior window. + // Used as the reference point for the new demand_index computation. + const cutoff = new Date(Date.now() - lookbackDays * 86_400_000).toISOString(); + const row = this.db + .prepare( + `SELECT AVG(demand_index) as avg FROM retirement_summaries + WHERE class_id = ? AND captured_at >= ?` + ) + .get(classId, cutoff) as { avg: number | null }; + return row.avg ?? 0; + } + + // ── 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-003-market-monitor/src/types.ts b/agent-003-market-monitor/src/types.ts new file mode 100644 index 0000000..317ca5f --- /dev/null +++ b/agent-003-market-monitor/src/types.ts @@ -0,0 +1,158 @@ +// ============================================================ +// Regen Ledger ecocredit marketplace types +// ============================================================ + +export interface CreditClass { + id: string; + admin: string; + credit_type: CreditType; + metadata: string; +} + +export interface CreditType { + abbreviation: string; + name: string; + unit: string; + precision: number; +} + +export interface CreditBatch { + denom: string; + project_id: string; + issuer: string; + start_date: string; + end_date: string; + total_amount: string; + metadata: string; + open: boolean; +} + +export interface BatchSupply { + batch_denom: string; + tradable_amount: string; + retired_amount: string; + cancelled_amount: string; +} + +export interface SellOrder { + id: string; + seller: string; + batch_denom: string; + quantity: string; + ask_denom: string; + ask_amount: string; + disable_auto_retire: boolean; + expiration: string; +} + +/** Synthesized trade record (from filled sell orders or tx logs) */ +export interface Trade { + id: string; + batchDenom: string; + classId: string; + seller: string; + buyer: string; + quantity: number; + pricePerCredit: number; // USD-equivalent + askDenom: string; + executedAt: string; +} + +/** Synthesized retirement record */ +export interface Retirement { + txHash: string; + batchDenom: string; + classId: string; + retiree: string; + quantity: number; + jurisdiction: string | null; + reason: string | null; + retiredAt: string; +} + +// ============================================================ +// OODA loop types (shared shape mirrors agent-002) +// ============================================================ + +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"; +export type AnomalySeverity = "INFO" | "WARNING" | "CRITICAL"; + +/** Price anomaly detection (WF-MM-01) */ +export interface PriceAnomaly { + tradeId: string; + batchDenom: string; + classId: string; + seller: string; + quantity: number; + pricePerCredit: number; + classMedian: number; + batchMedian: number; + zScoreVsClass: number; + zScoreVsBatch: number; + sampleSizeClass: number; + sampleSizeBatch: number; + severity: AnomalySeverity; + detectedAt: string; + confidence: number; +} + +/** Liquidity monitoring (WF-MM-02) */ +export interface LiquiditySnapshot { + classId: string; + totalListedQuantity: number; + totalListedValueUsd: number; + sellOrderCount: number; + lowestAskUsd: number; + highestAskUsd: number; + medianAskUsd: number; + meanAskUsd: number; + depthUsd: number; // sum of top-10 sell orders + healthScore: number; // 0-1 + health: "HEALTHY" | "DEGRADED" | "CRITICAL"; + capturedAt: string; +} + +/** Retirement tracking (WF-MM-03) */ +export interface RetirementSummary { + classId: string; + windowHours: number; + retirementCount: number; + totalQuantity: number; + totalValueUsd: number; + uniqueRetirees: number; + topRetiree: string | null; + topRetireeQuantity: number; + pctWithJurisdiction: number; + demandIndex: number; // 0-100, relative to trailing baseline + capturedAt: string; +} + +// ============================================================ +// Output types +// ============================================================ + +export interface OutputMessage { + workflow: string; + subjectId: string; // trade id / class id / batch denom + title: string; + content: string; + alertLevel: AlertLevel; + timestamp: Date; +} diff --git a/agent-003-market-monitor/src/utils.ts b/agent-003-market-monitor/src/utils.ts new file mode 100644 index 0000000..eb53ab3 --- /dev/null +++ b/agent-003-market-monitor/src/utils.ts @@ -0,0 +1,78 @@ +/** + * Shared numeric and parsing helpers used by more than one workflow. + * + * These started out duplicated across the three WF-MM workflows. + * Centralizing them here is the DRY fix asked for by the PR #99 + * Gemini review, and it gives the unit-test suite a single source of + * truth to pin so a regression in one workflow cannot silently diverge + * from the rest. + */ + +/** + * Deterministic statistical median. Returns 0 for empty input so + * callers do not need a special-case branch upstream — an empty + * sample set unambiguously means "no signal" in every callsite. + */ +export function median(values: number[]): number { + if (values.length === 0) return 0; + const sorted = [...values].sort((a, b) => a - b); + const mid = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 + ? (sorted[mid - 1]! + sorted[mid]!) / 2 + : sorted[mid]!; +} + +/** + * Sample standard deviation (n-1 denominator). Returns 0 for + * sample sizes below 2 because a single observation carries no + * dispersion information. + */ +export function stddev(values: number[], mean: number): number { + if (values.length <= 1) return 0; + const sumSq = values.reduce((acc, v) => acc + (v - mean) ** 2, 0); + return Math.sqrt(sumSq / (values.length - 1)); +} + +/** + * Extract the credit class identifier from a Regen batch denom. + * + * Regen batch denoms follow the shape + * `----` + * (for example, `C01-001-20240101-20241231-001`). The class id is + * always the leading token before the first dash. Strings without a + * dash are returned unchanged. + */ +export function classIdFromBatchDenom(denom: string): string { + const idx = denom.indexOf("-"); + return idx > 0 ? denom.slice(0, idx) : denom; +} + +/** + * Whether a Cosmos denom corresponds to a USD-pegged stablecoin that + * this MVP is willing to treat as 1:1 USD for pricing. Used to + * filter out non-USD sell orders so they do not pollute the baseline + * with arbitrary 1:1-valued noise. + */ +export function isUsdStableDenom(denom: string): boolean { + const d = denom.toLowerCase(); + return d.includes("usdc") || d.includes("usdt") || d.includes("dai"); +} + +/** + * Demand index on a bounded 0-100 scale. Inputs are rolling and a + * class with no trailing activity gets 0. The index is intentionally + * simple — it exists so the narrative layer has a single number to + * anchor the "demand up / demand down" story. Defined here so the + * unit-test suite can import it without pulling in the workflow's + * Store singleton (which opens SQLite on load). + */ +export function computeDemandIndex( + totalQuantity: number, + retirementCount: number, + uniqueRetirees: number +): number { + const volumeComponent = Math.min(60, Math.log10(Math.max(1, totalQuantity)) * 20); + const countComponent = Math.min(20, retirementCount * 2); + const breadthComponent = Math.min(20, uniqueRetirees * 4); + return Math.round(volumeComponent + countComponent + breadthComponent); +} diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts new file mode 100644 index 0000000..2e8e705 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import { askUsd, scoreHealth } from "./liquidity-monitor.js"; +import type { SellOrder } from "../types.js"; +import { config } from "../config.js"; + +// Small helper to build a well-typed SellOrder for a test case. +function makeOrder(overrides: Partial = {}): SellOrder { + return { + id: "order-test", + seller: "regen1seller", + batch_denom: "C-001-20240101-20241231-001", + quantity: "100", + ask_denom: "uusdc", + ask_amount: "5000", + disable_auto_retire: false, + expiration: "2030-01-01T00:00:00Z", + ...overrides, + }; +} + +// ============================================================ +// askUsd — per-unit ask price in USD +// ============================================================ + +describe("askUsd", () => { + it("divides ask_amount by quantity for a normal order", () => { + const order = makeOrder({ ask_amount: "5000", quantity: "100" }); + expect(askUsd(order)).toBe(50); + }); + + it("returns 0 when quantity is zero", () => { + expect(askUsd(makeOrder({ quantity: "0" }))).toBe(0); + }); + + it("returns 0 when quantity is negative", () => { + expect(askUsd(makeOrder({ quantity: "-10" }))).toBe(0); + }); + + it("returns 0 when ask_amount is not a finite number", () => { + expect(askUsd(makeOrder({ ask_amount: "not-a-number" }))).toBe(0); + }); + + it("returns 0 when quantity is not a finite number", () => { + expect(askUsd(makeOrder({ quantity: "NaN" }))).toBe(0); + }); + + it("handles fractional prices correctly", () => { + const order = makeOrder({ ask_amount: "125", quantity: "50" }); + expect(askUsd(order)).toBe(2.5); + }); +}); + +// ============================================================ +// scoreHealth — liquidity health tier classifier +// ============================================================ + +describe("scoreHealth", () => { + const floor = config.market.liquidityDepthFloor; // 5000 in the default config + + it("returns CRITICAL when depth is below half the floor", () => { + // depthUsd = 2000, floor = 5000 → 2000 < 2500 → CRITICAL + const result = scoreHealth(floor * 0.4, 5); + expect(result.tier).toBe("CRITICAL"); + }); + + it("returns DEGRADED when depth is below the floor but above half", () => { + // depthUsd = 3000 (floor 5000 → half 2500, so above half), + // 10 orders so countScore = 0.5, depthRatio = 3000/10000 = 0.3, + // score = 0.3*0.7 + 0.5*0.3 = 0.21 + 0.15 = 0.36 — not CRITICAL + // (0.36 >= 0.3), not HEALTHY (3000 < 5000), so DEGRADED. + const result = scoreHealth(floor * 0.6, 10); + expect(result.tier).toBe("DEGRADED"); + }); + + it("returns HEALTHY when depth clears the floor and order count is strong", () => { + // depthUsd = 15000, 20 orders → depthRatio clamped to 1.0, countScore = 1.0, score = 1.0 + const result = scoreHealth(floor * 3, 20); + expect(result.tier).toBe("HEALTHY"); + expect(result.score).toBeCloseTo(1.0, 10); + }); + + it("returns DEGRADED when depth is above the floor but score drops below 0.6", () => { + // depth = exactly floor, sellOrderCount = 2 → depthRatio = 0.5, countScore = 0.1, score = 0.38 + // 0.38 < 0.6 → DEGRADED + const result = scoreHealth(floor, 2); + expect(result.tier).toBe("DEGRADED"); + expect(result.score).toBeLessThan(0.6); + }); + + it("caps the depth score at 1.0 even for very high depth", () => { + const result = scoreHealth(floor * 100, 50); + expect(result.score).toBeLessThanOrEqual(1.0); + }); + + it("caps the count score at 20 orders", () => { + const low = scoreHealth(floor * 2, 20); + const high = scoreHealth(floor * 2, 200); + expect(low.score).toBeCloseTo(high.score, 10); + }); + + it("scores zero depth and zero orders at 0.0", () => { + const result = scoreHealth(0, 0); + expect(result.score).toBe(0); + expect(result.tier).toBe("CRITICAL"); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/liquidity-monitor.ts b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts new file mode 100644 index 0000000..0eb680e --- /dev/null +++ b/agent-003-market-monitor/src/workflows/liquidity-monitor.ts @@ -0,0 +1,231 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeLiquidity } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + LiquiditySnapshot, + SellOrder, + CreditClass, + AlertLevel, +} from "../types.js"; +import { median, classIdFromBatchDenom } from "../utils.js"; + +/** + * WF-MM-02: Liquidity Monitoring & Reporting + * + * Trigger: periodic (hourly) OR significant_trade (volume > $10k) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch all open sell orders, group by credit class + * Orient — Compute liquidity metrics per class (listed value, + * bid-ask spread proxy, top-10 depth, health score) + * Decide — Generate per-class liquidity reports. Flag classes + * whose depth falls below the configured floor. + * Act — Persist snapshot, output report, alert on degradation. + */ + +interface Observations { + classes: CreditClass[]; + ordersByClass: Map; +} + +interface Orientation { + snapshots: LiquiditySnapshot[]; +} + +interface Decision { + reports: { + snapshot: LiquiditySnapshot; + previous: LiquiditySnapshot | null; + report: string; + alertLevel: AlertLevel; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +export function askUsd(order: SellOrder): number { + const ask = Number(order.ask_amount); + const qty = Number(order.quantity); + if (!Number.isFinite(ask) || !Number.isFinite(qty) || qty <= 0) return 0; + // MVP: treat ask as already-USD; price oracle integration is future work. + return ask / qty; +} + +export function scoreHealth( + depthUsd: number, + sellOrderCount: number +): { score: number; tier: LiquiditySnapshot["health"] } { + const depthRatio = Math.min(1, depthUsd / (config.market.liquidityDepthFloor * 2)); + const countScore = Math.min(1, sellOrderCount / 20); + const score = depthRatio * 0.7 + countScore * 0.3; + + let tier: LiquiditySnapshot["health"]; + if (depthUsd < config.market.liquidityDepthFloor * 0.5 || score < 0.3) { + tier = "CRITICAL"; + } else if (depthUsd < config.market.liquidityDepthFloor || score < 0.6) { + tier = "DEGRADED"; + } else { + tier = "HEALTHY"; + } + + return { score, tier }; +} + +export function createLiquidityMonitorWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-02", + name: "Liquidity Monitoring & Reporting", + + async observe(): Promise { + const [classes, sellOrders] = await Promise.all([ + ledger.getCreditClasses(), + ledger.getSellOrders(500), + ]); + + const ordersByClass = new Map(); + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const bucket = ordersByClass.get(classId) || []; + bucket.push(order); + ordersByClass.set(classId, bucket); + } + + return { classes, ordersByClass }; + }, + + async orient(obs: Observations): Promise { + const snapshots: LiquiditySnapshot[] = []; + const capturedAt = new Date().toISOString(); + + // Snapshot every class that currently has any open sell orders. + // Classes with zero open orders are intentionally skipped — + // that's a "no market" state, distinct from a degraded market. + for (const [classId, orders] of obs.ordersByClass) { + if (orders.length === 0) continue; + + const prices = orders + .map((o) => askUsd(o)) + .filter((p) => p > 0) + .sort((a, b) => a - b); + if (prices.length === 0) continue; + + const quantities = orders + .map((o) => Number(o.quantity)) + .filter((q) => Number.isFinite(q) && q > 0); + const totalListedQuantity = quantities.reduce((a, b) => a + b, 0); + + const listedValues = orders.map((o) => { + const p = askUsd(o); + const q = Number(o.quantity); + return Number.isFinite(p) && Number.isFinite(q) ? p * q : 0; + }); + const totalListedValueUsd = listedValues.reduce((a, b) => a + b, 0); + + // Pre-compute askUsd once per order so the comparator is a plain + // number subtraction instead of recomputing the ratio on every + // sort invocation (sort can call the comparator O(n log n) times). + const pricedOrders = orders + .map((order) => ({ order, price: askUsd(order) })) + .sort((a, b) => a.price - b.price); + const top10 = pricedOrders.slice(0, 10); + const depthUsd = top10.reduce((acc, { order, price }) => { + const q = Number(order.quantity); + return acc + (Number.isFinite(price) && Number.isFinite(q) ? price * q : 0); + }, 0); + + const lowestAskUsd = prices[0]!; + const highestAskUsd = prices[prices.length - 1]!; + const medianAskUsd = median(prices); + const meanAskUsd = prices.reduce((a, b) => a + b, 0) / prices.length; + + const { score, tier } = scoreHealth(depthUsd, orders.length); + + snapshots.push({ + classId, + totalListedQuantity, + totalListedValueUsd, + sellOrderCount: orders.length, + lowestAskUsd, + highestAskUsd, + medianAskUsd, + meanAskUsd, + depthUsd, + healthScore: score, + health: tier, + capturedAt, + }); + } + + return { snapshots }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const snapshot of orientation.snapshots) { + const prevRow = store.getLatestLiquiditySnapshot(snapshot.classId); + let previous: LiquiditySnapshot | null = null; + if (prevRow) { + try { + previous = JSON.parse(prevRow.snapshot) as LiquiditySnapshot; + } catch { + previous = null; + } + } + + const report = await describeLiquidity(snapshot, previous); + + const alertLevel: AlertLevel = + snapshot.health === "CRITICAL" + ? "CRITICAL" + : snapshot.health === "DEGRADED" + ? "HIGH" + : "NORMAL"; + + reports.push({ snapshot, previous, report, alertLevel }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { snapshot, report, alertLevel } of decision.reports) { + store.saveLiquiditySnapshot( + snapshot.classId, + JSON.stringify(snapshot), + snapshot.health + ); + saved++; + + await output({ + workflow: "WF-MM-02", + subjectId: snapshot.classId, + title: `Liquidity ${snapshot.health} (score ${snapshot.healthScore.toFixed(2)})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + + if (alertLevel !== "NORMAL") alertsSent++; + } + + if (decision.reports.length === 0) { + console.log(" No credit classes with open sell orders."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts new file mode 100644 index 0000000..36a8435 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.test.ts @@ -0,0 +1,187 @@ +import { describe, it, expect } from "vitest"; +import { + median, + stddev, + classifyAnomaly, + classIdFromBatchDenom, + isUsdStableDenom, +} from "./price-anomaly-detection.js"; + +// ============================================================ +// median — deterministic statistical median +// ============================================================ + +describe("median", () => { + it("returns 0 for an empty list", () => { + expect(median([])).toBe(0); + }); + + it("returns the single value for a one-element list", () => { + expect(median([42])).toBe(42); + }); + + it("returns the middle value for an odd-length list", () => { + expect(median([1, 5, 3])).toBe(3); + expect(median([7, 2, 9, 4, 5])).toBe(5); + }); + + it("returns the mean of the two middle values for an even-length list", () => { + expect(median([1, 2, 3, 4])).toBe(2.5); + expect(median([10, 20])).toBe(15); + }); + + it("does not mutate the input array", () => { + const input = [3, 1, 2]; + median(input); + expect(input).toEqual([3, 1, 2]); + }); + + it("handles negative numbers correctly", () => { + expect(median([-5, -1, -3])).toBe(-3); + expect(median([-4, -2, 2, 4])).toBe(0); + }); + + it("handles floating point values", () => { + expect(median([0.1, 0.2, 0.3])).toBeCloseTo(0.2, 10); + expect(median([1.5, 2.5, 3.5, 4.5])).toBe(3); + }); +}); + +// ============================================================ +// stddev — sample standard deviation +// ============================================================ + +describe("stddev", () => { + it("returns 0 for an empty list", () => { + expect(stddev([], 0)).toBe(0); + }); + + it("returns 0 for a single value (no variance possible)", () => { + expect(stddev([42], 42)).toBe(0); + }); + + it("computes sample stddev for a two-element list", () => { + // [1, 3], mean 2, sum of sq = 1 + 1 = 2, divide by n-1 = 2, sqrt = sqrt(2) + expect(stddev([1, 3], 2)).toBeCloseTo(Math.SQRT2, 10); + }); + + it("computes sample stddev for a known five-element list", () => { + // [2, 4, 4, 4, 5, 5, 7, 9] has a textbook stddev of 2 (sample) + // but let's use a cleaner one: [1, 2, 3, 4, 5] mean 3 + // sum of sq dev = 4 + 1 + 0 + 1 + 4 = 10, / 4 = 2.5, sqrt ≈ 1.5811 + const values = [1, 2, 3, 4, 5]; + const mean = values.reduce((a, b) => a + b, 0) / values.length; + expect(stddev(values, mean)).toBeCloseTo(Math.sqrt(2.5), 10); + }); + + it("uses n-1 denominator (sample, not population)", () => { + // Population stddev of [1, 2] with mean 1.5 would be 0.5 + // Sample stddev is sqrt(0.5/1) ≈ 0.7071 + expect(stddev([1, 2], 1.5)).toBeCloseTo(Math.SQRT1_2, 10); + }); +}); + +// ============================================================ +// classifyAnomaly — severity classifier +// ============================================================ + +describe("classifyAnomaly", () => { + it("returns INFO for zero z-scores", () => { + expect(classifyAnomaly(0, 0)).toBe("INFO"); + }); + + it("returns INFO for z-scores below the WARNING threshold (2.0)", () => { + expect(classifyAnomaly(1.9, 1.5)).toBe("INFO"); + expect(classifyAnomaly(-1.9, 1.5)).toBe("INFO"); + }); + + it("returns WARNING at the exact WARNING threshold", () => { + expect(classifyAnomaly(2.0, 0)).toBe("WARNING"); + expect(classifyAnomaly(0, 2.0)).toBe("WARNING"); + }); + + it("returns WARNING between 2.0 and CRITICAL", () => { + expect(classifyAnomaly(2.5, 1.0)).toBe("WARNING"); + expect(classifyAnomaly(3.4, 0)).toBe("WARNING"); + }); + + it("returns CRITICAL at the exact CRITICAL threshold (3.5)", () => { + expect(classifyAnomaly(3.5, 0)).toBe("CRITICAL"); + expect(classifyAnomaly(0, 3.5)).toBe("CRITICAL"); + }); + + it("returns CRITICAL above the CRITICAL threshold", () => { + expect(classifyAnomaly(5, 1)).toBe("CRITICAL"); + expect(classifyAnomaly(10, 0)).toBe("CRITICAL"); + }); + + it("picks the larger absolute z-score from the two inputs", () => { + expect(classifyAnomaly(1.5, 4.0)).toBe("CRITICAL"); + expect(classifyAnomaly(2.8, 1.0)).toBe("WARNING"); + }); + + it("treats negative z-scores symmetrically with positive", () => { + expect(classifyAnomaly(-3.6, 0)).toBe("CRITICAL"); + expect(classifyAnomaly(-2.1, 0)).toBe("WARNING"); + }); +}); + +// ============================================================ +// classIdFromBatchDenom — class ID extractor +// ============================================================ + +describe("classIdFromBatchDenom", () => { + it("extracts the class ID from a canonical Regen batch denom", () => { + expect(classIdFromBatchDenom("C01-001-20240101-20241231-001")).toBe("C01"); + }); + + it("handles single-letter class codes", () => { + expect(classIdFromBatchDenom("C-001-20240101-20241231-001")).toBe("C"); + expect(classIdFromBatchDenom("BT-001-20240101-20241231-001")).toBe("BT"); + }); + + it("returns the whole string when no dash is present", () => { + expect(classIdFromBatchDenom("UNKNOWN")).toBe("UNKNOWN"); + }); + + it("handles an empty string", () => { + expect(classIdFromBatchDenom("")).toBe(""); + }); + + it("returns the whole string when the first character is a dash", () => { + // Degenerate but deterministic — the slice is empty before the dash. + expect(classIdFromBatchDenom("-leading-dash")).toBe("-leading-dash"); + }); +}); + +// ============================================================ +// isUsdStableDenom — stablecoin denom check +// ============================================================ + +describe("isUsdStableDenom", () => { + it("recognizes USDC denoms case-insensitively", () => { + expect(isUsdStableDenom("usdc")).toBe(true); + expect(isUsdStableDenom("USDC")).toBe(true); + expect(isUsdStableDenom("ibc/USDC-regen")).toBe(true); + }); + + it("recognizes USDT", () => { + expect(isUsdStableDenom("usdt")).toBe(true); + expect(isUsdStableDenom("ibc/USDT-axelar")).toBe(true); + }); + + it("recognizes DAI", () => { + expect(isUsdStableDenom("dai")).toBe(true); + expect(isUsdStableDenom("ibc/DAI-cosmos")).toBe(true); + }); + + it("rejects REGEN and other non-stable denoms", () => { + expect(isUsdStableDenom("uregen")).toBe(false); + expect(isUsdStableDenom("uatom")).toBe(false); + expect(isUsdStableDenom("eco-credits")).toBe(false); + }); + + it("rejects the empty string", () => { + expect(isUsdStableDenom("")).toBe(false); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts new file mode 100644 index 0000000..2bf5430 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/price-anomaly-detection.ts @@ -0,0 +1,255 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describePriceAnomaly } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { + Trade, + PriceAnomaly, + AnomalySeverity, + SellOrder, +} from "../types.js"; +import { + median, + stddev, + classIdFromBatchDenom, + isUsdStableDenom, +} from "../utils.js"; + +// Re-exported so the existing unit-test suite can import helpers from +// this workflow file (the tests pre-date the utils.ts extraction). +export { median, stddev, classIdFromBatchDenom, isUsdStableDenom }; + +/** + * WF-MM-01: Price Anomaly Detection + * + * Trigger: SellOrderCreated OR SellOrderFilled + * Layer: 1 (alerts) / Layer 2 (if action needed) + * SLA: Alert within one polling cycle of the trade landing on-chain + * + * OODA: + * Observe — Fetch current sell orders + record as synthetic trade + * observations. When Ledger MCP provides real fill data, + * the observe phase swaps in real trades. + * Orient — For each new observation, compute z-score vs class + * median and batch median using the trailing window. + * Decide — Classify severity (INFO / WARNING / CRITICAL) per the + * thresholds in config.market. Only surface WARNING or + * CRITICAL. Dedupe against previous alerts for this trade. + * Act — Persist, generate narrative via Claude, output to channels. + * + * Determinism: all numeric decisions (median, z-score, severity) are + * computed locally. Claude is only used for the narrative report. + */ + +interface Observations { + newTrades: Trade[]; +} + +interface Orientation { + anomalies: PriceAnomaly[]; +} + +interface Decision { + alertsToPublish: PriceAnomaly[]; +} + +interface Actions { + alerted: number; + skipped: number; +} + +/** + * Convert a sell order into a synthetic "trade observation" for this + * agent. Until Ledger MCP provides filled-trade events, the agent uses + * the ask price as a proxy signal — an unusually high/low ask price + * still indicates a market structure anomaly worth surfacing. + */ +function sellOrderToTrade(order: SellOrder, classId: string): Trade | null { + const qty = Number(order.quantity); + const ask = Number(order.ask_amount); + if (!Number.isFinite(qty) || !Number.isFinite(ask) || qty <= 0) return null; + + // Only USD stablecoin asks are priced — anything else would require + // an oracle we don't have yet, and letting non-USD orders into the + // baseline would pollute the class/batch medians with 1:1-priced + // garbage. Downstream code can plug in a real price oracle. + if (!isUsdStableDenom(order.ask_denom)) return null; + const pricePerCredit = ask / qty; + + return { + id: order.id, + batchDenom: order.batch_denom, + classId, + seller: order.seller, + buyer: "", + quantity: qty, + pricePerCredit, + askDenom: order.ask_denom, + executedAt: new Date().toISOString(), + }; +} + +export function classifyAnomaly( + zClass: number, + zBatch: number +): AnomalySeverity { + const maxZ = Math.max(Math.abs(zClass), Math.abs(zBatch)); + if (maxZ >= config.market.criticalZScore) return "CRITICAL"; + if (maxZ >= config.market.warningZScore) return "WARNING"; + return "INFO"; +} + +export function createPriceAnomalyDetectionWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-01", + name: "Price Anomaly Detection", + + async observe(): Promise { + const sellOrders = await ledger.getSellOrders(200); + + const newTrades: Trade[] = []; + for (const order of sellOrders) { + const classId = classIdFromBatchDenom(order.batch_denom); + const trade = sellOrderToTrade(order, classId); + if (trade) { + newTrades.push(trade); + store.recordTrade({ + tradeId: trade.id, + classId: trade.classId, + batchDenom: trade.batchDenom, + pricePerCredit: trade.pricePerCredit, + quantity: trade.quantity, + executedAt: trade.executedAt, + }); + } + } + + return { newTrades }; + }, + + async orient(obs: Observations): Promise { + const anomalies: PriceAnomaly[] = []; + + for (const trade of obs.newTrades) { + const classSamples = store.getTradesByClass( + trade.classId, + config.market.classMedianWindowDays + ); + const batchSamples = store.getTradesByBatch( + trade.batchDenom, + config.market.batchMedianWindowDays + ); + + if (classSamples.length < config.market.minSamples) continue; + + // Z-score numerator must pair with its denominator's center: + // stddev is measured around the mean, so the Z-score has to + // subtract the mean, not the median. Mixing median with stddev + // silently biases every score by (mean − median), which on a + // skewed price distribution can flip anomaly severity. + // + // We still keep the median around as the human-readable + // reference price on the anomaly record — median reads more + // intuitively than mean for a thin market — but it is not used + // in the statistical test. + const classMedian = median(classSamples); + const classMean = classSamples.reduce((a, b) => a + b, 0) / classSamples.length; + const classStd = stddev(classSamples, classMean); + const zClass = classStd > 0 ? (trade.pricePerCredit - classMean) / classStd : 0; + + const batchMedian = batchSamples.length > 0 ? median(batchSamples) : classMedian; + const batchMean = + batchSamples.length > 0 + ? batchSamples.reduce((a, b) => a + b, 0) / batchSamples.length + : classMean; + const batchStd = batchSamples.length > 1 ? stddev(batchSamples, batchMean) : classStd; + const zBatch = batchStd > 0 ? (trade.pricePerCredit - batchMean) / batchStd : 0; + + const severity = classifyAnomaly(zClass, zBatch); + if (severity === "INFO") continue; + + // Confidence rises with sample size and drops when class and + // batch z-scores disagree (which often indicates the batch is + // just thinly traded rather than a real anomaly). + const sampleConfidence = Math.min(1, classSamples.length / 30); + const agreement = + Math.sign(zClass) === Math.sign(zBatch) && Math.abs(zBatch) > 0 + ? 1 + : 0.6; + const confidence = Math.max(0, Math.min(1, sampleConfidence * agreement)); + + anomalies.push({ + tradeId: trade.id, + batchDenom: trade.batchDenom, + classId: trade.classId, + seller: trade.seller, + quantity: trade.quantity, + pricePerCredit: trade.pricePerCredit, + classMedian, + batchMedian, + zScoreVsClass: zClass, + zScoreVsBatch: zBatch, + sampleSizeClass: classSamples.length, + sampleSizeBatch: batchSamples.length, + severity, + detectedAt: new Date().toISOString(), + confidence, + }); + } + + return { anomalies }; + }, + + async decide(orientation: Orientation): Promise { + const alertsToPublish: PriceAnomaly[] = []; + + for (const anomaly of orientation.anomalies) { + const existing = store.getAnomalyAlertSeverity(anomaly.tradeId); + // Dedupe: don't re-alert at the same or lower severity. + if (existing === "CRITICAL") continue; + if (existing === "WARNING" && anomaly.severity !== "CRITICAL") continue; + + alertsToPublish.push(anomaly); + } + + return { alertsToPublish }; + }, + + async act(decision: Decision): Promise { + let alerted = 0; + let skipped = 0; + + for (const anomaly of decision.alertsToPublish) { + const report = await describePriceAnomaly(anomaly); + + store.saveAnomalyAlert({ + tradeId: anomaly.tradeId, + severity: anomaly.severity, + zClass: anomaly.zScoreVsClass, + zBatch: anomaly.zScoreVsBatch, + report, + }); + + await output({ + workflow: "WF-MM-01", + subjectId: anomaly.tradeId, + title: `${anomaly.severity} — ${anomaly.classId} / ${anomaly.batchDenom}`, + content: report, + alertLevel: anomaly.severity === "CRITICAL" ? "CRITICAL" : "HIGH", + timestamp: new Date(), + }); + alerted++; + } + + if (decision.alertsToPublish.length === 0) { + console.log(" No new price anomalies above threshold."); + } + + return { alerted, skipped }; + }, + }; +} diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts new file mode 100644 index 0000000..05e1008 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.test.ts @@ -0,0 +1,152 @@ +import { describe, it, expect } from "vitest"; +// Import computeDemandIndex from utils so the test does not +// transitively pull in the workflow module, which constructs the +// SQLite-backed Store singleton at module load. aggregateRetirementsByClass +// stays in retirement-tracking — only used by tests that already exercise it. +import { computeDemandIndex } from "../utils.js"; +import { aggregateRetirementsByClass } from "./retirement-tracking.js"; +import type { Retirement } from "../types.js"; + +// ============================================================ +// computeDemandIndex — bounded 0-100 demand signal +// ============================================================ + +describe("computeDemandIndex", () => { + it("returns 0 for zero-activity inputs", () => { + expect(computeDemandIndex(0, 0, 0)).toBe(0); + }); + + it("caps the volume component at 60", () => { + expect(computeDemandIndex(1e100, 0, 0)).toBe(60); + }); + + it("caps the count component at 20 (10 retirements)", () => { + const big = computeDemandIndex(0, 50, 0); + const exact = computeDemandIndex(0, 10, 0); + expect(big).toBe(20); + expect(exact).toBe(20); + }); + + it("caps the breadth component at 20 (5 unique retirees)", () => { + const big = computeDemandIndex(0, 0, 100); + const exact = computeDemandIndex(0, 0, 5); + expect(big).toBe(20); + expect(exact).toBe(20); + }); + + it("caps the total at 100 when all three components max out", () => { + expect(computeDemandIndex(1e100, 50, 50)).toBe(100); + }); + + it("returns a mid-range value for moderate activity", () => { + expect(computeDemandIndex(10000, 5, 3)).toBe(82); + }); + + it("uses log10 scaling so each order of magnitude adds ~20 pre-cap", () => { + const a = computeDemandIndex(10, 0, 0); + const b = computeDemandIndex(100, 0, 0); + expect(b - a).toBe(20); + }); + + it("treats totalQuantity < 1 as 1 (log10 floor)", () => { + expect(computeDemandIndex(0.5, 0, 0)).toBe(0); + expect(computeDemandIndex(0.0001, 0, 0)).toBe(0); + }); + + it("rounds the final index to the nearest integer", () => { + expect(computeDemandIndex(3, 0, 0)).toBe(10); + }); +}); + +// ============================================================ +// aggregateRetirementsByClass — per-class summary builder +// ============================================================ + +function mkRetirement(overrides: Partial = {}): Retirement { + return { + txHash: "tx-test", + batchDenom: "C01-001-20240101-20241231-001", + classId: "C01", + retiree: "regen1retiree", + quantity: 100, + jurisdiction: null, + reason: null, + retiredAt: "2026-02-18T12:00:00Z", + ...overrides, + }; +} + +describe("aggregateRetirementsByClass", () => { + it("returns an empty map for zero retirements", () => { + const result = aggregateRetirementsByClass([]); + expect(result.size).toBe(0); + }); + + it("aggregates a single retirement into a one-entry summary", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ quantity: 500 }), + ]); + expect(result.size).toBe(1); + const summary = result.get("C01")!; + expect(summary.retirementCount).toBe(1); + expect(summary.totalQuantity).toBe(500); + expect(summary.uniqueRetirees).toBe(1); + expect(summary.topRetiree).toBe("regen1retiree"); + expect(summary.topRetireeQuantity).toBe(500); + }); + + it("groups multiple retirements by class id", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ classId: "C01", quantity: 100 }), + mkRetirement({ classId: "C01", quantity: 200, retiree: "regen1other" }), + mkRetirement({ classId: "BT", quantity: 50 }), + ]); + expect(result.size).toBe(2); + expect(result.get("C01")!.totalQuantity).toBe(300); + expect(result.get("C01")!.retirementCount).toBe(2); + expect(result.get("C01")!.uniqueRetirees).toBe(2); + expect(result.get("BT")!.totalQuantity).toBe(50); + expect(result.get("BT")!.retirementCount).toBe(1); + }); + + it("identifies the top retiree by cumulative quantity, not count", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ quantity: 10, retiree: "regen1spammer" }), + mkRetirement({ quantity: 10, retiree: "regen1spammer" }), + mkRetirement({ quantity: 10, retiree: "regen1spammer" }), + mkRetirement({ quantity: 100, retiree: "regen1whale" }), + ]); + const summary = result.get("C01")!; + expect(summary.uniqueRetirees).toBe(2); + expect(summary.topRetiree).toBe("regen1whale"); + expect(summary.topRetireeQuantity).toBe(100); + }); + + it("computes the jurisdiction metadata percentage", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ jurisdiction: "US-CA" }), + mkRetirement({ jurisdiction: "US-NY" }), + mkRetirement({ jurisdiction: null }), + mkRetirement({ jurisdiction: null }), + ]); + const summary = result.get("C01")!; + expect(summary.pctWithJurisdiction).toBe(50); + }); + + it("skips classes whose retirements all sum to zero quantity", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ quantity: 0 }), + ]); + expect(result.size).toBe(0); + }); + + it("handles retirements with empty retiree string without crashing", () => { + const result = aggregateRetirementsByClass([ + mkRetirement({ retiree: "", quantity: 100 }), + ]); + const summary = result.get("C01")!; + expect(summary.totalQuantity).toBe(100); + expect(summary.uniqueRetirees).toBe(0); + expect(summary.topRetiree).toBeNull(); + }); +}); diff --git a/agent-003-market-monitor/src/workflows/retirement-tracking.ts b/agent-003-market-monitor/src/workflows/retirement-tracking.ts new file mode 100644 index 0000000..4124310 --- /dev/null +++ b/agent-003-market-monitor/src/workflows/retirement-tracking.ts @@ -0,0 +1,243 @@ +import { LedgerClient } from "../ledger.js"; +import { store } from "../store.js"; +import { config } from "../config.js"; +import { describeRetirementSummary } from "../monitor.js"; +import { output } from "../output.js"; +import type { OODAWorkflow } from "../ooda.js"; +import type { Retirement, RetirementSummary } from "../types.js"; +import { classIdFromBatchDenom, computeDemandIndex } from "../utils.js"; + +/** + * WF-MM-03: Retirement Pattern Analysis + * + * Trigger: MsgRetire events (LCD tx-search) + * Layer: 1 (Fully Automated) + * + * OODA: + * Observe — Fetch recent MsgRetire tx responses from the LCD + * tx-search endpoint and parse them into Retirement + * records. Each tx can emit multiple retirements if + * the user batched them. + * Orient — Group retirements by credit class. Aggregate totals, + * unique retirees, jurisdiction-metadata share, top + * retiree, and derive the demand index. + * Decide — Generate narrative summary for classes that moved. + * Act — Persist, output, alert on large positive demand shifts. + * + * Previous versions of this workflow used the per-batch supply + * query as an MVP proxy for retirement activity — comparing + * `retired_amount` across cycles to derive a delta. That worked + * but could not carry retiree identity, jurisdiction, or + * per-event timestamp. The current implementation uses the real + * MsgRetire tx stream via `ledger.getRecentRetirementTxs`, so all + * five fields on the Retirement record are now populated from + * ledger state rather than synthesized. + */ + +interface Observations { + retirements: Retirement[]; +} + +interface Orientation { + summariesByClass: Map; +} + +interface Decision { + reports: { + summary: RetirementSummary; + baselineDemand: number; + report: string; + }[]; +} + +interface Actions { + saved: number; + alertsSent: number; +} + +/** + * Aggregate a list of Retirement records into per-class summaries. + * Exported so unit tests can feed it synthetic input without going + * through the observe phase. + * + * `classPriceUsd` provides a per-class USD reference price (dollars + * per credit) that valueing the retirement volume relies on. In + * production the prices come from the most recent WF-MM-02 median ask + * snapshot; tests inject a Map directly. Classes missing from the + * map fall back to 1:1 USD, which preserves the prior behavior and + * documents the "no price oracle yet" state. + */ +export function aggregateRetirementsByClass( + retirements: Retirement[], + capturedAt: string = new Date().toISOString(), + classPriceUsd: Map = new Map() +): Map { + const byClass = new Map(); + for (const r of retirements) { + const bucket = byClass.get(r.classId) || []; + bucket.push(r); + byClass.set(r.classId, bucket); + } + + const summariesByClass = new Map(); + + for (const [classId, rows] of byClass) { + if (rows.length === 0) continue; + + let totalQuantity = 0; + const retireeSet = new Set(); + const retireeQuantity = new Map(); + let jurisdictionCount = 0; + + for (const r of rows) { + totalQuantity += r.quantity; + if (r.retiree) { + retireeSet.add(r.retiree); + retireeQuantity.set( + r.retiree, + (retireeQuantity.get(r.retiree) ?? 0) + r.quantity + ); + } + if (r.jurisdiction) jurisdictionCount++; + } + + if (totalQuantity === 0) continue; + + let topRetiree: string | null = null; + let topRetireeQuantity = 0; + for (const [retiree, qty] of retireeQuantity) { + if (qty > topRetireeQuantity) { + topRetiree = retiree; + topRetireeQuantity = qty; + } + } + + const retirementCount = rows.length; + const uniqueRetirees = retireeSet.size; + const pctWithJurisdiction = + retirementCount > 0 ? (jurisdictionCount / retirementCount) * 100 : 0; + + // Value retirement volume using the most recent median ask + // price from WF-MM-02 for this class when available. Falls back + // to 1:1 USD when no snapshot exists yet — this keeps the field + // populated without pretending there's a price we don't have. + const unitPriceUsd = classPriceUsd.get(classId) ?? 1; + const totalValueUsd = totalQuantity * unitPriceUsd; + const demandIndex = computeDemandIndex( + totalQuantity, + retirementCount, + uniqueRetirees + ); + + summariesByClass.set(classId, { + classId, + windowHours: config.market.retirementWindowHours, + retirementCount, + totalQuantity, + totalValueUsd, + uniqueRetirees, + topRetiree, + topRetireeQuantity, + pctWithJurisdiction, + demandIndex, + capturedAt, + }); + } + + return summariesByClass; +} + +export function createRetirementTrackingWorkflow( + ledger: LedgerClient +): OODAWorkflow { + return { + id: "WF-MM-03", + name: "Retirement Pattern Analysis", + + async observe(): Promise { + // Pull the most recent retirement transactions from the LCD + // tx-search endpoint. Each tx response can emit multiple + // MsgRetire events (batched retirements) — the ledger client + // flattens them into individual Retirement records. + // + // The tx-stream replaces the older batch-supply-delta approach + // wholesale (cap 100 batches + concurrency-5 fan-out + per-batch + // stored cumulative deltas), so that code is intentionally not + // preserved here. Event-sourcing is exact where the delta method + // was approximate. + const retirements = await ledger.getRecentRetirementTxs(200); + return { retirements }; + }, + + async orient(obs: Observations): Promise { + // Build the per-class price map from the latest liquidity + // snapshots so aggregation can value retirement volume without + // duplicating the ask-price parsing logic. + const classPriceUsd = new Map(); + const distinctClassIds = new Set(obs.retirements.map((r) => r.classId)); + for (const classId of distinctClassIds) { + const price = store.getLatestMedianAsk(classId); + if (price !== null) classPriceUsd.set(classId, price); + } + + const summariesByClass = aggregateRetirementsByClass( + obs.retirements, + undefined, + classPriceUsd + ); + return { summariesByClass }; + }, + + async decide(orientation: Orientation): Promise { + const reports: Decision["reports"] = []; + + for (const summary of orientation.summariesByClass.values()) { + const baselineDemand = store.getBaselineDemand(summary.classId, 7); + const report = await describeRetirementSummary(summary, baselineDemand); + reports.push({ summary, baselineDemand, report }); + } + + return { reports }; + }, + + async act(decision: Decision): Promise { + let saved = 0; + let alertsSent = 0; + + for (const { summary, baselineDemand, report } of decision.reports) { + store.saveRetirementSummary({ + classId: summary.classId, + windowHours: summary.windowHours, + totalQuantity: summary.totalQuantity, + totalValueUsd: summary.totalValueUsd, + retirementCount: summary.retirementCount, + demandIndex: summary.demandIndex, + summary: report, + }); + saved++; + + // Alert if demand index jumped meaningfully above the baseline. + // Threshold of +15 index points mirrors the agent character's + // "moderate-high" boundary (65 → 72 example). + const jumped = baselineDemand > 0 && summary.demandIndex - baselineDemand >= 15; + const alertLevel = jumped ? "HIGH" : "NORMAL"; + if (alertLevel !== "NORMAL") alertsSent++; + + await output({ + workflow: "WF-MM-03", + subjectId: summary.classId, + title: `Retirement summary (demand ${summary.demandIndex}${baselineDemand ? ` vs ${baselineDemand.toFixed(0)}` : ""})`, + content: report, + alertLevel, + timestamp: new Date(), + }); + } + + if (decision.reports.length === 0) { + console.log(" No retirement activity detected in any class."); + } + + return { saved, alertsSent }; + }, + }; +} diff --git a/agent-003-market-monitor/tsconfig.json b/agent-003-market-monitor/tsconfig.json new file mode 100644 index 0000000..e6a11e8 --- /dev/null +++ b/agent-003-market-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-003-market-monitor/vitest.config.ts b/agent-003-market-monitor/vitest.config.ts new file mode 100644 index 0000000..ce36a74 --- /dev/null +++ b/agent-003-market-monitor/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + environment: "node", + }, +}); diff --git a/rfcs/GRADUATION_RULES.md b/rfcs/GRADUATION_RULES.md new file mode 100644 index 0000000..20e45a0 --- /dev/null +++ b/rfcs/GRADUATION_RULES.md @@ -0,0 +1,168 @@ +# Spec → RFC → On-chain Proposal: Graduation Rules + +This document defines the **lifecycle and promotion rules** for the three artifact types that govern the agentic-tokenomics → forum → on-chain pipeline. Together they form the deliberation chamber of the bicameral R&D pipeline. + +Companion documents: +- [`_template.md`](./_template.md) — copyable RFC template +- [`schema/`](./schema/) — machine-readable governance review schema for agent + human reviewers + +## The three artifact types + +The agentic-tokenomics repo houses three distinct artifact types, with three distinct lifecycles. Each has a **purpose**, an **audience**, an **author scope** (who can write it), and a **decision scope** (what it commits the system to). + +| | Spec | RFC | On-chain Proposal | +|---|---|---|---| +| **Purpose** | Explore a mechanism design — what could exist | Decide a question — what should exist | Execute a change — what now exists | +| **Audience** | Designers, contributors | Stewards, $REGEN stakers, partners | Validators, on-chain governance | +| **Author scope** | Anyone (humans + agents) | Humans + agents (with human sponsor) | Humans only (no agent-authored proposals to chain — for now) | +| **Decision scope** | Non-binding; specs can branch and proliferate | Binding within the repo; one ratified RFC per question | Binding on-chain; irreversible without superseding proposal | +| **Location** | `mechanisms//SPEC.md`, `phase-N/...` | `rfcs/RFC-XXXX.md` | `phase-4/proposals/RFC-XXXX-proposal.json` | +| **Lifecycle states** | draft → merged → superseded | draft → forum-posted → ratified → enacted (or parked / withdrawn) | drafted → submitted → voting → passed/rejected → enacted | +| **Versioning** | semver per spec; multiple drafts allowed | sequential RFC IDs; one canonical RFC per question; supersession rather than re-versioning | one proposal per attempt; if rejected, file a new one with new ID | + +## Why three layers, not one? + +Two failure modes this is designed to avoid: + +1. **The "everything is a spec" problem.** When everything is a spec, nothing is decisive. Specs accumulate, branch, become impossible to navigate. There needs to be a graduation point where the system says "this is the canonical answer to this question, dispute it via supersession." + +2. **The "specs go directly to chain" problem.** When specs become on-chain proposals without a deliberation step, the chain inherits the noise of design exploration. Validators get asked to vote on under-deliberated changes. Either they reject by default (chilling effect) or they rubber-stamp (loss of governance legitimacy). + +The RFC layer is the **deliberation chamber**. It's where claims get sharpened, evidence gets challenged, and a human sponsor takes accountability before anything reaches chain. + +## Promotion criteria + +### Spec → RFC + +A spec graduates to an RFC when **all** of the following are true: + +1. **There is a decisive claim to be made.** The spec is no longer exploring "what could M013's fee split look like" — it is asserting "M013's fee split should be 15/30/50/5". If the work is still exploratory, it stays in `mechanisms/`. +2. **A human sponsor has volunteered.** No spec graduates without a named human accountable for shepherding the RFC to ratification or withdrawal. +3. **At least one decisive evidence item exists.** Per the RFC template's §2 (Sufficiency assertion), there must be at least one piece of evidence that, if invalidated, would invalidate the claim. Pure-design RFCs without empirical or simulation evidence are not allowed. +4. **The audience tier is declared, and the reference accessibility gate is passable.** If the RFC is targeted at `public`, every evidence URL must already resolve for a public reader, OR there must be a plan to elevate references before forum-posting. +5. **No live conflicting RFC.** If there's an unresolved RFC on the same question, that one must be ratified, withdrawn, or superseded first. + +**Author scope:** anyone. **Decision authority:** the named sponsor (proposes promotion) + a steward confirms. + +### RFC → forum-posted + +An RFC graduates from `draft` to `forum-posted` when: + +1. **The sponsor has prepared a forum post** with all references audience-accessible to the forum's reader population. (For commonwealth.im threads, that's a "public-or-commons" audience.) +2. **The discussion deadline is set** in the RFC's §5 — minimum 7 days, recommended 14, longer for high-stakes proposals. +3. **The RFC is linked from the forum post**, and the forum thread URL is added to the RFC. + +**Author scope:** sponsor only. **Decision authority:** sponsor. + +### RFC → ratified + +An RFC graduates from `forum-posted` to `ratified` when: + +1. **The discussion deadline has passed.** +2. **All key counter-positions raised in §5 have been addressed** — either incorporated into the RFC's claim/methodology, or explicitly noted as "considered and rejected because ". +3. **All open questions in §8 are either resolved or explicitly carried as known limitations.** +4. **The decisive evidence in §2 has not been invalidated** during the discussion period. +5. **A ratifier (a steward distinct from the sponsor) signs off.** The ratifier's job is to confirm the deliberation was sufficient — not to re-litigate the claim. Two-eyes principle. + +**Author scope:** sponsor + ratifier. **Decision authority:** ratifier. + +### RFC → enacted + +An RFC graduates from `ratified` to `enacted` when: + +1. **An on-chain proposal generated from the RFC has passed** (or, for off-chain ratified RFCs that don't require chain action, when the corresponding code/process change is merged and live). +2. **The RFC's §7 (On-chain enactment) is filled out** with the actual proposal ID, voting result, and post-enactment notes. +3. **The lineage in §4 is updated** to reflect the realized outcome. + +**Author scope:** sponsor or proposer. **Decision authority:** automatic once on-chain proposal passes. + +### RFC → on-chain proposal + +A `ratified` RFC produces an on-chain proposal when: + +1. **A human proposer (typically the sponsor) generates the proposal artifact** from the RFC — using deterministic generation rules from §7 wherever possible. +2. **Pre-flight checks listed in §7 have all passed.** (Unit tests, integration tests, simulation reruns, etc.) +3. **The proposal references the RFC** by ID and KOI IRI in its on-chain `description` field. +4. **The proposer commits the deposit** and submits via `regen tx gov submit-proposal`. + +**Critical rule:** **agents do not submit on-chain proposals.** They can draft the JSON, run pre-flight checks, and surface the proposal for human review — but the `submit-proposal` transaction must be signed by a human key. This is the accountability boundary. + +## Demotion / parking / withdrawal + +Some RFCs don't progress. The lifecycle accommodates this: + +- **Parked.** The sponsor can move an RFC from `draft` or `forum-posted` to `parked` if it's blocked on an external dependency (e.g., waiting on M012 enactment before M013 makes sense). Parked RFCs retain their ID and can be revived later by the sponsor; the parking reason is logged in §6. +- **Withdrawn.** The sponsor can withdraw at any state before `enacted`. Withdrawn RFCs are not deleted; their ID is permanently retired. A new RFC on the same question gets a new ID. +- **Superseded.** A new RFC can supersede a `ratified` or `enacted` RFC. The new RFC must explicitly cite the superseded one in its header and explain the delta. The old RFC's status moves to `superseded`. (For `enacted` RFCs, supersession requires a new on-chain proposal.) + +## Author roles: agents vs humans + +This is where the accountability chain lives. Mapping each role to who can occupy it: + +| Role | Codex agent | ElizaOS agent | Human contributor | Human steward | +|---|---|---|---|---| +| Spec author | yes | yes | yes | yes | +| RFC author | yes (with sponsor) | yes (with sponsor) | yes | yes | +| RFC sponsor | no | no | yes | yes | +| RFC ratifier | no | no | no | yes | +| Forum poster | drafts only — sponsor posts | drafts only — sponsor posts | yes | yes | +| On-chain proposer | no | no | yes | yes | + +The pattern: **agents accelerate the cheap parts (drafting, evidence-gathering, JSON generation, pre-flight checks); humans hold the accountable parts (sponsorship, ratification, on-chain submission).** + +## How this integrates with the rest of the system + +- **KOI** indexes specs, RFCs, simulation runs, forum threads, and on-chain proposals under stable IRIs. The `koi:rfc:RFC-XXXX` namespace becomes the canonical reference for RFC artifacts. Cross-document lineage (per §4 of the template) is queryable as graph traversal. + +- **Claims engine** uses the same 4-layer schema (claim, evidence, methodology, status) for ecological credits as the RFC template uses for governance. Same shape, different domain. This means tooling built for one chamber works on the other. + +- **RegenOS** is the coordinator. It watches: + - new RFCs (notifies stewards of incoming sponsorship requests) + - RFCs at `forum-posted` reaching their discussion deadline (prompts ratifier) + - RFCs at `ratified` (prompts proposer to generate on-chain proposal) + - RFCs at `enacted` (closes the loop, updates lineage) + +- **Reference accessibility gate** is the publication gate for any RFC graduating from `draft` to `forum-posted`. The audience tier in the RFC header drives the gate's strictness. + +- **Codex agent** plugs in at the spec layer and at the RFC drafting layer. Its PRs that touch governance-relevant surface (specs, simulations, contract behavior) are flagged for RFC twin generation; its PRs that don't (CI, lint, tests, docs) merge directly. This is the filter that converts agent throughput from "merge volume" into "candidate proposals worth elevating." + +## Governance Review Schema + +The graduation criteria above are designed so an agent can mechanically judge whether an RFC meets them. The canonical review schema lives in [`schema/rfc-review-schema.yaml`](./schema/rfc-review-schema.yaml), with the corresponding judgment output shape in [`schema/rfc-review-judgment.yaml`](./schema/rfc-review-judgment.yaml). See [`schema/README.md`](./schema/README.md) for how agents and human reviewers use them. + +The schema is structured as: + +- **`always` rules** — apply to any RFC at any state ≥ `draft`. Cover header completeness, claim decisiveness, evidence sufficiency, methodology lineage, append-only status log. +- **Gate-specific rules** — `spec-to-rfc`, `rfc-to-forum-posted`, `rfc-to-ratified`, `rfc-to-enacted`. Each gate's rules map directly to the corresponding §Promotion criteria section above. +- **Severities** — `blocker` (prevents graduation), `warning` (must be acknowledged but not blocking), `info` (advisory). + +A reviewer (agent or human) emits a structured judgment with verdict (`pass | needs-revision | fail`), per-rule findings with citations, and a `next_action` directive. This converts "is this RFC ready?" from a vibes-check into a structured checklist with citations, so reviewer attention concentrates on the 3–4 judgment-heavy rules per gate rather than mechanical hygiene. + +**What the schema deliberately does NOT do:** + +- It does not decide ratification. The schema can verify that ratification *criteria are met* but the ratifier (a human steward) still signs the §6 log row. This preserves the two-eyes principle. +- It does not resolve substantive disputes. If two reviewers disagree on whether a counter-position has been "addressed", the schema will flag both findings; humans arbitrate. The schema is a checklist, not a judge of philosophical adequacy. +- It does not handle on-chain submission. Schema rules verify post-hoc that the proposer was human, but never authorize submission. + +## Smallest possible first cut + +If the goal is to ship the smallest viable version of this: + +1. ✅ Create `rfcs/` directory with `_template.md`, `GRADUATION_RULES.md`, and `schema/` (this commit). +2. Pick **one** existing live question — e.g., the M013 fee split (28/25/45/2 vs. 15/30/50/5) — and write **RFC-0001** for it. This forces the template through real use and surfaces what's missing. +3. Run the full lifecycle once (draft → forum-posted → ratified → enacted) before scaling the pattern to the agent stack. +4. Then, and only then, retrofit the Codex agent's PR pipeline with the spec/RFC filter. + +The risk of designing the whole pipeline before running one RFC end-to-end is that the template will be wrong in ways that aren't visible until lived experience reveals them. + +## Open questions for these rules + +- **Who are the stewards / ratifiers?** This document assumes RND PBC members + designated community stewards. Needs explicit roster and rotation rules. +- **How do we handle RFCs that span multiple repos?** (e.g., a fee-split change touches agentic-tokenomics + regen-ledger + a frontend). Proposed: RFC lives in agentic-tokenomics if it's primarily a tokenomics decision; if multi-domain, sponsor decides repo and cross-references. +- **Dispute escalation.** What happens if the sponsor and ratifier disagree on whether deliberation was sufficient? Proposed: third steward arbitrates; if no resolution, RFC parks. +- **Relationship to existing forum governance.** Today, $REGEN governance has its own forum norms. This RFC track needs to either replace or sit alongside that. Recommendation: sit alongside initially, and let lived use determine whether it absorbs the existing track. +- **Relationship to DUNA / Regen Commons governance.** That's a parallel deliberation chamber for the Commons; this is for tokenomics. They likely converge over time but don't need to start unified. + +## Status + +This is `v0.1` of the graduation rules. Expect refinement through 1.0 as RFC-0001 surfaces gaps in real use. diff --git a/rfcs/RFC-0001-m013-fee-split.md b/rfcs/RFC-0001-m013-fee-split.md new file mode 100644 index 0000000..30ed775 --- /dev/null +++ b/rfcs/RFC-0001-m013-fee-split.md @@ -0,0 +1,181 @@ +# RFC-0001 — Adopt M013 Fee Distribution Split 15/30/50/5 + +## 0. Header +- **ID:** RFC-0001 +- **Title:** Adopt M013 Fee Distribution Split (burn / validator / community / agent pools) +- **Status:** draft +- **Author(s):** Claude Code (AI agent, brawlaphant accountable) +- **Sponsor:** +- **Audience:** internal (Regen Network tokenomics working group, validators, governance participants) +- **Last updated:** 2026-06-24 +- **Supersedes:** None +- **Superseded by:** None + +## 1. Claim + +The M013 Value-Based Fee Routing mechanism should split protocol fees across four pools as follows: **15% burn, 30% validator, 50% community, 5% agent** — replacing the interim 28/25/45/2 split and the earlier Model A (30/40/25/5) and Model B (25-35/15-25/50-60/0) framings. + +This distribution balances three strategic objectives: (1) **meaningful but not aggressive deflation** (15% burn maintains deflationary credibility while freeing capital for ecosystem development), (2) **validator sustainability** (30% supports a compensated 15-21 validator set), and (3) **ecosystem growth** (50% community pool funds M015 activity rewards and governance-directed spending; 5% agent pool bootstraps AI infrastructure). + +## 2. Evidence + +| # | Evidence | Type | Source | KOI IRI | URL (audience-accessible) | +|---|---|---|---|---|---| +| E1 | M013 cadCAD simulation (fee-split sweep, burn_share 0-35%) | simulation | `simulations/cadcad/` | `koi:sim:m013-fee-split-sweep-2026-03` | See `mechanisms/m013-value-based-fee-routing/SPEC.md` Appendix C | +| E2 | M012 equilibrium analysis (supply dynamics at 15% burn) | analysis | `simulations/cadcad/equilibrium_analysis.md` §1.3.1 | `koi:analysis:m012-equilibrium-15pct-burn` | In-repo path: `docs/governance/needs-governance-proposals.md` line 88 | +| E3 | Economic Reboot Proposals governance document (Proposal A) | governance | `docs/governance/needs-governance-proposals.md` | `koi:gov:proposal-a-parameters-2026-03` | In-repo path (audience: internal RNG) | +| E4 | M013 SPEC §4 (fee distribution parameter space) | specification | `mechanisms/m013-value-based-fee-routing/SPEC.md` | `koi:spec:m013-fee-distribution` | In-repo mechanism document | +| E5 | M012 SPEC (hard cap interaction with burn dynamics) | specification | `mechanisms/m012-fixed-cap-dynamic-supply/SPEC.md` | `koi:spec:m012-hard-cap-burn` | In-repo mechanism document | + +**Sufficiency assertion:** Evidence E1 and E2 are **decisive**: the simulation validates that 15% burn produces ~220.42M REGEN equilibrium supply (below the proposed 221M hard cap) and that validator compensation at 30% reaches $343-480/validator/month at moderate ($24K/month) fee volumes. E3 and E4 are **supporting**: they document the prior governance deliberation and the mechanism's design range. E5 is **supporting**: it contextualizes the burn mechanism within the M012 hard-cap framework. + +The 15/30/50/5 split is directly derived from the Working Group's economic reboot analysis (E3). No simulation or empirical data contradicts adoption. The evidence suffices for community ratification. + +## 3. Methodology + +- **Governance lineage:** `docs/governance/open-questions-resolution.md` (OQ-M013-1, OQ-M013-5 analysis, §42–88). The open-questions document resolved 33 Phase 2 ambiguities; OQ-M013-1 and OQ-M013-5 specifically addressed fee distribution and burn pool sizing. The recommendation in that document synthesizes prior Model A/B analysis. + +- **Economics lineage:** `docs/governance/needs-governance-proposals.md` (Proposal A, §35–129). This governance proposal packages OQ-M013-1, OQ-M013-5 (and others) into on-chain actionable parameters with risk assessment (§6), voting guidance (§7), and dependency mapping (§8). + +- **Spec lineage:** `mechanisms/m013-value-based-fee-routing/SPEC.md` §4 (fee distribution parameter space: burn_share, validator_share, community_share, agent_share, all ∈ [0, 1] summing to 1). + +- **Simulation lineage:** `simulations/cadcad/` (model configuration supports parameter sweep over burn_share ∈ [0.00, 0.35]; run outputs validate M012 equilibrium at burn_share=0.15). + +- **Contract lineage:** `contracts/fee-router/src/contract.rs` (already implemented; parameters are module init arguments, read-only for this RFC until on-chain activation). + +## 4. Lineage + +```mermaid +graph LR + Phase2["Phase 2: Open Questions"] --> OQ["OQ-M013-1, OQ-M013-5
(open-questions-resolution.md)"] + OQ --> ProposalA["Proposal A
(needs-governance-proposals.md)"] + ProposalA --> RFC["RFC-0001
(this document)"] + + M013Spec["mechanisms/m013 SPEC"] --> ProposalA + M012Spec["mechanisms/m012 SPEC"] --> ProposalA + SimResults["cadCAD simulations
(M012 equilibrium @ 15% burn)"] --> ProposalA + + RFC --> Forum["Forum discussion
(commonwealth.im)"] + Forum --> OnChainProp["On-chain Proposal
(parameter change)"] + OnChainProp --> ChainUpgrade["regen-1 Upgrade
(M013 activation with parameters)"] + + ContractM013["contracts/fee-router/"] -.-> ChainUpgrade +``` + +**Inbound dependencies:** +- Governed by M013 SPEC §4 (parameter bounds) +- Informed by M012 equilibrium dynamics (E2) +- Derived from Phase 2 open-questions resolution + +**Outbound dependencies:** +- Unblocks Economic Reboot Proposal 1 (M013 on-chain activation) +- Unblocks Economic Reboot Proposal 3 (requires clarity on burn pool size for M012 hard-cap calculation) +- Sets context for M015 (activity rewards pool) activation parameters +- Sets context for M014 (validator governance) seed-set sizing + +## 5. Discussion surface + +- **Forum thread:** Not yet posted. Once a human sponsor is assigned, the sponsor will post to commonwealth.im and link here. +- **Discussion deadline:** TBD (minimum 7 days after forum post; coordinator will assign upon sponsor assignment) +- **Key counter-positions:** None yet (document is in draft; feedback invited) + +## 6. Status transitions (lifecycle log) + +| Date | From | To | Trigger | Approver | +|---|---|---|---|---| +| 2026-06-24 | — | draft | Agent authored RFC-0001; awaiting human sponsor assignment | brawlaphant (accountable) | + +## 7. On-chain enactment + +*To be filled in only when status reaches `ratified`.* + +Enactment will be a parameter-change on-chain proposal to the fee-router module, setting: + +``` +distribution = { + burn_share: 0.15, # (1500 bps) + validator_share: 0.30, # (3000 bps) + community_share: 0.50, # (5000 bps) + agent_share: 0.05 # (500 bps) +} +``` + +See `docs/governance/needs-governance-proposals.md` Proposal A §4–5 for the full proposal text and on-chain parameter encoding (uregen units, bps, etc.). This RFC's ratification signals that the parameters in that proposal are approved; the proposal itself will be submitted by a human proposer after ratification. + +- **Action type:** parameter-change +- **Target chain:** regen-1 +- **Generated proposal artifact:** `phase-4/proposals/RFC-0001-proposal.json` (not yet generated; coordinator generates upon ratification) +- **Voting period:** 7 days (standard for parameter changes) +- **Pre-flight checks:** + - Verify fee-router contract compiles (`cargo clippy` in `contracts/fee-router/`) + - Verify integration test `test_fee_distribution_splits()` passes with these parameters + - Re-run M012 equilibrium simulation with burn_share=0.15 to confirm ~220.42M supply result +- **Proposer (must be human):** TBD (will be assigned by Regen governance team upon ratification) +- **On-chain proposal ID:** TBD (filled after submission) +- **Result:** TBD (filled after voting closes) + +## 8. Open questions + +- [ ] **OQ1** — Who will serve as human sponsor for this RFC? (Required before graduation to forum-posted; accountable: brawlaphant, target: immediately upon PR review) +- [ ] **OQ2** — Does the community prefer the 15/30/50/5 split, or does forum discussion reveal preference for Model A, Model B, or an alternative? (Required before graduation to ratified; accountable: sponsor, target: 7+ days after forum post) +- [ ] **OQ3** — Should the 6-month review vote on fee distribution be mandatory (as Proposal A §4 recommends) or optional? (Required before graduation to ratified; accountable: sponsor; target: decided in forum) + +## 9. References + +All references are audience-restricted to internal RNG (Regen Network Development PBC and active governance participants). Public versions will be published once governance matures. + +- **In-repo governance docs (audience: internal):** + - `docs/governance/open-questions-resolution.md` — Phase 2 open-questions resolution; OQ-M013-1 and OQ-M013-5 analysis + - `docs/governance/needs-governance-proposals.md` — Proposal A (Economic Parameters Resolution) + - `mechanisms/m013-value-based-fee-routing/SPEC.md` — Mechanism specification; parameter bounds + - `mechanisms/m012-fixed-cap-dynamic-supply/SPEC.md` — M012 hard-cap specification; burn-pool interaction + - `simulations/cadcad/equilibrium_analysis.md` — M012 supply equilibrium at burn_share=0.15 + +- **External (audience: public commonwealth.im forum, once posted):** + - Commonwealth.im forum thread (TBD link; will be posted by sponsor) + +--- + +## Appendix A: Fee Distribution Tradeoff Analysis + +The three candidate models presented during Phase 2 governance: + +| Model | Burn | Validator | Community | Agent | Rationale | +|-------|------|-----------|-----------|-------|-----------| +| **Model A (original)** | 30% | 40% | 25% | 5% | Prioritizes validator compensation; strong deflation signal | +| **Model B (alternative)** | 25-35% | 15-25% | 50-60% | 0% | Prioritizes community/ecosystem development; eliminates agent pool | +| **RFC-0001 (adopted)** | **15%** | **30%** | **50%** | **5%** | Balances deflation + ecosystem + validator + AI infrastructure | + +**Why 15/30/50/5?** + +1. **Deflation credibility (15%):** Below Model B minimum (25%) but above zero. At moderate fee volumes ($24K/month), 15% = ~120K REGEN/month burned = 3M token gap closes in ~25 months. Meaningful but achievable; leaves room for governance adjustment. + +2. **Validator sustainability (30%):** Between Model B minimum (15%) and Model A (40%). At $24K/month, 30% = $7,200/month ÷ 15-21 validators = $343-480/validator/month. Marginal solo but supplemented by M015 activity rewards. Sufficient to retain quality validator cohort. + +3. **Ecosystem acceleration (50%):** Model B's level. Funds M015 activity rewards (primary driver of contribution growth) and retains governance flexibility (unabsorbed funds accumulate for governance-directed spending). + +4. **AI bootstrap (5%):** Matches Model A. Sufficient for estimated $282/month operational cost of agent infrastructure; allows composability with M010 reputation signals. + +**Conservative vs. aggressive:** + +- **More conservative (higher burn %)** — Model A (30%) prioritizes price appreciation through scarcity. Delays ecosystem funding 1-2 years. +- **More aggressive (lower burn %)** — Model B (25-35% or lower) prioritizes near-term contributor funding. Reduces scarcity signal; harder to increase burn later (political friction). +- **Middle path (15%)** — RFC-0001. Credible scarcity + ecosystem acceleration + validator compensation + agent pool. Allows 6-month review to adjust if community prefers more/less burn. + +## Appendix B: Simulation Validation Summary + +**M012 Equilibrium at burn_share=0.15:** + +From `simulations/cadcad/equilibrium_analysis.md` §1.3.1: + +- Equilibrium supply: ~220.42M REGEN (given hard_cap=221M) +- Burn per epoch (weekly, moderate fee volume $24K/month): ~120K REGEN +- Time to close 3M gap: ~25 months (180K REGEN/month = 2,400 per week) +- Validator compensation at 30% share: $343-480/validator/month (adequate with M015 supplements) +- Community pool absorption (50%): $12,000/month at moderate volumes (sufficient for M015 initial deployment) + +All metrics are within acceptable ranges per the Working Group's prior analysis. No simulation outcome contradicts adoption. + +--- + +**Note for reviewer:** This RFC is agent-authored (Claude Code) but marks brawlaphant as the accountable party. Per `rfcs/GRADUATION_RULES.md` §2.1, **a human sponsor is required before this RFC can graduate past draft**. The sponsor will post the forum thread, manage discussion, and shepherd ratification. The agent does not claim ratification authority. diff --git a/rfcs/_template.md b/rfcs/_template.md new file mode 100644 index 0000000..0eb7ef0 --- /dev/null +++ b/rfcs/_template.md @@ -0,0 +1,104 @@ +# RFC-XXXX — + + + +## 0. Header +- **ID:** RFC-XXXX +- **Title:** +- **Status:** draft +- **Author(s):** +- **Sponsor:** +- **Audience:** internal | partner | commons | public +- **Last updated:** YYYY-MM-DD +- **Supersedes:** +- **Superseded by:** + +## 1. Claim + +*One paragraph. The decisive proposition this RFC asserts. Not "we're exploring X" — that's a spec. RFCs make a claim that can be ratified or rejected. State it sharp.* + +Example: "REGEN's fee router (M013) should split protocol fees 15/30/50/5 between burn / treasury / validator / community pool, replacing the interim 28/25/45/2 split, because ." + +## 2. Evidence + +*Every claim has supporting evidence. Use the table below. Every evidence item must resolve for the audience tier declared in §0 (see the Reference Accessibility Gate in the workspace CLAUDE.md). KOI IRIs are preferred where available.* + +| # | Evidence | Type | Source | KOI IRI | URL (audience-accessible) | +|---|---|---|---|---|---| +| E1 | | simulation | simulations/M013/run-2026-04-20 | `koi:sim:m013-fee-split-2026-04-20` | | +| E2 | | empirical | regen-network mainnet | `koi:onchain:fee-dist-q1-2026` | | +| E3 | | deliberative | commonwealth.im | `koi:forum:thread-NNN` | | + +**Sufficiency assertion:** *Why this evidence is sufficient for the claim. State explicitly which evidence is **decisive** (load-bearing for ratification) vs. **supporting** (corroborates but doesn't independently establish).* + +## 3. Methodology + +*How the claim was derived from the evidence. Cite the spec(s), simulation runs, contract behaviors, or analytic frameworks involved. This section is where the lineage to the **code chamber** lives — every methodology entry should resolve to a path in `mechanisms/`, `simulations/`, `contracts/`, or an external method (with KOI IRI).* + +- **Spec lineage:** `mechanisms/m013-value-based-fee-routing/SPEC.md` §5.2 +- **Simulation lineage:** `simulations/M013/scenarios/fee-split-sweep.py` (run hash ``) +- **Contract lineage:** `contracts/fee-router/src/contract.rs` (commit ``) +- **External methodology:** + +## 4. Lineage + +*Where this RFC sits in the broader pipeline. Both directions:* + +- **Inbound:** specs / prior RFCs / claims this RFC depends on +- **Outbound:** RFCs / on-chain proposals / contract changes that depend on this one + +```mermaid +graph LR + M013[mechanisms/m013 SPEC] --> RFC[RFC-XXXX] + RFC --> Forum[Forum thread #NNN] + Forum --> Prop[On-chain proposal #MMM] + Prop --> ChainUpgrade[regen-1 upgrade] +``` + +## 5. Discussion surface + +*How and where this RFC is being deliberated.* + +- **Forum thread:** +- **Discussion deadline:** YYYY-MM-DD +- **Key counter-positions raised:** + +## 6. Status transitions (lifecycle log) + +*Append-only log. Each transition is a row. Never edit prior rows. The graduation rules (`rfcs/GRADUATION_RULES.md`) define the criteria for each move.* + +| Date | From | To | Trigger | Approver | +|---|---|---|---|---| +| YYYY-MM-DD | — | draft | author drafted | | + +## 7. On-chain enactment + +*Filled in only when status reaches `ratified`. Must specify: (a) the type of on-chain action; (b) deterministic generation rules so the proposal text can be reconstructed from this RFC.* + +- **Action type:** +- **Target chain:** regen-1 | redwood-1 | other +- **Generated proposal artifact:** `phase-4/proposals/RFC-XXXX-proposal.json` +- **Voting period:** +- **Pre-flight checks:** +- **Proposer (must be human):** +- **On-chain proposal ID:** +- **Result:** + +## 8. Open questions + +*Things that need resolution before this RFC graduates further. Each should have an owner and a target date. All must be resolved or explicitly carried as known limitations before graduating to ratified.* + +- [ ] OQ1 — (owner: , by: YYYY-MM-DD) +- [ ] OQ2 — + +## 9. References + +*Audience-accessibility-checked. No phantom references. If the §0 audience is `public`, only public URLs allowed. If `internal`, internal Drives are okay but unpack the substance inline. See the Reference Accessibility Gate in the workspace CLAUDE.md.* + +- [E1] +- [E2] +- [E3] diff --git a/rfcs/schema/rfc-review-schema.yaml b/rfcs/schema/rfc-review-schema.yaml new file mode 100644 index 0000000..b2540b8 --- /dev/null +++ b/rfcs/schema/rfc-review-schema.yaml @@ -0,0 +1,223 @@ +# RFC Review Schema +# Canonical machine-readable review schema for the agentic-tokenomics RFC +# process. An agent or human reviewer runs the rules below over an RFC +# document and emits a structured judgment per rfc-review-judgment.yaml. +# +# Source of truth: rfcs/GRADUATION_RULES.md + +meta: + schema_version: "0.1" + applies_to: rfcs/RFC-*.md + review_authority: "any agent or human reviewer; binding decisions still require the human roles in GRADUATION_RULES.md §Author roles" + source: rfcs/GRADUATION_RULES.md + +categories: + process: "structural / format / completeness checks; mostly mechanical" + content: "substantive claim/evidence/methodology checks; require LLM judgment" + +severities: + blocker: "rule failure prevents graduation; verdict cannot be 'pass'" + warning: "rule failure does not block but must be acknowledged in judgment" + info: "advisory; does not affect verdict" + +# ───────────────────────────────────────────────────────────────── +# Always-on rules (apply to any RFC at any state >= draft) +# ───────────────────────────────────────────────────────────────── + +always: + - id: R-HEADER-001 + category: process + severity: blocker + rule: "Header §0 contains all required fields with non-empty values" + required_fields: [id, title, status, authors, sponsor, audience, last_updated] + passes_when: "every required field is present and non-empty" + fails_when: "any required field is missing or blank" + + - id: R-HEADER-002 + category: process + severity: blocker + rule: "Sponsor is human, not an agent ID" + passes_when: "sponsor field resolves to a human contributor" + fails_when: "sponsor matches agent-id pattern (codex-*, eliza-*, brawlaphant, *-bot)" + cite: "GRADUATION_RULES.md §Author roles" + + - id: R-HEADER-003 + category: process + severity: blocker + rule: "Status field is one of the allowed lifecycle states" + allowed_values: [draft, forum-posted, ratified, enacted, parked, withdrawn, superseded] + passes_when: "status is in allowed_values" + fails_when: "status is anything else" + + - id: R-HEADER-004 + category: process + severity: blocker + rule: "Audience tier is declared and valid" + allowed_values: [internal, partner, commons, public] + passes_when: "audience is in allowed_values" + + - id: R-LIFECYCLE-001 + category: process + severity: blocker + rule: "Status transition log §6 is append-only and consistent with current status" + passes_when: "the most recent log row's `to` field equals the header status; no row has been edited (per git history)" + fails_when: "header status disagrees with last log row, or rows have been retroactively edited" + + - id: R-CLAIM-001 + category: content + severity: blocker + rule: "§1 contains a decisive claim, not exploratory framing" + passes_when: "§1 is a single declarative statement asserting what should be done; can be a paragraph but must commit to a position" + fails_when: "§1 uses exploratory hedges ('we could', 'might consider', 'is exploring', 'this RFC investigates')" + cite: "GRADUATION_RULES.md §Promotion criteria — Spec → RFC, item 1" + + - id: R-EVIDENCE-001 + category: content + severity: blocker + rule: "§2 declares at least one decisive evidence item" + passes_when: "the §2 sufficiency assertion explicitly identifies >=1 evidence row as decisive (load-bearing for ratification)" + fails_when: "no evidence is marked decisive, or sufficiency assertion is missing" + + - id: R-EVIDENCE-002 + category: process + severity: warning + rule: "Each evidence row provides at minimum one resolvable reference (URL or KOI IRI)" + passes_when: "every row has a non-empty URL or KOI IRI column" + fails_when: "any row has neither a URL nor an IRI" + + - id: R-METHODOLOGY-001 + category: content + severity: blocker + rule: "§3 cites at least one repo path (spec, simulation, or contract) that supports the claim" + passes_when: "at least one path under mechanisms/, simulations/, or contracts/ is cited and resolves" + fails_when: "no repo path cited, or cited paths do not resolve in the current commit" + + - id: R-LINEAGE-001 + category: process + severity: warning + rule: "§4 declares at least inbound lineage (specs/RFCs this depends on)" + passes_when: "§4 has at least one inbound entry; can be 'none' if explicitly stated" + fails_when: "§4 is empty or omitted" + +# ───────────────────────────────────────────────────────────────── +# Gate-specific rules +# ───────────────────────────────────────────────────────────────── + +gates: + + spec-to-rfc: + description: "A spec graduates into RFC draft." + cite: "GRADUATION_RULES.md §Promotion criteria — Spec → RFC" + rules: + - id: R-G-S2R-001 + category: process + severity: blocker + rule: "A human sponsor has volunteered (header §0 Sponsor is non-empty and human)" + delegates_to: [R-HEADER-002] + + - id: R-G-S2R-002 + category: content + severity: blocker + rule: "No live conflicting RFC exists on the same question" + passes_when: "no RFC in state in {draft, forum-posted, ratified} addresses the same decision" + fails_when: "one or more conflicting RFCs exist; must be resolved/withdrawn first" + check_method: "scan rfcs/ for RFCs whose claim semantically overlaps; surface candidates for human disambiguation" + + - id: R-G-S2R-003 + category: content + severity: blocker + rule: "Reference accessibility gate is passable for declared audience tier" + passes_when: "every reference in §9 either resolves at the declared audience tier, OR has a noted elevation plan" + fails_when: "any reference is workspace-local or otherwise inaccessible to declared audience without an elevation plan" + cite: "workspace CLAUDE.md — Reference Accessibility Gate" + + rfc-to-forum-posted: + description: "Draft RFC moves to forum-posted state." + cite: "GRADUATION_RULES.md §RFC → forum-posted" + rules: + - id: R-G-FP-001 + category: process + severity: blocker + rule: "Forum thread URL is present in §5" + passes_when: "§5 forum thread field is a resolvable URL on commonwealth.im (or equivalent forum)" + + - id: R-G-FP-002 + category: process + severity: blocker + rule: "Discussion deadline is set and >=7 days from forum-post date" + passes_when: "§5 deadline is a future date >=7 days after the forum-posted log entry" + + - id: R-G-FP-003 + category: content + severity: blocker + rule: "Forum post references this RFC by ID and KOI IRI" + passes_when: "fetched forum thread body contains the RFC ID and koi:rfc:RFC-XXXX" + check_method: "fetch forum URL and grep for RFC ID + KOI IRI" + + rfc-to-ratified: + description: "Forum-posted RFC moves to ratified." + cite: "GRADUATION_RULES.md §RFC → ratified" + rules: + - id: R-G-RAT-001 + category: process + severity: blocker + rule: "Discussion deadline has passed" + passes_when: "today >= §5 deadline" + + - id: R-G-RAT-002 + category: content + severity: blocker + rule: "Every key counter-position in §5 has been addressed" + passes_when: "each entry in §5 counter-positions has either: (a) been incorporated into claim/methodology with a §6 log entry, or (b) been explicitly noted as 'considered and rejected because '" + fails_when: "any counter-position is unaddressed or noted as 'TODO'" + + - id: R-G-RAT-003 + category: content + severity: blocker + rule: "All §8 open questions are resolved or carried as known limitations" + passes_when: "every checkbox in §8 is either checked, or annotated 'carried as known limitation: '" + + - id: R-G-RAT-004 + category: content + severity: blocker + rule: "Decisive evidence in §2 has not been invalidated during discussion" + passes_when: "no §6 log entry or §5 counter-position invalidates a §2 row marked decisive without a replacement" + check_method: "LLM judgment over discussion + evidence delta" + + - id: R-G-RAT-005 + category: process + severity: blocker + rule: "Ratifier is named and distinct from sponsor" + passes_when: "the §6 log row transitioning to ratified has a non-empty Approver field that is not the sponsor" + cite: "GRADUATION_RULES.md §RFC → ratified, item 5 — two-eyes principle" + + rfc-to-enacted: + description: "Ratified RFC moves to enacted (on-chain proposal passed)." + cite: "GRADUATION_RULES.md §RFC → enacted" + rules: + - id: R-G-ENA-001 + category: process + severity: blocker + rule: "§7 on-chain enactment is fully filled out" + required_fields: [action_type, target_chain, generated_proposal_artifact, voting_period, pre_flight_checks] + passes_when: "all required_fields are present and non-empty" + + - id: R-G-ENA-002 + category: process + severity: blocker + rule: "On-chain proposal has a passed result" + passes_when: "queried chain returns proposal status = PASSED for the proposal ID cited in §7" + check_method: "use ledger MCP get_governance_proposal with cited proposal ID" + + - id: R-G-ENA-003 + category: process + severity: blocker + rule: "Pre-flight checks listed in §7 all show passing in CI" + passes_when: "every pre-flight check resolves to a green CI run on the merge commit prior to proposal submission" + + - id: R-G-ENA-004 + category: process + severity: blocker + rule: "On-chain proposer is human" + passes_when: "the on-chain submission tx is signed by a human key (not an agent service key)" + cite: "GRADUATION_RULES.md §RFC → on-chain proposal, critical rule"