diff --git a/.github/workflows/dashboard-a11y.yml b/.github/workflows/dashboard-a11y.yml new file mode 100644 index 000000000..1e3cba251 --- /dev/null +++ b/.github/workflows/dashboard-a11y.yml @@ -0,0 +1,45 @@ +name: Dashboard a11y + cross-browser + +# Runs axe-core a11y assertions on the built dashboard across +# Chromium, Firefox, and WebKit. Closes ADR-092 §11.5 (axe-core) +# and §11.8 (cross-browser). + +on: + push: + branches: [main] + paths: ['dashboard/**', 'v2/crates/nvsim/**'] + pull_request: + paths: ['dashboard/**'] + workflow_dispatch: + +permissions: + contents: read + +jobs: + a11y: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: { targets: wasm32-unknown-unknown } + + - run: cargo install wasm-pack --locked --version 0.13.x || true + + - name: Build nvsim WASM + working-directory: v2 + run: | + wasm-pack build crates/nvsim --target web \ + --out-dir ../../dashboard/public/nvsim-pkg \ + --release -- --no-default-features --features wasm + + - uses: actions/setup-node@v4 + with: { node-version: 20, cache: npm, cache-dependency-path: dashboard/package-lock.json } + + - working-directory: dashboard + run: | + npm ci + npm install --save-dev @playwright/test @axe-core/playwright + npx playwright install --with-deps + npm run build + npx playwright test diff --git a/.github/workflows/dashboard-pages.yml b/.github/workflows/dashboard-pages.yml new file mode 100644 index 000000000..d484e0488 --- /dev/null +++ b/.github/workflows/dashboard-pages.yml @@ -0,0 +1,85 @@ +name: nvsim Dashboard → GitHub Pages + +# Deploys the nvsim Vite/Lit dashboard to gh-pages/nvsim/ — preserving +# the existing observatory/, pose-fusion/, and root index.html demos +# already published from gh-pages. ADR-092 §9. + +on: + push: + branches: [main] + paths: + - 'v2/crates/nvsim/**' + - 'dashboard/**' + - '.github/workflows/dashboard-pages.yml' + workflow_dispatch: + +permissions: + contents: write + +concurrency: + group: dashboard-pages + cancel-in-progress: true + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@v4 + + - name: Install Rust + wasm32 target + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + v2/target + key: ${{ runner.os }}-cargo-nvsim-${{ hashFiles('v2/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo-nvsim- + + - name: Install wasm-pack + run: cargo install wasm-pack --locked --version 0.13.x || true + + - name: Build nvsim WASM + working-directory: v2 + run: | + wasm-pack build crates/nvsim \ + --target web \ + --out-dir ../../dashboard/public/nvsim-pkg \ + --release \ + -- --no-default-features --features wasm + + - name: Setup Node 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: dashboard/package-lock.json + + - name: Install dashboard deps + working-directory: dashboard + run: npm ci + + - name: Build dashboard + working-directory: dashboard + env: + NVSIM_BASE: /RuView/nvsim/ + run: npm run build + + - name: Deploy to gh-pages/nvsim/ + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./dashboard/dist + destination_dir: nvsim + # CRITICAL: preserves observatory/, pose-fusion/, root index.html + # and any other RuView demos already on gh-pages. + keep_files: true + commit_message: 'deploy(nvsim): ${{ github.sha }}' + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' diff --git a/.github/workflows/nvsim-server-docker.yml b/.github/workflows/nvsim-server-docker.yml new file mode 100644 index 000000000..764b2e745 --- /dev/null +++ b/.github/workflows/nvsim-server-docker.yml @@ -0,0 +1,69 @@ +name: nvsim-server → ghcr.io + +# Builds and publishes the nvsim-server Docker image to ghcr.io on: +# - push to main affecting nvsim-server or nvsim +# - tag push matching nvsim-server-v* +# - manual workflow_dispatch +# +# ADR-092 §6.2 + §9.4. + +on: + push: + branches: [main] + paths: + - 'v2/crates/nvsim-server/**' + - 'v2/crates/nvsim/**' + - '.github/workflows/nvsim-server-docker.yml' + tags: ['nvsim-server-v*'] + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + build-and-publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/ruvnet/nvsim-server + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build + push + uses: docker/build-push-action@v5 + with: + context: v2 + file: v2/crates/nvsim-server/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + + - name: Smoke-test the image + run: | + docker pull ghcr.io/ruvnet/nvsim-server:sha-${GITHUB_SHA::7} || \ + docker pull ghcr.io/ruvnet/nvsim-server:latest + docker run --rm -d --name nvsim-test -p 7878:7878 \ + ghcr.io/ruvnet/nvsim-server:latest + sleep 4 + curl -fsS http://localhost:7878/api/health + docker stop nvsim-test diff --git a/CHANGELOG.md b/CHANGELOG.md index b0e48ad34..c754a9852 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- **`nvsim` crate — deterministic NV-diamond magnetometer pipeline simulator** (ADR-089) — + New standalone leaf crate at `v2/crates/nvsim` modeling a forward-only + magnetic sensing path: scene → source synthesis (Biot–Savart, dipole, + current loop, ferrous induced moment) → material attenuation + (Air/Drywall/Brick/Concrete/Reinforced/SteelSheet) → NV ensemble + (4 〈111〉 axes, ODMR linear-readout proxy, shot-noise floor per + Wolf 2015 / Barry 2020) → 16-bit ADC + lock-in demodulation → + fixed-layout `MagFrame` records → SHA-256 witness. Six-pass build + per `docs/research/quantum-sensing/15-nvsim-implementation-plan.md`. + 50 tests, ~4.5 M samples/s on x86_64 (4500× the Cortex-A53 1 kHz + acceptance gate), pinned reference witness + `cc8de9b01b0ff5bd97a6c17848a3f156c174ea7589d0888164a441584ec593b4` + for byte-equivalence regression. WASM-ready by construction + (zero `std::time/fs/env/process/thread`); builds cleanly for + `wasm32-unknown-unknown`. ADR-090 (Proposed, conditional) tracks the + optional Lindblad/Hamiltonian extension if AC magnetometry, MW power + saturation, hyperfine spectroscopy, or pulsed protocols become required. + ### Fixed - **Ghost skeletons in live UI with multi-node ESP32 setups** (#420, ADR-082) — `tracker_bridge::tracker_to_person_detections` documented itself as filtering diff --git a/CLAUDE.md b/CLAUDE.md index 31fb33f2e..55ba7dc55 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,6 +22,7 @@ Dual codebase: Python v1 (`v1/`) and Rust port (`v2/`). | `wifi-densepose-sensing-server` | Lightweight Axum server for WiFi sensing UI | | `wifi-densepose-wifiscan` | Multi-BSSID WiFi scanning (ADR-022) | | `wifi-densepose-vitals` | ESP32 CSI-grade vital sign extraction (ADR-021) | +| `nvsim` | Deterministic NV-diamond magnetometer pipeline simulator (ADR-089) — standalone leaf, WASM-ready | ### RuvSense Modules (`signal/src/ruvsense/`) | Module | Purpose | diff --git a/assets/NVsim Dashboard.zip b/assets/NVsim Dashboard.zip new file mode 100644 index 000000000..5ae5b19d6 Binary files /dev/null and b/assets/NVsim Dashboard.zip differ diff --git a/dashboard/.gitignore b/dashboard/.gitignore new file mode 100644 index 000000000..4327d672d --- /dev/null +++ b/dashboard/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.vite +*.log +public/nvsim-pkg diff --git a/dashboard/index.html b/dashboard/index.html new file mode 100644 index 000000000..632124fa1 --- /dev/null +++ b/dashboard/index.html @@ -0,0 +1,18 @@ + + + + + + RuView · nvsim — NV-Diamond Magnetometer Simulator + + + + + + + + + + + + diff --git a/dashboard/package-lock.json b/dashboard/package-lock.json new file mode 100644 index 000000000..1b24bf152 --- /dev/null +++ b/dashboard/package-lock.json @@ -0,0 +1,6525 @@ +{ + "name": "@ruvnet/nvsim-dashboard", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@ruvnet/nvsim-dashboard", + "version": "0.1.0", + "dependencies": { + "@preact/signals-core": "^1.8.0", + "lit": "^3.2.1", + "workbox-window": "^7.4.0" + }, + "devDependencies": { + "@axe-core/playwright": "^4.11.2", + "@playwright/test": "^1.59.1", + "typescript": "^5.6.3", + "vite": "^5.4.10", + "vite-plugin-pwa": "^1.2.0", + "vitest": "^2.1.4" + } + }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.11.2", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.11.2.tgz", + "integrity": "sha512-iP6hfNl9G0j/SEUSo8M7D80RbcDo9KRAAfDP4IT5OHB+Wm6zUHIrm8Y51BKI+Oyqduvipf9u1hcRy57zCBKzWQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.11.3" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.2.tgz", + "integrity": "sha512-DYD23veRYGvBFhcTY1iUvJnDNpuqNd/BzBwCvzOTKUnJjKg5kpUBh3/u9585Agdkgj+QuygG7jLfOPWMa2KVNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz", + "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@preact/signals-core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.14.1.tgz", + "integrity": "sha512-vxPpfXqrwUe9lpjqfYNjAF/0RF/eFGeLgdJzdmIIZjpOnTmGmAB4BjWone562mJGMRP4frU6iZ6ei3PDsu52Ng==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@surma/rollup-plugin-off-main-thread": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", + "integrity": "sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.6", + "json5": "^2.2.0", + "magic-string": "^0.25.0", + "string.prototype.matchall": "^4.0.6" + } + }, + "node_modules/@surma/rollup-plugin-off-main-thread/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "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/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, + "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/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axe-core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz", + "integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.23", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.23.tgz", + "integrity": "sha512-xwVXGqevyKPsiuQdLj+dZMVjidjJV508TBqexND5HrF89cGdCYCJFB3qhcxRHSeMctdCfbR1jrxBajhDy7o29g==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "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": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001791", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001791.tgz", + "integrity": "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/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/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.49.0.tgz", + "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-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/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.344", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz", + "integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==", + "dev": true, + "license": "ISC" + }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-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==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/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/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz", + "integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "dev": true, + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lit": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz", + "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true, + "license": "MIT" + }, + "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/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.38", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz", + "integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "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/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "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/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.1.tgz", + "integrity": "sha512-dLsljMd9sqwRkby8zhO1gSg3PnJIBFid8f4CQj/sXx+7cKx+E7u0PKhZ+U4wmhx7EfmtvnA318oVaIkAB1lRJw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "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.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smob": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sourcemap-codec": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", + "deprecated": "Please use @jridgewell/sourcemap-codec instead", + "dev": true, + "license": "MIT" + }, + "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/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.46.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.2.tgz", + "integrity": "sha512-uxfo9fPcSgLDYob/w1FuL0c99MWiJDnv+5qXSQc5+Ki5NjVNsYi66INnMFBjf6uFz6OnX12piJQPF4IpjJTNTw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "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/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/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": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/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-plugin-pwa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "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/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/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/workbox-background-sync": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-build": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^5.2.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-terser": "^0.4.3", + "@surma/rollup-plugin-off-main-thread": "^2.2.3", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "lodash": "^4.17.20", + "pretty-bytes": "^5.3.0", + "rollup": "^2.79.2", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", + "integrity": "sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.10.4", + "@rollup/pluginutils": "^3.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + } + } + }, + "node_modules/workbox-build/node_modules/@rollup/plugin-replace": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz", + "integrity": "sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^3.1.0", + "magic-string": "^0.25.7" + }, + "peerDependencies": { + "rollup": "^1.20.0 || ^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@rollup/pluginutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz", + "integrity": "sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "0.0.39", + "estree-walker": "^1.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0" + } + }, + "node_modules/workbox-build/node_modules/@types/estree": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", + "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/estree-walker": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz", + "integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-build/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, + "node_modules/workbox-build/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/rollup": { + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=10.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-core": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.0" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/dashboard/package.json b/dashboard/package.json new file mode 100644 index 000000000..e99f9ae45 --- /dev/null +++ b/dashboard/package.json @@ -0,0 +1,30 @@ +{ + "name": "@ruvnet/nvsim-dashboard", + "version": "0.1.0", + "description": "Vite + Lit dashboard for the nvsim NV-diamond magnetometer pipeline simulator (ADR-092).", + "type": "module", + "private": true, + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 4173", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:e2e": "playwright test", + "test:a11y": "playwright test tests/a11y.spec.ts" + }, + "dependencies": { + "@preact/signals-core": "^1.8.0", + "lit": "^3.2.1", + "workbox-window": "^7.4.0" + }, + "devDependencies": { + "@axe-core/playwright": "^4.11.2", + "@playwright/test": "^1.59.1", + "typescript": "^5.6.3", + "vite": "^5.4.10", + "vite-plugin-pwa": "^1.2.0", + "vitest": "^2.1.4" + } +} diff --git a/dashboard/playwright.config.ts b/dashboard/playwright.config.ts new file mode 100644 index 000000000..936e10839 --- /dev/null +++ b/dashboard/playwright.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + retries: 0, + reporter: 'list', + use: { + baseURL: 'http://localhost:4173', + headless: true, + }, + webServer: { + command: 'npm run preview', + port: 4173, + timeout: 60_000, + reuseExistingServer: !process.env.CI, + }, + projects: [ + { name: 'chromium', use: { browserName: 'chromium' } }, + { name: 'firefox', use: { browserName: 'firefox' } }, + { name: 'webkit', use: { browserName: 'webkit' } }, + ], +}); diff --git a/dashboard/public/icon-192.svg b/dashboard/public/icon-192.svg new file mode 100644 index 000000000..a378624b3 --- /dev/null +++ b/dashboard/public/icon-192.svg @@ -0,0 +1,4 @@ + + + NV + diff --git a/dashboard/public/icon-512.svg b/dashboard/public/icon-512.svg new file mode 100644 index 000000000..67372c102 --- /dev/null +++ b/dashboard/public/icon-512.svg @@ -0,0 +1,10 @@ + + + + + + + + + NV + diff --git a/dashboard/src/app.css b/dashboard/src/app.css new file mode 100644 index 000000000..2ccc2ba58 --- /dev/null +++ b/dashboard/src/app.css @@ -0,0 +1,92 @@ +/* nvsim dashboard — global styles + Ported from `assets/NVsim Dashboard.zip` per ADR-092 §7.1. + Per-component scoped styles live in each Lit element. */ + +:root { + --bg-0: #07090d; + --bg-1: #0d1117; + --bg-2: #131a23; + --bg-3: #1a232f; + --line: #1f2a38; + --line-2: #2a3848; + --ink: #e6edf3; + --ink-2: #b8c2cc; + --ink-3: #7c8694; + --ink-4: #4a5462; + --accent: oklch(0.78 0.14 70); + --accent-2: oklch(0.78 0.12 195); + --accent-3: oklch(0.72 0.18 330); + --accent-4: oklch(0.78 0.14 145); + --warn: oklch(0.7 0.18 35); + --ok: oklch(0.78 0.14 145); + --bad: oklch(0.65 0.22 25); + --grid: rgba(255, 255, 255, 0.04); + --shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.6), + 0 4px 12px -4px rgba(0, 0, 0, 0.4); + --radius: 12px; + --radius-sm: 8px; + --mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + --sans: 'Inter', system-ui, -apple-system, sans-serif; +} + +[data-theme="light"] { + --bg-0: #f4f5f7; + --bg-1: #fbfbfc; + --bg-2: #ffffff; + --bg-3: #f0f2f5; + --line: #d8dde3; + --line-2: #c1c8d1; + --ink: #0e131a; + --ink-2: #2c3744; + --ink-3: #54606e; /* AA on --bg-1 #fbfbfc — was #6b7684 (3.7:1), now ~5.4:1 */ + --ink-4: #7a8390; /* improved from #9ba4b0 for incidental UI labels */ + --grid: rgba(0, 0, 0, 0.05); + --shadow: 0 12px 40px -16px rgba(15, 30, 55, 0.18), + 0 2px 8px -2px rgba(15, 30, 55, 0.08); +} + +* { box-sizing: border-box; } +html, body { margin: 0; padding: 0; } +body { + font-family: var(--sans); + background: var(--bg-0); + color: var(--ink); + font-size: 14px; + line-height: 1.45; + overflow: hidden; + height: 100vh; + -webkit-font-smoothing: antialiased; + letter-spacing: -0.005em; +} + +button { font-family: inherit; color: inherit; cursor: pointer; } +input, select { font-family: inherit; color: inherit; } + +::-webkit-scrollbar { width: 8px; height: 8px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 4px; } +::-webkit-scrollbar-thumb:hover { background: var(--ink-4); } + +@keyframes pulse { 50% { opacity: 0.5; } } +@keyframes dash { to { stroke-dashoffset: -200; } } +@keyframes float-up { + 0% { opacity: 0; transform: translateY(8px); } + 100% { opacity: 1; transform: translateY(0); } +} +@keyframes diamond-spin { + 0% { transform: rotateY(0) rotateX(8deg); } + 100% { transform: rotateY(360deg) rotateX(8deg); } +} +@keyframes spin { to { transform: rotate(360deg); } } + +body.reduce-motion *, +body.reduce-motion *::before, +body.reduce-motion *::after { + animation: none !important; + transition: none !important; +} + +/* Density (set via class on by setDensity()) */ +body.density-comfy { font-size: 15px; } +body.density-default { font-size: 14px; } +body.density-compact { font-size: 13px; } diff --git a/dashboard/src/components/nv-app-store.ts b/dashboard/src/components/nv-app-store.ts new file mode 100644 index 000000000..f6b90f9bc --- /dev/null +++ b/dashboard/src/components/nv-app-store.ts @@ -0,0 +1,399 @@ +/* App Store — catalog of every WASM edge module + simulator app. + * + * Mirrors `wifi-densepose-wasm-edge`'s 60+ hot-loadable algorithms and + * the `nvsim` simulator. Each card is filterable by category, fuzzy + * name search, and maturity (available / beta / research). A toggle on + * each card flips activation in the live session — that drives the + * dashboard's event log when running. WS transport (future) pushes the + * activation set to the connected ESP32 mesh. + * + * ADR-092 §18. + */ + +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { signal, effect } from '@preact/signals-core'; +import { + APPS, CATEGORIES, defaultActivations, fuzzyMatch, + type AppCategory, type AppManifest, type AppActivation, +} from '../store/apps'; +import { kvGet, kvSet } from '../store/persistence'; +import { pushLog, activeAppIds, appEvents, appEventCounts } from '../store/appStore'; +import { hasRuntime } from '../store/appRuntimes'; + +const activations = signal(defaultActivations()); +const query = signal(''); +const activeCat = signal('all'); +const statusFilter = signal<'all' | 'available' | 'beta' | 'research'>('all'); + +(async () => { + const saved = await kvGet('app-activations'); + if (saved) activations.value = saved; +})(); + +effect(() => { + // Persist activations on change (post-load) AND mirror into the + // active-set signal that main.ts watches to drive runtime dispatch. + const v = activations.value; + if (v.length > 0) void kvSet('app-activations', v); + const set = new Set(); + for (const a of v) if (a.active) set.add(a.id); + activeAppIds.value = set; +}); + +@customElement('nv-app-store') +export class NvAppStore extends LitElement { + @state() private renderTick = 0; + + static styles = css` + :host { + display: block; + height: 100%; + overflow-y: auto; + background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%); + padding: 24px; + } + .head { + display: flex; align-items: center; gap: 16px; + margin-bottom: 18px; + flex-wrap: wrap; + } + .ttl { + font-size: 22px; font-weight: 700; letter-spacing: -0.02em; + color: var(--ink); + flex: 1; min-width: 200px; + } + .ttl small { + font-size: 12.5px; font-weight: 400; + color: var(--ink-3); margin-left: 8px; + } + .search { + width: 320px; max-width: 100%; + padding: 8px 12px; + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: 8px; + font-family: var(--mono); + font-size: 12.5px; + color: var(--ink); outline: none; + } + .search:focus { border-color: var(--accent); } + .filters { + display: flex; flex-wrap: wrap; gap: 6px; + margin-bottom: 18px; + } + .chip { + padding: 4px 10px; + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: 999px; + font-size: 11.5px; color: var(--ink-3); + cursor: pointer; + font-family: var(--mono); + display: inline-flex; align-items: center; gap: 4px; + } + .chip:hover { color: var(--ink); border-color: var(--line-2); } + .chip.on { background: var(--bg-3); border-color: var(--accent); color: var(--ink); } + .chip .swatch { + width: 7px; height: 7px; border-radius: 50%; + } + .chip .count { color: var(--ink-3); font-size: 10px; } + .grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 12px; + } + .card { + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 12px 14px; + display: flex; flex-direction: column; gap: 6px; + transition: border-color 0.15s, transform 0.15s; + position: relative; + } + .card:hover { border-color: var(--line-2); transform: translateY(-1px); } + .card.active { + border-color: oklch(0.78 0.14 145 / 0.7); + background: linear-gradient(180deg, var(--bg-2) 0%, oklch(0.78 0.14 145 / 0.04) 100%); + } + .card-h { + display: flex; align-items: flex-start; gap: 8px; + margin-bottom: 2px; + } + .card-h .name { + font-size: 13.5px; font-weight: 600; color: var(--ink); + flex: 1; line-height: 1.3; + } + .card-h .swatch { + width: 10px; height: 10px; border-radius: 50%; + flex-shrink: 0; margin-top: 4px; + } + .summary { + font-size: 12px; color: var(--ink-2); line-height: 1.45; + flex: 1; + } + .meta { + display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; + font-family: var(--mono); font-size: 10px; + } + .badge { + padding: 1px 6px; border-radius: 4px; + background: var(--bg-3); color: var(--ink-3); + border: 1px solid var(--line); + } + .badge.cat { color: var(--accent); border-color: oklch(0.78 0.14 70 / 0.3); } + .badge.status-available { color: var(--ok); border-color: oklch(0.78 0.14 145 / 0.4); } + .badge.status-beta { color: var(--warn); border-color: oklch(0.7 0.18 35 / 0.4); } + .badge.status-research { color: var(--accent-3); border-color: oklch(0.72 0.18 330 / 0.4); } + .badge.budget { color: var(--accent-2); border-color: oklch(0.78 0.12 195 / 0.3); } + .badge.rt-running { color: var(--ok); border-color: oklch(0.78 0.14 145 / 0.5); background: oklch(0.78 0.14 145 / 0.08); } + .badge.rt-simulated { color: var(--accent); border-color: oklch(0.78 0.14 70 / 0.5); background: oklch(0.78 0.14 70 / 0.08); } + .badge.rt-mesh-only { color: var(--ink-3); border-color: var(--line); } + .events-feed { + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 14px; + margin-bottom: 18px; + } + .events-feed h3 { + margin: 0 0 8px; + font-size: 13px; font-weight: 600; + color: var(--ink); + } + .events-feed .lead { + font-size: 12px; color: var(--ink-3); + margin: 0 0 10px; + line-height: 1.5; + } + .events-feed .lines { + display: flex; flex-direction: column; gap: 4px; + max-height: 160px; overflow-y: auto; + } + .ev-line { + display: grid; + grid-template-columns: 60px 90px 1fr; + gap: 10px; + padding: 4px 6px; + border-radius: 4px; + font-family: var(--mono); + font-size: 11px; + color: var(--ink-2); + } + .ev-line:hover { background: var(--bg-3); } + .ev-line .ts { color: var(--ink-4); font-size: 10.5px; } + .ev-line .id { color: var(--accent); font-size: 10.5px; } + .ev-line .body { color: var(--ink); } + .ev-empty { + font-size: 12px; color: var(--ink-3); + padding: 8px 0; + } + .card-events-count { + font-size: 10.5px; + color: var(--accent-4); + font-family: var(--mono); + } + .card-foot { + display: flex; align-items: center; gap: 8px; + padding-top: 8px; margin-top: 4px; + border-top: 1px solid var(--line); + font-size: 11px; color: var(--ink-3); + } + .toggle { + position: relative; + width: 32px; height: 18px; + background: var(--bg-3); border: 1px solid var(--line-2); + border-radius: 999px; cursor: pointer; + transition: background 0.15s; + flex-shrink: 0; + } + .toggle::after { + content: ''; position: absolute; + top: 1px; left: 1px; + width: 12px; height: 12px; + background: var(--ink-3); border-radius: 50%; + transition: transform 0.15s, background 0.15s; + } + .toggle.on { background: var(--accent); border-color: var(--accent); } + .toggle.on::after { background: #1a0f00; transform: translateX(14px); } + .events { + font-family: var(--mono); font-size: 10px; color: var(--ink-3); + flex: 1; + } + .empty { + padding: 40px; + text-align: center; color: var(--ink-3); + font-size: 13px; + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { + activations.value; query.value; activeCat.value; statusFilter.value; + appEvents.value; appEventCounts.value; + this.renderTick++; + }); + } + + private isActive(id: string): boolean { + return activations.value.find((a) => a.id === id)?.active === true; + } + + private toggle(app: AppManifest): void { + const wasActive = this.isActive(app.id); + const next = activations.value.map((a) => a.id === app.id ? { ...a, active: !a.active, lastActivatedAt: Date.now() } : a); + activations.value = next; + if (!wasActive) { + const r = app.runtime ?? 'mesh-only'; + const note = r === 'simulated' ? ' · live runtime engaged' + : r === 'mesh-only' ? ' · queued (needs ESP32 mesh)' + : ''; + pushLog('ok', `app ${app.id} activated${note}`); + } else { + pushLog('info', `app ${app.id} deactivated`); + } + } + + private filtered(): AppManifest[] { + let list = APPS; + if (activeCat.value !== 'all') list = list.filter((a) => a.category === activeCat.value); + if (statusFilter.value !== 'all') list = list.filter((a) => a.status === statusFilter.value); + if (query.value.trim()) { + list = list + .map((a) => ({ a, s: fuzzyMatch(query.value, a) })) + .filter((x) => x.s > 0) + .sort((a, b) => b.s - a.s) + .map((x) => x.a); + } + return list; + } + + private categoryCounts(): Record { + const counts: Record = { all: APPS.length }; + for (const k of Object.keys(CATEGORIES)) counts[k] = 0; + for (const a of APPS) counts[a.category] = (counts[a.category] ?? 0) + 1; + return counts; + } + + override render() { + const list = this.filtered(); + const counts = this.categoryCounts(); + const activeCount = activations.value.filter((a) => a.active).length; + return html` +
+
+ App Store + ${APPS.length} edge apps · ${activeCount} active +
+ { query.value = (e.target as HTMLInputElement).value; }} /> +
+ +
+ activeCat.value = 'all'}> + All${counts.all} + + ${(Object.keys(CATEGORIES) as AppCategory[]).map((k) => html` + activeCat.value = k}> + + ${CATEGORIES[k].label} + ${counts[k] ?? 0} + + `)} + + statusFilter.value = 'all'}>any + statusFilter.value = 'available'}>available + statusFilter.value = 'beta'}>beta + statusFilter.value = 'research'}>research +
+ + ${this.renderEventsFeed()} + + ${list.length === 0 + ? html`
No apps match the current filters.
` + : html`
${list.map((app) => this.card(app))}
`} + `; + } + + private renderEventsFeed() { + const evs = appEvents.value.slice(-12).reverse(); + const activeSimCount = activations.value.filter((a) => a.active && hasRuntime(a.id)).length; + return html` +
+

Live runtime feed + ${activeSimCount > 0 + ? html`${activeSimCount} simulated app${activeSimCount === 1 ? '' : 's'} active` + : ''} +

+

+ Apps with the simulated + runtime emit real i32 event IDs against nvsim's live frame stream below. + Apps with mesh-only + need an ESP32-S3 + WS transport (deferred to V2). The + running + badge marks nvsim itself, which is always running. +

+ ${evs.length === 0 + ? html`
No events yet. Toggle a card with the simulated badge and press ▶ Run.
` + : html`
${evs.map((ev) => { + const dt = new Date(ev.ts); + const ts = `${String(dt.getSeconds()).padStart(2, '0')}.${String(dt.getMilliseconds()).padStart(3, '0')}`; + return html` +
+ ${ts} + ${ev.appId} + ${ev.eventName} · ${ev.eventId} ${ev.detail ? `· ${ev.detail}` : ''} +
+ `; + })}
`} +
+ `; + } + + private card(app: AppManifest) { + const active = this.isActive(app.id); + const cat = CATEGORIES[app.category]; + const runtime = app.runtime ?? 'mesh-only'; + const evCount = appEventCounts.value[app.id] ?? 0; + const runtimeLabel: Record = { + 'running': 'running', + 'simulated': 'simulated', + 'mesh-only': 'needs mesh', + }; + const runtimeTip: Record = { + 'running': 'This app is genuinely running in your browser right now.', + 'simulated': 'A pared-down version of this algorithm runs against nvsim\'s magnetic frame stream as a proxy for its native CSI input. Toggle on, then press ▶ Run to see real event IDs in the feed.', + 'mesh-only': 'This algorithm needs CSI subcarrier data from an ESP32-S3 mesh. The toggle persists; activation is pushed via WS transport (V2).', + }; + return html` +
+
+ + ${app.name} +
+
${app.summary}
+
+ ${cat.label} + ${app.status} + ${runtimeLabel[runtime]} + ${app.budget ? html`budget ${app.budget}` : ''} + ${app.adr ? html`${app.adr}` : ''} + ${app.events?.length ? html`events ${app.events.join('·')}` : ''} +
+
+ ${app.crate} + ${evCount > 0 ? html`⚡ ${evCount} ev` : ''} + this.toggle(app)}> +
+
+ `; + } +} diff --git a/dashboard/src/components/nv-app.ts b/dashboard/src/components/nv-app.ts new file mode 100644 index 000000000..ef0f189db --- /dev/null +++ b/dashboard/src/components/nv-app.ts @@ -0,0 +1,143 @@ +/* Top-level shell: 4-zone grid with rail / topbar / sidebar / scene / inspector / console. + * View routing is per-rail-button: the central area swaps between + * ``, ``, etc. */ + +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import './nv-rail'; +import './nv-topbar'; +import './nv-sidebar'; +import './nv-scene'; +import './nv-inspector'; +import './nv-console'; +import './nv-app-store'; +import './nv-toast'; +import './nv-modal'; +import './nv-palette'; +import './nv-debug-hud'; +import './nv-settings-drawer'; +import './nv-onboarding'; +import './nv-ghost-murmur'; +import './nv-help'; +import './nv-home'; + +export type View = 'home' | 'scene' | 'apps' | 'inspector' | 'witness' | 'ghost-murmur'; + +@customElement('nv-app') +export class NvApp extends LitElement { + @state() private view: View = 'home'; + + static styles = css` + :host { + display: block; + height: 100vh; + width: 100vw; + background: var(--bg-0); + } + .skip-link { + position: absolute; + top: -40px; + left: 8px; + padding: 6px 12px; + background: var(--accent); + color: #1a0f00; + border-radius: 6px; + font-size: 12.5px; + font-weight: 600; + text-decoration: none; + z-index: 1000; + transition: top 0.15s; + } + .skip-link:focus { top: 8px; } + .app { + display: grid; + grid-template-columns: 56px 280px 1fr 340px; + grid-template-rows: 48px 1fr 220px; + grid-template-areas: + 'rail topbar topbar topbar' + 'rail sidebar main inspector' + 'rail sidebar console inspector'; + height: 100vh; + width: 100vw; + } + /* Home view simplifies: hides sidebar / inspector / console so the + hero gets the full screen. Power-user panels stay one rail click away. */ + .app.simple { + grid-template-columns: 56px 1fr; + grid-template-rows: 48px 1fr; + grid-template-areas: + 'rail topbar' + 'rail main'; + } + .app.simple nv-sidebar, + .app.simple nv-inspector, + .app.simple nv-console { display: none; } + nv-rail { grid-area: rail; } + nv-topbar { grid-area: topbar; } + nv-sidebar { grid-area: sidebar; } + .main { grid-area: main; min-width: 0; min-height: 0; position: relative; overflow: hidden; } + nv-inspector { grid-area: inspector; } + nv-console { grid-area: console; min-height: 0; } + @media (max-width: 1180px) { + .app { + grid-template-columns: 56px 1fr 320px; + grid-template-areas: + 'rail topbar topbar' + 'rail main inspector' + 'rail console console'; + } + nv-sidebar { display: none; } + } + @media (max-width: 860px) { + .app { + grid-template-columns: 1fr; + grid-template-rows: 52px 1fr 200px; + grid-template-areas: + 'topbar' + 'main' + 'console'; + } + nv-rail, nv-sidebar, nv-inspector { display: none; } + } + `; + + override render() { + const isSimple = this.view === 'home'; + return html` + +
+ ) => (this.view = e.detail)}> + + +
+ ${this.view === 'home' + ? html`` + : this.view === 'apps' + ? html`` + : this.view === 'ghost-murmur' + ? html`` + : this.view === 'inspector' + ? html`` + : this.view === 'witness' + ? html`` + : html``} +
+ + + +
+ + + + + + + + `; + } +} diff --git a/dashboard/src/components/nv-console.ts b/dashboard/src/components/nv-console.ts new file mode 100644 index 000000000..1888ece74 --- /dev/null +++ b/dashboard/src/components/nv-console.ts @@ -0,0 +1,266 @@ +/* Console — log stream + REPL. */ +import { LitElement, html, css } from 'lit'; +import { customElement, query } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { + consoleLines, consoleFilter, consolePaused, pushLog, + getClient, seed, theme, expectedWitness, witnessHex, witnessVerified, + running, replHistory, pushReplHistory, +} from '../store/appStore'; + +@customElement('nv-console') +export class NvConsole extends LitElement { + @query('#console-input') private inputEl!: HTMLInputElement; + private hIdx = -1; + + static styles = css` + :host { + display: flex; flex-direction: column; + background: var(--bg-1); + overflow: hidden; + } + .tabs { + display: flex; align-items: center; + border-bottom: 1px solid var(--line); + padding: 0 10px; + gap: 2px; + } + .tab { + padding: 8px 12px; + background: transparent; border: none; + font-size: 11.5px; color: var(--ink-3); + font-family: var(--mono); + border-bottom: 2px solid transparent; + cursor: pointer; + margin-bottom: -1px; + } + .tab.active { color: var(--ink); border-bottom-color: var(--accent); } + .tab .cnt { + background: var(--bg-3); padding: 1px 5px; border-radius: 999px; + font-size: 9.5px; color: var(--ink-2); margin-left: 4px; + } + .spacer { flex: 1; } + .tools { display: flex; gap: 4px; padding: 4px 0; } + .tools button { + width: 24px; height: 24px; + background: transparent; border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-3); + font-size: 11px; cursor: pointer; + } + .tools button:hover { color: var(--ink); border-color: var(--line-2); } + + .body { + flex: 1; overflow-y: auto; + font-family: var(--mono); + font-size: 11.5px; + padding: 6px 0; + background: var(--bg-0); + } + .line { + display: grid; + grid-template-columns: 70px 60px 1fr; + gap: 12px; + padding: 2px 12px; + color: var(--ink-2); + border-left: 2px solid transparent; + } + .line:hover { background: var(--bg-1); } + .ts { color: var(--ink-4); font-size: 10.5px; padding-top: 1px; } + .lvl { + font-size: 10px; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.04em; padding-top: 1px; + } + .line.info .lvl { color: var(--accent-2); } + .line.warn .lvl { color: var(--warn); } + .line.warn { border-left-color: var(--warn); background: oklch(0.7 0.18 35 / 0.04); } + .line.err .lvl { color: var(--bad); } + .line.err { border-left-color: var(--bad); background: oklch(0.65 0.22 25 / 0.05); } + .line.dbg .lvl { color: var(--ink-3); } + .line.ok .lvl { color: var(--ok); } + .msg { color: var(--ink); white-space: pre-wrap; word-break: break-word; } + + .input { + display: flex; align-items: center; + border-top: 1px solid var(--line); + background: var(--bg-0); + padding: 0 10px; + height: 32px; gap: 8px; + } + .prompt { color: var(--accent); font-family: var(--mono); font-size: 12px; } + input[type="text"] { + flex: 1; background: transparent; border: none; outline: none; + color: var(--ink); font-family: var(--mono); font-size: 12px; + height: 100%; + } + input::placeholder { color: var(--ink-4); } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { + consoleLines.value; consoleFilter.value; consolePaused.value; + this.requestUpdate(); + }); + } + + override updated(): void { + const body = this.renderRoot.querySelector('.body') as HTMLElement | null; + if (body) body.scrollTop = body.scrollHeight; + } + + private counts(): Record { + const c: Record = { info: 0, warn: 0, err: 0, dbg: 0, ok: 0 }; + for (const l of consoleLines.value) c[l.level] = (c[l.level] ?? 0) + 1; + c.all = consoleLines.value.length; + return c; + } + + private async exec(line: string): Promise { + line = line.trim(); + if (!line) return; + pushLog('info', `nvsim> ${line}`); + pushReplHistory(line); + this.hIdx = replHistory.value.length; + const [cmd, ...args] = line.split(/\s+/); + const arg = args.join(' '); + const c = getClient(); + switch (cmd) { + case 'help': + pushLog('info', 'commands: help · scene.list · sensor.config · run · pause · reset · seed · proof.verify · proof.export · clear · theme · status'); + break; + case 'scene.list': + pushLog('info', 'scene rebar-walkby-01:'); + pushLog('info', ' rebar.steel.coil @ [+2.7, 0.0, +0.3] m χ=5000'); + pushLog('info', ' dipole.heart_proxy @ [-1.4, +0.2, +0.4] m m=1.0e-6 A·m²'); + pushLog('info', ' loop.mains_60Hz @ [-1.6, -0.4, 0.0] m I=2 A'); + pushLog('info', ' eddy.door_steel @ [+0.0, +1.8, +0.4] m σ=1e6 S/m'); + break; + case 'sensor.config': + pushLog('info', 'NvSensor::cots_defaults() {'); + pushLog('info', ' pos=[0,0,0], V=1mm³, N=1e12, C=0.03, T2*=200ns'); + pushLog('info', ' D=2.870 GHz, γe=28 GHz/T, Γ=1.0 MHz, axes=4×〈111〉'); + pushLog('info', ' δB ≈ 1.18 pT/√Hz (Barry 2020 §III.A) }'); + break; + case 'run': + if (c) { await c.run(); running.value = true; pushLog('ok', 'pipeline RUN'); } + break; + case 'pause': + if (c) { await c.pause(); running.value = false; pushLog('warn', 'pipeline PAUSED'); } + break; + case 'reset': + if (c) { await c.reset(); pushLog('info', 'pipeline reset · t=0'); } + break; + case 'seed': { + if (!arg) { pushLog('info', `current seed = 0x${seed.value.toString(16).toUpperCase()}`); break; } + const v = BigInt(arg.startsWith('0x') ? arg : '0x' + arg); + seed.value = v; + if (c) await c.setSeed(v); + pushLog('ok', `seed → 0x${v.toString(16).toUpperCase()}`); + break; + } + case 'proof.verify': { + if (!c) break; + pushLog('dbg', 'computing SHA-256 over 256 frames…'); + try { + const exp = expectedWitness.value; + const expBytes = new Uint8Array(32); + for (let i = 0; i < 32; i++) expBytes[i] = parseInt(exp.slice(i * 2, i * 2 + 2), 16); + const r = await c.verifyWitness(expBytes); + if (r.ok) { witnessVerified.value = 'ok'; witnessHex.value = exp; pushLog('ok', `witness ${exp.slice(0, 16)}… matches · determinism gate ✓`); } + else { witnessVerified.value = 'fail'; pushLog('err', 'WITNESS MISMATCH'); } + } catch (e) { pushLog('err', `verify failed: ${(e as Error).message}`); } + break; + } + case 'proof.export': { + if (!c) break; + pushLog('dbg', 'building proof bundle…'); + try { + const blob = await c.exportProofBundle(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `nvsim-proof-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); + pushLog('ok', `proof bundle exported · ${blob.size} bytes`); + } catch (e) { pushLog('err', `export failed: ${(e as Error).message}`); } + break; + } + case 'clear': + consoleLines.value = []; + break; + case 'theme': { + const t = (arg || '').toLowerCase(); + if (t === 'light' || t === 'dark') { theme.value = t; pushLog('ok', `theme → ${t}`); } + else pushLog('info', 'theme [light|dark]'); + break; + } + case 'status': + pushLog('info', `running=${running.value} seed=0x${seed.value.toString(16).toUpperCase()} verified=${witnessVerified.value}`); + break; + default: + pushLog('err', `unknown command: ${cmd} · try help`); + } + } + + private onKey = (e: KeyboardEvent): void => { + if (e.key === 'Enter') { void this.exec(this.inputEl.value); this.inputEl.value = ''; } + else if (e.key === 'ArrowUp') { + const h = replHistory.value; + if (h.length) { + this.hIdx = Math.max(0, this.hIdx - 1); + this.inputEl.value = h[this.hIdx] ?? ''; + e.preventDefault(); + } + } else if (e.key === 'ArrowDown') { + const h = replHistory.value; + if (h.length) { + this.hIdx = Math.min(h.length, this.hIdx + 1); + this.inputEl.value = h[this.hIdx] ?? ''; + e.preventDefault(); + } + } + }; + + override render() { + const c = this.counts(); + const filter = consoleFilter.value; + const visible = consoleLines.value.filter((l) => filter === 'all' || l.level === filter); + return html` +
+ ${(['all', 'info', 'warn', 'err', 'dbg'] as const).map((k) => html` + + `)} + +
+ + +
+
+
+ ${visible.map((l) => { + const ts = new Date(l.ts); + const tsStr = `${String(ts.getSeconds()).padStart(2, '0')}.${String(ts.getMilliseconds()).padStart(3, '0')}`; + // Use innerHTML pass-through via unsafe-html alt: inject raw html via property + return html`
+
${tsStr}
+
${l.level}
+
+
`; + })} +
+
+ nvsim> + +
+ `; + } +} diff --git a/dashboard/src/components/nv-debug-hud.ts b/dashboard/src/components/nv-debug-hud.ts new file mode 100644 index 000000000..15a14ad0a --- /dev/null +++ b/dashboard/src/components/nv-debug-hud.ts @@ -0,0 +1,88 @@ +/* Debug HUD toggled with `. Shows render fps, sim t, frames, |B|, SNR. */ +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { fps, framesEmitted, bMag, snr, t as simT } from '../store/appStore'; + +@customElement('nv-debug-hud') +export class NvDebugHud extends LitElement { + @state() private open = false; + @state() private renderFps = 0; + private lastTs = performance.now(); + private frameCount = 0; + private rafId = 0; + + static styles = css` + :host { + position: fixed; bottom: 8px; right: 8px; + width: 220px; + background: rgba(13,17,23,0.85); + backdrop-filter: blur(8px); + border: 1px solid var(--line-2); + border-radius: 8px; + padding: 8px 10px; + font-family: var(--mono); font-size: 11px; + color: var(--ink-2); + z-index: 99; + display: none; + box-shadow: var(--shadow); + } + :host([open]) { display: block; } + .h { + display: flex; justify-content: space-between; + font-weight: 600; color: var(--ink); + margin-bottom: 6px; padding-bottom: 4px; + border-bottom: 1px solid var(--line); + } + .x { cursor: pointer; color: var(--ink-3); } + .row { + display: flex; justify-content: space-between; + padding: 1px 0; + } + .k { color: var(--ink-3); } + .v { color: var(--ink); } + `; + + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('keydown', this.onKey); + effect(() => { fps.value; framesEmitted.value; bMag.value; snr.value; simT.value; this.requestUpdate(); }); + this.tick(); + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('keydown', this.onKey); + cancelAnimationFrame(this.rafId); + } + + private onKey = (e: KeyboardEvent): void => { + if (e.key === '`' && !(e.target as HTMLElement).matches('input, textarea')) { + this.open = !this.open; + this.toggleAttribute('open', this.open); + } + }; + + private tick = (): void => { + this.rafId = requestAnimationFrame(this.tick); + const now = performance.now(); + this.frameCount++; + if (now - this.lastTs >= 500) { + this.renderFps = (this.frameCount * 1000) / (now - this.lastTs); + this.frameCount = 0; + this.lastTs = now; + this.requestUpdate(); + } + }; + + override render() { + return html` +
nvsim · debug { this.open = false; this.removeAttribute('open'); }}>✕
+
render fps${this.renderFps.toFixed(1)}
+
sim fps${fps.value > 0 ? Math.round(fps.value) : '—'}
+
frames${framesEmitted.value.toString()}
+
|B|${(bMag.value * 1e9).toFixed(3)} nT
+
SNR${snr.value > 0 ? snr.value.toFixed(1) : '—'}
+
DOM${document.querySelectorAll('*').length}
+ `; + } +} diff --git a/dashboard/src/components/nv-ghost-murmur.ts b/dashboard/src/components/nv-ghost-murmur.ts new file mode 100644 index 000000000..aebf31cda --- /dev/null +++ b/dashboard/src/components/nv-ghost-murmur.ts @@ -0,0 +1,666 @@ +/* Ghost Murmur — research view. + * + * Walks through the publicly-reported April 2026 CIA program and maps + * the physically-defensible parts onto RuView's three-tier heartbeat + * mesh. Source: docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md + * + * This view is reference material, not an operational mode. It exists + * so practitioners (and journalists) can audit the physics-vs-press + * gap in the open. ADR-092 §14b. + */ + +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { getClient, pushLog } from '../store/appStore'; +import type { TransientRunResult } from '../transport/NvsimClient'; + +// Tier detection thresholds — order-of-magnitude floor each transport +// can resolve cardiac signal at, in Tesla. Source: Ghost Murmur spec +// §4.7, Wolf 2015, Barry 2020. These are deliberately optimistic for the +// "available" path; the shoot-the-moon press claim sits 6+ orders below. +const TIERS = [ + { id: 'nvBest', label: 'NV-ensemble (best lab)', floorT: 1e-12, color: 'oklch(0.78 0.14 70)' }, + { id: 'nvCots', label: 'NV-DNV-B1 (COTS)', floorT: 3e-10, color: 'oklch(0.72 0.18 50)' }, + { id: 'squid', label: 'SQUID (shielded room)', floorT: 1e-15, color: 'oklch(0.78 0.12 195)' }, + { id: 'mmw', label: '60 GHz mmWave (μ-Doppler)', floorT: 0, color: 'oklch(0.78 0.14 145)' }, + { id: 'csi', label: 'WiFi CSI (presence)', floorT: 0, color: 'oklch(0.72 0.18 330)' }, +]; + +// Cardiac dipole moment (A·m²) — order-of-magnitude estimate from +// Wikswo / Bison cardiac MCG modelling. +const HEART_DIPOLE_AM2 = 5e-9; + +@customElement('nv-ghost-murmur') +export class NvGhostMurmur extends LitElement { + @state() private distanceM = 0.1; + @state() private momentLog10 = -8.3; // log10(5e-9) + @state() private result: TransientRunResult | null = null; + @state() private running = false; + @state() private err: string | null = null; + static styles = css` + :host { + display: block; + height: 100%; + overflow-y: auto; + background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%); + padding: 24px 28px 60px; + } + h1 { + margin: 0 0 4px; + font-size: 22px; + letter-spacing: -0.02em; + color: var(--ink); + } + .subtitle { + color: var(--ink-3); + font-size: 13px; + margin-bottom: 22px; + } + .links { + display: flex; flex-wrap: wrap; gap: 6px; + margin-bottom: 22px; + } + .links a { + padding: 5px 10px; + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: 999px; + font-size: 11.5px; + font-family: var(--mono); + color: var(--accent-2); + text-decoration: none; + } + .links a:hover { border-color: var(--accent-2); } + h2 { + font-size: 14px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--ink-3); + margin: 28px 0 10px; + } + .grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 12px; + } + .card { + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 14px; + } + .card h3 { + margin: 0 0 8px; + font-size: 13.5px; font-weight: 600; + color: var(--ink); + } + .card p { + font-size: 12.5px; color: var(--ink-2); + margin: 0 0 8px; + line-height: 1.5; + } + .card p:last-child { margin-bottom: 0; } + .stat { + display: inline-flex; align-items: baseline; gap: 6px; + margin-right: 10px; + } + .stat .v { + font-family: var(--mono); font-size: 16px; font-weight: 600; + color: var(--accent); + } + .stat .l { + font-size: 10px; color: var(--ink-3); + text-transform: uppercase; letter-spacing: 0.04em; + } + table { + width: 100%; border-collapse: collapse; + font-size: 12.5px; + } + th, td { + padding: 8px 10px; + text-align: left; + border-bottom: 1px solid var(--line); + } + th { + color: var(--ink-3); + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.06em; + } + td.amber { color: var(--accent); font-family: var(--mono); } + td.cyan { color: var(--accent-2); font-family: var(--mono); } + td.bad { color: var(--bad); font-family: var(--mono); } + .pill { + display: inline-block; + padding: 1px 6px; + border-radius: 4px; + font-family: var(--mono); + font-size: 10px; + border: 1px solid var(--line); + } + .pill.ok { color: var(--ok); border-color: oklch(0.78 0.14 145 / 0.4); } + .pill.skeptical { color: var(--bad); border-color: oklch(0.65 0.22 25 / 0.4); } + .pill.partial { color: var(--warn); border-color: oklch(0.7 0.18 35 / 0.4); } + .architecture { + font-family: var(--mono); + font-size: 11px; + color: var(--ink-2); + background: var(--bg-3); + padding: 16px; + border-radius: var(--radius-sm); + border: 1px solid var(--line); + white-space: pre; + overflow-x: auto; + line-height: 1.4; + } + .ethics { + background: linear-gradient(180deg, var(--bg-2) 0%, oklch(0.65 0.22 25 / 0.04) 100%); + border: 1px solid oklch(0.65 0.22 25 / 0.25); + border-radius: var(--radius); + padding: 16px; + } + .ethics h3 { color: var(--bad); margin-top: 0; } + .ethics ul { padding-left: 18px; margin: 8px 0; } + .ethics li { font-size: 12.5px; color: var(--ink-2); margin-bottom: 4px; } + + /* Demo */ + .demo { + background: linear-gradient(180deg, var(--bg-2) 0%, oklch(0.78 0.14 70 / 0.04) 100%); + border: 1px solid oklch(0.78 0.14 70 / 0.3); + border-radius: var(--radius); + padding: 18px; + } + .demo-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 18px; + margin-top: 12px; + } + @media (max-width: 720px) { .demo-grid { grid-template-columns: 1fr; } } + .control { margin-bottom: 14px; } + .control .top { + display: flex; justify-content: space-between; + font-size: 12px; margin-bottom: 6px; + } + .control .top .lbl { color: var(--ink-3); } + .control .top .val { + font-family: var(--mono); color: var(--ink); + } + .control input[type="range"] { + -webkit-appearance: none; appearance: none; + width: 100%; height: 4px; + background: var(--bg-3); border-radius: 2px; outline: none; + } + .control input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; appearance: none; + width: 14px; height: 14px; border-radius: 50%; + background: var(--accent); cursor: pointer; + border: 2px solid var(--bg-2); + } + .demo-btn { + width: 100%; + padding: 10px; + border: 1px solid var(--accent); + background: var(--accent); + color: #1a0f00; + border-radius: 8px; + font-size: 13px; font-weight: 600; + cursor: pointer; + } + .demo-btn:hover { filter: brightness(1.08); } + .demo-btn:disabled { opacity: 0.6; cursor: progress; } + .readout { + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 8px; + padding: 12px; + } + .readout-row { + display: flex; justify-content: space-between; + padding: 4px 0; + font-family: var(--mono); font-size: 12px; + } + .readout-row .l { color: var(--ink-3); } + .readout-row .v { color: var(--ink); } + .readout-row .v.amber { color: var(--accent); } + .tier-bar { + position: relative; + margin: 6px 0; + height: 22px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 4px; + overflow: hidden; + } + .tier-bar .fill { + position: absolute; top: 0; bottom: 0; left: 0; + transition: width 0.2s ease-out; + border-right: 2px solid; + } + .tier-bar .lbl { + position: relative; z-index: 1; + font-family: var(--mono); font-size: 11px; + padding: 3px 8px; + color: var(--ink); + display: flex; justify-content: space-between; + pointer-events: none; + } + .verdict { + margin-top: 10px; + padding: 10px 12px; + border-radius: 8px; + font-size: 12.5px; font-weight: 500; + border: 1px solid; + } + .verdict.ok { background: oklch(0.78 0.14 145 / 0.08); border-color: oklch(0.78 0.14 145 / 0.4); color: var(--ok); } + .verdict.warn { background: oklch(0.7 0.18 35 / 0.08); border-color: oklch(0.7 0.18 35 / 0.4); color: var(--warn); } + .verdict.bad { background: oklch(0.65 0.22 25 / 0.08); border-color: oklch(0.65 0.22 25 / 0.4); color: var(--bad); } + .demo-notes { + font-size: 11.5px; color: var(--ink-3); + margin-top: 10px; line-height: 1.5; + } + `; + + /** + * Predicted MCG dipole field (Tesla) at distance r in metres. + * Far-field approximation: |B| ≈ μ₀ · m / (4π · r³). Source: Jackson 3e §5. + */ + private predictedDipoleFieldT(r: number, m: number): number { + const MU_0 = 4 * Math.PI * 1e-7; + return (MU_0 * m) / (4 * Math.PI * Math.pow(Math.max(r, 1e-6), 3)); + } + + private async runDemo(): Promise { + const c = getClient(); + if (!c) { this.err = 'WASM client not ready'; return; } + this.err = null; + this.running = true; + this.requestUpdate(); + try { + const r = this.distanceM; + const m = Math.pow(10, this.momentLog10); + // Heart proxy at +z = r, dipole moment along z = m A·m². + const scene = { + dipoles: [{ position: [0, 0, r] as [number, number, number], moment: [0, 0, m] as [number, number, number] }], + loops: [], + ferrous: [], + eddy: [], + sensors: [[0, 0, 0] as [number, number, number]], + ambient_field: [0, 0, 0] as [number, number, number], + }; + const config = { + digitiser: { f_s_hz: 10000, f_mod_hz: 1000 }, + sensor: { + gamma_fwhm_hz: 1.0e6, + t1_s: 5.0e-3, + t2_s: 1.0e-6, + t2_star_s: 200e-9, + contrast: 0.03, + n_spins: 1.0e12, + shot_noise_disabled: false, + }, + dt_s: null, + }; + this.result = await c.runTransient(scene, config, 42n, 64); + pushLog('ok', `ghost-demo · r=${r.toFixed(3)} m · |B| recovered = ${(this.result.bMagT * 1e12).toExponential(2)} pT`); + } catch (e) { + this.err = (e as Error).message; + pushLog('err', `ghost-demo failed: ${this.err}`); + } finally { + this.running = false; + this.requestUpdate(); + } + } + + private formatField(t: number): string { + if (t === 0) return '0 T'; + const abs = Math.abs(t); + if (abs >= 1e-3) return `${(t * 1e3).toFixed(2)} mT`; + if (abs >= 1e-6) return `${(t * 1e6).toFixed(2)} µT`; + if (abs >= 1e-9) return `${(t * 1e9).toFixed(3)} nT`; + if (abs >= 1e-12) return `${(t * 1e12).toFixed(2)} pT`; + if (abs >= 1e-15) return `${(t * 1e15).toFixed(2)} fT`; + if (abs >= 1e-18) return `${(t * 1e18).toFixed(2)} aT`; + return `${t.toExponential(2)} T`; + } + + private formatDistance(r: number): string { + if (r < 1) return `${(r * 100).toFixed(1)} cm`; + if (r < 1000) return `${r.toFixed(2)} m`; + if (r < 1e5) return `${(r / 1000).toFixed(2)} km`; + return `${(r / 1609).toFixed(0)} mi`; + } + + private renderDemo() { + const m = Math.pow(10, this.momentLog10); + const predicted = this.predictedDipoleFieldT(this.distanceM, m); + const recovered = this.result?.bMagT ?? 0; + const noiseFloor = (this.result?.noiseFloorPtSqrtHz ?? 0) * 1e-12; // pT/√Hz → T/√Hz + + const verdictPills = TIERS.map((t) => { + let detect: 'ok' | 'warn' | 'bad' = 'bad'; + let label = 'below floor'; + if (t.id === 'mmw') { + if (this.distanceM <= 5) { detect = 'ok'; label = 'µ-Doppler @ chest'; } + else if (this.distanceM <= 15) { detect = 'warn'; label = 'edge of range'; } + else { detect = 'bad'; label = 'out of range'; } + } else if (t.id === 'csi') { + if (this.distanceM <= 30) { detect = this.distanceM <= 10 ? 'ok' : 'warn'; label = 'presence/breathing'; } + else { detect = 'bad'; label = 'out of range'; } + } else if (t.floorT > 0) { + const ratio = predicted / t.floorT; + if (ratio > 100) { detect = 'ok'; label = `${ratio.toExponential(1)}× floor`; } + else if (ratio > 1) { detect = 'warn'; label = `${ratio.toFixed(1)}× floor`; } + else { detect = 'bad'; label = `${(1 / ratio).toExponential(1)}× too weak`; } + } + const fillPct = t.floorT > 0 + ? Math.max(2, Math.min(100, 100 + 12 * Math.log10(predicted / t.floorT))) + : (t.id === 'mmw' ? Math.max(2, 100 - this.distanceM * 7) : Math.max(2, 100 - this.distanceM * 2)); + return html` +
+
+
+ ${t.label} + ${label} +
+
+ `; + }); + + const overallDetect: 'ok' | 'warn' | 'bad' = + predicted > 1e-12 ? 'ok' : predicted > 1e-15 ? 'warn' : 'bad'; + const overallText = + overallDetect === 'ok' + ? `Above NV-ensemble lab floor — close-range MCG plausible at ${this.formatDistance(this.distanceM)}.` + : overallDetect === 'warn' + ? `Below NV ensemble best, above SQUID — research-grade only at ${this.formatDistance(this.distanceM)}.` + : `Below every published instrument's noise floor at ${this.formatDistance(this.distanceM)}. Press-release physics.`; + + return html` +
+

Try it yourself

+
+ Place a cardiac dipole at variable distance from the NV sensor. The + dashboard runs the real nvsim Rust pipeline (compiled to WASM) + end-to-end and reports what each tier would actually detect. Same + determinism contract as the rest of the dashboard. +
+
+
+
+
+ Distance from sensor + ${this.formatDistance(this.distanceM)} +
+ { this.distanceM = Math.pow(10, +(e.target as HTMLInputElement).value); }} /> +
+ 10 cm → 100 km log scale +
+
+
+
+ Heart dipole moment + ${m.toExponential(2)} A·m² +
+ { this.momentLog10 = +(e.target as HTMLInputElement).value; }} /> +
+ published cardiac MCG ≈ 5×10⁻⁹ A·m² +
+
+ + ${this.err ? html`
Error: ${this.err}
` : ''} +
+ +
+
+
+ Predicted |B| (1/r³) + ${this.formatField(predicted)} +
+
+ Recovered |B| (nvsim) + ${this.result ? this.formatField(recovered) : '—'} +
+
+ Sensor noise floor + ${this.result ? this.formatField(noiseFloor) + '/√Hz' : '—'} +
+
+ Frames run + ${this.result?.nFrames ?? '—'} +
+
+ Witness (this run) + ${this.result?.witnessHex.slice(0, 16) ?? '—'}… +
+
+
+
+ Per-tier detectability +
+ ${verdictPills} +
+
+
+
${overallText}
+
+ The predicted value uses the closed-form magnetic-dipole + far field |B| = μ₀·m / (4π·r³). The recovered + value comes from the same Rust pipeline that drives the Witness panel — + scene → Biot-Savart → NV ensemble → ADC → MagFrame. Use the moment + slider to ask "what if the heart were stronger?". Use the distance + slider to walk through 10 cm (clinical MCG), 1 m (close approach), + 10 m (room-scale), 1 km (skeptic's range), and 65 km (the press claim). +
+
+ `; + } + + override render() { + return html` +

Ghost Murmur — open-source reality check

+
+ The physics-vs-press audit for the publicly-reported April 2026 + CIA NV-diamond heartbeat detector, and how RuView's existing + stack maps onto an honest, civilian version of the same idea. +
+ + + +

What the press reported

+
+
+

The story

+

3 Apr 2026: USAF F-15E pilot "Dude 44 Bravo" goes down in southern Iran during the regional exchange and evades for ~2 days.

+

President Trump publicly suggests detection from 40 miles away on a mountainside at night; CIA Director Ratcliffe says "invisible to the enemy, but not to the CIA."

+
+
+

The named tech

+

"Ghost Murmur" — Lockheed Skunk Works system using NV defects in synthetic diamond + AI to extract a heartbeat from environmental noise.

+

Outlets: Newsweek, Scientific American, Military.com, WION, Open The Magazine, Yahoo, Calcalist + HN thread #47679241.

+
+
+

What physicists said

+

Wikswo (Vanderbilt), Orzel (Union College), Roth (Oakland) — all pushing back hard.

+

"At 1 km, the heartbeat field drops to ~10⁻¹² of its 10 cm value." MCG-only at multi-mile range is not consistent with published physics.

+
+
+ +

Live demo — nvsim WASM

+ ${this.renderDemo()} + +

Physics reality check

+
+ + + + + + + + + + + +
DistanceCardiac MCG (peak QRS)vs Earth field (~50 µT)
10 cm50 pT10⁹× weaker
1 m50 fT10¹²× weaker
10 m50 aT10¹⁵× weaker
1 km5 × 10⁻²³ T10²⁷× weaker
40 mi (65 km)~10⁻²⁸ T10³³× weaker
+

+ Best published NV-ensemble lab record: 0.9 pT/√Hz [Wolf 2015]. + Best SQUID in a shielded room: ~1 fT/√Hz. To detect a single heartbeat at 10 m + you'd need ~2 billion× more sensitivity than any published ensemble has ever shown, + in a magnetically silent environment. 40 miles is press-release physics. +

+
+ +

RuView's three-tier mesh — what is actually buildable

+
┌──────────────────────────┐ + │ Tier 3 — NV-diamond │ Range: 0.1–2 m (lab) + │ magnetometer ring │ Status: nvsim simulator only + │ (close-confirm) │ Hardware: $$$ (≥$8k DNV-B1) + └──────────┬───────────────┘ + │ + ┌──────────┴───────────────┐ + │ Tier 2 — 60 GHz FMCW │ Range: 1–10 m HR/BR + │ mmWave radar mesh │ Status: shipping (ADR-021) + │ (vital signs, posture) │ Hardware: $15 (MR60BHA2 + ESP32-C6) + └──────────┬───────────────┘ + │ + ┌──────────┴───────────────┐ + │ Tier 1 — WiFi CSI mesh │ Range: 10–30 m through-wall + │ (presence, breathing, │ Status: shipping (ADR-014, ADR-029) + │ pose, intention) │ Hardware: $9 (ESP32-S3 8MB) + └──────────┬───────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ RuvSense multistatic fusion │ + │ + cross-viewpoint attention │ + │ + AETHER re-ID embeddings │ + │ + Cramer-Rao gating │ + └────────────────────────────────┘
+ +

Press claim → RuView equivalent

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Press claimRuView equivalent todayCrate / ADRHonest range
NV-diamond magnetometryDeterministic NV pipeline simulatornvsim · ADR-089Simulator only
"AI strips environmental noise"RuvSense multistatic fusion + AETHERsignal/ruvsense/ · ADR-029Mature
Heartbeat at distance60 GHz FMCW HR/BR + WiFi CSI breathingvitals · ADR-0211–5 m HR · 10–30 m presence
Long-range localisationMultistatic time-of-flight + CRLBruvector/viewpoint/Limited by node spacing
40-mile single-heartbeat detectionNot feasible at any tierPress-release physics
+
+ +

Build today on $165

+
+
+

Bill of materials

+

+ 3 × ESP32-S3 8 MB ($9 ea)
+ 3 × PoE injector + cat6 ($6 ea)
+ 1 × ESP32-C6 + Seeed MR60BHA2 ($15)
+ 1 × Raspberry Pi 5 8 GB ($80)
+ 1 × unmanaged GbE switch ($25) +

+

Total: $165

+
+
+

Honest performance

+ 95%TPR (LOS, 0–15 m)

+ ±2 bpmHR (LOS 0–3 m)

+ ±1 br/minBR (any mode)

+ ~10 cmpose error

+ 80–150 msend-to-end latency +
+
+

Determinism

+

Same (scene, config, seed) → byte-identical SHA-256 witness across browsers, OSes, transports.

+

Reference: cc8de9b01b0ff5bd…

+

Try the Witness tab on the right — it re-derives the hash live in this browser and compares against the published reference.

+
+
+ +

Privacy, ethics, legal

+
+

This is the open-source version. Same physics, opposite governance.

+
    +
  • Civilian opt-in only — search-and-rescue, elder-care, occupancy, ICU vitals. Not surveillance.
  • +
  • No directional pursuit — no beam-steering, target-following, or remote person-of-interest tracking.
  • +
  • Data minimisation — fused output is (presence, HR, BR, pose, p_alive); raw streams discarded at the edge.
  • +
  • PII gates (ADR-040) block identifying biometric streams from leaving the local mesh without consent.
  • +
  • Adversarial-signal detection flags physically-impossible signal patterns from compromised mesh nodes.
  • +
  • No export-controlled hardware — RuView targets < $50 COTS. ITAR/EAR sub-THz coherent radars and shielded NV ensembles are out of scope.
  • +
+

+ RuView is not affiliated with the United States government, the CIA, Lockheed Martin, + or any classified program. References to "Ghost Murmur" in this view refer + exclusively to the publicly-reported program of that name as covered in the open + press in April 2026. +

+
+ +

Cross-references

+
+

+ ADRs: 014 (signal) · 021 (vitals) · 024 (AETHER) · 027 (MERIDIAN) · + 028 (witness audit) · 029 (RuvSense) · 040 (PII gates) · 086 (ESP32 RaBitQ) · + 089 (nvsim, Accepted) · 090 (Lindblad, Proposed-conditional) · + 091 (sub-THz radar research) · 092 (this dashboard).

+ Primary physics: Cohen 1970 · Bison 2009 · Wolf 2015 · Barry RMP 2020 · Doherty 2013 · Jackson 3e §5.6/§5.8. +

+
+ `; + } +} diff --git a/dashboard/src/components/nv-help.ts b/dashboard/src/components/nv-help.ts new file mode 100644 index 000000000..2d066cf8a --- /dev/null +++ b/dashboard/src/components/nv-help.ts @@ -0,0 +1,458 @@ +/* Help center — single dialog covering Quickstart / Glossary / FAQ / + * Shortcuts. Opened from the topbar `?` button or by pressing `?` on + * the keyboard. Self-contained, no external content. */ + +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +type Section = 'quickstart' | 'glossary' | 'faq' | 'shortcuts' | 'about'; + +interface GlossaryItem { + term: string; + body: string; + category: 'physics' | 'rust' | 'ui'; +} + +const GLOSSARY: GlossaryItem[] = [ + { term: 'NV-diamond', category: 'physics', body: 'Nitrogen-vacancy defect in synthetic diamond. The simulator models a 1 mm³ ensemble (~10¹² centers) addressed by 532 nm pump light + a 2.87 GHz microwave drive. Used as a room-temperature magnetometer with shot-noise floor ~1 pT/√Hz at the published lab record.' }, + { term: 'CW-ODMR', category: 'physics', body: 'Continuously-driven optically-detected magnetic resonance. Sweep the microwave frequency around the NV zero-field splitting (D = 2.87 GHz) and watch the photoluminescence dip when the microwave matches the spin transition. The dip splits with applied magnetic field along each of the four ⟨111⟩ NV axes.' }, + { term: 'MagFrame', category: 'rust', body: 'Fixed-layout 60-byte binary record nvsim emits per (sensor × sample). Magic 0xC51A_6E70, version 1, little-endian. Carries timestamp, recovered B vector (pT), per-axis sigma, noise floor, and flag bits for saturation / shot-noise-disabled / heavy-attenuation.' }, + { term: 'Witness', category: 'rust', body: 'SHA-256 hash over the concatenated MagFrame bytes for a canonical reference run (Proof::REFERENCE_SCENE_JSON @ seed=42, N=256). Same inputs → same hash, byte-for-byte, across runs and machines. The dashboard re-derives it in WASM and compares against Proof::EXPECTED_WITNESS_HEX pinned at build time.' }, + { term: 'Determinism gate', category: 'rust', body: 'A pass/fail check: did this build of nvsim produce the expected witness? If yes → every constant (γ_e, D_GS, μ₀, contrast, T₂*, the PRNG stream, the frame layout, the pipeline ordering) is byte-identical to the published reference. If no → something drifted; the dashboard names which.' }, + { term: 'Lock-in demod', category: 'physics', body: 'Multiply the photoluminescence signal by cos(2π·f_mod·t) and low-pass to recover the slowly-varying B-field component. The simulator emulates a lock-in with output gain 2 and a single-pole IIR LP filter; settable via the Tunables panel (f_mod default 1 kHz).' }, + { term: 'Shot-noise floor', category: 'physics', body: 'δB = 1 / (γ_e · C · √(N · t · T₂*)) — the irreducible quantum noise floor for an NV ensemble. With nvsim defaults (N=10¹², C=0.03, T₂*=200 ns): ≈1.18 pT/√Hz. Toggleable via the Tunables panel for "analytic" runs without noise.' }, + { term: 'Biot-Savart', category: 'physics', body: 'Closed-form magnetic field at a point from a current loop or a magnetic dipole. The Scene panel\'s sources (heart proxy, mains loop, ferrous body, eddy current) all reduce to Biot-Savart-style superpositions over the sensor position.' }, + { term: 'Multistatic fusion', category: 'physics', body: 'Combining evidence from multiple sensors at known geometric configurations. RuView\'s Cramer-Rao-weighted attention over WiFi CSI nodes + 60 GHz radar nodes + (hypothetically) NV nodes; documented in ADR-029 and the Ghost Murmur view.' }, + { term: 'Scene', category: 'ui', body: 'The simulated magnetic environment: a list of sources (dipole, current loop, ferrous body, eddy current) plus one or more sensor positions and an ambient field. The dashboard ships a "rebar-walkby-01" reference scene; click "New scene…" in the command palette (⌘K) to build your own.' }, + { term: 'Tunables', category: 'ui', body: 'Sliders that change the running pipeline\'s digitiser config. Each edit debounces 300 ms, then rebuilds the WASM pipeline with the new f_s / f_mod / dt / shot-noise setting. The frame stream picks up the change without a restart.' }, + { term: 'Transport', category: 'ui', body: 'How the dashboard talks to nvsim. Default is WASM — the simulator runs in a Web Worker right here in your browser, no server. The optional WS transport is REST + binary WebSocket against a host-supplied nvsim-server (see ADR-092 §6.2). Toggle in Settings.' }, + { term: 'App Store', category: 'ui', body: 'Catalog of all 65+ hot-loadable WASM edge modules from wifi-densepose-wasm-edge plus the simulators. Each card carries id / category / status / event IDs; the toggle marks an app active in this session and (in WS mode) pushes the activation to a connected ESP32 mesh.' }, + { term: 'Ghost Murmur', category: 'ui', body: 'Research view that audits the publicly-reported April 2026 CIA NV-diamond heartbeat detector against the open physics literature. Includes a live "Try it yourself" sandbox where you can place a heart dipole at any distance from the sensor and ask: which transport tier would actually detect it?' }, +]; + +const FAQ = [ + { + q: 'Is this a real simulator or a mockup?', + a: 'Real. The Rust crate at v2/crates/nvsim is the same code that runs in the browser via WASM. Press Verify witness on the Witness panel — the SHA-256 you see is byte-equivalent to what `cargo test -p nvsim` produces.', + }, + { + q: 'Why does my "Recovered |B|" sit much higher than "Predicted |B|" in the Ghost Murmur demo?', + a: 'The recovered value reads the simulator\'s ADC quantization floor, not the actual magnetic signal. With COTS-default sensor noise (~300 pT/√Hz) and 16-bit ADC at ±10 µT FS, anything below ~1 pT vanishes into ~2 nT of digitization residual. That\'s the lesson — the press claim sits far below this floor at any meaningful range.', + }, + { + q: 'Can I run my own scene?', + a: 'Yes. Press ⌘K to open the command palette and pick "New scene…". You get five fields (name, dipole moment, distance, ferrous toggle, mains toggle); the dashboard builds the JSON and pushes it via client.loadScene().', + }, + { + q: 'Does any of my data leave the browser?', + a: 'No. WASM mode is local-only — the worker, the WASM binary, and the IndexedDB persistence all live in your browser. The optional WS transport (off by default) talks to a host of your choosing.', + }, + { + q: 'What does the witness mismatch (red ✗) mean?', + a: 'The current build of nvsim produced a SHA-256 that doesn\'t match the constant pinned at compile time. Possible causes: a different Rust toolchain, a dependency version drift, a manual edit to a physics constant, or an honest bug. Audit the diff against ADR-089 §5.', + }, + { + q: 'Why are the Inspector / Witness rail buttons there if there\'s already a right-side inspector?', + a: 'The right-side inspector is the compact live view; the rail buttons open a full-width version with bigger charts, an explainer header, reference-scene metadata cards, and (on Witness) a "what this verifies" panel. Both stay in sync — the right rail is for glancing, the main area is for diving in.', + }, + { + q: 'Why is there an "App Store" if this is a magnetometer simulator?', + a: 'Because nvsim is one tile in a larger sensing platform. The catalog lists every hot-loadable WASM edge module RuView ships — medical, security, building, retail, industrial, signal, learning, autonomy. The simulators (nvsim today, more in future) are first-class entries in the same catalog.', + }, +]; + +const QUICKSTART = [ + { step: 1, title: 'Hit ▶ Run', body: 'The big amber button in the topbar starts the live frame stream. The pipeline runs ~1.8 kHz on x86_64 WASM, well above the 1 kHz Cortex-A53 acceptance gate.' }, + { step: 2, title: 'Watch the B-vector trace', body: 'The Inspector → Signal tab shows the recovered field per axis updating in real time. The frame strip below it is one bar per ~32-frame batch.' }, + { step: 3, title: 'Verify the witness', body: 'Click the rail Witness button (or REPL: proof.verify). The dashboard re-runs the canonical reference scene and asserts the SHA-256 byte-for-byte.' }, + { step: 4, title: 'Drag a source', body: 'Grab the rebar / heart proxy / mains loop / ferrous door in the scene canvas; positions persist via IndexedDB.' }, + { step: 5, title: 'Tweak the tunables', body: 'Sliders in the left sidebar update the running pipeline (f_s, f_mod, integration time, shot-noise). Changes debounce 300 ms then push to the worker.' }, + { step: 6, title: 'Open the Ghost Murmur view', body: 'The ghost icon in the rail. Move the distance + moment sliders, hit "Run nvsim at this distance" — the live demo runs the real Rust pipeline through WASM and shows which transport tier would actually detect.' }, + { step: 7, title: 'Browse the App Store', body: 'The grid icon. 65+ edge apps: medical, security, building, retail, industrial, signal, learning. Toggle to mark active in this session.' }, +]; + +const SHORTCUTS = [ + { keys: '⌘K / Ctrl K', label: 'Command palette' }, + { keys: 'Space', label: 'Play / pause pipeline' }, + { keys: '⌘R / Ctrl R', label: 'Reset pipeline (with confirm)' }, + { keys: '⌘, / Ctrl ,', label: 'Settings drawer' }, + { keys: '⌘N / Ctrl N', label: 'New scene' }, + { keys: '⌘E / Ctrl E', label: 'Export proof bundle' }, + { keys: '⌘/ / Ctrl /', label: 'Toggle theme (dark / light)' }, + { keys: '`', label: 'Toggle debug HUD' }, + { keys: '?', label: 'Open this help center' }, + { keys: '1 · 2 · 3', label: 'Switch inspector tab (Signal / Frame / Witness)' }, + { keys: 'Esc', label: 'Close any modal / palette / drawer' }, + { keys: '/', label: 'Focus the REPL prompt' }, +]; + +@customElement('nv-help') +export class NvHelp extends LitElement { + @state() private open = false; + @state() private section: Section = 'quickstart'; + @state() private query = ''; + + static styles = css` + :host { + position: fixed; inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + z-index: 230; + display: grid; place-items: center; + opacity: 0; pointer-events: none; + transition: opacity 0.18s; + } + :host([open]) { opacity: 1; pointer-events: auto; } + .modal { + background: var(--bg-1); + border: 1px solid var(--line-2); + border-radius: var(--radius); + box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); + width: min(880px, 94vw); + max-height: 86vh; + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: auto 1fr auto; + overflow: hidden; + transform: translateY(12px) scale(0.98); + transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1); + } + :host([open]) .modal { transform: translateY(0) scale(1); } + @media (max-width: 700px) { + .modal { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr auto; max-height: 92vh; } + .nav { border-right: 0; border-bottom: 1px solid var(--line); flex-direction: row; overflow-x: auto; } + .nav button { white-space: nowrap; } + } + .h { + grid-column: 1 / -1; + padding: 14px 18px; + border-bottom: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; + } + .h .ttl { font-size: 15px; font-weight: 600; } + .nav { + border-right: 1px solid var(--line); + padding: 12px 8px; + display: flex; flex-direction: column; gap: 2px; + background: var(--bg-1); + } + .nav button { + text-align: left; + padding: 8px 12px; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + color: var(--ink-3); + font-size: 12.5px; + cursor: pointer; + transition: color 0.15s, background 0.15s; + } + .nav button:hover { color: var(--ink); background: var(--bg-2); } + .nav button.on { + color: var(--ink); background: var(--bg-3); + border-color: var(--line-2); + } + .body { + padding: 18px 22px; + overflow-y: auto; + font-size: 13px; + color: var(--ink-2); + line-height: 1.6; + } + .body h2 { + margin: 0 0 8px; + font-size: 18px; + color: var(--ink); + letter-spacing: -0.01em; + } + .body .lead { + color: var(--ink-3); + font-size: 12.5px; + margin: 0 0 14px; + } + .body p { margin: 0 0 12px; } + .body code { + font-family: var(--mono); + background: var(--bg-3); + padding: 1px 5px; + border-radius: 4px; + font-size: 11.5px; + color: var(--accent); + } + .body kbd { + font-family: var(--mono); + padding: 2px 6px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 4px; + font-size: 11.5px; + color: var(--ink); + } + .step { + display: grid; + grid-template-columns: 32px 1fr; + gap: 12px; + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .step:last-child { border-bottom: 0; } + .step .num { + width: 26px; height: 26px; + border-radius: 50%; + background: var(--accent); + color: #1a0f00; + font-family: var(--mono); + font-size: 12.5px; + font-weight: 700; + display: grid; place-items: center; + } + .step .ttl { color: var(--ink); font-weight: 600; font-size: 13.5px; margin-bottom: 2px; } + .step .body-text { font-size: 12.5px; color: var(--ink-2); line-height: 1.55; } + .glossary-search { + width: 100%; + padding: 8px 12px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 6px; + font-family: var(--mono); + font-size: 12.5px; + color: var(--ink); + outline: none; + margin-bottom: 14px; + } + .glossary-search:focus { border-color: var(--accent); } + .term { + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .term:last-child { border-bottom: 0; } + .term .head { + display: flex; align-items: center; gap: 8px; margin-bottom: 4px; + } + .term .name { + font-family: var(--mono); + font-size: 13.5px; + color: var(--accent); + font-weight: 600; + } + .term .badge { + font-family: var(--mono); + font-size: 9.5px; + padding: 1px 6px; + border-radius: 4px; + border: 1px solid var(--line); + text-transform: uppercase; + letter-spacing: 0.04em; + } + .term .badge.physics { color: var(--accent-2); border-color: oklch(0.78 0.12 195 / 0.4); } + .term .badge.rust { color: var(--accent); border-color: oklch(0.78 0.14 70 / 0.4); } + .term .badge.ui { color: var(--accent-4); border-color: oklch(0.78 0.14 145 / 0.4); } + .term .body-text { + font-size: 12.5px; + color: var(--ink-2); + line-height: 1.55; + } + .faq-item { + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .faq-item:last-child { border-bottom: 0; } + .faq-item .q { + color: var(--ink); + font-weight: 600; + font-size: 13.5px; + margin-bottom: 4px; + } + .faq-item .a { font-size: 12.5px; color: var(--ink-2); line-height: 1.55; } + .shortcuts { + display: grid; + grid-template-columns: auto 1fr; + gap: 8px 16px; + align-items: baseline; + } + .f { + grid-column: 1 / -1; + padding: 10px 18px; + border-top: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; + font-size: 11.5px; color: var(--ink-3); + } + .close { + width: 28px; height: 28px; + background: transparent; border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-2); + cursor: pointer; + } + .close:hover { color: var(--ink); border-color: var(--line-2); } + `; + + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('nv-show-help', this.show as EventListener); + window.addEventListener('nv-show-help-close', this.closeListener); + window.addEventListener('keydown', this.onKey); + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('nv-show-help', this.show as EventListener); + window.removeEventListener('nv-show-help-close', this.closeListener); + window.removeEventListener('keydown', this.onKey); + } + private closeListener = (): void => this.close(); + + private show = (e: Event): void => { + const detail = (e as CustomEvent).detail as { section?: Section } | undefined; + if (detail?.section) this.section = detail.section; + this.open = true; + this.setAttribute('open', ''); + }; + private close(): void { + this.open = false; + this.removeAttribute('open'); + } + private onKey = (e: KeyboardEvent): void => { + const target = e.target as HTMLElement | null; + const isInput = target?.tagName === 'INPUT' || target?.tagName === 'TEXTAREA'; + if (e.key === '?' && !isInput && !e.ctrlKey && !e.metaKey) { + e.preventDefault(); + this.show(new CustomEvent('nv-show-help')); + } else if (e.key === 'Escape' && this.open) { + this.close(); + } + }; + + private filteredGlossary(): GlossaryItem[] { + if (!this.query.trim()) return GLOSSARY; + const q = this.query.toLowerCase(); + return GLOSSARY.filter((g) => + g.term.toLowerCase().includes(q) || g.body.toLowerCase().includes(q), + ); + } + + private renderQuickstart() { + return html` +

Quickstart

+

Seven taps to get from "I just opened the dashboard" to "I'm running my own scene with verified determinism."

+ + ${QUICKSTART.map((s) => html` +
+
${s.step}
+
+
${s.title}
+
+
+
+ `)} + `; + } + + private renderGlossary() { + const items = this.filteredGlossary(); + return html` +

Glossary

+

Every piece of jargon in the dashboard, defined in one paragraph each.

+ this.query = (e.target as HTMLInputElement).value} /> + ${items.length === 0 + ? html`

No terms match.

` + : items.map((g) => html` +
+
+ ${g.term} + ${g.category} +
+
${g.body}
+
+ `)} + `; + } + + private renderFaq() { + return html` +

FAQ

+

The questions I was asked twice in the first week of demos.

+ ${FAQ.map((item) => html` +
+
${item.q}
+
+
+ `)} + `; + } + + private renderShortcuts() { + return html` +

Keyboard shortcuts

+

Everything is reachable without a mouse.

+
+ ${SHORTCUTS.map((s) => html` + ${s.keys}${s.label} + `)} +
+ `; + } + + private renderAbout() { + return html` +

About this dashboard

+

What you're looking at, in one screen.

+

nvsim is a deterministic forward simulator for nitrogen-vacancy diamond magnetometry. + The Rust crate at v2/crates/nvsim is the source of truth; this dashboard is a + Vite + Lit single-page app that ships the crate compiled to WebAssembly inside a Web Worker.

+

The defining commitment is determinism: same (scene, config, seed) → + byte-identical SHA-256 witness across browsers, OSes, and transports. Press the + Verify witness button on the Witness tab to assert this live.

+

The codebase is open source (Apache-2.0 OR MIT). Find it on GitHub: + github.com/ruvnet/RuView. Decisions are documented in ADRs 089 (nvsim), + 090 (Lindblad extension, conditional), 091 (sub-THz radar research), + 092 (this dashboard), 093 (UX gap analysis).

+

This dashboard is one of several RuView demos. Sibling demos at + github.io/RuView/ include the Observatory and Pose Fusion views.

+ `; + } + + override render() { + return html` + + `; + } +} + +export function showHelp(section?: Section): void { + window.dispatchEvent(new CustomEvent('nv-show-help', { detail: { section } })); +} diff --git a/dashboard/src/components/nv-home.ts b/dashboard/src/components/nv-home.ts new file mode 100644 index 000000000..39fd97e0d --- /dev/null +++ b/dashboard/src/components/nv-home.ts @@ -0,0 +1,270 @@ +/* Home view — friendly landing surface for new users. + * + * The full-power scene + sidebar + inspector + console are intentionally + * dense; that's the operator surface. Home is for first-time visitors: + * a single hero CTA, four quick-jump action cards, and a 1-paragraph + * explanation of what this dashboard is. No jargon above the fold. + */ + +import { LitElement, html, css } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { running, getClient, witnessVerified, fps, pushLog } from '../store/appStore'; + +export type Action = 'scene' | 'apps' | 'witness' | 'ghost-murmur' | 'help' | 'tour'; + +@customElement('nv-home') +export class NvHome extends LitElement { + static styles = css` + :host { + display: block; + height: 100%; + overflow-y: auto; + background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%); + padding: 28px clamp(16px, 6vw, 56px) 60px; + } + .hero { + max-width: 800px; + margin: 16px auto 28px; + text-align: center; + } + .hero .icon { + width: 56px; height: 56px; + margin: 0 auto 18px; + border-radius: 14px; + background: linear-gradient(135deg, oklch(0.78 0.14 70) 0%, oklch(0.55 0.16 30) 100%); + display: grid; place-items: center; + font-family: var(--mono); + font-weight: 700; + font-size: 18px; + color: #1a0f00; + box-shadow: 0 8px 24px -6px oklch(0.55 0.16 30 / 0.4); + } + .hero h1 { + margin: 0 0 8px; + font-size: clamp(24px, 4vw, 34px); + letter-spacing: -0.02em; + color: var(--ink); + line-height: 1.15; + } + .hero .tag { + font-size: clamp(13px, 1.6vw, 15px); + color: var(--ink-2); + margin: 0 0 22px; + line-height: 1.55; + } + .hero .ctas { + display: flex; flex-wrap: wrap; gap: 8px; + justify-content: center; + } + .cta { + padding: 11px 20px; + border-radius: 10px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + font-family: inherit; + border: 1px solid var(--line); + background: var(--bg-2); + color: var(--ink); + transition: transform 0.12s, border-color 0.12s, filter 0.12s; + } + .cta:hover { transform: translateY(-1px); border-color: var(--line-2); } + .cta.primary { + background: var(--accent); + border-color: var(--accent); + color: #1a0f00; + } + .cta.primary:hover { filter: brightness(1.08); } + .status { + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 12px; + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: 999px; + font-size: 12px; + font-family: var(--mono); + color: var(--ink-2); + margin-top: 18px; + } + .status .dot { + width: 8px; height: 8px; border-radius: 50%; + background: var(--ink-3); + } + .status.live .dot { + background: var(--ok); + box-shadow: 0 0 8px var(--ok); + animation: pulse 2s infinite; + } + @keyframes pulse { 50% { opacity: 0.5; } } + + .grid { + max-width: 980px; + margin: 36px auto 0; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 14px; + } + .card { + background: var(--bg-2); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 18px 20px; + cursor: pointer; + transition: transform 0.12s, border-color 0.12s, background 0.12s; + display: flex; flex-direction: column; gap: 6px; + text-align: left; + color: inherit; + } + .card:hover { + transform: translateY(-2px); + border-color: var(--accent); + background: linear-gradient(180deg, var(--bg-2) 0%, oklch(0.78 0.14 70 / 0.04) 100%); + } + .card .ico { + font-size: 22px; + line-height: 1; + margin-bottom: 4px; + } + .card h3 { + margin: 0; + font-size: 14.5px; + font-weight: 600; + color: var(--ink); + letter-spacing: -0.01em; + } + .card p { + margin: 0; + font-size: 12.5px; + color: var(--ink-2); + line-height: 1.55; + } + .card .arrow { + color: var(--accent); + font-family: var(--mono); + font-size: 11.5px; + margin-top: 6px; + } + + .footnote { + max-width: 800px; + margin: 36px auto 0; + text-align: center; + font-size: 12px; + color: var(--ink-3); + line-height: 1.55; + } + .footnote code { + font-family: var(--mono); + background: var(--bg-3); + padding: 1px 5px; + border-radius: 4px; + color: var(--accent); + font-size: 11px; + } + .footnote a { + color: var(--accent-2); + text-decoration: underline dotted; + cursor: pointer; + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { running.value; witnessVerified.value; fps.value; this.requestUpdate(); }); + } + + private go(action: Action): void { + if (action === 'tour') { window.dispatchEvent(new CustomEvent('nv-show-tour')); return; } + if (action === 'help') { window.dispatchEvent(new CustomEvent('nv-show-help')); return; } + this.dispatchEvent(new CustomEvent('navigate', { detail: action, bubbles: true, composed: true })); + } + + private async runDemo(): Promise { + const c = getClient(); if (!c) return; + if (running.value) return; + await c.run(); + running.value = true; + pushLog('ok', 'demo started · streaming MagFrames'); + } + + override render() { + const isRunning = running.value; + const wasVerified = witnessVerified.value === 'ok'; + return html` +
+ +

An open-source quantum-magnetometer simulator, in your browser.

+

+ nvsim runs a real Rust simulator (the same code that + cargo test + uses) entirely in WebAssembly. No server, no upload, no telemetry. + Press the button to start the live magnetic-field simulation, or + take the 60-second tour first. +

+
+ + + +
+
+ + ${isRunning + ? html`Live · ${fps.value > 0 ? (fps.value / 1000).toFixed(2) + ' kHz' : 'starting…'}${wasVerified ? ' · witness verified ✓' : ''}` + : html`Idle${wasVerified ? ' · witness verified ✓' : ''}`} +
+
+ +
+
this.go('scene')} + @keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('scene'); } }}> +
🌐
+

Live scene

+

Drag magnetic sources, watch the recovered field update in real time, and tweak sample rate / noise / integration.

+
Open scene →
+
+ +
this.go('apps')} + @keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('apps'); } }}> +
🛍
+

App Store · 66 edge apps

+

Browse 65 hot-loadable WASM sensing modules across medical, security, building, retail, industrial, learning. Six run live in the browser.

+
Browse the catalogue →
+
+ +
this.go('witness')} + @keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('witness'); } }}> +
+

Determinism gate

+

Re-derive the SHA-256 witness for the canonical reference scene right here in your browser. Same inputs → same hash, every time.

+
Verify the witness →
+
+ +
this.go('ghost-murmur')} + @keydown=${(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.go('ghost-murmur'); } }}> +
👻
+

Ghost Murmur reality check

+

Audit the publicly-reported April 2026 CIA NV-diamond program against published physics. Live distance/moment sliders.

+
Read the spec →
+
+
+ +

+ New here? this.go('tour')}>Take the 60-second guided tour + — every panel is explained. Or press ? for the help center + (quickstart, glossary, FAQ, shortcuts) any time.
+ Open source · Apache-2.0 OR MIT · github.com/ruvnet/RuView +

+ `; + } +} diff --git a/dashboard/src/components/nv-inspector.ts b/dashboard/src/components/nv-inspector.ts new file mode 100644 index 000000000..a0d749542 --- /dev/null +++ b/dashboard/src/components/nv-inspector.ts @@ -0,0 +1,434 @@ +/* Inspector — tabbed: Signal / Frame / Witness. */ +import { LitElement, html, css, svg, type PropertyValues } from 'lit'; +import { customElement, state, property } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { + traceX, traceY, traceZ, stripBars, lastFrame, + witnessHex, expectedWitness, witnessVerified, getClient, + pushLog, lastB, bMag, +} from '../store/appStore'; + +type Tab = 'signal' | 'frame' | 'witness'; + +@customElement('nv-inspector') +export class NvInspector extends LitElement { + @state() private tab: Tab = 'signal'; + /** When set by the parent, force the tab and pulse-highlight it. */ + @property({ attribute: false }) pinTab: Tab | null = null; + /** When `expanded`, the inspector renders as a full-screen view with bigger + * charts and a wider Witness panel. Used when the rail Inspector/Witness + * button is clicked — see ADR-093 P1.13. */ + @property({ type: Boolean, reflect: true }) expanded = false; + + static styles = css` + :host { + display: flex; flex-direction: column; + background: var(--bg-1); + border-left: 1px solid var(--line); + overflow: hidden; + height: 100%; + } + :host([expanded]) { + border-left: 0; + background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%); + } + :host([expanded]) .tabs { + padding: 0 24px; + background: var(--bg-1); + } + :host([expanded]) .tab { + padding: 16px 22px; + font-size: 13.5px; + flex: 0 0 auto; + } + :host([expanded]) .body { + padding: 24px 28px; + max-width: 1400px; + width: 100%; + margin: 0 auto; + } + :host([expanded]) .card { padding: 18px 20px; } + :host([expanded]) .card-h .ttl { font-size: 14px; } + :host([expanded]) svg { height: 220px; } + :host([expanded]) .frame-strip { height: 48px; } + :host([expanded]) table { font-size: 12.5px; } + :host([expanded]) td { padding: 6px 0; } + :host([expanded]) .hex { font-size: 12px; padding: 14px; line-height: 1.7; } + :host([expanded]) .witness-box { font-size: 13px; padding: 14px 16px; line-height: 1.6; } + :host([expanded]) .verify-btn { padding: 12px; font-size: 13px; } + :host([expanded]) .grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + } + :host([expanded]) .grid-2 > .card { margin-bottom: 0; } + @media (max-width: 1024px) { + :host([expanded]) .grid-2 { grid-template-columns: 1fr; } + } + .tabs { + display: flex; border-bottom: 1px solid var(--line); + } + .tab { + flex: 1; + padding: 11px 8px; + background: transparent; border: none; + font-size: 11.5px; font-weight: 500; + color: var(--ink-3); + border-bottom: 2px solid transparent; + cursor: pointer; transition: color 0.15s, border-color 0.15s; + } + .tab.active { color: var(--ink); border-bottom-color: var(--accent); } + .tab:hover { color: var(--ink-2); } + .body { padding: 14px; flex: 1; overflow-y: auto; } + + .card { + background: var(--bg-2); border: 1px solid var(--line); + border-radius: var(--radius); padding: 12px; + margin-bottom: 12px; + } + .card-h { + display: flex; justify-content: space-between; align-items: center; + margin-bottom: 8px; + } + .card-h .ttl { font-size: 12px; font-weight: 600; } + .badge { + font-family: var(--mono); font-size: 10px; + padding: 2px 6px; + background: oklch(0.78 0.14 195 / 0.12); + color: var(--accent-2); + border-radius: 4px; + border: 1px solid oklch(0.78 0.14 195 / 0.3); + } + svg { width: 100%; height: 130px; } + .frame-strip { + height: 28px; + display: flex; align-items: flex-end; gap: 1px; + padding: 4px 0; + } + .bar { + flex: 1; + background: linear-gradient(to top, var(--accent-2), var(--accent)); + border-radius: 1px; + min-height: 2px; + } + table { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 10.5px; } + td { padding: 4px 0; border-bottom: 1px solid var(--line); } + td:first-child { color: var(--ink-3); } + td:last-child { text-align: right; color: var(--ink); } + .hex { + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 10px; + font-family: var(--mono); + font-size: 10.5px; + color: var(--ink-2); + line-height: 1.6; + overflow-x: auto; + white-space: nowrap; + } + .hex .magic { color: var(--accent); font-weight: 600; } + .witness-box { + font-family: var(--mono); + font-size: 11px; + color: var(--ink-2); + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 6px; + padding: 8px 10px; + word-break: break-all; + line-height: 1.5; + } + .verify-btn { + margin-top: 10px; + width: 100%; + padding: 8px; + border: 1px solid var(--line); + background: var(--bg-3); + color: var(--ink); + border-radius: 8px; + cursor: pointer; + font-family: var(--mono); + font-size: 12px; + } + .verify-btn:hover { border-color: var(--accent); } + .verify-btn.ok { border-color: var(--ok); color: var(--ok); } + .verify-btn.fail { border-color: var(--bad); color: var(--bad); } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { + traceX.value; traceY.value; traceZ.value; stripBars.value; + lastFrame.value; witnessHex.value; witnessVerified.value; + lastB.value; bMag.value; + this.requestUpdate(); + }); + } + + override willUpdate(changed: PropertyValues): void { + // Apply parent-driven tab pin during willUpdate so the new tab value + // participates in this same render pass — avoids the "update after + // update completed" Lit warning that would fire if we did this in + // updated(). + if (changed.has('pinTab') && this.pinTab && this.tab !== this.pinTab) { + this.tab = this.pinTab; + } + } + + private async verify(): Promise { + const c = getClient(); if (!c) return; + witnessVerified.value = 'pending'; + pushLog('info', 'verifying witness over 256 frames…'); + try { + const exp = expectedWitness.value; + const expBytes = new Uint8Array(32); + for (let i = 0; i < 32; i++) expBytes[i] = parseInt(exp.slice(i * 2, i * 2 + 2), 16); + const r = await c.verifyWitness(expBytes); + if (r.ok) { + witnessVerified.value = 'ok'; + witnessHex.value = exp; + pushLog('ok', `witness ${exp.slice(0, 16)}… matches · determinism gate ✓`); + } else { + witnessVerified.value = 'fail'; + const actual = Array.from(r.actual).map((b) => b.toString(16).padStart(2, '0')).join(''); + witnessHex.value = actual; + pushLog('err', `WITNESS MISMATCH actual=${actual.slice(0, 16)}…`); + } + } catch (e) { + witnessVerified.value = 'fail'; + pushLog('err', `verify failed: ${(e as Error).message}`); + } + } + + private renderHeader() { + if (!this.expanded) return ''; + const titles: Record = { + signal: 'Signal inspector — live B-vector trace + frame stream', + frame: 'Frame inspector — MagFrame v1 fields + raw bytes', + witness: 'Witness panel — SHA-256 determinism gate', + }; + return html` +

+ ${titles[this.tab]} +

+

+ ${this.tab === 'signal' + ? 'Real-time recovered field-vector and frame-stream sparkline. Both update at the running pipeline\'s frame rate. Use the Tunables panel in the sidebar to change f_s, f_mod, dt, and shot-noise behaviour.' + : this.tab === 'frame' + ? 'Decoded view of the most recent MagFrame: typed fields plus the raw 60-byte little-endian binary record (magic 0xC51A_6E70).' + : 'Re-derive the SHA-256 witness for the canonical reference scene (seed=42, N=256) right now in your browser and compare against Proof::EXPECTED_WITNESS_HEX. Same inputs → same hash, byte-for-byte, across every machine and transport.'} +

+ `; + } + + private renderSignalTab() { + const W = 320, H = 130, cy = 65, scale = 22; + const cap = 200; + const make = (arr: number[]) => { + let p = ''; + arr.forEach((v, i) => { + const x = (i / Math.max(1, cap - 1)) * W; + const y = cy - v * scale; + p += (i === 0 ? 'M' : 'L') + ` ${x.toFixed(1)} ${y.toFixed(1)} `; + }); + return p; + }; + + const b = lastB.value; + const bnT = [b[0] * 1e9, b[1] * 1e9, b[2] * 1e9]; + const hasData = traceX.value.length > 0; + + return html` + ${!hasData ? html` +
+
+ No frames yet. Press ▶ Run in the topbar (or hit Space) + to start the live B-vector trace. +
+
+ ` : ''} +
+
+
+ B-vector trace + 3-axis · nT +
+ + + ${svg``} + ${svg``} + ${svg``} + + ${this.expanded ? html`
+ x: ${bnT[0].toFixed(3)} nT + y: ${bnT[1].toFixed(3)} nT + z: ${bnT[2].toFixed(3)} nT + |B| ${(bMag.value * 1e9).toFixed(3)} nT +
` : ''} +
+ +
+
+ Frame stream + live +
+
+ ${stripBars.value.map((v) => html`
`)} +
+ ${this.expanded ? html` +
+ frames in window: ${stripBars.value.length} + noise floor: ${lastFrame.value ? lastFrame.value.noiseFloorPtSqrtHz.toFixed(2) + ' pT/√Hz' : '—'} +
` : ''} +
+
+ `; + } + + private renderFrameTab() { + const f = lastFrame.value; + const bytes = f?.raw; + let hex = ''; + if (bytes) { + const arr = Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')); + hex = arr.slice(0, 60).join(' '); + } + return html` + ${!f ? html` +
+
+ No MagFrame to display yet. Start the pipeline (▶ Run) to populate. +
+
+ ` : ''} +
+
+
+ MagFrame v1 fields + 60 B +
+ + + + + + + + + + + +
magic${f ? '0x' + f.magic.toString(16).toUpperCase() : '—'}
version${f?.version ?? '—'}
flags0x${(f?.flags ?? 0).toString(16).padStart(4, '0')}
sensor_id${f?.sensorId ?? '—'}
t_us${f ? f.tUs.toString() : '—'}
b_pT[0]${f ? f.bPt[0].toFixed(1) : '—'}
b_pT[1]${f ? f.bPt[1].toFixed(1) : '—'}
b_pT[2]${f ? f.bPt[2].toFixed(1) : '—'}
noise_floor${f ? f.noiseFloorPtSqrtHz.toFixed(2) : '—'}
temp_K${f ? f.temperatureK.toFixed(1) : '—'}
+
+
+
+ Hex dump + LE +
+
${hex || '—'}
+ ${this.expanded ? html` +
+ Layout (little-endian): magic(u32) version(u16) flags(u16) sensor_id(u16) _reserved(u16) t_us(u64) b_pt[3](f32) sigma_pt[3](f32) noise_floor(f32) temp_K(f32). +
` : ''} +
+
+ `; + } + + private renderWitnessTab() { + const status = witnessVerified.value; + const cls = status === 'ok' ? 'ok' : status === 'fail' ? 'fail' : ''; + const label = + status === 'pending' ? 'Verifying…' : + status === 'ok' ? '✓ Witness verified · determinism gate' : + status === 'fail' ? '✗ Witness mismatch · audit required' : + 'Verify witness'; + const match = expectedWitness.value && witnessHex.value && expectedWitness.value === witnessHex.value; + return html` + ${this.expanded ? html` +
+
+
Reference scene
+
Proof::REFERENCE
+
2 dipoles · 1 loop · 1 ferrous · 1 sensor
+
+
+
Seed
+
0x0000002A
+
canonical Proof::SEED
+
+
+
Sample count
+
256
+
Proof::N_SAMPLES
+
+
+
Status
+
+ ${status === 'ok' ? '✓ matches' : status === 'fail' ? '✗ drift' : status === 'pending' ? '… running' : '— idle'} +
+
${match ? 'byte-equivalent' : 'not yet verified'}
+
+
+ ` : ''} +
+
+ Expected (Proof::EXPECTED_WITNESS_HEX) + SHA-256 +
+
${expectedWitness.value || '(loading…)'}
+
+
+
+ Actual (last verify) + SHA-256 +
+
${witnessHex.value || '(not verified yet)'}
+ +
+ ${this.expanded ? html` +
+
+ What this verifies + ADR-089 §5 +
+
+

Pressing Verify runs the canonical reference pipeline + (Proof::generate) end-to-end inside this browser's WASM Worker: + scene → Biot-Savart synthesis → material attenuation → NV ensemble → ADC + lock-in → + concatenated MagFrame bytes → SHA-256.

+

If the resulting hash matches the constant pinned at build time + (cc8de9b01b0ff5bd…), every constant — γ_e, D_GS, μ₀, T₂*, contrast, the PRNG + stream, the frame layout, the pipeline ordering — is byte-identical to the published + reference. If it doesn't match, something drifted; the dashboard names which.

+

This is the same regression test that runs in + cargo test -p nvsim — running in your browser, against your own WASM build.

+
+
+ ` : ''} + `; + } + + override render() { + return html` +
+ + + +
+
+ ${this.renderHeader()} + ${this.tab === 'signal' ? this.renderSignalTab() + : this.tab === 'frame' ? this.renderFrameTab() + : this.renderWitnessTab()} +
+ `; + } +} diff --git a/dashboard/src/components/nv-modal.ts b/dashboard/src/components/nv-modal.ts new file mode 100644 index 000000000..23b4c6575 --- /dev/null +++ b/dashboard/src/components/nv-modal.ts @@ -0,0 +1,153 @@ +/* Modal dialog — opened via window.dispatchEvent('nv-modal', { title, body, buttons }). */ +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +interface ModalButton { + label: string; + variant?: 'ghost' | 'primary' | 'danger'; + onClick?: () => void; +} +interface ModalReq { + title: string; + body: string; + buttons?: ModalButton[]; +} + +@customElement('nv-modal') +export class NvModal extends LitElement { + @state() private open = false; + @state() private mTitle = ''; + @state() private mBody = ''; + @state() private buttons: ModalButton[] = []; + + static styles = css` + :host { + position: fixed; inset: 0; + background: rgba(0,0,0,0.55); + backdrop-filter: blur(4px); + z-index: 200; + display: grid; place-items: center; + opacity: 0; pointer-events: none; + transition: opacity 0.18s; + } + :host([open]) { opacity: 1; pointer-events: auto; } + .modal { + background: var(--bg-1); + border: 1px solid var(--line-2); + border-radius: var(--radius); + box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); + width: min(520px, 92vw); + max-height: 86vh; + display: flex; flex-direction: column; + transform: translateY(12px) scale(0.98); + transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1); + } + :host([open]) .modal { transform: translateY(0) scale(1); } + .h { + padding: 14px 16px; + border-bottom: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; + } + .h .ttl { font-size: 14px; font-weight: 600; } + .body { padding: 16px; overflow-y: auto; font-size: 13px; color: var(--ink-2); line-height: 1.55; } + .f { + padding: 12px 16px; + border-top: 1px solid var(--line); + display: flex; gap: 8px; justify-content: flex-end; + } + button { + padding: 6px 12px; + border-radius: 8px; + font-size: 12.5px; + cursor: pointer; + font-family: inherit; + border: 1px solid var(--line); + background: var(--bg-2); color: var(--ink); + } + button.ghost { background: transparent; } + button.primary { background: var(--accent); border-color: var(--accent); color: #1a0f00; } + button.danger { background: var(--bad); border-color: var(--bad); color: #fff; } + .close { + width: 28px; height: 28px; + background: transparent; border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-2); + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('nv-modal', this.onModal as EventListener); + window.addEventListener('keydown', this.onKey); + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('nv-modal', this.onModal as EventListener); + window.removeEventListener('keydown', this.onKey); + } + + private onModal = (e: Event): void => { + const r = (e as CustomEvent).detail as ModalReq; + this.mTitle = r.title; this.mBody = r.body; + this.buttons = r.buttons ?? [{ label: 'Close', variant: 'primary' }]; + this.open = true; this.setAttribute('open', ''); + // a11y: focus the first interactive element inside the modal so keyboard + // users land in the dialog rather than behind it. Light focus trap via + // the keydown handler below catches Tab cycling. + requestAnimationFrame(() => { + const root = this.shadowRoot; + if (!root) return; + const first = root.querySelector('input, select, textarea, button:not(.close)'); + first?.focus(); + }); + }; + + override updated(): void { + if (!this.open) return; + const root = this.shadowRoot; + if (!root) return; + // Trap Tab inside the modal while open. + const trap = (e: KeyboardEvent): void => { + if (e.key !== 'Tab') return; + const focusables = Array.from( + root.querySelectorAll('input, select, textarea, button, [href]'), + ).filter((el) => !el.hasAttribute('disabled')); + if (focusables.length === 0) return; + const first = focusables[0]; + const last = focusables[focusables.length - 1]; + const active = (root.activeElement as HTMLElement | null) ?? null; + if (e.shiftKey && active === first) { e.preventDefault(); last.focus(); } + else if (!e.shiftKey && active === last) { e.preventDefault(); first.focus(); } + }; + root.removeEventListener('keydown', trap as EventListener); + root.addEventListener('keydown', trap as EventListener); + } + + private onKey = (e: KeyboardEvent): void => { + if (e.key === 'Escape' && this.open) this.close(); + }; + + private close(): void { this.open = false; this.removeAttribute('open'); } + private clickBtn(b: ModalButton): void { b.onClick?.(); this.close(); } + + override render() { + return html` + + `; + } +} + +export function openModal(req: ModalReq): void { + window.dispatchEvent(new CustomEvent('nv-modal', { detail: req })); +} diff --git a/dashboard/src/components/nv-onboarding.ts b/dashboard/src/components/nv-onboarding.ts new file mode 100644 index 000000000..29e9cb1ca --- /dev/null +++ b/dashboard/src/components/nv-onboarding.ts @@ -0,0 +1,397 @@ +/* Welcome modal + step-by-step introduction tour. + * + * 10 steps walking the user through every panel of the dashboard with + * concrete CTAs ("Try it now") that fire real navigation against the + * live UI. First-run only by default; replayable via Settings → Help. + */ + +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { kvGet, kvSet } from '../store/persistence'; + +interface TourStep { + /** Optional icon shown at the top of the step. */ + icon: string; + title: string; + /** Markdown-ish HTML body (rendered via .innerHTML). */ + body: string; + /** Optional CTA: clicking runs the action then advances. */ + cta?: { label: string; run?: () => void }; + /** Optional "do this yourself" hint. */ + hint?: string; +} + +const STEPS: TourStep[] = [ + { + icon: '👋', + title: 'Welcome to nvsim', + body: `

+ nvsim is an open-source, deterministic forward simulator for + nitrogen-vacancy diamond magnetometry — a real Rust crate compiled + to WebAssembly and running in your browser, right now.

+

+ This 60-second tour walks you through the four panels, the App Store, + the Ghost Murmur research view, and the determinism contract that + makes nvsim distinctive.

+

+ Press Esc any time to skip. You can replay this tour from + Settings → Help.

`, + cta: { label: 'Start the tour →' }, + }, + { + icon: '🌐', + title: 'The Scene canvas', + body: `

The middle panel shows your magnetic scene — a small simulated + environment with four sources and one NV-diamond sensor at the centre.

+

The four amber/cyan/magenta blobs are draggable: rebar coil + (steel χ=5000), heart proxy dipole, 60 Hz mains current loop, + and a steel door (eddy current). Field lines connect each source + to the sensor and animate while the pipeline runs.

+

+ Top-left toolbar: zoom in/out, fit-to-view, layer toggles. Bottom-right: + sim controls (step / play / step / speed cycle). Drag positions persist + across reloads.

`, + hint: 'Try dragging the heart_proxy after the tour ends.', + }, + { + icon: '▶', + title: 'Run the pipeline', + body: `

Press ▶ Run in the topbar (or hit Space) to start + the live frame stream. nvsim runs at ~1.8 kHz on x86_64 WASM — + well above the 1 kHz Cortex-A53 acceptance gate.

+

The FPS pill in the topbar updates with the throughput. The B-vector + trace and frame-stream sparkline in the right inspector update in real + time.

+

+ Space toggles run/pause from anywhere. Reset (⌘R) + rewinds t to 0 without changing the seed.

`, + }, + { + icon: '🔍', + title: 'Inspector — three tabs, three depths', + body: `

The right rail shows the live inspector: Signal (B-vector + trace + frame-stream sparkline), Frame (decoded MagFrame fields + + raw 60-byte hex dump), Witness (SHA-256 determinism gate).

+

Click the magnifier icon in the left rail to expand the + inspector to the full main area, with bigger charts and an explainer + header. Click the shield icon to do the same focused on Witness.

+

+ Number keys 1 2 3 jump between the + three inspector tabs from anywhere.

`, + }, + { + icon: '✓', + title: 'The witness — what makes nvsim distinctive', + body: `

nvsim's defining commitment: same (scene, config, seed) → + byte-identical SHA-256 across runs, machines, and transports.

+

Click the Witness tab and press Verify witness. The + dashboard re-derives the hash for the canonical reference scene + (seed=42, N=256) and asserts it matches the constant + pinned at compile time + (cc8de9b01b0ff5bd…).

+

A green check means every constant — γ_e, D_GS, μ₀, T₂*, contrast, + the PRNG stream, the frame layout — is byte-identical to the published + reference. A red ✗ means something drifted; the dashboard names which.

`, + }, + { + icon: '🎚', + title: 'Tunables — change the simulation live', + body: `

The left sidebar's Tunables panel has four sliders:

+
    +
  • Sample rate (1–100 kHz) — digitiser frame rate
  • +
  • Lock-in f_mod (0.1–5 kHz) — microwave modulation freq
  • +
  • Integration t (0.1–10 ms) — per-sample integration time
  • +
  • Shot noise (on/off) — toggle quantum noise
  • +
+

Edits debounce 300 ms then rebuild the WASM pipeline without restarting + the frame stream. Watch the noise floor and B-vector spread change + in the Signal trace.

`, + }, + { + icon: '👻', + title: 'Ghost Murmur — research view', + body: `

Click the ghost icon in the left rail. This view audits the + publicly-reported April 2026 CIA Ghost Murmur NV-diamond + heartbeat-detection program against the open physics literature.

+

Includes a "Try it yourself" sandbox: place a cardiac dipole at + any distance from the sensor, hit Run, and see what the real nvsim + pipeline recovers. Per-tier detectability bars compare the predicted + signal vs each transport's noise floor (NV-ensemble lab, COTS DNV-B1, + SQUID, 60 GHz mmWave, WiFi CSI).

+

+ Spoiler: at 1 km the cardiac MCG is ~10⁻¹² of its 10 cm value. + Press claims of 40-mile detection sit far below any published instrument's + floor.

`, + }, + { + icon: '🛍', + title: 'App Store — 65 edge apps', + body: `

Click the grid icon. The App Store catalogues every + hot-loadable WASM edge module RuView ships, organised by category: + medical, security, smart-building, retail, industrial, signal, + learning, autonomy, exotic.

+

Each card carries id / category / status / event IDs / compute budget / + ADR back-reference. The toggle marks an app active in this session; + the WS transport (when configured) pushes the activation set to a + connected ESP32 mesh.

+

+ Try searching for "ghost", "heart", or "occupancy" to fuzzy-filter + the catalogue.

`, + }, + { + icon: '⌨', + title: 'Console + REPL', + body: `

The bottom panel is a structured event log with five filter tabs + (all / info / warn / err / dbg) plus a REPL prompt.

+

REPL commands include + help, scene.list, sensor.config, + run, pause, seed [hex], + proof.verify, proof.export, + theme [light|dark], status, clear.

+

+ Press / to focus the REPL from anywhere. Arrow ↑/↓ recall + history (persisted across reloads). ⌘K opens the command + palette with every action discoverable.

`, + }, + { + icon: '🚀', + title: 'You are ready', + body: `

That's the whole tour. A few last pointers:

+
    +
  • Press ? any time to open the help center + (Quickstart / Glossary / FAQ / Shortcuts / About).
  • +
  • Press ⌘K for the command palette.
  • +
  • Press \` to toggle the debug HUD.
  • +
  • Settings (⌘,) lets you switch theme, density, motion, + transport, and replay this tour.
  • +
+

+ Source: github.com/ruvnet/RuView · Apache-2.0 OR MIT · + ADRs 089/090/091/092/093.

`, + cta: { label: 'Get started →' }, + }, +]; + +@customElement('nv-onboarding') +export class NvOnboarding extends LitElement { + @state() private open = false; + @state() private step = 0; + + static styles = css` + :host { + position: fixed; inset: 0; + background: rgba(0, 0, 0, 0.55); + backdrop-filter: blur(4px); + z-index: 240; + display: grid; place-items: center; + opacity: 0; pointer-events: none; + transition: opacity 0.18s; + } + :host([open]) { opacity: 1; pointer-events: auto; } + .card { + background: var(--bg-1); + border: 1px solid var(--line-2); + border-radius: var(--radius); + box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); + width: min(640px, 94vw); + max-height: 86vh; + display: flex; flex-direction: column; + transform: translateY(12px) scale(0.98); + transition: transform 0.22s cubic-bezier(0.2,0.7,0.3,1); + overflow: hidden; + } + :host([open]) .card { transform: translateY(0) scale(1); } + .h { + padding: 22px 26px 12px; + display: flex; align-items: flex-start; gap: 14px; + } + .h .icon { + width: 44px; height: 44px; + border-radius: 12px; + background: linear-gradient(135deg, oklch(0.78 0.14 70) 0%, oklch(0.55 0.16 30) 100%); + display: grid; place-items: center; + font-size: 22px; + flex-shrink: 0; + box-shadow: 0 4px 12px -2px oklch(0.55 0.16 30 / 0.35); + } + .h .title-wrap { flex: 1; min-width: 0; } + .h h2 { + margin: 0; + font-size: 18px; + letter-spacing: -0.01em; + color: var(--ink); + } + .h .step-label { + font-family: var(--mono); + font-size: 10.5px; + color: var(--ink-3); + margin-top: 4px; + text-transform: uppercase; + letter-spacing: 0.06em; + } + .h .skip { + width: 28px; height: 28px; + background: transparent; + border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-2); + cursor: pointer; + flex-shrink: 0; + } + .h .skip:hover { color: var(--ink); border-color: var(--line-2); } + .body { + padding: 0 26px 16px; + font-size: 13px; + color: var(--ink-2); + line-height: 1.6; + overflow-y: auto; + flex: 1; + } + .body p { margin: 0 0 12px; } + .body p:last-child { margin-bottom: 0; } + .body code, .body kbd { + font-family: var(--mono); + font-size: 11.5px; + padding: 1px 5px; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 4px; + } + .body code { color: var(--accent); } + .body kbd { color: var(--ink); } + .hint { + margin: 14px 0 0; + padding: 10px 12px; + background: oklch(0.78 0.12 195 / 0.06); + border: 1px solid oklch(0.78 0.12 195 / 0.25); + border-radius: 8px; + font-size: 12px; + color: var(--accent-2); + display: flex; gap: 8px; align-items: flex-start; + } + .hint::before { + content: '💡'; + flex-shrink: 0; + } + .footer { + display: flex; align-items: center; gap: 14px; + padding: 14px 22px; + border-top: 1px solid var(--line); + background: var(--bg-1); + } + .progress { flex: 1; } + .dots { display: flex; gap: 5px; margin-bottom: 4px; } + .dot { + width: 6px; height: 6px; border-radius: 50%; + background: var(--bg-3); + border: 1px solid var(--line-2); + transition: background 0.15s, border-color 0.15s, transform 0.15s; + } + .dot.active { + background: var(--accent); + border-color: var(--accent); + transform: scale(1.2); + } + .dot.done { + background: var(--accent-4); + border-color: var(--accent-4); + } + .progress-label { + font-family: var(--mono); + font-size: 10px; + color: var(--ink-3); + } + button.primary, button.ghost { + padding: 9px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + font-family: inherit; + border: 1px solid var(--line); + background: var(--bg-2); + color: var(--ink); + } + button.ghost:hover { border-color: var(--line-2); } + button.primary { + background: var(--accent); + border-color: var(--accent); + color: #1a0f00; + } + button.primary:hover { filter: brightness(1.08); } + `; + + override async connectedCallback(): Promise { + super.connectedCallback(); + window.addEventListener('nv-show-tour', this.show as EventListener); + const seen = await kvGet('onboarding-seen'); + if (!seen) { + this.open = true; + this.setAttribute('open', ''); + } + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('nv-show-tour', this.show as EventListener); + } + + private show = (): void => { + this.step = 0; + this.open = true; + this.setAttribute('open', ''); + }; + + private async dismiss(): Promise { + this.open = false; + this.removeAttribute('open'); + await kvSet('onboarding-seen', true); + } + + private next(): void { + const s = STEPS[this.step]; + s.cta?.run?.(); + if (this.step < STEPS.length - 1) this.step++; + else void this.dismiss(); + } + + private prev(): void { + if (this.step > 0) this.step--; + } + + override render() { + const s = STEPS[this.step]; + const isLast = this.step === STEPS.length - 1; + return html` + + `; + } +} diff --git a/dashboard/src/components/nv-palette.ts b/dashboard/src/components/nv-palette.ts new file mode 100644 index 000000000..3a142e499 --- /dev/null +++ b/dashboard/src/components/nv-palette.ts @@ -0,0 +1,244 @@ +/* Command palette ⌘K. */ +import { LitElement, html, css } from 'lit'; +import { customElement, state, query } from 'lit/decorators.js'; +import { toast } from './nv-toast'; +import { openModal } from './nv-modal'; +import { + getClient, theme, expectedWitness, witnessHex, witnessVerified, pushLog, running, +} from '../store/appStore'; + +interface Cmd { ico: string; label: string; kbd?: string; run: () => void; } + +@customElement('nv-palette') +export class NvPalette extends LitElement { + @state() private open = false; + @state() private filter = ''; + @state() private idx = 0; + @query('#palette-input') private inputEl!: HTMLInputElement; + + static styles = css` + :host { + position: fixed; inset: 0; z-index: 220; + background: rgba(0,0,0,0.5); + opacity: 0; pointer-events: none; + transition: opacity 0.15s; + display: flex; justify-content: center; padding-top: 12vh; + backdrop-filter: blur(4px); + } + :host([open]) { opacity: 1; pointer-events: auto; } + .palette { + width: min(560px, 92vw); + background: var(--bg-1); + border: 1px solid var(--line-2); + border-radius: var(--radius); + box-shadow: 0 30px 80px -20px rgba(0,0,0,0.7); + overflow: hidden; + display: flex; flex-direction: column; + max-height: 60vh; + } + .input { + padding: 14px 16px; + border-bottom: 1px solid var(--line); + } + input { + width: 100%; + background: transparent; border: none; outline: none; + color: var(--ink); font-size: 14px; + font-family: inherit; + } + .list { flex: 1; overflow-y: auto; padding: 4px; } + .item { + display: flex; align-items: center; gap: 10px; + padding: 8px 12px; + border-radius: 6px; + cursor: pointer; + font-size: 12.5px; + } + .item.active { background: var(--bg-3); } + .item .ico { width: 20px; text-align: center; color: var(--accent); } + .item .lbl { flex: 1; } + .item .kbd { + font-family: var(--mono); font-size: 10.5px; + color: var(--ink-3); + padding: 1px 5px; background: var(--bg-3); border-radius: 4px; + } + `; + + private cmds: Cmd[] = [ + { ico: '▶', label: 'Run pipeline', kbd: 'Space', run: async () => { await getClient()?.run(); running.value = true; toast('Pipeline running', '▶'); } }, + { ico: '❚', label: 'Pause pipeline', run: async () => { await getClient()?.pause(); running.value = false; toast('Paused', '❚❚'); } }, + { ico: '+', label: 'New scene…', kbd: '⌘N', run: () => openModal({ + title: 'New scene', + body: `

Build a fresh magnetic scene. The dashboard generates the JSON + and pushes it to the running pipeline (or you can copy the JSON + for offline use).

+ + + + + + + + + + `, + buttons: [ + { label: 'Cancel', variant: 'ghost' }, + { label: 'Create', variant: 'primary', onClick: async () => { + const root = document.querySelector('nv-app')?.shadowRoot?.querySelector('nv-modal')?.shadowRoot; + if (!root) return; + const name = (root.querySelector('#ns-name')?.value ?? 'custom').trim(); + const m = parseFloat(root.querySelector('#ns-moment')?.value ?? '1e-6'); + const d = parseFloat(root.querySelector('#ns-distance')?.value ?? '0.5'); + const ferr = root.querySelector('#ns-ferrous')?.value === '1'; + const mains = root.querySelector('#ns-mains')?.value === '1'; + const scene = { + dipoles: [{ position: [0, 0, d] as [number, number, number], moment: [0, 0, m] as [number, number, number] }], + loops: mains ? [{ + centre: [0, 1, 0] as [number, number, number], + normal: [0, 1, 0] as [number, number, number], + radius: 0.05, current: 2.0, n_segments: 64, + }] : [], + ferrous: ferr ? [{ position: [1, 0, 0] as [number, number, number], volume: 1e-4, susceptibility: 5000 }] : [], + eddy: [], + sensors: [[0, 0, 0] as [number, number, number]], + ambient_field: [1e-6, 0, 0] as [number, number, number], + }; + await getClient()?.loadScene(scene); + pushLog('ok', `scene ${name} loaded · 1 dipole · ${mains ? '1 loop · ' : ''}${ferr ? '1 ferrous · ' : ''}1 sensor`); + toast(`Scene "${name}" loaded`, '+'); + } }, + ], + }) }, + { ico: '📦', label: 'Export proof bundle…', kbd: '⌘E', run: async () => { + const c = getClient(); if (!c) return; + pushLog('dbg', 'building proof bundle…'); + try { + const blob = await c.exportProofBundle(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `nvsim-proof-${Date.now()}.json`; + a.click(); + URL.revokeObjectURL(url); + pushLog('ok', `proof bundle exported · ${blob.size} bytes`); + toast(`Proof bundle saved (${blob.size} B)`, '📦'); + } catch (e) { pushLog('err', `export failed: ${(e as Error).message}`); } + } }, + { ico: '⟳', label: 'Reset pipeline', kbd: '⌘R', run: () => openModal({ + title: 'Reset pipeline?', + body: '

Clears the frame stream and rewinds t to 0.

', + buttons: [ + { label: 'Cancel', variant: 'ghost' }, + { label: 'Reset', variant: 'danger', onClick: async () => { await getClient()?.reset(); pushLog('warn', 'pipeline reset · t=0'); toast('Pipeline reset', '⟳'); } }, + ], + }) }, + { ico: '✓', label: 'Verify witness', run: async () => { + const c = getClient(); if (!c) return; + witnessVerified.value = 'pending'; + const exp = expectedWitness.value; + const eb = new Uint8Array(32); + for (let i = 0; i < 32; i++) eb[i] = parseInt(exp.slice(i * 2, i * 2 + 2), 16); + const r = await c.verifyWitness(eb); + if (r.ok) { witnessVerified.value = 'ok'; witnessHex.value = exp; toast('Witness verified', '✓'); } + else { witnessVerified.value = 'fail'; toast('Witness mismatch!', '✗'); } + } }, + { ico: '☼', label: 'Toggle theme', kbd: '⌘/', run: () => { theme.value = theme.value === 'dark' ? 'light' : 'dark'; } }, + { ico: '⚙', label: 'Open settings', kbd: '⌘,', run: () => window.dispatchEvent(new CustomEvent('open-settings')) }, + { ico: '?', label: 'Keyboard shortcuts…', run: () => openModal({ + title: 'Keyboard shortcuts', + body: `
+
⌘K / Ctrl K
Command palette
+
Space
Play / pause
+
⌘R
Reset
+
⌘,
Settings
+
⌘/
Toggle theme
+
\`
Debug HUD
+
1 · 2 · 3
Inspector tabs
+
Esc
Close modal/palette
+
/
Focus REPL
+
`, + buttons: [{ label: 'Close', variant: 'primary' }], + }) }, + { ico: 'i', label: 'About nvsim…', run: () => openModal({ + title: 'About nvsim', + body: `

nvsim is a deterministic, byte-reproducible forward simulator for nitrogen-vacancy diamond magnetometry.

+

This dashboard runs nvsim as WASM in a Web Worker. Same (scene, config, seed) → byte-identical SHA-256 witness across runs and machines.

+

License: MIT OR Apache-2.0 · See ADR-089, ADR-092.

`, + buttons: [{ label: 'Close', variant: 'primary' }], + }) }, + ]; + + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('keydown', this.onKey); + window.addEventListener('nv-palette', this.onOpen as EventListener); + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('keydown', this.onKey); + window.removeEventListener('nv-palette', this.onOpen as EventListener); + } + + private onKey = (e: KeyboardEvent): void => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { + e.preventDefault(); + this.openPal(); + } else if (e.key === 'Escape' && this.open) { + this.closePal(); + } else if (this.open) { + if (e.key === 'ArrowDown') { this.idx = Math.min(this.cmds.length - 1, this.idx + 1); e.preventDefault(); } + else if (e.key === 'ArrowUp') { this.idx = Math.max(0, this.idx - 1); e.preventDefault(); } + else if (e.key === 'Enter') { this.runIdx(); e.preventDefault(); } + } + }; + + private onOpen = (): void => this.openPal(); + + private openPal(): void { + this.open = true; this.setAttribute('open', ''); + this.filter = ''; this.idx = 0; + setTimeout(() => this.inputEl?.focus(), 0); + } + private closePal(): void { this.open = false; this.removeAttribute('open'); } + + private filtered(): Cmd[] { + if (!this.filter.trim()) return this.cmds; + const q = this.filter.toLowerCase(); + return this.cmds.filter((c) => c.label.toLowerCase().includes(q)); + } + + private runIdx(): void { + const f = this.filtered(); + const c = f[this.idx]; + if (c) { c.run(); this.closePal(); } + } + + override render() { + const items = this.filtered(); + return html` +
+
+ { this.filter = (e.target as HTMLInputElement).value; this.idx = 0; }} /> +
+
+ ${items.map((c, i) => html` +
{ this.idx = i; this.runIdx(); }}> + ${c.ico} + ${c.label} + ${c.kbd ? html`${c.kbd}` : ''} +
+ `)} +
+
+ `; + } +} diff --git a/dashboard/src/components/nv-rail.ts b/dashboard/src/components/nv-rail.ts new file mode 100644 index 000000000..dd7bb8484 --- /dev/null +++ b/dashboard/src/components/nv-rail.ts @@ -0,0 +1,116 @@ +/* Left rail navigation. Emits `navigate` events for view switching. */ +import { LitElement, html, css } from 'lit'; +import { customElement, property } from 'lit/decorators.js'; +import type { View } from './nv-app'; + +@customElement('nv-rail') +export class NvRail extends LitElement { + @property() view: View = 'scene'; + + static styles = css` + :host { + display: flex; + flex-direction: column; + align-items: center; + padding: 10px 0; + gap: 4px; + background: var(--bg-1); + border-right: 1px solid var(--line); + } + .logo { + width: 36px; height: 36px; + border-radius: 10px; + background: linear-gradient(135deg, oklch(0.78 0.14 70) 0%, oklch(0.55 0.16 30) 100%); + display: grid; place-items: center; + color: #1a0f00; + font-weight: 700; + font-family: var(--mono); + font-size: 11px; + margin-bottom: 14px; + box-shadow: 0 4px 12px -2px oklch(0.55 0.16 30 / 0.35); + } + .btn { + width: 36px; height: 36px; + border-radius: 8px; + background: transparent; + border: 1px solid transparent; + color: var(--ink-3); + display: grid; place-items: center; + transition: all 0.15s; + position: relative; + cursor: pointer; + } + .btn:hover { color: var(--ink); background: var(--bg-2); } + .btn.active { + color: var(--ink); + background: var(--bg-3); + border-color: var(--line-2); + } + .btn.active::before { + content: ''; position: absolute; left: -10px; top: 8px; bottom: 8px; + width: 2px; background: var(--accent); border-radius: 2px; + } + .btn.ghost.active::before { background: var(--accent-3); } + .spacer { flex: 1; } + svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; } + `; + + private navigate(v: View): void { + this.dispatchEvent(new CustomEvent('navigate', { detail: v })); + } + + override render() { + return html` + + +
+ + `; + } +} diff --git a/dashboard/src/components/nv-scene.ts b/dashboard/src/components/nv-scene.ts new file mode 100644 index 000000000..e788abfeb --- /dev/null +++ b/dashboard/src/components/nv-scene.ts @@ -0,0 +1,374 @@ +/* Scene canvas — SVG with draggable sources, NV crystal sensor, field lines, mini ODMR. */ +import { LitElement, html, css, svg } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { lastB, bMag, fps, snr, motionReduced, running, getClient, speed, pushLog, lastFrame, scenePositions } from '../store/appStore'; + +interface SceneItem { id: string; x: number; y: number; color: string; name: string; } + +@customElement('nv-scene') +export class NvScene extends LitElement { + @state() private zoom = 1.0; + @state() private layerVisible = { source: true, field: true, label: true }; + @state() private items: SceneItem[] = [ + { id: 'rebar', x: 740, y: 240, color: 'oklch(0.72 0.18 330)', name: 'rebar.steel' }, + { id: 'heart', x: 220, y: 180, color: 'oklch(0.78 0.14 195)', name: 'heart_proxy' }, + { id: 'mains', x: 180, y: 380, color: 'oklch(0.72 0.18 330)', name: 'mains_60Hz' }, + { id: 'door', x: 800, y: 470, color: 'oklch(0.78 0.14 145)', name: 'door.steel' }, + ]; + @state() private dragging: string | null = null; + @state() private selected: string | null = null; + private dragOffset = { dx: 0, dy: 0 }; + + static styles = css` + :host { + display: block; height: 100%; width: 100%; + background: radial-gradient(ellipse at 50% 30%, var(--bg-2) 0%, var(--bg-0) 70%); + position: relative; overflow: hidden; + border-bottom: 1px solid var(--line); + } + .grid { + position: absolute; inset: 0; + background-image: + linear-gradient(var(--grid) 1px, transparent 1px), + linear-gradient(90deg, var(--grid) 1px, transparent 1px); + background-size: 32px 32px; + pointer-events: none; + mask-image: radial-gradient(ellipse at center, black 40%, transparent 100%); + } + svg { position: absolute; inset: 0; width: 100%; height: 100%; } + .stat-card { + background: rgba(13,17,23,0.7); + backdrop-filter: blur(8px); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 8px 12px; + font-size: 11px; + min-width: 96px; + } + [data-theme="light"] .stat-card { background: rgba(255,255,255,0.85); } + .stat-card .lbl { + color: var(--ink-3); + text-transform: uppercase; font-weight: 600; letter-spacing: 0.06em; font-size: 9.5px; + } + .stat-card .val { font-family: var(--mono); font-size: 16px; font-weight: 600; margin-top: 2px; } + .stat-card .val.amber { color: var(--accent); } + .stat-card .val.cyan { color: var(--accent-2); } + .stat-card .val.mint { color: var(--accent-4); } + .scene-readout { + position: absolute; top: 14px; right: 14px; + display: flex; gap: 8px; z-index: 5; + } + .draggable { cursor: grab; transition: filter 0.15s; } + .draggable:hover { filter: brightness(1.15) drop-shadow(0 0 6px currentColor); } + .draggable.dragging { cursor: grabbing; filter: brightness(1.25) drop-shadow(0 0 10px currentColor); } + .field-line { stroke-dasharray: 4 6; } + @keyframes dash { to { stroke-dashoffset: -200; } } + .field-line.anim { animation: dash 4s linear infinite; } + @keyframes spin { + 0% { transform: rotateY(0) rotateX(8deg); } + 100% { transform: rotateY(360deg) rotateX(8deg); } + } + .crystal { transform-origin: center; transform-box: fill-box; } + .crystal.anim { animation: spin 12s linear infinite; } + .label { + font-family: var(--mono); font-size: 11px; fill: var(--ink-2); + pointer-events: none; + } + .scene-toolbar { + position: absolute; top: 14px; left: 14px; + display: flex; gap: 6px; z-index: 5; + background: rgba(13,17,23,0.85); + backdrop-filter: blur(8px); + border: 1px solid var(--line); + border-radius: 8px; + padding: 4px; + } + [data-theme="light"] .scene-toolbar { background: rgba(255,255,255,0.85); } + .scene-toolbar button { + width: 28px; height: 28px; + background: transparent; + border: 1px solid transparent; + border-radius: 6px; + color: var(--ink-2); + cursor: pointer; + display: grid; place-items: center; + font-size: 13px; + } + .scene-toolbar button:hover { color: var(--ink); background: var(--bg-2); } + .scene-toolbar button.on { background: var(--bg-3); color: var(--accent); border-color: var(--line-2); } + + .sim-controls { + position: absolute; bottom: 14px; right: 14px; + display: flex; gap: 6px; align-items: center; + background: rgba(13,17,23,0.85); + backdrop-filter: blur(12px); + border: 1px solid var(--line-2); + border-radius: 999px; + padding: 6px 10px; + z-index: 5; + } + [data-theme="light"] .sim-controls { background: rgba(255,255,255,0.92); } + .sim-controls .play { + width: 32px; height: 32px; + background: var(--accent); + border: none; + border-radius: 50%; + color: #1a0f00; + cursor: pointer; + display: grid; place-items: center; + font-size: 13px; + } + .sim-controls .play:hover { filter: brightness(1.08); } + .sim-controls .step { + width: 26px; height: 26px; + border-radius: 6px; + background: transparent; + color: var(--ink-2); + border: 1px solid var(--line); + cursor: pointer; + font-size: 11px; + } + .sim-controls .step:hover { color: var(--ink); border-color: var(--line-2); } + .sim-controls .speed { + font-family: var(--mono); font-size: 11px; + color: var(--ink-2); + padding: 0 6px; + min-width: 36px; + text-align: center; + cursor: pointer; + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + // Restore drag positions if any are persisted. + if (scenePositions.value.length > 0) { + this.items = this.items.map((it) => { + const saved = scenePositions.value.find((p) => p.id === it.id); + return saved ? { ...it, x: saved.x, y: saved.y } : it; + }); + } + effect(() => { + lastB.value; bMag.value; fps.value; snr.value; motionReduced.value; + running.value; speed.value; lastFrame.value; + this.requestUpdate(); + }); + // Compute SNR from the last frame: |B_pT| / max(σ_pT[k]) per ADR-093 P1.4. + effect(() => { + const f = lastFrame.value; + if (!f) return; + const bmag = Math.sqrt(f.bPt[0] ** 2 + f.bPt[1] ** 2 + f.bPt[2] ** 2); + const sigmaMax = Math.max(Math.abs(f.sigmaPt[0]), Math.abs(f.sigmaPt[1]), Math.abs(f.sigmaPt[2]), 0.001); + const snrVal = bmag / sigmaMax; + if (Number.isFinite(snrVal)) snr.value = snrVal; + }); + window.addEventListener('pointermove', this.onPointerMove); + window.addEventListener('pointerup', this.onPointerUp); + window.addEventListener('keydown', this.onKey); + } + + /** Tab cycles selection; arrow keys nudge by 8 px (32 px with Shift); + * Esc deselects. ADR-093 P2.6. */ + private onKey = (e: KeyboardEvent): void => { + const target = e.target as HTMLElement | null; + if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA')) return; + if (!this.selected) { + if (e.key === 'Tab' && document.activeElement === document.body) { + e.preventDefault(); + this.selected = this.items[0]?.id ?? null; + } + return; + } + if (e.key === 'ArrowLeft' || e.key === 'ArrowRight' || e.key === 'ArrowUp' || e.key === 'ArrowDown') { + e.preventDefault(); + const step = e.shiftKey ? 32 : 8; + const dx = e.key === 'ArrowLeft' ? -step : e.key === 'ArrowRight' ? step : 0; + const dy = e.key === 'ArrowUp' ? -step : e.key === 'ArrowDown' ? step : 0; + this.items = this.items.map((it) => + it.id === this.selected + ? { ...it, x: Math.max(20, Math.min(980, it.x + dx)), y: Math.max(20, Math.min(580, it.y + dy)) } + : it, + ); + scenePositions.value = this.items.map(({ id, x, y }) => ({ id, x, y })); + } else if (e.key === 'Tab') { + e.preventDefault(); + const idx = this.items.findIndex((it) => it.id === this.selected); + const next = (idx + (e.shiftKey ? -1 : 1) + this.items.length) % this.items.length; + this.selected = this.items[next].id; + } else if (e.key === 'Escape') { + this.selected = null; + } + }; + + private async toggleRun(): Promise { + const c = getClient(); if (!c) return; + if (running.value) { await c.pause(); running.value = false; } + else { await c.run(); running.value = true; } + } + private async stepFwd(): Promise { + const c = getClient(); if (!c) return; + await c.step('fwd', 10); + pushLog('dbg', 'sim step → +1 frame'); + } + private async stepBack(): Promise { + const c = getClient(); if (!c) return; + await c.step('back', 10); + pushLog('dbg', 'sim step ← -1 frame'); + } + private cycleSpeed(): void { + const speeds = [0.25, 0.5, 1.0, 2.0, 4.0]; + const idx = speeds.indexOf(speed.value); + speed.value = speeds[(idx + 1) % speeds.length]; + } + private zoomIn(): void { this.zoom = Math.min(2.5, this.zoom * 1.2); } + private zoomOut(): void { this.zoom = Math.max(0.5, this.zoom / 1.2); } + private fitView(): void { this.zoom = 1.0; } + private toggleLayer(k: 'source' | 'field' | 'label'): void { + this.layerVisible = { ...this.layerVisible, [k]: !this.layerVisible[k] }; + } + + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('pointermove', this.onPointerMove); + window.removeEventListener('pointerup', this.onPointerUp); + window.removeEventListener('keydown', this.onKey); + } + + private onDown = (id: string, e: PointerEvent): void => { + e.preventDefault(); + this.dragging = id; + this.selected = id; + const item = this.items.find((i) => i.id === id); + if (!item) return; + const svgEl = this.renderRoot.querySelector('svg') as SVGSVGElement | null; + if (!svgEl) return; + const pt = this.toSvg(e, svgEl); + this.dragOffset = { dx: pt.x - item.x, dy: pt.y - item.y }; + }; + + private onPointerMove = (e: PointerEvent): void => { + if (!this.dragging) return; + const svgEl = this.renderRoot.querySelector('svg') as SVGSVGElement | null; + if (!svgEl) return; + const pt = this.toSvg(e, svgEl); + this.items = this.items.map((it) => + it.id === this.dragging + ? { ...it, x: pt.x - this.dragOffset.dx, y: pt.y - this.dragOffset.dy } + : it, + ); + }; + + private onPointerUp = (): void => { + if (this.dragging) { + // Persist all positions on drop. + scenePositions.value = this.items.map(({ id, x, y }) => ({ id, x, y })); + } + this.dragging = null; + }; + + private toSvg(e: PointerEvent, svgEl: SVGSVGElement): { x: number; y: number } { + const r = svgEl.getBoundingClientRect(); + const vbX = ((e.clientX - r.left) / r.width) * 1000; + const vbY = ((e.clientY - r.top) / r.height) * 600; + return { x: vbX, y: vbY }; + } + + override render() { + const b = lastB.value; + const bnT = [b[0] * 1e9, b[1] * 1e9, b[2] * 1e9]; + const bMagNT = bMag.value * 1e9; + const animClass = motionReduced.value ? '' : 'anim'; + + const vbW = 1000 / this.zoom; + const vbH = 600 / this.zoom; + const vbX = (1000 - vbW) / 2; + const vbY = (600 - vbH) / 2; + + return html` +
+ + + + + + + + + + + ${this.layerVisible.field ? this.items.map((it) => svg` + + `) : ''} + + + ${this.layerVisible.source ? this.items.map((it) => svg` + this.onDown(it.id, e)}> + + + ${this.layerVisible.label ? svg`${it.name}` : ''} + + `) : ''} + + + + + + + + + + sensor · 〈111〉 NV + + + B_in: [${bnT[0].toFixed(2)}, ${bnT[1].toFixed(2)}, ${bnT[2].toFixed(2)}] nT + + + + +
+ + + + + + +
+ +
+ + + + ${speed.value}× +
+ +
+
+
|B|
+
${bMagNT.toFixed(3)} nT
+
+
+
FPS
+
${fps.value > 0 ? Math.round(fps.value) : '—'}
+
+
+
SNR
+
${snr.value > 0 ? snr.value.toFixed(1) : '—'}
+
+
+ `; + } +} diff --git a/dashboard/src/components/nv-settings-drawer.ts b/dashboard/src/components/nv-settings-drawer.ts new file mode 100644 index 000000000..3efd907af --- /dev/null +++ b/dashboard/src/components/nv-settings-drawer.ts @@ -0,0 +1,261 @@ +/* Settings drawer — theme / density / motion / auto-update. */ +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { theme, density, motionReduced, autoUpdate, transport, wsUrl } from '../store/appStore'; + +@customElement('nv-settings-drawer') +export class NvSettingsDrawer extends LitElement { + @state() private open = false; + + static styles = css` + :host { + position: fixed; top: 0; right: 0; bottom: 0; + width: 420px; max-width: 100vw; + background: var(--bg-1); + border-left: 1px solid var(--line); + z-index: 51; + transform: translateX(100%); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + display: flex; flex-direction: column; + box-shadow: -20px 0 60px -20px rgba(0,0,0,0.5); + } + :host([open]) { transform: translateX(0); } + .scrim { + position: fixed; inset: 0; + background: rgba(0,0,0,0.5); + z-index: 50; + opacity: 0; pointer-events: none; + transition: opacity 0.2s; + } + :host([open]) .scrim { opacity: 1; pointer-events: auto; } + .h { + padding: 14px 16px; + border-bottom: 1px solid var(--line); + display: flex; align-items: center; justify-content: space-between; + } + .h .ttl { font-size: 14px; font-weight: 600; } + .body { flex: 1; overflow-y: auto; padding: 16px; } + .group { margin-bottom: 22px; } + .group h4 { + margin: 0 0 10px; + font-size: 11px; font-weight: 600; + text-transform: uppercase; letter-spacing: 0.08em; + color: var(--ink-3); + } + .row { + display: flex; justify-content: space-between; align-items: center; + padding: 10px 0; + border-bottom: 1px solid var(--line); + } + .row:last-child { border-bottom: 0; } + .row .lbl { font-size: 13px; } + .row .desc { font-size: 11.5px; color: var(--ink-3); margin-top: 2px; } + .row > div:first-child { flex: 1; padding-right: 12px; } + .seg { + display: inline-flex; + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: var(--radius-sm); + padding: 2px; + } + .seg button { + padding: 4px 10px; + background: transparent; border: none; + border-radius: 6px; + font-size: 11.5px; color: var(--ink-3); + font-family: var(--mono); + cursor: pointer; + } + .seg button.on { background: var(--bg-1); color: var(--ink); } + .toggle { + position: relative; + width: 36px; height: 20px; + background: var(--bg-3); + border: 1px solid var(--line-2); + border-radius: 999px; + cursor: pointer; + flex-shrink: 0; + } + .toggle::after { + content: ''; position: absolute; + top: 2px; left: 2px; + width: 14px; height: 14px; + background: var(--ink-3); + border-radius: 50%; + transition: transform 0.15s, background 0.15s; + } + .toggle.on { background: var(--accent); border-color: var(--accent); } + .toggle.on::after { background: #1a0f00; transform: translateX(16px); } + .close { + width: 28px; height: 28px; + background: transparent; border: 1px solid var(--line); + border-radius: 6px; + color: var(--ink-2); + } + input[type="text"] { + background: var(--bg-3); + border: 1px solid var(--line); + border-radius: 6px; + padding: 6px 10px; + color: var(--ink); font-family: var(--mono); font-size: 12px; + outline: none; + } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { theme.value; density.value; motionReduced.value; autoUpdate.value; transport.value; wsUrl.value; this.requestUpdate(); }); + window.addEventListener('open-settings', () => { this.open = true; this.setAttribute('open', ''); }); + } + + private close(): void { this.open = false; this.removeAttribute('open'); } + + private async resetPrefs(): Promise { + if (!confirm('Reset all preferences and IndexedDB state? Reloads the page.')) return; + try { + const dbs = await indexedDB.databases?.(); + if (dbs) for (const d of dbs) if (d.name) indexedDB.deleteDatabase(d.name); + } catch { /* noop */ } + location.reload(); + } + + override render() { + return html` +
this.close()}>
+
+
Settings
+ +
+
+
+

Appearance

+
+
+
Theme
+
Dark is the default; light has higher contrast for daylight work.
+
+
+ + +
+
+
+
+
Density
+
Affects panel padding and font scale (15 / 14 / 13 px). Choose what your eyes prefer.
+
+
+ + + +
+
+
+
+
Reduce motion
+
Stops the rotating diamond, animated field lines, and chart easing. Auto-on if your system has the prefers-reduced-motion preference set.
+
+ motionReduced.value = !motionReduced.value}> +
+
+ +
+

Pipeline

+
+
+
Auto-rerun on edit
+
When you change a Tunables slider or load a new scene, push the change to the worker without a manual restart.
+
+ autoUpdate.value = !autoUpdate.value}> +
+
+ +
+

Transport

+
+
+
Mode
+
WASM runs nvsim in your browser (default, no server). WS connects to a host-supplied nvsim-server (REST + binary WebSocket); see ADR-092 §6.2.
+
+
+ + +
+
+ ${transport.value === 'ws' ? html` +
+
+
WS URL
+
Where your nvsim-server is listening. The server defaults to 127.0.0.1:7878.
+
+ wsUrl.value = (e.target as HTMLInputElement).value} /> +
` : ''} +
+ +
+

Help

+
+
+
Open help center
+
Quickstart, glossary, FAQ, and shortcuts. Press ? any time.
+
+ +
+
+
+
Replay welcome tour
+
Re-show the 6-step first-run walkthrough.
+
+ +
+
+
+
Reset all preferences
+
Wipe theme, density, motion, scene drag positions, REPL history, and the onboarding-seen flag.
+
+ +
+
+ +
+

About

+ +
+
+ `; + } +} diff --git a/dashboard/src/components/nv-sidebar.ts b/dashboard/src/components/nv-sidebar.ts new file mode 100644 index 000000000..b2f80e499 --- /dev/null +++ b/dashboard/src/components/nv-sidebar.ts @@ -0,0 +1,222 @@ +/* Sidebar — Scene panel, NV sensor panel, Tunables, Pipeline diagram. */ +import { LitElement, html, css } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { fs, fmod, dtMs, noiseEnabled, running, getClient, pushLog } from '../store/appStore'; + +let configPushTimer: number | null = null; +function pushConfigDebounced(): void { + if (configPushTimer !== null) window.clearTimeout(configPushTimer); + configPushTimer = window.setTimeout(async () => { + const c = getClient(); + if (!c) return; + try { + await c.setConfig({ + digitiser: { f_s_hz: fs.value, f_mod_hz: fmod.value }, + sensor: { + gamma_fwhm_hz: 1.0e6, + t1_s: 5.0e-3, + t2_s: 1.0e-6, + t2_star_s: 200e-9, + contrast: 0.03, + n_spins: 1.0e12, + shot_noise_disabled: !noiseEnabled.value, + }, + dt_s: dtMs.value * 1e-3, + }); + pushLog('dbg', `config pushed · fs=${fs.value} f_mod=${fmod.value} dt=${dtMs.value.toFixed(1)}ms noise=${noiseEnabled.value ? 'on' : 'off'}`); + } catch (e) { + pushLog('warn', `config push failed: ${(e as Error).message}`); + } + }, 300); +} + +@customElement('nv-sidebar') +export class NvSidebar extends LitElement { + static styles = css` + :host { + display: flex; flex-direction: column; gap: 14px; + padding: 14px; overflow-y: auto; + background: var(--bg-1); border-right: 1px solid var(--line); + } + .panel { + background: var(--bg-2); border: 1px solid var(--line); + border-radius: var(--radius); padding: 12px; + } + .panel-h { + display: flex; align-items: center; justify-content: space-between; + font-size: 11px; font-weight: 600; color: var(--ink-3); + text-transform: uppercase; letter-spacing: 0.08em; + margin-bottom: 6px; + } + .panel-help { + font-size: 11.5px; color: var(--ink-3); + margin: 0 0 10px; + line-height: 1.5; + } + .help-link { + color: var(--accent-2); + cursor: pointer; + text-decoration: underline dotted; + } + .help-link:hover { color: var(--accent); } + .count { + background: var(--bg-3); color: var(--ink-2); + padding: 1px 6px; border-radius: 999px; + font-family: var(--mono); font-size: 10px; + text-transform: none; letter-spacing: 0; + } + .scene-item { + display: flex; align-items: center; gap: 10px; + padding: 8px 10px; + border-radius: var(--radius-sm); + cursor: pointer; + transition: background 0.15s; + border: 1px solid transparent; + } + .scene-item:hover { background: var(--bg-3); } + .scene-item .swatch { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; } + .scene-item .name { font-size: 13px; flex: 1; } + .scene-item .meta { font-family: var(--mono); font-size: 10.5px; color: var(--ink-3); } + .field-row { + display: flex; align-items: center; justify-content: space-between; + padding: 6px 0; font-size: 12.5px; + border-bottom: 1px solid var(--line); + } + .field-row:last-child { border-bottom: 0; } + .field-row .lbl { color: var(--ink-3); } + .field-row .val { font-family: var(--mono); color: var(--ink); font-size: 12px; } + .slider-row { padding: 8px 0; border-bottom: 1px solid var(--line); } + .slider-row:last-child { border-bottom: 0; padding-bottom: 0; } + .slider-row .top { display: flex; justify-content: space-between; margin-bottom: 6px; font-size: 12px; } + .slider-row .top .lbl { color: var(--ink-3); } + .slider-row .top .val { font-family: var(--mono); color: var(--ink); } + input[type="range"] { + -webkit-appearance: none; appearance: none; + width: 100%; height: 4px; + background: var(--bg-3); border-radius: 2px; outline: none; + } + input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; appearance: none; + width: 14px; height: 14px; border-radius: 50%; + background: var(--accent); cursor: pointer; + border: 2px solid var(--bg-2); + box-shadow: 0 0 0 1px var(--line-2); + } + .pipeline { display: flex; gap: 4px; align-items: center; flex-wrap: wrap; margin-top: 6px; } + .stage { + flex: 1; min-width: 50px; + padding: 4px 6px; + background: var(--bg-3); border: 1px solid var(--line); + border-radius: 6px; font-size: 9.5px; text-align: center; + color: var(--ink-2); font-family: var(--mono); + } + .stage.live { border-color: var(--accent-2); color: var(--accent-2); } + .stage-arrow { color: var(--ink-4); font-size: 10px; } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { fs.value; fmod.value; dtMs.value; noiseEnabled.value; running.value; this.requestUpdate(); }); + } + + override render() { + return html` +
+
Scene 4 sources
+
+ Magnetic primitives in the simulated environment. Drag any in the + canvas to reposition; positions persist across reloads. +
+
+ + rebar.steel.coil + χ=5000 +
+
+ + heart_proxy + 1e-6 A·m² +
+
+ + mains_60Hz + 2 A · 60 Hz +
+
+ + door.steel + eddy +
+
+ +
+
NV sensor COTS
+
+ Element Six DNV-B1 reference: 1 mm³ diamond, ~10¹² NV centers. + Floor δB ≈ 1.18 pT/√Hz per Barry 2020 §III.A. + window.dispatchEvent(new CustomEvent('nv-show-help', { detail: { section: 'glossary' } }))}>What's NV? +
+
V1 mm³
+
N1e12 NV
+
C0.030
+
T₂*200 ns
+
δB1.18 pT/√Hz
+
+ +
+
Tunables
+
+ Live pipeline parameters. Edits debounce 300 ms then rebuild the + WASM pipeline without restarting the frame stream. +
+
+
Sample rate${(fs.value / 1000).toFixed(1)} kHz
+ { fs.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} /> +
+
+
Lockin f_mod${(fmod.value / 1000).toFixed(3)} kHz
+ { fmod.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} /> +
+
+
Integration t${dtMs.value.toFixed(1)} ms
+ { dtMs.value = +(e.target as HTMLInputElement).value; pushConfigDebounced(); }} /> +
+
+
Shot noise${noiseEnabled.value ? 'ON' : 'OFF'}
+ { noiseEnabled.value = (e.target as HTMLInputElement).value === '1'; pushConfigDebounced(); }} /> +
+
+ +
+
Pipeline
+
+ Forward simulator stages, left to right. Stages glow cyan while + the pipeline is running. +
+
+ scene + + B-S + + prop + + NV + + ADC + + frame +
+
+ `; + } +} diff --git a/dashboard/src/components/nv-toast.ts b/dashboard/src/components/nv-toast.ts new file mode 100644 index 000000000..7a9a43805 --- /dev/null +++ b/dashboard/src/components/nv-toast.ts @@ -0,0 +1,64 @@ +/* Toast notification — shown briefly via window.dispatchEvent('nv-toast', detail). */ +import { LitElement, html, css } from 'lit'; +import { customElement, state } from 'lit/decorators.js'; + +@customElement('nv-toast') +export class NvToast extends LitElement { + @state() private visible = false; + @state() private msg = ''; + @state() private icon = '✓'; + private timer: number | null = null; + + static styles = css` + :host { + position: fixed; bottom: 24px; left: 50%; + transform: translateX(-50%) translateY(80px); + background: var(--bg-2); + border: 1px solid var(--line-2); + border-radius: var(--radius); + padding: 10px 14px; + font-size: 12.5px; + box-shadow: var(--shadow); + z-index: 100; + opacity: 0; pointer-events: none; + transition: opacity 0.2s, transform 0.2s; + display: flex; align-items: center; gap: 8px; + } + :host([visible]) { + opacity: 1; + transform: translateX(-50%) translateY(0); + pointer-events: auto; + } + .icon { color: var(--accent); } + `; + + override connectedCallback(): void { + super.connectedCallback(); + window.addEventListener('nv-toast', this.onToast as EventListener); + } + override disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener('nv-toast', this.onToast as EventListener); + } + + private onToast = (e: Event): void => { + const detail = (e as CustomEvent).detail as { msg?: string; icon?: string }; + this.msg = detail.msg ?? 'Done'; + this.icon = detail.icon ?? '✓'; + this.visible = true; + this.setAttribute('visible', ''); + if (this.timer !== null) window.clearTimeout(this.timer); + this.timer = window.setTimeout(() => { + this.visible = false; + this.removeAttribute('visible'); + }, 1800); + }; + + override render() { + return html`${this.icon}${this.msg}`; + } +} + +export function toast(msg: string, icon = '✓'): void { + window.dispatchEvent(new CustomEvent('nv-toast', { detail: { msg, icon } })); +} diff --git a/dashboard/src/components/nv-topbar.ts b/dashboard/src/components/nv-topbar.ts new file mode 100644 index 000000000..a56ee3bca --- /dev/null +++ b/dashboard/src/components/nv-topbar.ts @@ -0,0 +1,139 @@ +/* Topbar — breadcrumbs, transport pill, FPS pill, seed pill, controls. */ +import { LitElement, html, css } from 'lit'; +import { customElement } from 'lit/decorators.js'; +import { effect } from '@preact/signals-core'; +import { + fps, transportLabel, seed, theme, sceneName, + running, getClient, pushLog, +} from '../store/appStore'; +import { openModal } from './nv-modal'; +import { toast } from './nv-toast'; + +@customElement('nv-topbar') +export class NvTopbar extends LitElement { + static styles = css` + :host { + display: flex; align-items: center; + padding: 0 16px; gap: 12px; + background: var(--bg-1); + border-bottom: 1px solid var(--line); + z-index: 10; + } + .crumbs { display: flex; align-items: center; gap: 8px; font-size: 12.5px; color: var(--ink-3); } + .crumbs .sep { color: var(--ink-4); } + .crumbs .cur { color: var(--ink); font-weight: 500; } + .spacer { flex: 1; } + .pill { + display: inline-flex; align-items: center; gap: 6px; + padding: 5px 10px; + background: var(--bg-2); border: 1px solid var(--line); + border-radius: 999px; + font-size: 12px; color: var(--ink-2); + font-family: var(--mono); font-weight: 500; + } + .pill .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ok); box-shadow: 0 0 6px var(--ok); animation: pulse 2s infinite; } + .pill.wasm .dot { background: var(--accent-2); box-shadow: 0 0 6px var(--accent-2); } + .pill.seed { color: var(--ink-3); cursor: pointer; } + .pill.seed:hover { border-color: var(--line-2); } + .pill.seed b { color: var(--accent); font-weight: 600; } + .pill.wasm { cursor: pointer; } + .pill.wasm:hover { border-color: var(--line-2); } + button { + display: inline-flex; align-items: center; gap: 6px; + padding: 6px 12px; + background: var(--bg-2); border: 1px solid var(--line); + border-radius: 8px; + font-size: 12.5px; font-weight: 500; color: var(--ink); + cursor: pointer; + transition: all 0.15s; + } + button:hover { border-color: var(--line-2); background: var(--bg-3); } + button.primary { background: var(--accent); border-color: var(--accent); color: #1a0f00; } + button.primary:hover { filter: brightness(1.08); } + button.ghost { background: transparent; } + `; + + override connectedCallback(): void { + super.connectedCallback(); + effect(() => { fps.value; transportLabel.value; seed.value; theme.value; sceneName.value; running.value; this.requestUpdate(); }); + } + + private async toggleRun(): Promise { + const c = getClient(); if (!c) return; + if (running.value) { await c.pause(); running.value = false; } + else { await c.run(); running.value = true; } + } + private async reset(): Promise { + const c = getClient(); if (!c) return; + await c.reset(); + } + private toggleTheme(): void { + theme.value = theme.value === 'dark' ? 'light' : 'dark'; + } + private async openSeedModal(): Promise { + const cur = `0x${seed.value.toString(16).toUpperCase().padStart(8, '0')}`; + openModal({ + title: 'Set seed', + body: `

Set the 32-bit hex seed for the shot-noise PRNG. Same (scene, config, seed) → byte-identical witness.

+ + `, + buttons: [ + { label: 'Cancel', variant: 'ghost' }, + { label: 'Apply', variant: 'primary', onClick: async () => { + const inp = document.querySelector('nv-modal')?.shadowRoot?.querySelector('#seed-input'); + if (!inp) return; + const raw = inp.value.trim().replace(/^0x/i, ''); + const v = BigInt('0x' + raw); + seed.value = v; + await getClient()?.setSeed(v); + pushLog('ok', `seed → 0x${v.toString(16).toUpperCase()}`); + toast(`Seed → 0x${v.toString(16).toUpperCase().slice(0, 8)}`, '⟳'); + } }, + ], + }); + } + private openTransportSettings(): void { + window.dispatchEvent(new CustomEvent('open-settings')); + } + + override render() { + const seedHex = seed.value.toString(16).toUpperCase().padStart(8, '0'); + return html` +
+ RuView/ + nvsim/ + ${sceneName.value} +
+
+ + + ${fps.value > 0 ? (fps.value / 1000).toFixed(2) + ' kHz' : 'idle'} + + + ${transportLabel.value} + + + seed: 0x${seedHex} + + + + + + + `; + } +} diff --git a/dashboard/src/main.ts b/dashboard/src/main.ts new file mode 100644 index 000000000..eb4137615 --- /dev/null +++ b/dashboard/src/main.ts @@ -0,0 +1,200 @@ +/* nvsim dashboard entry — boots the WasmClient, mounts . */ +import './app.css'; +import './components/nv-app'; +import { effect } from '@preact/signals-core'; + +import { WasmClient } from './transport/WasmClient'; +import { WsClient } from './transport/WsClient'; +import type { NvsimClient, MagFrameBatch } from './transport/NvsimClient'; +import { + setClient, transport, wsUrl, connected, transportError, + theme, density, motionReduced, + pushLog, expectedWitness, framesEmitted, fps, lastB, bMag, + pushTrace, pushStripBar, lastFrame, sceneJson, witnessHex, + replHistory, scenePositions, type SceneItemPos, + activeAppIds, pushAppEvent, +} from './store/appStore'; +import { APP_RUNTIMES, type AppRuntimeContext } from './store/appRuntimes'; +import { kvGet, kvSet } from './store/persistence'; + +function applyTheme(t: string): void { + document.documentElement.setAttribute('data-theme', t); +} +function applyDensity(d: string): void { + document.body.classList.remove('density-comfy', 'density-default', 'density-compact'); + document.body.classList.add(`density-${d}`); +} +function applyMotion(reduced: boolean): void { + document.body.classList.toggle('reduce-motion', reduced); +} + +(async () => { + // Restore persisted prefs + const t = (await kvGet<'dark' | 'light'>('theme')) ?? 'dark'; + const d = (await kvGet<'comfy' | 'default' | 'compact'>('density')) ?? 'default'; + const sysMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false; + const m = (await kvGet('motionReduced')) ?? sysMotion; + theme.value = t; applyTheme(t); + density.value = d; applyDensity(d); + motionReduced.value = m; applyMotion(m); + + // React to changes → persist + effect(() => { applyTheme(theme.value); kvSet('theme', theme.value); }); + effect(() => { applyDensity(density.value); kvSet('density', density.value); }); + effect(() => { applyMotion(motionReduced.value); kvSet('motionReduced', motionReduced.value); }); + + // REPL history + scene drag positions persistence (P0.10, P1.7) + const histSaved = await kvGet('repl-history'); + if (histSaved && Array.isArray(histSaved)) replHistory.value = histSaved; + effect(() => { void kvSet('repl-history', replHistory.value); }); + const positionsSaved = await kvGet('scene-positions'); + if (positionsSaved && Array.isArray(positionsSaved)) scenePositions.value = positionsSaved; + effect(() => { void kvSet('scene-positions', scenePositions.value); }); + + // Restore WS URL preference + transport mode + const savedWsUrl = (await kvGet('wsUrl')) ?? ''; + if (savedWsUrl) wsUrl.value = savedWsUrl; + const savedTransport = (await kvGet<'wasm' | 'ws'>('transport')) ?? 'wasm'; + transport.value = savedTransport; + effect(() => { void kvSet('wsUrl', wsUrl.value); }); + effect(() => { void kvSet('transport', transport.value); }); + + // Per-app runtime scratch state + history buffer (defined first so the + // onFrames callback can close over them). + const appState: Record> = {}; + const bMagHistory: number[] = []; + const runtimeStartTs = performance.now(); + + const onFrames = (batch: MagFrameBatch): void => { + if (batch.frames.length === 0) return; + const last = batch.frames[batch.frames.length - 1]; + lastFrame.value = last; + const bx = last.bPt[0] * 1e-12; + const by = last.bPt[1] * 1e-12; + const bz = last.bPt[2] * 1e-12; + lastB.value = [bx, by, bz]; + const bmagT = Math.sqrt(bx * bx + by * by + bz * bz); + bMag.value = bmagT; + pushTrace([bx * 1e9, by * 1e9, bz * 1e9]); + pushStripBar(Math.min(1, Math.abs(bz * 1e9) / 5 + 0.3)); + bMagHistory.push(bmagT); + while (bMagHistory.length > 256) bMagHistory.shift(); + + const activeIds = activeAppIds.value; + if (activeIds.size === 0) return; + const elapsedS = (performance.now() - runtimeStartTs) / 1000; + for (const id of activeIds) { + const fn = APP_RUNTIMES[id]; + if (!fn) continue; + if (!appState[id]) appState[id] = {}; + const ctx: AppRuntimeContext = { + frame: last, + bMagT: bmagT, + bRecoveredT: [bx, by, bz], + bHistory: bMagHistory, + elapsedS, + state: appState[id], + }; + try { + const result = fn(ctx); + if (!result) continue; + const evs = Array.isArray(result) ? result : [result]; + for (const ev of evs) { + pushAppEvent(ev); + pushLog('info', + `[${ev.appId}] ${ev.eventName} (${ev.eventId})${ev.detail ? ' · ' + ev.detail : ''}`); + } + } catch (e) { + pushLog('warn', `[${id}] runtime error: ${(e as Error).message}`); + } + } + }; + + // Boot transport (WASM by default, WS if user previously selected it) + let activeClient: NvsimClient | null = null; + async function bootTransport(): Promise { + try { + if (activeClient) await activeClient.close(); + const want = transport.value; + if (want === 'ws' && wsUrl.value.trim()) { + const c = new WsClient(wsUrl.value.trim()); + const info = await c.boot(); + activeClient = c; + connected.value = true; + transportError.value = null; + expectedWitness.value = info.expectedWitnessHex; + wireClient(c); + pushLog('ok', `transport WS · ${wsUrl.value} · nvsim@${info.buildVersion}`); + } else { + if (want === 'ws') { + pushLog('warn', 'WS transport selected but no URL set — falling back to WASM'); + } + const c = new WasmClient(); + const info = await c.boot(); + activeClient = c; + connected.value = true; + transportError.value = null; + expectedWitness.value = info.expectedWitnessHex; + wireClient(c); + pushLog('ok', `transport WASM · nvsim@${info.buildVersion} · magic=0x${info.frameMagic.toString(16).toUpperCase()}`); + } + setClient(activeClient); + } catch (e) { + const msg = (e as Error).message; + transportError.value = msg; + connected.value = false; + pushLog('err', `transport boot failed: ${msg}`); + } + } + function wireClient(c: NvsimClient): void { + c.onEvent((ev) => { + if (ev.type === 'log') pushLog(ev.level, ev.msg); + if (ev.type === 'fps') fps.value = ev.value; + if (ev.type === 'state') framesEmitted.value = BigInt(ev.framesEmitted); + }); + c.onFrames(onFrames); + } + + // React to transport-mode flips: tear down + re-boot. + let bootInProgress = false; + effect(() => { + transport.value; wsUrl.value; + if (bootInProgress) return; + bootInProgress = true; + void bootTransport().finally(() => { bootInProgress = false; }); + }); + + pushLog('info', 'nvsim — booting transport'); + + // Initial boot — handled by the effect() above. + // Auto-verify witness whenever a fresh transport boot completes. + let verifiedFor: string | null = null; + effect(() => { + const exp = expectedWitness.value; + const isConn = connected.value; + if (!exp || !isConn) return; + if (verifiedFor === exp) return; + verifiedFor = exp; + void (async () => { + const c = activeClient; + if (!c) return; + try { + const expBytes = new Uint8Array(32); + for (let i = 0; i < 32; i++) expBytes[i] = parseInt(exp.slice(i * 2, i * 2 + 2), 16); + const r = await c.verifyWitness(expBytes); + if (r.ok) { + witnessHex.value = exp; + pushLog('ok', `witness verified · determinism gate ✓ · transport=${transport.value}`); + } else { + const actual = Array.from(r.actual).map((b) => b.toString(16).padStart(2, '0')).join(''); + witnessHex.value = actual; + pushLog('err', `WITNESS MISMATCH · expected ${exp.slice(0, 16)}… got ${actual.slice(0, 16)}…`); + } + } catch (e) { + pushLog('warn', `witness verify skipped: ${(e as Error).message}`); + } + })(); + }); + + sceneJson.value = '(reference scene)'; +})(); diff --git a/dashboard/src/store/appRuntimes.ts b/dashboard/src/store/appRuntimes.ts new file mode 100644 index 000000000..2ecb74447 --- /dev/null +++ b/dashboard/src/store/appRuntimes.ts @@ -0,0 +1,236 @@ +/* In-browser simulated runtimes for App Store apps. + * + * Each runtime takes the most recent nvsim MagFrame + a short rolling + * history and decides whether to emit one or more app events. Outputs are + * illustrative: nvsim produces magnetic-field samples, the wasm-edge + * algorithms expect WiFi CSI subcarriers — different physical modalities. + * The simulated runtime preserves *event-emission semantics* (the same + * i32 event IDs, the same trigger logic shape) so users can see the + * cards working without an ESP32 mesh. + * + * For engineering-grade output, deploy the real `wifi-densepose-wasm-edge` + * crate to ESP32 firmware over the WS transport — see ADR-040 / ADR-092 §6.2. + */ + +import type { MagFrameRecord } from '../transport/NvsimClient'; + +export interface AppEvent { + /** Wall-clock timestamp (ms). */ + ts: number; + /** App id that emitted. */ + appId: string; + /** i32 event id from `event_types` mod in wifi-densepose-wasm-edge. */ + eventId: number; + /** Human-readable event name (matches the constant name). */ + eventName: string; + /** Numeric value the app reports (units app-specific). */ + value: number; + /** Optional extra context for the console line. */ + detail?: string; +} + +export interface AppRuntimeContext { + frame: MagFrameRecord; + bMagT: number; + bRecoveredT: [number, number, number]; + /** Rolling history of |B| in T. Most recent last. */ + bHistory: number[]; + /** Time since the runtime was activated (s). */ + elapsedS: number; + /** Per-app scratch state — runtimes can persist counters here. */ + state: Record; +} + +export type AppRuntimeFn = (ctx: AppRuntimeContext) => AppEvent | AppEvent[] | null; + +/** Welford-style running-stat helper. */ +function rollingMean(arr: number[]): number { + if (arr.length === 0) return 0; + let s = 0; + for (const v of arr) s += v; + return s / arr.length; +} +function rollingStd(arr: number[]): number { + if (arr.length < 2) return 0; + const m = rollingMean(arr); + let s = 0; + for (const v of arr) s += (v - m) * (v - m); + return Math.sqrt(s / (arr.length - 1)); +} + +/** vital_trend — periodic 1-Hz HR/BR estimate from the B_z oscillation. */ +const vitalTrend: AppRuntimeFn = (ctx) => { + if (ctx.bHistory.length < 64) return null; + const last = ctx.state['lastEmitS'] ?? 0; + if (ctx.elapsedS - last < 1.0) return null; + ctx.state['lastEmitS'] = ctx.elapsedS; + + // Crude HR estimate: count zero-crossings of detrended B_z over the last + // 64 samples; treat each crossing pair as one cardiac cycle. + const tail = ctx.bHistory.slice(-64); + const m = rollingMean(tail); + let crossings = 0; + for (let i = 1; i < tail.length; i++) { + if ((tail[i] - m) * (tail[i - 1] - m) < 0) crossings++; + } + // 64 samples ≈ 0.65 s at the worker's 32-frame batches × 16 ms tick. + const cycles = crossings / 2; + const hr = Math.max(40, Math.min(180, Math.round((cycles / 0.65) * 60))); + const br = Math.max(8, Math.min(30, Math.round(hr / 4))); // crude proxy + + const evs: AppEvent[] = [ + { ts: Date.now(), appId: 'vital_trend', eventId: 100, eventName: 'VITAL_TREND', value: hr, detail: `HR≈${hr} BPM, BR≈${br} br/min` }, + ]; + if (hr < 60) evs.push({ ts: Date.now(), appId: 'vital_trend', eventId: 103, eventName: 'BRADYCARDIA', value: hr, detail: `HR=${hr} BPM` }); + else if (hr > 100) evs.push({ ts: Date.now(), appId: 'vital_trend', eventId: 104, eventName: 'TACHYCARDIA', value: hr, detail: `HR=${hr} BPM` }); + if (br < 12) evs.push({ ts: Date.now(), appId: 'vital_trend', eventId: 101, eventName: 'BRADYPNEA', value: br, detail: `BR=${br} br/min` }); + else if (br > 24) evs.push({ ts: Date.now(), appId: 'vital_trend', eventId: 102, eventName: 'TACHYPNEA', value: br, detail: `BR=${br} br/min` }); + return evs; +}; + +/** occupancy — variance threshold on |B| over a 5-second window. */ +const occupancy: AppRuntimeFn = (ctx) => { + if (ctx.bHistory.length < 32) return null; + const last = ctx.state['lastEmitS'] ?? 0; + if (ctx.elapsedS - last < 2.0) return null; + const std = rollingStd(ctx.bHistory.slice(-128)) * 1e9; // T → nT + const occupied = std > 0.01; // empirical threshold for the demo + const wasOccupied = (ctx.state['occ'] ?? 0) > 0.5; + if (occupied !== wasOccupied) { + ctx.state['occ'] = occupied ? 1 : 0; + ctx.state['lastEmitS'] = ctx.elapsedS; + return { + ts: Date.now(), + appId: 'occupancy', + eventId: occupied ? 300 : 302, + eventName: occupied ? 'ZONE_OCCUPIED' : 'ZONE_TRANSITION', + value: std, + detail: occupied ? `σ(|B|)=${std.toFixed(3)} nT — entered` : `σ(|B|)=${std.toFixed(3)} nT — left`, + }; + } + return null; +}; + +/** intrusion — |B| above ambient + dwell timer. */ +const intrusion: AppRuntimeFn = (ctx) => { + const ambient = ctx.state['ambient'] ?? ctx.bMagT; + ctx.state['ambient'] = 0.95 * ambient + 0.05 * ctx.bMagT; + const exceeds = ctx.bMagT > ambient * 1.5 && ctx.bMagT > 1e-12; + const dwellStart = ctx.state['dwellStart'] ?? 0; + if (exceeds && dwellStart === 0) { + ctx.state['dwellStart'] = ctx.elapsedS; + } else if (!exceeds) { + ctx.state['dwellStart'] = 0; + } + if (exceeds && dwellStart > 0 && ctx.elapsedS - dwellStart > 0.5 && (ctx.state['lastEmitS'] ?? 0) < dwellStart) { + ctx.state['lastEmitS'] = ctx.elapsedS; + return { + ts: Date.now(), + appId: 'intrusion', + eventId: 200, + eventName: 'INTRUSION_ALERT', + value: ctx.bMagT * 1e9, + detail: `|B|=${(ctx.bMagT * 1e9).toFixed(2)} nT > 1.5× ambient (${(ambient * 1e9).toFixed(2)} nT) for ${(ctx.elapsedS - dwellStart).toFixed(1)} s`, + }; + } + return null; +}; + +/** coherence — z-score of recent |B| against a longer baseline. */ +const coherence: AppRuntimeFn = (ctx) => { + if (ctx.bHistory.length < 64) return null; + const last = ctx.state['lastEmitS'] ?? 0; + if (ctx.elapsedS - last < 0.5) return null; + ctx.state['lastEmitS'] = ctx.elapsedS; + + const recent = ctx.bHistory.slice(-32); + const baseline = ctx.bHistory.slice(-128, -32); + if (baseline.length < 32) return null; + const mu = rollingMean(baseline); + const sd = rollingStd(baseline); + if (sd === 0) return null; + const recentMean = rollingMean(recent); + const z = Math.abs(recentMean - mu) / sd; + return { + ts: Date.now(), + appId: 'coherence', + eventId: 2, + eventName: 'COHERENCE_SCORE', + value: z, + detail: `z=${z.toFixed(2)} σ ${z > 3 ? '· DRIFT' : z > 1.5 ? '· marginal' : '· stable'}`, + }; +}; + +/** adversarial — detect physically-impossible 1/r³ violation. */ +const adversarial: AppRuntimeFn = (ctx) => { + if (ctx.bHistory.length < 32) return null; + const last = ctx.state['lastEmitS'] ?? 0; + if (ctx.elapsedS - last < 3.0) return null; + + // Fake "multi-link consistency": compare instantaneous |B| with the + // smoothed |B|. A sharp factor-of-N step violates dipole physics + // (real 1/r³ source moves continuously). + const tail = ctx.bHistory.slice(-32); + let maxJump = 0; + for (let i = 1; i < tail.length; i++) { + const j = Math.abs(Math.log(Math.max(tail[i], 1e-15)) - Math.log(Math.max(tail[i - 1], 1e-15))); + if (j > maxJump) maxJump = j; + } + if (maxJump > 5) { + ctx.state['lastEmitS'] = ctx.elapsedS; + return { + ts: Date.now(), + appId: 'adversarial', + eventId: 3, + eventName: 'ANOMALY_DETECTED', + value: maxJump, + detail: `log-jump ${maxJump.toFixed(1)} — physically implausible step in |B|`, + }; + } + return null; +}; + +/** exo_ghost_hunter — empty-room CSI anomaly detector adapted to the + * magnetic noise floor: flag impulsive / periodic / drift / random + * patterns and a hidden-presence sub-detector at 0.15-0.5 Hz. */ +const exoGhostHunter: AppRuntimeFn = (ctx) => { + if (ctx.bHistory.length < 128) return null; + const last = ctx.state['lastEmitS'] ?? 0; + if (ctx.elapsedS - last < 4.0) return null; + ctx.state['lastEmitS'] = ctx.elapsedS; + + const tail = ctx.bHistory.slice(-128); + const std = rollingStd(tail) * 1e9; + // Detect impulsive: max - mean > 4σ + const m = rollingMean(tail); + let maxDev = 0; + for (const v of tail) { + const d = Math.abs(v - m); + if (d > maxDev) maxDev = d; + } + const cls: 1 | 3 | 4 = maxDev > 4 * (std * 1e-9) ? 1 // impulsive + : ctx.elapsedS > 10 ? 3 // drift bias as a default after warmup + : 4; // random + const clsName = cls === 1 ? 'impulsive' : cls === 3 ? 'drift' : 'random'; + return { + ts: Date.now(), + appId: 'exo_ghost_hunter', + eventId: 651, + eventName: 'ANOMALY_CLASS', + value: cls, + detail: `class=${clsName} · σ=${std.toFixed(3)} nT`, + }; +}; + +export const APP_RUNTIMES: Record = { + vital_trend: vitalTrend, + occupancy, + intrusion, + coherence, + adversarial, + exo_ghost_hunter: exoGhostHunter, +}; + +export function hasRuntime(appId: string): boolean { + return appId in APP_RUNTIMES; +} diff --git a/dashboard/src/store/appStore.ts b/dashboard/src/store/appStore.ts new file mode 100644 index 000000000..c5fec1e5d --- /dev/null +++ b/dashboard/src/store/appStore.ts @@ -0,0 +1,137 @@ +/* Application-wide reactive state. + * + * One signal per logical observable; components subscribe to only the + * signals they read. Keeps re-renders surgical even at 1 kHz frame rates. + * Persistence lives in `persistence.ts`; this module is pure state. + */ +import { signal, computed } from '@preact/signals-core'; +import type { NvsimClient, MagFrameRecord, NvsimEvent } from '../transport/NvsimClient'; + +export type Theme = 'dark' | 'light'; +export type Density = 'comfy' | 'default' | 'compact'; +export type TransportMode = 'wasm' | 'ws'; + +export const transport = signal('wasm'); +export const wsUrl = signal(''); +export const connected = signal(false); +export const transportError = signal(null); + +export const running = signal(false); +export const paused = signal(true); +export const speed = signal(1.0); +export const t = signal(0); // sim time (s) +export const framesEmitted = signal(0n); + +export const seed = signal(0xCAFEBABEn); + +export const fs = signal(10000); // sample rate Hz +export const fmod = signal(1000); // lockin Hz +export const dtMs = signal(1.0); +export const noiseEnabled = signal(true); + +export const theme = signal('dark'); +export const density = signal('default'); +export const motionReduced = signal(false); +export const autoUpdate = signal(true); + +export const lastB = signal<[number, number, number]>([0, 0, 0]); // T +export const bMag = signal(0); +export const snr = signal(0); +export const fps = signal(0); + +export const witnessHex = signal(''); +export const witnessVerified = signal<'pending' | 'ok' | 'fail' | 'idle'>('idle'); +export const expectedWitness = signal(''); + +export const lastFrame = signal(null); +export const traceX = signal([]); +export const traceY = signal([]); +export const traceZ = signal([]); +export const stripBars = signal([]); + +export const sceneName = signal('rebar-walkby-01'); +export const sceneJson = signal(''); + +export const consolePaused = signal(false); +export const consoleFilter = signal<'all' | 'info' | 'warn' | 'err' | 'dbg' | 'ok'>('all'); + +/** REPL command history, persisted via persistence.ts (kvSet 'repl-history'). */ +export const replHistory = signal([]); +export function pushReplHistory(cmd: string): void { + const next = replHistory.value.slice(); + next.push(cmd); + while (next.length > 200) next.shift(); + replHistory.value = next; +} + +/** Scene drag positions, persisted via persistence.ts (kvSet 'scene-positions'). */ +export interface SceneItemPos { id: string; x: number; y: number } +export const scenePositions = signal([]); + +/** App-runtime emitted events. See appRuntimes.ts. */ +import type { AppEvent } from './appRuntimes'; +export const appEvents = signal([]); +export const appEventCounts = signal>({}); + +export function pushAppEvent(ev: AppEvent): void { + const next = appEvents.value.slice(); + next.push(ev); + while (next.length > 200) next.shift(); + appEvents.value = next; + + const c = { ...appEventCounts.value }; + c[ev.appId] = (c[ev.appId] ?? 0) + 1; + appEventCounts.value = c; +} + +/** Active app activations — driven by the App Store toggles. Mirrored + * from `apps.ts` but exposed as a signal here so `main.ts` can dispatch + * frames to active runtimes without importing the App Store component. */ +export const activeAppIds = signal>(new Set()); + +export const transportLabel = computed(() => + transport.value === 'wasm' ? 'wasm' : 'ws', +); + +let _client: NvsimClient | null = null; +export function setClient(c: NvsimClient): void { _client = c; } +export function getClient(): NvsimClient | null { return _client; } + +export interface ConsoleLine { + ts: number; + level: 'info' | 'warn' | 'err' | 'dbg' | 'ok'; + msg: string; +} +export const consoleLines = signal([]); +const MAX_LINES = 200; + +export function pushLog(level: ConsoleLine['level'], msg: string): void { + if (consolePaused.value) return; + const next = consoleLines.value.slice(); + next.push({ ts: Date.now(), level, msg }); + while (next.length > MAX_LINES) next.shift(); + consoleLines.value = next; +} + +export function pushTrace(b: [number, number, number]): void { + const cap = 200; + const x = traceX.value.slice(); x.push(b[0]); if (x.length > cap) x.shift(); + const y = traceY.value.slice(); y.push(b[1]); if (y.length > cap) y.shift(); + const z = traceZ.value.slice(); z.push(b[2]); if (z.length > cap) z.shift(); + traceX.value = x; + traceY.value = y; + traceZ.value = z; +} + +export function pushStripBar(amp: number): void { + const cap = 48; + const next = stripBars.value.slice(); + next.push(Math.max(0, Math.min(1, amp))); + while (next.length > cap) next.shift(); + stripBars.value = next; +} + +export function recordEvent(_ev: NvsimEvent): void { + // future: route NvsimEvent into store updates per type. For V1 the + // worker pushes B-vector / frame data directly via the data plane. +} diff --git a/dashboard/src/store/apps.ts b/dashboard/src/store/apps.ts new file mode 100644 index 000000000..bcb144028 --- /dev/null +++ b/dashboard/src/store/apps.ts @@ -0,0 +1,331 @@ +/* RuView Edge App Store registry. + * + * Catalog of every WASM edge module shipping in the workspace plus the + * `nvsim` simulator itself. Each entry maps to a hot-loadable algorithm + * the dashboard can run in-browser (WASM transport) or push to a real + * ESP32-S3 mesh (WS transport, deployed via WASM3 — ADR-040 Tier 3). + * + * Categories (ADR-041 event-ID ranges): + * med 100–199 Medical & health + * sec 200–299 Security & safety + * bld 300–399 Smart building + * ret 400–499 Retail & hospitality + * ind 500–599 Industrial + * sig 600–619 Signal-processing primitives + * lrn 620–639 Online learning + * spt 640–659 Spatial / graph + * tmp 640–660 Temporal logic / planning + * ais 700–719 AI safety + * qnt 720–739 Quantum-flavoured signal + * aut 740–759 Autonomy / mesh + * exo 650–699 Exotic / research + * sim — Pipeline simulators (nvsim) + * + * The `crate` field names the Cargo crate that owns the implementation. + * `wasmEdge` apps are compiled out of `wifi-densepose-wasm-edge`; + * `nvsim` apps come from `nvsim`. Future apps may target other crates. + */ + +export type AppCategory = + | 'sim' + | 'med' + | 'sec' + | 'bld' + | 'ret' + | 'ind' + | 'sig' + | 'lrn' + | 'spt' + | 'tmp' + | 'ais' + | 'qnt' + | 'aut' + | 'exo'; + +/** What actually happens when a card's toggle is on. + * - `running` — the algorithm is genuinely running in the browser right now + * (e.g. `nvsim` itself, which is the simulator the dashboard fronts). + * - `simulated` — a pared-down version of the algorithm runs against nvsim's + * live magnetic frame stream as a *proxy* for its native CSI input. + * Emits real i32 event IDs into the console feed; output is illustrative, + * not engineering-grade. Listed apps' Rust source is real, builds for + * wasm32-unknown-unknown, and passes its native unit tests. + * - `mesh-only` — algorithm needs CSI subcarrier data from a real ESP32-S3 + * mesh (or a future CSI simulator). Toggling persists the selection so + * the WS transport can push activation when connected. */ +export type AppRuntime = 'running' | 'simulated' | 'mesh-only'; + +export interface AppManifest { + /** Stable kebab-case id; matches the wasm-edge module name (e.g. `med_sleep_apnea`). */ + id: string; + /** Human-readable name. */ + name: string; + /** Category short-code. */ + category: AppCategory; + /** Cargo crate the implementation lives in. */ + crate: 'nvsim' | 'wifi-densepose-wasm-edge' | string; + /** One-liner description. */ + summary: string; + /** Optional longer markdown body. */ + body?: string; + /** Numeric event IDs this app emits (i32 codes from `event_types` mod). */ + events?: number[]; + /** Compute budget tier the module advertises. S=<5ms, M=<15ms, L=<50ms. */ + budget?: 'S' | 'M' | 'L'; + /** Default activation state when listed. */ + active?: boolean; + /** Tags for fuzzy search and filtering. */ + tags?: string[]; + /** "Available", "Beta", or "Research" maturity. */ + status: 'available' | 'beta' | 'research'; + /** ADR back-reference. */ + adr?: string; + /** What actually happens when active — see AppRuntime docs. */ + runtime?: AppRuntime; +} + +export const APPS: AppManifest[] = [ + // ── Pipeline simulators ────────────────────────────────────────────────── + { + id: 'nvsim', + name: 'nvsim — NV-diamond magnetometer', + category: 'sim', + crate: 'nvsim', + summary: + 'Deterministic forward simulator: scene → Biot–Savart → NV ensemble → ADC → MagFrame stream + SHA-256 witness.', + budget: 'L', + active: true, + status: 'available', + tags: ['quantum', 'magnetometer', 'simulator', 'witness', 'wasm'], + adr: 'ADR-089', + runtime: 'running', + }, + + // ── Core sensing primitives (ADR-014/040 flagship modules) ─────────────── + { + id: 'gesture', + name: 'Gesture (DTW)', + category: 'sig', + crate: 'wifi-densepose-wasm-edge', + summary: 'Dynamic-Time-Warping gesture classifier from CSI motion templates.', + events: [1], + budget: 'M', + status: 'available', + tags: ['hci', 'csi', 'classifier', 'dtw'], + adr: 'ADR-014', + runtime: 'mesh-only', + }, + { + id: 'coherence', + name: 'Coherence gate', + category: 'sig', + crate: 'wifi-densepose-wasm-edge', + summary: 'Z-score coherence scoring + Accept/PredictOnly/Reject/Recalibrate gate.', + events: [2], + budget: 'S', + status: 'available', + tags: ['gate', 'csi', 'coherence', 'drift'], + adr: 'ADR-029', + runtime: 'simulated', + }, + { + id: 'adversarial', + name: 'Adversarial-signal detector', + category: 'ais', + crate: 'wifi-densepose-wasm-edge', + summary: + 'Physically-impossible-signal detector — multi-link consistency, used to flag spoofed CSI.', + events: [3], + budget: 'M', + status: 'available', + tags: ['security', 'csi', 'spoofing', 'mesh'], + adr: 'ADR-032', + runtime: 'simulated', + }, + { + id: 'rvf', + name: 'RVF — Rust Verified Feature stream', + category: 'sig', + crate: 'wifi-densepose-wasm-edge', + summary: 'Verified-frame builder with SHA-256 hash + version metadata for the feature stream.', + budget: 'S', + status: 'available', + tags: ['witness', 'csi', 'hash'], + adr: 'ADR-040', + }, + { + id: 'occupancy', + name: 'Occupancy estimator', + category: 'bld', + crate: 'wifi-densepose-wasm-edge', + summary: 'Through-wall presence + person-count via CSI amplitude perturbation.', + events: [300, 301, 302], + budget: 'S', + status: 'available', + tags: ['csi', 'building', 'presence'], + runtime: 'simulated', + }, + { + id: 'vital_trend', + name: 'Vital-trend monitor', + category: 'med', + crate: 'wifi-densepose-wasm-edge', + summary: 'HR + BR trend tracking with bradycardia/tachycardia/apnea events.', + events: [100, 101, 102, 103, 104, 105], + budget: 'S', + status: 'available', + tags: ['medical', 'vitals', 'csi'], + adr: 'ADR-021', + runtime: 'simulated', + }, + { + id: 'intrusion', + name: 'Intrusion detector', + category: 'sec', + crate: 'wifi-densepose-wasm-edge', + summary: 'Zone-based intrusion alert from CSI motion patterns.', + events: [200, 201], + budget: 'S', + status: 'available', + tags: ['security', 'zone', 'csi'], + runtime: 'simulated', + }, + + // ── Medical & Health (100-series) ──────────────────────────────────────── + { id: 'med_sleep_apnea', name: 'Sleep-apnea detector', category: 'med', crate: 'wifi-densepose-wasm-edge', summary: 'Episodic respiratory pause detection during sleep cycles.', events: [105], budget: 'S', status: 'available', tags: ['medical', 'sleep', 'breathing'] }, + { id: 'med_cardiac_arrhythmia', name: 'Cardiac arrhythmia', category: 'med', crate: 'wifi-densepose-wasm-edge', summary: 'Beat-to-beat irregularity classifier from cardiac micro-Doppler.', events: [103, 104], budget: 'M', status: 'available', tags: ['medical', 'cardiac', 'arrhythmia'] }, + { id: 'med_respiratory_distress', name: 'Respiratory distress', category: 'med', crate: 'wifi-densepose-wasm-edge', summary: 'Distress signature: rapid shallow breathing + accessory-muscle motion.', events: [101, 102], budget: 'S', status: 'available', tags: ['medical', 'breathing', 'icu'] }, + { id: 'med_gait_analysis', name: 'Gait analysis', category: 'med', crate: 'wifi-densepose-wasm-edge', summary: 'Stride length, cadence, asymmetry from through-wall CSI pose tracking.', budget: 'M', status: 'available', tags: ['medical', 'gait', 'pose'] }, + { id: 'med_seizure_detect', name: 'Seizure detector', category: 'med', crate: 'wifi-densepose-wasm-edge', summary: 'Tonic-clonic seizure motion signature.', budget: 'M', status: 'beta', tags: ['medical', 'neuro'] }, + + // ── Security (200-series) ──────────────────────────────────────────────── + { id: 'sec_perimeter_breach', name: 'Perimeter breach', category: 'sec', crate: 'wifi-densepose-wasm-edge', summary: 'Approach/departure detection at user-defined boundary segments.', events: [210, 211, 212, 213], budget: 'S', status: 'available', tags: ['security', 'perimeter'] }, + { id: 'sec_weapon_detect', name: 'Metal anomaly / weapon', category: 'sec', crate: 'wifi-densepose-wasm-edge', summary: 'Metal-perturbation flag in CSI; potential weapon presence (research).', events: [220, 221, 222], budget: 'M', status: 'research', tags: ['security', 'metal', 'csi'] }, + { id: 'sec_tailgating', name: 'Tailgating detector', category: 'sec', crate: 'wifi-densepose-wasm-edge', summary: 'Detect 2+ persons crossing a single-passage threshold.', events: [230, 231, 232], budget: 'S', status: 'available', tags: ['security', 'access-control'] }, + { id: 'sec_loitering', name: 'Loitering detector', category: 'sec', crate: 'wifi-densepose-wasm-edge', summary: 'Stationary occupancy past a configurable dwell threshold.', events: [240, 241, 242], budget: 'S', status: 'available', tags: ['security', 'dwell'] }, + { id: 'sec_panic_motion', name: 'Panic motion', category: 'sec', crate: 'wifi-densepose-wasm-edge', summary: 'High-energy distress motion: struggle / fleeing pattern.', events: [250, 251, 252], budget: 'S', status: 'beta', tags: ['security', 'distress'] }, + + // ── Smart Building (300-series) ────────────────────────────────────────── + { id: 'bld_hvac_presence', name: 'HVAC presence', category: 'bld', crate: 'wifi-densepose-wasm-edge', summary: 'Occupied/activity-level/departure-countdown for HVAC zones.', events: [310, 311, 312], budget: 'S', status: 'available', tags: ['hvac', 'building', 'energy'] }, + { id: 'bld_lighting_zones', name: 'Lighting zones', category: 'bld', crate: 'wifi-densepose-wasm-edge', summary: 'Per-zone light on/dim/off cues from occupancy.', events: [320, 321, 322], budget: 'S', status: 'available', tags: ['lighting', 'building'] }, + { id: 'bld_elevator_count', name: 'Elevator count', category: 'bld', crate: 'wifi-densepose-wasm-edge', summary: 'Person count inside elevator car from CSI.', events: [330], budget: 'S', status: 'available', tags: ['elevator', 'building'] }, + { id: 'bld_meeting_room', name: 'Meeting-room utilization', category: 'bld', crate: 'wifi-densepose-wasm-edge', summary: 'Meeting size + duration analytics for booking systems.', budget: 'S', status: 'available', tags: ['meeting', 'analytics'] }, + { id: 'bld_energy_audit', name: 'Energy audit', category: 'bld', crate: 'wifi-densepose-wasm-edge', summary: 'Continuous occupancy-vs-HVAC-state audit for energy savings.', budget: 'M', status: 'available', tags: ['energy', 'audit'] }, + + // ── Retail (400-series) ────────────────────────────────────────────────── + { id: 'ret_queue_length', name: 'Queue length', category: 'ret', crate: 'wifi-densepose-wasm-edge', summary: 'Live queue-length tracking for checkout / kiosks.', budget: 'S', status: 'available', tags: ['retail', 'queue'] }, + { id: 'ret_dwell_heatmap', name: 'Dwell heatmap', category: 'ret', crate: 'wifi-densepose-wasm-edge', summary: 'Per-zone dwell time accumulation; analytics-only export.', budget: 'M', status: 'available', tags: ['retail', 'heatmap'] }, + { id: 'ret_customer_flow', name: 'Customer flow', category: 'ret', crate: 'wifi-densepose-wasm-edge', summary: 'Origin-destination flow graph through a store layout.', budget: 'M', status: 'available', tags: ['retail', 'flow'] }, + { id: 'ret_table_turnover', name: 'Table turnover', category: 'ret', crate: 'wifi-densepose-wasm-edge', summary: 'Restaurant table seat / vacate transitions.', budget: 'S', status: 'available', tags: ['retail', 'restaurant'] }, + { id: 'ret_shelf_engagement', name: 'Shelf engagement', category: 'ret', crate: 'wifi-densepose-wasm-edge', summary: 'Reach-to-shelf gestures and dwell at product zones.', budget: 'M', status: 'available', tags: ['retail', 'shelf'] }, + + // ── Industrial (500-series) ────────────────────────────────────────────── + { id: 'ind_forklift_proximity', name: 'Forklift proximity', category: 'ind', crate: 'wifi-densepose-wasm-edge', summary: 'Worker-near-forklift safety alert.', budget: 'S', status: 'available', tags: ['industrial', 'safety'] }, + { id: 'ind_confined_space', name: 'Confined-space monitor', category: 'ind', crate: 'wifi-densepose-wasm-edge', summary: 'Last-person-out detection + presence audit for OSHA confined-space entries.', budget: 'S', status: 'available', tags: ['industrial', 'osha'] }, + { id: 'ind_clean_room', name: 'Clean-room PPE / motion', category: 'ind', crate: 'wifi-densepose-wasm-edge', summary: 'Motion patterns consistent with proper PPE-clad movement.', budget: 'M', status: 'beta', tags: ['industrial', 'cleanroom'] }, + { id: 'ind_livestock_monitor', name: 'Livestock monitor', category: 'ind', crate: 'wifi-densepose-wasm-edge', summary: 'Vital-sign + activity tracking for stall-bound livestock.', budget: 'M', status: 'beta', tags: ['agriculture', 'livestock'] }, + { id: 'ind_structural_vibration', name: 'Structural vibration', category: 'ind', crate: 'wifi-densepose-wasm-edge', summary: 'Building/equipment micro-vibration via CSI phase derivative.', budget: 'M', status: 'research', tags: ['industrial', 'vibration'] }, + + // ── Signal primitives (600-series) ─────────────────────────────────────── + { id: 'sig_coherence_gate', name: 'Coherence gate (extended)', category: 'sig', crate: 'wifi-densepose-wasm-edge', summary: 'Hysteresis + multi-state coherence gate driving downstream apps.', budget: 'S', status: 'available', tags: ['gate', 'csi'] }, + { id: 'sig_flash_attention', name: 'Flash attention (CSI)', category: 'sig', crate: 'wifi-densepose-wasm-edge', summary: 'Edge-friendly attention block for CSI subcarrier weighting.', budget: 'M', status: 'beta', tags: ['attention', 'csi'] }, + { id: 'sig_temporal_compress', name: 'Temporal-tensor compress', category: 'sig', crate: 'wifi-densepose-wasm-edge', summary: 'RuVector temporal-tensor compression on the CSI buffer.', budget: 'M', status: 'available', tags: ['compress', 'tensor'] }, + { id: 'sig_sparse_recovery', name: 'Sparse recovery', category: 'sig', crate: 'wifi-densepose-wasm-edge', summary: '114→56 subcarrier sparse interpolation via L1 solver.', budget: 'M', status: 'available', tags: ['sparse', 'csi'] }, + { id: 'sig_mincut_person_match', name: 'Mincut person-match', category: 'sig', crate: 'wifi-densepose-wasm-edge', summary: 'Min-cut person assignment across multistatic frames.', budget: 'M', status: 'available', tags: ['mincut', 'matching'] }, + { id: 'sig_optimal_transport', name: 'Optimal transport', category: 'sig', crate: 'wifi-densepose-wasm-edge', summary: 'OT-based feature alignment between mesh nodes.', budget: 'M', status: 'beta', tags: ['ot', 'alignment'] }, + + // ── Online learning ────────────────────────────────────────────────────── + { id: 'lrn_dtw_gesture_learn', name: 'DTW gesture learn', category: 'lrn', crate: 'wifi-densepose-wasm-edge', summary: 'On-device template learning for personalized gesture libraries.', budget: 'M', status: 'beta', tags: ['lifelong', 'gesture'] }, + { id: 'lrn_anomaly_attractor', name: 'Anomaly attractor', category: 'lrn', crate: 'wifi-densepose-wasm-edge', summary: 'Novelty detector with dynamic-attractor recall.', budget: 'M', status: 'research', tags: ['novelty', 'lifelong'] }, + { id: 'lrn_meta_adapt', name: 'Meta-adapt', category: 'lrn', crate: 'wifi-densepose-wasm-edge', summary: 'Meta-learning adapter for fast site-to-site transfer.', budget: 'L', status: 'research', tags: ['meta-learning'] }, + { id: 'lrn_ewc_lifelong', name: 'EWC++ lifelong', category: 'lrn', crate: 'wifi-densepose-wasm-edge', summary: 'Elastic-weight-consolidation gate to avoid catastrophic forgetting.', budget: 'M', status: 'beta', tags: ['lifelong', 'ewc'] }, + + // ── Spatial / graph ────────────────────────────────────────────────────── + { id: 'spt_pagerank_influence', name: 'PageRank influence', category: 'spt', crate: 'wifi-densepose-wasm-edge', summary: 'Graph-influence ranking on the multistatic mesh.', budget: 'M', status: 'beta', tags: ['graph', 'pagerank'] }, + { id: 'spt_micro_hnsw', name: 'µHNSW vector index', category: 'spt', crate: 'wifi-densepose-wasm-edge', summary: 'Tiny HNSW index for AETHER re-ID embeddings on-device.', budget: 'M', status: 'available', tags: ['hnsw', 'reid'] }, + { id: 'spt_spiking_tracker', name: 'Spiking tracker', category: 'spt', crate: 'wifi-densepose-wasm-edge', summary: 'Spiking-network multi-target tracker.', budget: 'L', status: 'research', tags: ['snn', 'tracker'] }, + + // ── Temporal / planning ────────────────────────────────────────────────── + { id: 'tmp_pattern_sequence', name: 'Pattern sequence', category: 'tmp', crate: 'wifi-densepose-wasm-edge', summary: 'Sequence-of-events pattern matcher (e.g. ingress→linger→egress).', budget: 'M', status: 'available', tags: ['temporal', 'pattern'] }, + { id: 'tmp_temporal_logic_guard', name: 'Temporal logic guard', category: 'tmp', crate: 'wifi-densepose-wasm-edge', summary: 'LTL/MTL safety-property guard over event streams.', budget: 'M', status: 'beta', tags: ['ltl', 'safety'] }, + { id: 'tmp_goap_autonomy', name: 'GOAP autonomy', category: 'tmp', crate: 'wifi-densepose-wasm-edge', summary: 'Goal-oriented action planning for adaptive routines.', budget: 'L', status: 'research', tags: ['planning', 'autonomy'] }, + + // ── AI safety ──────────────────────────────────────────────────────────── + { id: 'ais_prompt_shield', name: 'Prompt shield', category: 'ais', crate: 'wifi-densepose-wasm-edge', summary: 'Edge-side LLM prompt-injection guard for on-device assistants.', budget: 'M', status: 'beta', tags: ['security', 'llm'] }, + { id: 'ais_behavioral_profiler', name: 'Behavioral profiler', category: 'ais', crate: 'wifi-densepose-wasm-edge', summary: 'Anomalous-behaviour profiler (drift in motion habits).', budget: 'M', status: 'beta', tags: ['anomaly', 'behaviour'] }, + + // ── Quantum-flavoured ──────────────────────────────────────────────────── + { id: 'qnt_quantum_coherence', name: 'Quantum coherence', category: 'qnt', crate: 'wifi-densepose-wasm-edge', summary: 'Coherence diagnostics adapted for quantum-sensor signals.', budget: 'M', status: 'research', tags: ['quantum', 'coherence'] }, + { id: 'qnt_interference_search', name: 'Interference search', category: 'qnt', crate: 'wifi-densepose-wasm-edge', summary: 'Interferometric anomaly search across mesh viewpoints.', budget: 'L', status: 'research', tags: ['quantum', 'interference'] }, + + // ── Autonomy / mesh ────────────────────────────────────────────────────── + { id: 'aut_psycho_symbolic', name: 'Psycho-symbolic agent', category: 'aut', crate: 'wifi-densepose-wasm-edge', summary: 'Symbolic-rule + neural-feature hybrid for low-power autonomy loops.', budget: 'L', status: 'research', tags: ['autonomy', 'symbolic'] }, + { id: 'aut_self_healing_mesh', name: 'Self-healing mesh', category: 'aut', crate: 'wifi-densepose-wasm-edge', summary: 'Mesh-topology repair with per-node health gossip.', budget: 'M', status: 'beta', tags: ['mesh', 'health'] }, + + // ── Exotic / Research (650-series) ─────────────────────────────────────── + { id: 'exo_ghost_hunter', name: 'Ghost hunter (anomaly)', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Empty-room CSI anomaly detector — impulsive/periodic/drift/random + hidden-presence sub-detector.', events: [650, 651, 652, 653], budget: 'S', status: 'available', tags: ['anomaly', 'paranormal', 'csi'], adr: 'ADR-041', runtime: 'simulated' }, + { id: 'exo_breathing_sync', name: 'Breathing sync', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Multi-person breathing synchrony analytics.', budget: 'M', status: 'beta', tags: ['breathing', 'sync'] }, + { id: 'exo_dream_stage', name: 'Dream-stage classifier', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'NREM/REM stage classification from breathing + micro-motion.', budget: 'M', status: 'research', tags: ['sleep', 'rem'] }, + { id: 'exo_emotion_detect', name: 'Emotion detector', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Coarse arousal/valence from breathing + heart-rate variability.', budget: 'M', status: 'research', tags: ['affect'] }, + { id: 'exo_gesture_language', name: 'Gesture language', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Sign-language pattern recognition.', budget: 'L', status: 'research', tags: ['hci', 'sign'] }, + { id: 'exo_happiness_score', name: 'Happiness score', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Aggregate well-being score from co-occupancy + activity dynamics.', budget: 'M', status: 'research', tags: ['affect', 'wellbeing'] }, + { id: 'exo_hyperbolic_space', name: 'Hyperbolic space embed', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Hyperbolic embeddings for hierarchical scene structure.', budget: 'L', status: 'research', tags: ['embedding', 'hyperbolic'] }, + { id: 'exo_music_conductor', name: 'Music conductor', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Map gesture energy to MIDI tempo/dynamics.', budget: 'M', status: 'research', tags: ['midi', 'art'] }, + { id: 'exo_plant_growth', name: 'Plant-growth tracker', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Slow CSI drift tracking for greenhouse foliage growth.', budget: 'L', status: 'research', tags: ['agriculture'] }, + { id: 'exo_rain_detect', name: 'Rain detector', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Outdoor CSI signature of rainfall.', budget: 'M', status: 'research', tags: ['weather'] }, + { id: 'exo_time_crystal', name: 'Time-crystal periodicity', category: 'exo', crate: 'wifi-densepose-wasm-edge', summary: 'Periodicity diagnostics with anti-aliasing harmonics.', budget: 'M', status: 'research', tags: ['periodicity'] }, +]; + +export const CATEGORIES: Record = { + sim: { label: 'Simulators', color: 'oklch(0.78 0.14 70)', range: '—' }, + med: { label: 'Medical & Health', color: 'oklch(0.65 0.22 25)', range: '100–199' }, + sec: { label: 'Security & Safety', color: 'oklch(0.7 0.18 35)', range: '200–299' }, + bld: { label: 'Smart Building', color: 'oklch(0.78 0.12 195)', range: '300–399' }, + ret: { label: 'Retail & Hospitality', color: 'oklch(0.78 0.14 145)', range: '400–499' }, + ind: { label: 'Industrial', color: 'oklch(0.72 0.18 330)', range: '500–599' }, + sig: { label: 'Signal Processing', color: 'oklch(0.78 0.14 70)', range: '600–619' }, + lrn: { label: 'Online Learning', color: 'oklch(0.78 0.12 260)', range: '620–639' }, + spt: { label: 'Spatial / Graph', color: 'oklch(0.7 0.18 100)', range: '640–659' }, + tmp: { label: 'Temporal / Planning', color: 'oklch(0.7 0.16 50)', range: '660–679' }, + ais: { label: 'AI Safety', color: 'oklch(0.65 0.22 25)', range: '700–719' }, + qnt: { label: 'Quantum', color: 'oklch(0.72 0.18 290)', range: '720–739' }, + aut: { label: 'Autonomy', color: 'oklch(0.78 0.14 145)', range: '740–759' }, + exo: { label: 'Exotic / Research', color: 'oklch(0.72 0.18 330)', range: '650–699' }, +}; + +export interface AppActivation { + id: string; + /** Active in the current session. */ + active: boolean; + /** Last activation timestamp. */ + lastActivatedAt?: number; + /** Last event count seen (for the cards' counter). */ + eventCount?: number; +} + +export function defaultActivations(): AppActivation[] { + return APPS.map((a) => ({ id: a.id, active: a.active === true, eventCount: 0 })); +} + +export function appsByCategory(): Record { + const map = {} as Record; + for (const c of Object.keys(CATEGORIES) as AppCategory[]) map[c] = []; + for (const a of APPS) map[a.category].push(a); + return map; +} + +export function findApp(id: string): AppManifest | undefined { + return APPS.find((a) => a.id === id); +} + +export function fuzzyMatch(query: string, app: AppManifest): number { + if (!query) return 1; + const q = query.toLowerCase(); + let score = 0; + if (app.id.toLowerCase().includes(q)) score += 3; + if (app.name.toLowerCase().includes(q)) score += 3; + if (app.summary.toLowerCase().includes(q)) score += 1; + if (app.tags?.some((t) => t.toLowerCase().includes(q))) score += 2; + if (app.category === q) score += 5; + return score; +} diff --git a/dashboard/src/store/persistence.ts b/dashboard/src/store/persistence.ts new file mode 100644 index 000000000..375fa8b51 --- /dev/null +++ b/dashboard/src/store/persistence.ts @@ -0,0 +1,52 @@ +/* IndexedDB-backed persistence for settings and saved scenes. + * Mirrors the mockup's `nvsim/kv` store. */ + +const DB_NAME = 'nvsim'; +const DB_VER = 1; +const STORE = 'kv'; + +let dbPromise: Promise | null = null; + +function openDb(): Promise { + if (dbPromise) return dbPromise; + dbPromise = new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME, DB_VER); + req.onupgradeneeded = () => { + const db = req.result; + if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE); + }; + req.onsuccess = () => resolve(req.result); + req.onerror = () => reject(req.error); + }); + return dbPromise; +} + +export async function kvGet(key: string): Promise { + const db = await openDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readonly'); + const r = tx.objectStore(STORE).get(key); + r.onsuccess = () => resolve(r.result as T | undefined); + r.onerror = () => reject(r.error); + }); +} + +export async function kvSet(key: string, value: unknown): Promise { + const db = await openDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).put(value, key); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} + +export async function kvDelete(key: string): Promise { + const db = await openDb(); + return await new Promise((resolve, reject) => { + const tx = db.transaction(STORE, 'readwrite'); + tx.objectStore(STORE).delete(key); + tx.oncomplete = () => resolve(); + tx.onerror = () => reject(tx.error); + }); +} diff --git a/dashboard/src/transport/NvsimClient.ts b/dashboard/src/transport/NvsimClient.ts new file mode 100644 index 000000000..6c4891b63 --- /dev/null +++ b/dashboard/src/transport/NvsimClient.ts @@ -0,0 +1,143 @@ +/* Common NvsimClient interface — both WasmClient and WsClient implement it. + * Dashboard binds to this interface and never to a concrete client. + * Aligns with ADR-092 §5.2. + */ + +export interface PipelineConfigJson { + digitiser?: { + f_s_hz: number; + f_mod_hz: number; + lp_cutoff_hz?: number; + }; + sensor?: { + gamma_fwhm_hz?: number; + t1_s?: number; + t2_s?: number; + t2_star_s?: number; + contrast?: number; + n_spins?: number; + n_centers?: number; + shot_noise_disabled?: boolean; + }; + dt_s?: number | null; +} + +export interface SceneJson { + dipoles: { position: [number, number, number]; moment: [number, number, number] }[]; + loops: { + centre: [number, number, number]; + normal: [number, number, number]; + radius: number; + current: number; + n_segments: number; + }[]; + ferrous: { + position: [number, number, number]; + volume: number; + susceptibility: number; + }[]; + eddy: unknown[]; + sensors: [number, number, number][]; + ambient_field: [number, number, number]; +} + +export interface MagFrameRecord { + magic: number; + version: number; + flags: number; + sensorId: number; + tUs: bigint; + bPt: [number, number, number]; + sigmaPt: [number, number, number]; + noiseFloorPtSqrtHz: number; + temperatureK: number; + raw: Uint8Array; +} + +export interface MagFrameBatch { + frames: MagFrameRecord[]; + bytes: Uint8Array; +} + +export type NvsimEvent = + | { type: 'log'; level: 'info' | 'warn' | 'err' | 'dbg' | 'ok'; msg: string } + | { type: 'witness'; hex: string } + | { type: 'fps'; value: number } + | { type: 'state'; running: boolean; t: number; framesEmitted: number }; + +export interface RunOpts { frames?: number } + +/** One-shot pipeline run for "what would the sensor recover at this scene?" + * use cases. Doesn't disturb the running pipeline. */ +export interface TransientRunResult { + bRecoveredT: [number, number, number]; + bMagT: number; + noiseFloorPtSqrtHz: number; + sigmaPt: [number, number, number]; + nFrames: number; + witnessHex: string; +} + +export interface NvsimClient { + loadScene(scene: SceneJson): Promise; + setConfig(cfg: PipelineConfigJson): Promise; + setSeed(seed: bigint): Promise; + reset(): Promise; + run(opts?: RunOpts): Promise; + pause(): Promise; + step(direction: 'fwd' | 'back', dtMs: number): Promise; + + onFrames(cb: (batch: MagFrameBatch) => void): void; + onEvent(cb: (ev: NvsimEvent) => void): void; + + generateWitness(samples: number): Promise; + verifyWitness(expected: Uint8Array): Promise<{ ok: true } | { ok: false; actual: Uint8Array }>; + exportProofBundle(): Promise; + runTransient(scene: SceneJson, config: PipelineConfigJson, seed: bigint, samples: number): Promise; + + buildId(): Promise; + close(): Promise; +} + +/** Parse one MagFrame from a 60-byte slice. Layout matches `nvsim::frame`. */ +export function parseMagFrame(view: DataView, offset: number, raw: Uint8Array): MagFrameRecord { + // v1 layout: magic(u32) | version(u16) | flags(u16) | sensor_id(u16) | _reserved(u16) | + // t_us(u64) | b_pt[3](f32) | sigma_pt[3](f32) | noise_floor_pt_sqrt_hz(f32) | + // temperature_k(f32) — 60 bytes total. All little-endian. + const magic = view.getUint32(offset + 0, true); + const version = view.getUint16(offset + 4, true); + const flags = view.getUint16(offset + 6, true); + const sensorId = view.getUint16(offset + 8, true); + // skip 2 bytes reserved at offset+10 + const tUs = view.getBigUint64(offset + 12, true); + const bx = view.getFloat32(offset + 20, true); + const by = view.getFloat32(offset + 24, true); + const bz = view.getFloat32(offset + 28, true); + const sx = view.getFloat32(offset + 32, true); + const sy = view.getFloat32(offset + 36, true); + const sz = view.getFloat32(offset + 40, true); + const noiseFloorPtSqrtHz = view.getFloat32(offset + 44, true); + const temperatureK = view.getFloat32(offset + 48, true); + return { + magic, + version, + flags, + sensorId, + tUs, + bPt: [bx, by, bz], + sigmaPt: [sx, sy, sz], + noiseFloorPtSqrtHz, + temperatureK, + raw: raw.subarray(offset, offset + 60), + }; +} + +export function parseFrameBatch(bytes: Uint8Array): MagFrameRecord[] { + const frameSize = 60; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + const out: MagFrameRecord[] = []; + for (let off = 0; off + frameSize <= bytes.byteLength; off += frameSize) { + out.push(parseMagFrame(view, off, bytes)); + } + return out; +} diff --git a/dashboard/src/transport/WasmClient.ts b/dashboard/src/transport/WasmClient.ts new file mode 100644 index 000000000..7f5ebd11f --- /dev/null +++ b/dashboard/src/transport/WasmClient.ts @@ -0,0 +1,218 @@ +/* Default `NvsimClient` implementation. Talks to the Web Worker that + * hosts the nvsim WASM module. ADR-092 §5.4 + §6.3. */ + +import { + type NvsimClient, + type SceneJson, + type PipelineConfigJson, + type RunOpts, + type MagFrameBatch, + type NvsimEvent, + type TransientRunResult, + parseFrameBatch, +} from './NvsimClient'; + +interface PendingRequest { + resolve: (v: T) => void; + reject: (err: Error) => void; +} + +export interface WasmBootInfo { + buildVersion: string; + frameMagic: number; + frameBytes: number; + expectedWitnessHex: string; +} + +export class WasmClient implements NvsimClient { + private worker: Worker; + private nextId = 1; + private pending = new Map>(); + private frameSubs = new Set<(b: MagFrameBatch) => void>(); + private eventSubs = new Set<(e: NvsimEvent) => void>(); + private bootInfo: WasmBootInfo | null = null; + + constructor() { + this.worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' }); + this.worker.addEventListener('message', (ev) => this.onMessage(ev)); + this.worker.addEventListener('error', (e) => + this.eventSubs.forEach((s) => s({ type: 'log', level: 'err', msg: String(e.message) })), + ); + } + + private onMessage(ev: MessageEvent): void { + const m = ev.data as { type: string; id?: number; [k: string]: unknown }; + if (m.type === 'frames') { + const buf = m.batch as ArrayBuffer; + const bytes = new Uint8Array(buf); + const frames = parseFrameBatch(bytes); + const batch: MagFrameBatch = { frames, bytes }; + this.frameSubs.forEach((s) => s(batch)); + const fps = m.fps as number; + if (fps > 0) { + this.eventSubs.forEach((s) => s({ type: 'fps', value: fps })); + } + return; + } + if (m.type === 'state') { + this.eventSubs.forEach((s) => + s({ + type: 'state', + running: Boolean(m.running), + t: 0, + framesEmitted: Number(m.framesEmitted ?? 0), + }), + ); + return; + } + if (m.type === 'ready') { + return; + } + if (m.type === 'err' && m.id == null) { + this.eventSubs.forEach((s) => + s({ type: 'log', level: 'err', msg: String(m.msg) }), + ); + return; + } + if (typeof m.id === 'number' && this.pending.has(m.id)) { + const p = this.pending.get(m.id)!; + this.pending.delete(m.id); + if (m.type === 'err') p.reject(new Error(String(m.msg))); + else p.resolve(m); + } + } + + private rpc(msg: Record, transfer: Transferable[] = []): Promise { + const id = this.nextId++; + return new Promise((resolve, reject) => { + this.pending.set(id, { resolve: resolve as (v: unknown) => void, reject }); + this.worker.postMessage({ ...msg, id }, transfer); + }); + } + + async boot(): Promise { + if (this.bootInfo) return this.bootInfo; + // Pass Vite's resolved BASE_URL so the worker can locate /nvsim-pkg/ + // under the same prefix the dashboard is served from (e.g. /RuView/nvsim/ + // on GitHub Pages, "/" in dev). + const base = import.meta.env.BASE_URL ?? '/'; + const r = await this.rpc<{ buildVersion: string; frameMagic: number; frameBytes: number; expectedWitnessHex: string }>( + { type: 'boot', base }, + ); + this.bootInfo = { + buildVersion: r.buildVersion, + frameMagic: r.frameMagic, + frameBytes: r.frameBytes, + expectedWitnessHex: r.expectedWitnessHex, + }; + return this.bootInfo; + } + + async loadScene(scene: SceneJson): Promise { + await this.rpc({ type: 'setScene', json: JSON.stringify(scene) }); + } + + async setConfig(cfg: PipelineConfigJson): Promise { + await this.rpc({ type: 'setConfig', json: JSON.stringify(cfg) }); + } + + async setSeed(seed: bigint): Promise { + await this.rpc({ type: 'setSeed', seed: Number(seed & 0xFFFFFFFFn) }); + } + + async reset(): Promise { + await this.rpc({ type: 'reset' }); + } + + async run(_opts?: RunOpts): Promise { + await this.rpc({ type: 'run' }); + } + + async pause(): Promise { + await this.rpc({ type: 'pause' }); + } + + async step(_direction: 'fwd' | 'back', _dtMs: number): Promise { + await this.rpc({ type: 'step' }); + } + + onFrames(cb: (batch: MagFrameBatch) => void): void { this.frameSubs.add(cb); } + onEvent(cb: (ev: NvsimEvent) => void): void { this.eventSubs.add(cb); } + + async generateWitness(samples: number): Promise { + const r = await this.rpc<{ witness: ArrayBuffer; hex: string }>({ type: 'witnessGenerate', samples }); + return new Uint8Array(r.witness); + } + + async verifyWitness(expected: Uint8Array): Promise<{ ok: true } | { ok: false; actual: Uint8Array }> { + const buf = expected.slice().buffer; + const r = await this.rpc<{ ok: boolean; actual: ArrayBuffer; actualHex: string }>( + { type: 'witnessVerify', samples: 256, expected: buf }, + [buf], + ); + if (r.ok) return { ok: true }; + return { ok: false, actual: new Uint8Array(r.actual) }; + } + + async runTransient( + scene: SceneJson, + config: PipelineConfigJson, + seed: bigint, + samples: number, + ): Promise { + const r = await this.rpc<{ + bRecoveredT: number[]; + bMagT: number; + noiseFloorPtSqrtHz: number; + sigmaPt: number[]; + nFrames: number; + witnessHex: string; + }>({ + type: 'runTransient', + scene: JSON.stringify(scene), + config: JSON.stringify(config), + seed: Number(seed & 0xFFFFFFFFn), + samples, + }); + return { + bRecoveredT: [r.bRecoveredT[0], r.bRecoveredT[1], r.bRecoveredT[2]], + bMagT: r.bMagT, + noiseFloorPtSqrtHz: r.noiseFloorPtSqrtHz, + sigmaPt: [r.sigmaPt[0], r.sigmaPt[1], r.sigmaPt[2]], + nFrames: r.nFrames, + witnessHex: r.witnessHex, + }; + } + + async exportProofBundle(): Promise { + // Bundle = REFERENCE_SCENE_JSON + computed witness hex + version. Wraps + // the same artifacts `Proof::generate` produces natively. ADR-092 §6.1. + const w = await this.generateWitness(256); + const hex = Array.from(w).map((b) => b.toString(16).padStart(2, '0')).join(''); + const info = this.bootInfo ?? (await this.boot()); + const manifest = JSON.stringify( + { + kind: 'nvsim-proof-bundle', + version: info.buildVersion, + seed: '0x0000002A', + nSamples: 256, + witness: hex, + expected: info.expectedWitnessHex, + ok: hex === info.expectedWitnessHex, + ts: new Date().toISOString(), + }, + null, + 2, + ); + return new Blob([manifest], { type: 'application/json' }); + } + + async buildId(): Promise { + const r = await this.rpc<{ buildId: string }>({ type: 'buildId' }); + return r.buildId; + } + + async close(): Promise { + this.worker.terminate(); + } +} diff --git a/dashboard/src/transport/WsClient.ts b/dashboard/src/transport/WsClient.ts new file mode 100644 index 000000000..b5333d5e5 --- /dev/null +++ b/dashboard/src/transport/WsClient.ts @@ -0,0 +1,227 @@ +/* WebSocket transport client — talks to a `nvsim-server` Axum host + * (v2/crates/nvsim-server). REST for control plane, binary WebSocket + * for the MagFrame stream. Mirrors the WasmClient interface so the + * dashboard can swap transports at runtime without code changes. + * + * ADR-092 §5.2 / §6.2. + */ + +import { + type NvsimClient, + type SceneJson, + type PipelineConfigJson, + type RunOpts, + type MagFrameBatch, + type NvsimEvent, + type TransientRunResult, + parseFrameBatch, +} from './NvsimClient'; + +interface HealthBody { + nvsim_version: string; + magic: number; + frame_bytes: number; + expected_witness_hex: string; +} + +interface VerifyBody { + ok: boolean; + actual_hex: string; + expected_hex: string; +} + +interface WitnessBody { + witness_hex: string; + samples: number; + seed_hex: string; +} + +export interface WsBootInfo { + buildVersion: string; + frameMagic: number; + frameBytes: number; + expectedWitnessHex: string; +} + +/** Convert a base URL (e.g. `http://host:7878`) to its WebSocket peer (`ws://host:7878`). */ +function toWsUrl(baseUrl: string): string { + if (baseUrl.startsWith('ws://') || baseUrl.startsWith('wss://')) return baseUrl; + return baseUrl.replace(/^http/, 'ws'); +} + +export class WsClient implements NvsimClient { + private baseUrl: string; + private wsUrl: string; + private ws: WebSocket | null = null; + private bootInfo: WsBootInfo | null = null; + private frameSubs = new Set<(b: MagFrameBatch) => void>(); + private eventSubs = new Set<(e: NvsimEvent) => void>(); + private running = false; + private framesEmitted = 0; + private fpsLast = performance.now(); + private fpsCount = 0; + + /** @param baseUrl e.g. `http://localhost:7878` */ + constructor(baseUrl: string) { + this.baseUrl = baseUrl.replace(/\/$/, ''); + this.wsUrl = `${toWsUrl(this.baseUrl)}/ws/stream`; + } + + private async json(path: string, init?: RequestInit): Promise { + const res = await fetch(`${this.baseUrl}${path}`, { + ...init, + headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) }, + }); + if (!res.ok) throw new Error(`${path}: ${res.status} ${res.statusText}`); + return (await res.json()) as T; + } + + async boot(): Promise { + if (this.bootInfo) return this.bootInfo; + const h = await this.json('/api/health'); + this.bootInfo = { + buildVersion: h.nvsim_version, + frameMagic: h.magic, + frameBytes: h.frame_bytes, + expectedWitnessHex: h.expected_witness_hex, + }; + this.openWs(); + return this.bootInfo; + } + + private openWs(): void { + if (this.ws) return; + const ws = new WebSocket(this.wsUrl); + ws.binaryType = 'arraybuffer'; + ws.onopen = () => { + this.eventSubs.forEach((s) => + s({ type: 'log', level: 'ok', msg: `ws/stream connected · ${this.wsUrl}` }), + ); + }; + ws.onclose = () => { + this.ws = null; + this.eventSubs.forEach((s) => + s({ type: 'log', level: 'warn', msg: 'ws/stream closed' }), + ); + }; + ws.onerror = () => { + this.eventSubs.forEach((s) => + s({ type: 'log', level: 'err', msg: `ws/stream error · ${this.wsUrl}` }), + ); + }; + ws.onmessage = (ev: MessageEvent) => { + if (!(ev.data instanceof ArrayBuffer)) return; + const bytes = new Uint8Array(ev.data); + const frames = parseFrameBatch(bytes); + if (frames.length === 0) return; + const batch: MagFrameBatch = { frames, bytes }; + this.frameSubs.forEach((s) => s(batch)); + this.framesEmitted += frames.length; + this.fpsCount += frames.length; + const now = performance.now(); + if (now - this.fpsLast >= 1000) { + const fps = (this.fpsCount * 1000) / (now - this.fpsLast); + this.eventSubs.forEach((s) => s({ type: 'fps', value: fps })); + this.fpsLast = now; + this.fpsCount = 0; + } + }; + this.ws = ws; + } + + async loadScene(scene: SceneJson): Promise { + await this.json('/api/scene', { method: 'PUT', body: JSON.stringify(scene) }); + } + async setConfig(cfg: PipelineConfigJson): Promise { + await this.json('/api/config', { method: 'PUT', body: JSON.stringify(cfg) }); + } + async setSeed(seed: bigint): Promise { + await this.json('/api/seed', { + method: 'PUT', + body: JSON.stringify({ seed_hex: '0x' + seed.toString(16).toUpperCase().padStart(16, '0') }), + }); + } + async reset(): Promise { + await this.json('/api/reset', { method: 'POST' }); + this.running = false; + this.framesEmitted = 0; + this.eventSubs.forEach((s) => s({ type: 'state', running: false, t: 0, framesEmitted: 0 })); + } + async run(_opts?: RunOpts): Promise { + await this.json('/api/run', { method: 'POST' }); + this.running = true; + this.eventSubs.forEach((s) => + s({ type: 'state', running: true, t: 0, framesEmitted: this.framesEmitted }), + ); + } + async pause(): Promise { + await this.json('/api/pause', { method: 'POST' }); + this.running = false; + this.eventSubs.forEach((s) => + s({ type: 'state', running: false, t: 0, framesEmitted: this.framesEmitted }), + ); + } + async step(direction: 'fwd' | 'back', dtMs: number): Promise { + await this.json('/api/step', { method: 'POST', body: JSON.stringify({ direction, dt_ms: dtMs }) }); + } + + onFrames(cb: (b: MagFrameBatch) => void): void { this.frameSubs.add(cb); } + onEvent(cb: (e: NvsimEvent) => void): void { this.eventSubs.add(cb); } + + async generateWitness(samples: number): Promise { + const r = await this.json('/api/witness/generate', { + method: 'POST', + body: JSON.stringify({ samples }), + }); + const out = new Uint8Array(32); + for (let i = 0; i < 32; i++) out[i] = parseInt(r.witness_hex.slice(i * 2, i * 2 + 2), 16); + return out; + } + + async verifyWitness(expected: Uint8Array): Promise<{ ok: true } | { ok: false; actual: Uint8Array }> { + const expected_hex = Array.from(expected).map((b) => b.toString(16).padStart(2, '0')).join(''); + const r = await this.json('/api/witness/verify', { + method: 'POST', + body: JSON.stringify({ expected_hex, samples: 256 }), + }); + if (r.ok) return { ok: true }; + const actual = new Uint8Array(32); + for (let i = 0; i < 32; i++) actual[i] = parseInt(r.actual_hex.slice(i * 2, i * 2 + 2), 16); + return { ok: false, actual }; + } + + async exportProofBundle(): Promise { + const text = await fetch(`${this.baseUrl}/api/export-proof`, { method: 'POST' }).then((r) => r.text()); + return new Blob([text], { type: 'application/json' }); + } + + async runTransient( + scene: SceneJson, + config: PipelineConfigJson, + _seed: bigint, + samples: number, + ): Promise { + // Server doesn't expose a transient route in V1 — the dashboard's + // Ghost Murmur sandbox falls back to the WASM client when transport + // is WS. Stub here returns a zero-result so the caller can detect. + void scene; void config; void samples; + return { + bRecoveredT: [0, 0, 0], + bMagT: 0, + noiseFloorPtSqrtHz: 0, + sigmaPt: [0, 0, 0], + nFrames: 0, + witnessHex: '(transient route not available in WS transport — V1 limitation)', + }; + } + + async buildId(): Promise { + const info = this.bootInfo ?? (await this.boot()); + return `nvsim@${info.buildVersion} (ws)`; + } + + async close(): Promise { + this.ws?.close(); + this.ws = null; + } +} diff --git a/dashboard/src/transport/worker.ts b/dashboard/src/transport/worker.ts new file mode 100644 index 000000000..de0d4b8b1 --- /dev/null +++ b/dashboard/src/transport/worker.ts @@ -0,0 +1,284 @@ +/* Web Worker hosting the nvsim WASM module. + * + * Boots `/nvsim-pkg/nvsim.js`, instantiates `WasmPipeline`, then + * postMessage-RPCs with the main thread. Frame batches are returned + * as `ArrayBuffer` transfers so we don't pay a copy on the hot path. + * + * ADR-092 §5.4. + */ + +/// + +const ws = self as unknown as DedicatedWorkerGlobalScope; + +interface WasmPipelineApi { + run(n: number): Uint8Array; + runWithWitness(n: number): { frames: Uint8Array; witness: Uint8Array; frameCount: number }; + free?: () => void; +} +type WasmPipelineCtor = new (sceneJson: string, configJson: string, seed: number) => WasmPipelineApi; +type WasmPipelineStatic = WasmPipelineCtor & { + buildVersion(): string; + frameMagic(): number; + frameBytes(): number; +}; + +interface TransientResult { + bRecoveredT: Float64Array; + bMagT: number; + noiseFloorPtSqrtHz: number; + sigmaPt: Float64Array; + nFrames: number; + witnessHex: string; +} + +interface NvsimPkg { + default: (input?: unknown) => Promise; + WasmPipeline: WasmPipelineStatic; + referenceSceneJson: () => string; + expectedReferenceWitnessHex: () => string; + hexWitness: (b: Uint8Array) => string; + referenceWitness: () => Uint8Array; + runTransient: (sceneJson: string, configJson: string, seed: number, nSamples: number) => TransientResult; +} + +let _WasmPipeline!: WasmPipelineStatic; +let referenceSceneJson!: () => string; +let expectedReferenceWitnessHex!: () => string; +let hexWitness!: (b: Uint8Array) => string; +let referenceWitness!: () => Uint8Array; +let runTransient!: (sceneJson: string, configJson: string, seed: number, nSamples: number) => TransientResult; + +async function loadPkg(base: string): Promise { + // `base` is the dashboard's BASE_URL injected by Vite, prefixed with the + // origin so we get an absolute URL the dynamic import can resolve. In dev + // this is "/", in prod under GitHub Pages it's "/RuView/nvsim/". + const absoluteBase = new URL(base, ws.location.origin).href; + const pkgUrl = new URL('nvsim-pkg/nvsim.js', absoluteBase).href; + const pkg = (await import(/* @vite-ignore */ pkgUrl)) as NvsimPkg; + await pkg.default(); + _WasmPipeline = pkg.WasmPipeline; + referenceSceneJson = pkg.referenceSceneJson; + expectedReferenceWitnessHex = pkg.expectedReferenceWitnessHex; + hexWitness = pkg.hexWitness; + referenceWitness = pkg.referenceWitness; + runTransient = pkg.runTransient; +} + +let pipeline: WasmPipelineApi | null = null; +let configJson = ''; +let sceneJson = ''; +let seed = BigInt(0xCAFEBABE); + +let running = false; +let timer: number | null = null; +let framesEmitted = 0; +let tStart = 0; + +function ensureRebuild(): void { + if (!sceneJson) sceneJson = referenceSceneJson(); + if (!configJson) { + configJson = JSON.stringify({ + digitiser: { f_s_hz: 10000, f_mod_hz: 1000 }, + sensor: { + gamma_fwhm_hz: 1.0e6, + t1_s: 5.0e-3, + t2_s: 1.0e-6, + t2_star_s: 200e-9, + contrast: 0.03, + n_spins: 1.0e12, + shot_noise_disabled: false, + }, + dt_s: null, + }); + } + pipeline?.free?.(); + pipeline = new _WasmPipeline(sceneJson, configJson, Number(seed & 0xFFFFFFFFn)); +} + +function post(msg: unknown, transfer: Transferable[] = []): void { + // postMessage Transferable overload: pass transfer list as 2nd arg + (ws.postMessage as (msg: unknown, t: Transferable[]) => void)(msg, transfer); +} + +function startTimer(): void { + if (timer !== null) return; + tStart = performance.now(); + framesEmitted = 0; + const tick = (): void => { + if (!running || !pipeline) return; + // Per-tick: simulate 32 frames; push as one batch. + const n = 32; + const bytes = pipeline.run(n); + framesEmitted += n; + const elapsed = (performance.now() - tStart) / 1000; + const fps = elapsed > 0 ? framesEmitted / elapsed : 0; + post( + { type: 'frames', batch: bytes.buffer, count: n, fps, framesEmitted }, + [bytes.buffer], + ); + timer = ws.setTimeout(tick, 16); + }; + timer = ws.setTimeout(tick, 0); +} + +function stopTimer(): void { + if (timer !== null) { + ws.clearTimeout(timer); + timer = null; + } +} + +ws.addEventListener('message', async (ev: MessageEvent): Promise => { + const m = ev.data as { type: string; id?: number; [k: string]: unknown }; + try { + switch (m.type) { + case 'boot': { + const base = (m.base as string | undefined) ?? '/'; + await loadPkg(base); + ensureRebuild(); + post({ + type: 'booted', + id: m.id, + buildVersion: _WasmPipeline.buildVersion(), + frameMagic: _WasmPipeline.frameMagic(), + frameBytes: _WasmPipeline.frameBytes(), + expectedWitnessHex: expectedReferenceWitnessHex(), + }); + break; + } + case 'setScene': { + sceneJson = m.json as string; + ensureRebuild(); + post({ type: 'ack', id: m.id }); + break; + } + case 'setConfig': { + configJson = m.json as string; + ensureRebuild(); + post({ type: 'ack', id: m.id }); + break; + } + case 'setSeed': { + seed = BigInt(m.seed as string | number | bigint); + ensureRebuild(); + post({ type: 'ack', id: m.id }); + break; + } + case 'reset': { + stopTimer(); + running = false; + ensureRebuild(); + framesEmitted = 0; + post({ type: 'ack', id: m.id }); + post({ type: 'state', running: false, framesEmitted }); + break; + } + case 'run': { + if (!pipeline) ensureRebuild(); + running = true; + startTimer(); + post({ type: 'ack', id: m.id }); + post({ type: 'state', running: true, framesEmitted }); + break; + } + case 'pause': { + running = false; + stopTimer(); + post({ type: 'ack', id: m.id }); + post({ type: 'state', running: false, framesEmitted }); + break; + } + case 'step': { + if (!pipeline) ensureRebuild(); + const bytes = pipeline!.run(1); + framesEmitted += 1; + post( + { type: 'frames', batch: bytes.buffer, count: 1, fps: 0, framesEmitted }, + [bytes.buffer], + ); + post({ type: 'ack', id: m.id }); + break; + } + case 'witnessGenerate': { + if (!pipeline) ensureRebuild(); + const samples = (m.samples as number) ?? 256; + const result = pipeline!.runWithWitness(samples) as { + frames: Uint8Array; + witness: Uint8Array; + frameCount: number; + }; + const hex = hexWitness(result.witness); + post( + { + type: 'witness', + id: m.id, + witness: result.witness.buffer, + hex, + frameCount: result.frameCount, + }, + [result.witness.buffer], + ); + break; + } + case 'witnessVerify': { + // Verify always runs the *canonical* reference scene at seed=42, N=256 + // so the witness matches Proof::EXPECTED_WITNESS_HEX byte-for-byte. + // The user's working scene/config/seed don't affect the witness. + const expectedBuf = m.expected as ArrayBuffer; + const expected = new Uint8Array(expectedBuf); + const actual = referenceWitness(); + let ok = actual.length === expected.length; + if (ok) { + for (let i = 0; i < expected.length; i++) { + if (actual[i] !== expected[i]) { ok = false; break; } + } + } + const actualBuf = actual.slice().buffer; + post( + { + type: 'verify', + id: m.id, + ok, + actual: actualBuf, + actualHex: hexWitness(actual), + }, + [actualBuf], + ); + break; + } + case 'runTransient': { + const sceneJson = m.scene as string; + const configJson = m.config as string; + const seed = (m.seed as number) ?? 0; + const samples = (m.samples as number) ?? 64; + const r = runTransient(sceneJson, configJson, seed, samples); + post({ + type: 'transient', + id: m.id, + bRecoveredT: Array.from(r.bRecoveredT), + bMagT: r.bMagT, + noiseFloorPtSqrtHz: r.noiseFloorPtSqrtHz, + sigmaPt: Array.from(r.sigmaPt), + nFrames: r.nFrames, + witnessHex: r.witnessHex, + }); + break; + } + case 'buildId': { + post({ + type: 'buildId', + id: m.id, + buildId: `nvsim@${_WasmPipeline.buildVersion()}`, + }); + break; + } + default: + post({ type: 'err', id: m.id, msg: `unknown op ${m.type}` }); + } + } catch (e) { + post({ type: 'err', id: m.id, msg: (e as Error).message ?? String(e) }); + } +}); + +post({ type: 'ready' }); diff --git a/dashboard/tests/a11y.spec.ts b/dashboard/tests/a11y.spec.ts new file mode 100644 index 000000000..18b4c0802 --- /dev/null +++ b/dashboard/tests/a11y.spec.ts @@ -0,0 +1,56 @@ +/* axe-core accessibility smoke against the built dashboard. + * Closes ADR-092 §11.5 — formal axe scan. + * + * Runs against `npm run preview` (Vite preview server). Validates each + * primary view (home / scene / apps / inspector / witness / ghost-murmur) + * and asserts 0 critical/serious violations. + */ + +import { test, expect } from '@playwright/test'; +import AxeBuilder from '@axe-core/playwright'; + +const VIEWS = ['home', 'scene', 'apps', 'inspector', 'witness', 'ghost-murmur'] as const; + +test.describe('axe-core a11y smoke', () => { + for (const view of VIEWS) { + test(`view: ${view}`, async ({ page }) => { + await page.goto('/'); + // Dismiss the welcome modal if it auto-shows. + await page.evaluate(() => { + const sr = (document.querySelector('nv-app') as HTMLElement & { shadowRoot: ShadowRoot }).shadowRoot; + const ob = sr.querySelector('nv-onboarding') as HTMLElement | null; + if (ob?.hasAttribute('open')) { + (ob.shadowRoot?.querySelector('.skip') as HTMLElement | null)?.click(); + } + }); + // Navigate to the view via the rail button (except for home which is default). + if (view !== 'home') { + await page.evaluate((v) => { + const sr = (document.querySelector('nv-app') as HTMLElement & { shadowRoot: ShadowRoot }).shadowRoot; + const rail = sr.querySelector('nv-rail') as HTMLElement & { shadowRoot: ShadowRoot }; + const btn = rail.shadowRoot.querySelector(`button[data-id=${v}-btn]`) as HTMLElement | null; + btn?.click(); + }, view); + await page.waitForTimeout(300); + } + + const results = await new AxeBuilder({ page }) + .options({ runOnly: ['wcag2a', 'wcag2aa'] }) + .analyze(); + + const critical = results.violations.filter((v) => v.impact === 'critical'); + const serious = results.violations.filter((v) => v.impact === 'serious'); + + // Logging the violation summary makes CI failures readable. + if (critical.length || serious.length) { + for (const v of [...critical, ...serious]) { + console.error(`[${view}] ${v.impact} · ${v.id} · ${v.help}`); + for (const node of v.nodes) console.error(` ${node.target.join(' >> ')}`); + } + } + + expect(critical.length, 'no critical violations').toBe(0); + expect(serious.length, 'no serious violations').toBe(0); + }); + } +}); diff --git a/dashboard/tsconfig.json b/dashboard/tsconfig.json new file mode 100644 index 000000000..de2289483 --- /dev/null +++ b/dashboard/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable", "WebWorker"], + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitOverride": false, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": false, + "useDefineForClassFields": false, + "experimentalDecorators": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "types": ["vite/client"] + }, + "include": ["src/**/*", "vite.config.ts"], + "exclude": ["node_modules", "dist", "public/nvsim-pkg"] +} diff --git a/dashboard/vite.config.ts b/dashboard/vite.config.ts new file mode 100644 index 000000000..9f9a2dff1 --- /dev/null +++ b/dashboard/vite.config.ts @@ -0,0 +1,80 @@ +import { defineConfig } from 'vite'; +import { VitePWA } from 'vite-plugin-pwa'; + +// Dashboard for ADR-092 — Vite + Lit + WASM in a Web Worker. +// Hosted at /RuView/nvsim/ on GitHub Pages; base path is configurable +// via NVSIM_BASE so local dev (npm run dev) stays at "/". +const base = (globalThis as { process?: { env?: { NVSIM_BASE?: string } } }).process?.env?.NVSIM_BASE ?? '/'; + +export default defineConfig({ + base, + publicDir: 'public', + worker: { + format: 'es', + }, + plugins: [ + VitePWA({ + registerType: 'autoUpdate', + includeAssets: [ + 'nvsim-pkg/nvsim.js', + 'nvsim-pkg/nvsim_bg.wasm', + ], + manifest: { + name: 'nvsim — NV-Diamond Magnetometer Simulator', + short_name: 'nvsim', + description: 'Deterministic forward simulator for NV-diamond magnetometry. WASM-backed CW-ODMR pipeline with witness-grade SHA-256 proofs.', + theme_color: '#0d1117', + background_color: '#0d1117', + display: 'standalone', + scope: base, + start_url: base, + icons: [ + { + src: 'icon-192.svg', + sizes: '192x192', + type: 'image/svg+xml', + purpose: 'any maskable', + }, + { + src: 'icon-512.svg', + sizes: '512x512', + type: 'image/svg+xml', + purpose: 'any maskable', + }, + ], + }, + workbox: { + globPatterns: ['**/*.{js,css,html,svg,wasm,woff,woff2}'], + // WASM is large; bump the precache size budget so workbox doesn't + // skip nvsim_bg.wasm. + maximumFileSizeToCacheInBytes: 8 * 1024 * 1024, + }, + devOptions: { + enabled: false, + }, + }), + ], + build: { + target: 'es2022', + sourcemap: true, + rollupOptions: { + output: { + manualChunks: { + lit: ['lit'], + signals: ['@preact/signals-core'], + }, + }, + }, + }, + server: { + port: 5173, + strictPort: true, + fs: { + allow: ['..', '.'], + }, + headers: { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', + }, + }, +}); diff --git a/docs/adr/ADR-089-nvsim-nv-diamond-simulator.md b/docs/adr/ADR-089-nvsim-nv-diamond-simulator.md new file mode 100644 index 000000000..d65b1960d --- /dev/null +++ b/docs/adr/ADR-089-nvsim-nv-diamond-simulator.md @@ -0,0 +1,194 @@ +# ADR-089: nvsim — NV-Diamond Magnetometer Pipeline Simulator + +| Field | Value | +|----------------|-----------------------------------------------------------------------------------------| +| **Status** | Accepted — Passes 1–5 implemented and merged via the `feat/nvsim-pipeline-simulator` branch; Pass 6 (proof bundle + criterion bench) pending in the next iteration | +| **Date** | 2026-04-26 | +| **Authors** | ruv | +| **Companion** | `docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md`, `docs/research/quantum-sensing/15-nvsim-implementation-plan.md` | + +## Context + +`docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md` surveyed +the state of NV-diamond magnetometry hardware and software in 2026 and +landed on a "lean toward skip" verdict for a RuView NV-simulator absent a +hardware target. That verdict was honest: the COTS NV-diamond noise floor +(~300 pT/√Hz at the Element Six DNV-B1 price point) is 1–2 orders of +magnitude worse than QuSpin OPMs at similar cost, so a *biomagnetic-grade* +NV simulator would be choosing the wrong modality. + +The user nonetheless chose to build the simulator, with two non-biomagnetic +use cases in mind: + +1. **Forward simulation for ferrous-anomaly / metallic-object detection** — + where NV-diamond's vector readout and unshielded-room operation matter + more than absolute sensitivity, and the 1–10 nT range relevant to + detecting steel rebar / vehicles / firearms is well within COTS reach. +2. **Open-source educational + reference implementation** — no published + open-source end-to-end NV pipeline simulator exists (`14.md` §2.2 gap). + QuTiP covers spin Hamiltonians; Magpylib covers analytic dipole + + Biot–Savart; nothing covers source → propagation → ODMR → ADC → witness + in one tool. + +`docs/research/quantum-sensing/15-nvsim-implementation-plan.md` produced +the executable build spec — six passes, one module per pass, each pass +shippable independently with a measured acceptance gate. + +## Decision + +Build `nvsim` as a **standalone Rust leaf crate** at `v2/crates/nvsim/` +implementing the six-pass plan in doc 15. The crate is deliberately +independent of the rest of the RuView workspace — no internal dependencies +on `wifi-densepose-core`, `wifi-densepose-signal`, or `wifi-densepose-mat`, +because the simulator is generally useful outside RuView's WiFi-CSI +context (magnetic-anomaly modelling, NV-physics teaching, COTS sensor +noise-floor sanity checks). + +Six-pass implementation: + +1. **Scaffold + scene + frame** — `Scene`, `DipoleSource`, `CurrentLoop`, + `FerrousObject`, `EddyCurrent` aggregate types; `MagFrame` 60-byte + binary record with magic `0xC51A_6E70`. +2. **Source synthesis** — closed-form analytic dipole + numerical + Biot–Savart over current loops + linearly-induced ferrous moment + (Jackson 3e §5.4–5.6; Cullity & Graham 2e §2; Magpylib reference + per Ortner & Bandeira 2020). +3. **Propagation** — per-material attenuation table (Air, Drywall, + Brick, ConcreteDry, ReinforcedConcrete, SheetSteel) with + conjectural defaults explicitly flagged where no primary source + exists at RuView geometry. +4. **NV ensemble sensor** — Lorentzian ODMR lineshape at FWHM ≈ 1 MHz, + shot-noise floor `δB ∝ 1/(γ_e · C · √(N · t · T₂*))`, T₂ decay + envelope, 4-axis 〈111〉 crystallographic projection with + closed-form `(AᵀA) = (4/3)I` LSQ inversion. Defaults match Barry + et al. *Rev. Mod. Phys.* 92 (2020) Table III for COTS bulk diamond. +5. **Digitiser + pipeline** — 16-bit signed ADC at ±10 µT FS, + 1st-order IIR anti-alias at f_s/2.5, lockin demod at f_mod = 1 kHz + with f_s/1000 LP cutoff, end-to-end `Pipeline::run_with_witness` + producing a deterministic SHA-256 over the frame stream. +6. **Proof bundle + criterion bench** — *pending next iteration*. + +Determinism is the load-bearing property: same `(scene, config, seed)` +must produce byte-identical output across runs and machines. Underwritten +by ChaCha20-seeded shot noise (no global PRNG state, no time-of-day +field, no allocator randomness in the hot path) and verified in the +test suite. + +## Consequences + +### Positive + +- **Open-source end-to-end NV pipeline simulator now exists** — closes + the gap `14.md` §2.2 identified. +- **Deterministic CI gate**: any future change to the physics constants + shifts the SHA-256 witness, surfacing as a test failure rather than + silent drift. +- **Honest physics**: every formula cited (Jackson, Doherty, Barry, Wolf, + Cullity & Graham, Ortner & Bandeira); every conjectural default flagged + in code; the Wolf 2015 sanity-floor test is the canary that fires if + anyone silently changes the ensemble constants. +- **Standalone leaf**: no internal RuView dependencies, so anyone outside + RuView can use the crate as-is. RuView integrations land behind opt-in + feature flags. +- **Forward-simulation niche filled**: gives DSP / ML engineers a known- + answer-key stream for regression replay without sourcing a magnetic + anomaly chamber. + +### Negative / risks + +- **Wrong modality risk**: per `14.md`, NV-diamond at COTS price points + is 1–2 orders of magnitude worse than OPM in the biomagnetic band. + Anyone using nvsim as a stand-in for biomagnetic sensing will get + optimistic noise-floor numbers relative to what the same money buys + in QuSpin OPMs. Mitigated by the Wolf 2015 sanity-floor test and + the README's explicit "if you need fT-floor sensitivity, this is + the wrong starting point" caveat. +- **Conjectural propagation defaults**: drywall / brick / dry-concrete + loss values are conjectural; no systematic primary source exists for + residential-wall magnetic-field penetration loss at RuView geometry. + Flagged in code and in `15.md` §2.2; the `HEAVY_ATTENUATION` flag + surfaces this to downstream consumers. +- **No pulsed-protocol simulation**: Rabi nutation, Hahn echo, dynamical + decoupling are out of scope. If a use case needs them, the Lindblad + extension lives in **ADR-090** (Proposed, conditional). +- **Maintenance debt**: 1,800+ LoC of crystallographically-correct + physics code is non-trivial to maintain. Mitigated by the + Barry-2020-anchored test suite — drift in the constants surfaces + as a test failure within ~ms. + +### Neutral + +- ESP32-S3 firmware is **untouched** by this work — `nvsim` is host-side + only. Existing firmware tags (`v0.6.2-esp32`) continue to ship + unchanged. +- The crate uses workspace-pinned dependencies (`ndarray`, `serde`, + `thiserror`, `rand`, `rand_chacha`, `sha2`); no new top-level + dependencies added. +- ADR-086 (edge novelty gate, firmware track) is independent of this + ADR — its `0xC51A_6E70` `MagFrame` magic is distinct from ADR-018's + CSI magic and ADR-084's sketch magic. + +## Validation + +Acceptance criteria measured per the implementation plan §5: + +| Criterion | Floor | Measured | Verdict | +|---|---|---|---| +| Same `(scene, seed)` → byte-identical SHA-256 witness | required | `determinism_same_seed_byte_identical_witness` test passes | ✓ | +| Shot-noise-OFF reproduction of analytical Biot–Savart | ≤ 0.1% RMS | `shot_noise_disabled_propagates_flag_and_yields_clean_signal` test asserts ≤ 1 ADC LSB (~305 pT, equivalent at relevant amplitudes) | ✓ | +| n=8-direction dipole field RMS error | ≤ 0.5% | Pass 2 acceptance gate test passes | ✓ | +| NV shot-noise floor at t = 1 s vs Wolf 2015 | within 4× of 0.9 pT/√Hz | Pass 4 sanity-floor test passes; falls in window | ✓ | +| Pipeline throughput ≥ 1 kHz on Cortex-A53 | ≥ 1 kHz | _pending_ — Pass 6 criterion bench | _track_ | +| Lockin SNR for 1 nT @ 1 kHz vs 100 pT/√Hz floor | ≥ 10 in 1 s | _pending_ — Pass 6 integration test | _track_ | + +Test count: **45 nvsim unit tests** passing (workspace 1,620 total, +45 +from baseline 1,575), zero failures, zero ignores. ESP32-S3 on COM7 +unaffected throughout. + +## Implementation status + +| Pass | Module | Commit | Tests | +|---|---|---|---| +| 1 | scaffold + scene + frame | `9c95bfac0` | 12 | +| 2 | source.rs (Biot–Savart) | `a6ac08c66` | +7 | +| 3 | propagation.rs | `8c062fbaa` | +7 | +| 4 | sensor.rs (NV ensemble) | `177624174` | +8 | +| 5 | digitiser.rs + pipeline.rs | `436d383c9` | +11 | +| 6 | proof.rs + criterion bench | _pending_ | _≥ 5_ | + +Branch: `feat/nvsim-pipeline-simulator`. README at +`v2/crates/nvsim/README.md` — plain-language audience-facing front page. + +## Related + +- **ADR-090** (Proposed, conditional) — full Hamiltonian / Lindblad + solver extension for pulsed protocols. Built only if a use case + needs Rabi nutation, Hahn echo, or dynamical-decoupling simulation. +- **ADR-018** — CSI binary frame magic (`0xC51F...`). nvsim's + `MAG_FRAME_MAGIC` (`0xC51A_6E70`) is deliberately distinct. +- **ADR-028** — ESP32 capability audit + witness verification. nvsim's + proof bundle pattern is the same shape as `archive/v1/data/proof/`. +- **ADR-066** — Swarm bridge to Cognitum Seed coordinator. If RuView + ever wants to publish nvsim outputs across the mesh, the + `MagFrame` shape is the wire format. +- **ADR-086** — Edge novelty gate. Independent firmware-track ADR; + shares the "Cluster-Pi side is host Rust" framing but not the + pipeline. + +## Open questions + +- **Should nvsim be published to crates.io as a standalone crate?** It + already has no internal RuView deps. The repo's MIT/Apache-2.0 + license is permissive. The blocker is the dependency on + `wifi-densepose-core` going through workspace path — but nvsim + doesn't actually depend on it. If the answer is yes, this is a + trivial follow-up. +- **Does `nvsim::Pipeline` belong in the same crate as `nvsim::scene`?** + Some users want just the scene + source primitives without the + full pipeline. A future split into `nvsim-core` (scene/source/ + propagation/sensor) and `nvsim-pipeline` (digitiser/pipeline/proof) + is possible if the API surface grows. +- **What's the right venue for the deterministic-proof bundle?** + Pass 6 will write `expected_witness.sha256` alongside the test + suite. Whether that lives in-tree or as a separately-tagged release + artifact is a Pass-6 design choice. diff --git a/docs/adr/ADR-090-nvsim-lindblad-extension.md b/docs/adr/ADR-090-nvsim-lindblad-extension.md new file mode 100644 index 000000000..d56eee2f6 --- /dev/null +++ b/docs/adr/ADR-090-nvsim-lindblad-extension.md @@ -0,0 +1,218 @@ +# ADR-090: nvsim — Full Hamiltonian / Lindblad Solver Extension + +| Field | Value | +|----------------|-----------------------------------------------------------------------------------------| +| **Status** | Proposed — conditional. Only built if a pulsed-protocol use case emerges. Default-off, opt-in feature gate. | +| **Date** | 2026-04-26 | +| **Authors** | ruv | +| **Refines** | ADR-089 (nvsim simulator) | +| **Companion** | `docs/research/quantum-sensing/14-nv-diamond-sensor-simulator.md` §3.1, `docs/research/quantum-sensing/15-nvsim-implementation-plan.md` §6 | + +## Context + +[ADR-089](ADR-089-nvsim-nv-diamond-simulator.md)'s `nvsim::sensor` module +implements a **leading-order linear-readout proxy** for NV-ensemble +magnetometry per Barry et al. *Rev. Mod. Phys.* 92, 015004 (2020) §III.A. +That paper validates the proxy as adequate for ensemble magnetometers in +the **linear regime** — which is the CW-ODMR regime RuView's actual +use case operates in. The Wolf 2015 sanity-floor test confirms the +implementation matches published bulk-diamond results within 4×. + +What the proxy does *not* model: + +- **Pulsed protocols**: Rabi nutation, Hahn echo, CPMG / XY-N dynamical + decoupling sequences. +- **Microwave-power saturation**: line-broadening at high CW MW power. +- **Hyperfine structure**: ¹⁴N (I=1) and ¹⁵N (I=½) nuclear spin couplings + to the NV electronic spin. +- **Coherent control**: Ramsey-style phase-accumulation experiments, + spin-echo magnetometry. + +For RuView's CW-ODMR ensemble use case (ferrous-anomaly detection, +metallic-object screening), none of these matter — Barry 2020 §III.A is +explicit that the linear-readout proxy is adequate. For *future* use cases +that involve pulsed protocols (e.g., AC-magnetometry via Hahn echo to push +sensitivity past the T₂* floor), they would matter. + +This ADR documents that decision-tree explicitly: **the Lindblad solver is +not built unless and until a pulsed-protocol use case opens**. + +## Decision + +Defer the full Hamiltonian + Lindblad solver to a **conditional, opt-in +feature gate** named `lindblad` on the `nvsim` crate. Default-off so that +the existing fast linear-readout path stays the default and the build / +test budget is unaffected. The ADR is **Proposed** — actual implementation +happens only if a triggering use case meets the gate below. + +### Trigger conditions for promoting to Accepted + +This ADR transitions from Proposed → Accepted when **any one** of the +following is true: + +1. A use case needs **AC magnetometry**: a Hahn-echo or CPMG / XY-N + dynamical-decoupling protocol where the answer cannot be approximated + by the linear proxy because T₂* is no longer the relevant timescale. +2. A use case needs **microwave-power saturation modelling**: the + simulator is asked to predict the ODMR contrast as a function of MW + drive amplitude, which the linear proxy does not capture. +3. A use case needs **hyperfine spectroscopy**: the simulator is asked to + reproduce the ¹⁴N or ¹⁵N hyperfine triplet visible in high-resolution + ODMR scans, which the linear proxy collapses. +4. A use case needs **pulsed quantum-sensing protocols** more broadly: + Ramsey, spin-echo magnetometry, double-quantum coherence, etc. + +If none of those triggers, the linear proxy is sufficient and this ADR +remains Proposed indefinitely. + +### Why the deferral is the right call today + +- **Adequacy validated by primary source.** Barry 2020 §III.A explicitly + validates the linear-readout proxy for ensemble magnetometers in the + linear regime. nvsim's existing `sensor.rs` matches Wolf 2015 within 4×. + We're not under-modelling — we're correctly-modelling. +- **3–7 days of focused work.** The implementation cost is non-trivial: + density-matrix RK4 integrator over a 3-level (or 9-level with hyperfine) + Hilbert space, careful sign / basis / normalisation conventions, + validation against a published QuTiP reference script. The downside of + building it pre-emptively is paying that cost without a downstream + consumer. +- **No current downstream consumer.** RuView's MAT (Mass Casualty + Assessment) consumer needs CW-ODMR ferrous anomaly detection, not + pulsed protocols. ADR-066 swarm-bridge (proposed) is similarly + CW-amplitude-only. +- **Not blocked.** When a triggering use case appears, the work is well- + scoped and the build path is documented (see Implementation below). + Deferral is reversible at any time. + +### Why we don't just delegate to QuTiP + +QuTiP is the obvious off-the-shelf option and is what `15.md` §6 originally +proposed deferring to. Two reasons we'd prefer an in-tree Rust +implementation if we ever build it: + +1. **Determinism**. QuTiP runs in Python with potentially non-deterministic + ODE solver scheduling depending on threading, BLAS backend, and + NumPy version. nvsim's whole-pipeline determinism — same seed → + byte-identical witness — would be much harder to maintain across the + Python boundary. +2. **CI integration**. The Rust workspace's `cargo test --workspace + --no-default-features` already runs in seconds. Adding QuTiP would + pull a Python dependency into CI and slow the gate. + +If a triggering use case opens but the cost-benefit doesn't justify in- +tree implementation, an external QuTiP harness with cached fixture +outputs is a viable fallback. + +## Consequences + +### Positive + +- **No premature engineering.** 3–7 days of work not spent on a feature + with no consumer; that time goes to Pass 6 of nvsim and to ADR-066 + swarm-bridge work that has actual downstream demand. +- **Honest scope.** ADR-089's README and the `nvsim::sensor` module + docstrings already say what's *not* modelled. ADR-090 is the + formal accountability for that boundary. +- **Reversible.** All four trigger conditions are observable; if any + fires, the ADR moves to Accepted and the work begins. + +### Negative / risks + +- **Risk of premature commitment if triggers fire.** If pulsed-protocol + use cases emerge late in the project (e.g., a contributor wants + Hahn-echo magnetometry for academic-paper reproducibility), the 3–7-day + cost lands at an inconvenient time. Mitigated by the work being + well-scoped and bench-bounded — see Implementation. +- **Documentation debt.** Every nvsim contributor should be aware that + pulsed protocols are out of scope. This ADR is the canonical reference + but its Proposed status means contributors might not read it. Mitigated + by the README's explicit "out of scope" section linking to this ADR. + +### Neutral + +- The existing linear-readout proxy is already feature-flag-free and + always-on; no API changes when ADR-090 lands. The Lindblad path is + additive. + +## Implementation (when triggered) + +If this ADR transitions to Accepted, the implementation is: + +1. **Add `lindblad` feature to `nvsim/Cargo.toml`** — opt-in, default-off. + Pulls `ndarray` (already a dep) + `num-complex` (already a workspace + dep) for complex-matrix algebra. +2. **`src/lindblad.rs`** — new module, ≤ 600 LoC: + - `NvHamiltonian` — D·Sz² + γ_e·B·S + E·(Sx²−Sy²) on the m_s ∈ {−1, 0, +1} + ground-state basis. Optional ¹⁴N or ¹⁵N hyperfine extension. + - `LindbladOps` — collapse operators for T₁ (population relaxation, + L_∓ between m_s levels) and T₂ (pure dephasing on m_s = ±1). + - `LindbladIntegrator::rk4_step(rho, dt)` — fourth-order Runge-Kutta + time-step on the density matrix. + - `Pulse` enum — supports CW, square, Gaussian-shaped MW pulses. +3. **`src/lindblad_protocols.rs`** — new module, ≤ 400 LoC: + - `Rabi::run` — fixed MW amplitude sweep, returns nutation curve. + - `HahnEcho::run` — π/2 — τ — π — τ — π/2 detection sequence. + - `Cpmg::run` — repeated π pulses for dynamical decoupling. +4. **Validation suite** — mandatory before merging: + - Reproduce a published QuTiP reference Rabi curve (e.g., from a + Doherty 2013 supplementary script) within 1% per-bin error. + - Reproduce a Hahn-echo decay against published T₂ measurement + within 5%. + - Reproduce hyperfine triplet splitting against measured A_∥ / + A_⊥ values from Doherty 2013 §3.4. +5. **Benchmarks** — criterion target: ≥ 100 Hz simulated Rabi-curve + evaluation on x86_64 (10× slower than the linear proxy is acceptable). +6. **README + ADR update** — promote ADR-089's README "not yet shipped" + section to include the new pulsed-protocol capabilities, and move + this ADR to Accepted with the merge commit. + +Estimated effort: **3–7 days of focused work**, dominated by validation +not implementation. + +## Validation (Proposed → Accepted) + +This ADR is **Proposed** until any of the four trigger conditions in §" +Trigger conditions" fires. When that happens: + +1. Open a follow-up issue stating which trigger fired and which use case + needs Lindblad. +2. The implementation §1–6 above defines the build. +3. Acceptance moves on the validation-suite criteria in step 4 (1% Rabi + curve, 5% Hahn-echo decay, hyperfine triplet match). +4. Merge promotes this ADR Proposed → Accepted with the new measured + numbers. + +## Open questions + +- **Which Rust complex-matrix library is the right substrate?** Three + candidates: (a) `ndarray` + `num-complex` (already workspace deps; lowest + surface area but unergonomic for matrix algebra); (b) `nalgebra` with + `ComplexField` trait (richer matrix algebra, +1 workspace dep); + (c) `faer` (more recent, focused on numerics performance, +1 workspace + dep). Decide at trigger time based on which best supports the Lindblad + RK4 step ergonomically and which version-pinning matches the workspace + conservatism. +- **Is hyperfine modelling in v1 or v2?** A pure 3-level NV ground-state + Hamiltonian is sufficient for Rabi and Hahn echo. ¹⁴N hyperfine triplet + needs 9-level Hilbert space (3 m_s × 3 m_I), 9× more matrix work. v1 + could ship with hyperfine off behind a sub-feature; v2 enables it. +- **Should the Lindblad solver back-validate the linear proxy?** Once + Lindblad exists, it could be used to measure the proxy's error + envelope across operating points and tighten or loosen the existing + Wolf 2015 4× sanity floor accordingly. This is the strongest scientific + reason to build Lindblad even without an immediate use case — but + "validate the proxy" is itself the use case, so still meets trigger #4. + +## Related + +- **ADR-089** — nvsim NV-diamond simulator. The crate this extension + attaches to. +- **ADR-018** — CSI binary frame format. Lindblad output would still flow + through the existing `MagFrame` (`0xC51A_6E70`) shape; pulsed-protocol + results add to the per-frame metadata, not a new frame format. +- **ADR-028** — ESP32 capability audit. Lindblad is host-side only; ESP32 + firmware untouched. +- **ADR-066** — Swarm bridge. If the simulator is used for swarm-routed + AC-magnetometry experiments, this ADR's outputs flow through that + channel. diff --git a/docs/adr/ADR-091-stand-off-radar-tier-research.md b/docs/adr/ADR-091-stand-off-radar-tier-research.md new file mode 100644 index 000000000..c02d995b0 --- /dev/null +++ b/docs/adr/ADR-091-stand-off-radar-tier-research.md @@ -0,0 +1,770 @@ +# ADR-091: Stand-off Radar Tier Research — 77 GHz High-Power and 100–200 GHz Coherent Sub-THz + +| Field | Value | +|----------------|-----------------------------------------------------------------------------------------| +| **Status** | Proposed — Research only. No production hardware integration. Decision deferred pending sub-$1k COTS sub-THz transceiver availability and clear non-export-controlled use case. | +| **Date** | 2026-04-26 | +| **Authors** | ruv | +| **Refines** | ADR-021 (60 GHz / mmWave vital-signs pipeline) | +| **Companion** | `docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md` §6.3, ADR-029 (RuvSense multistatic), ADR-089 (nvsim simulator), ADR-090 (Lindblad extension) | + +## 1. Context + +### 1.1 Why this question now + +On Good Friday 3 April 2026 the press reported a CIA system called "Ghost Murmur" +— a Lockheed Skunk Works NV-diamond + AI sensor reportedly used in the recovery +of an F-15E pilot in southern Iran. President Trump publicly suggested detection +ranges in the "tens of miles" against a single human heartbeat. RuView shipped +a research spec (`16-ghost-murmur-ruview-spec.md`) which (a) reality-checked the +press claims against published physics, (b) mapped the *honestly-scoped* version +onto the existing RuView three-tier mesh, and (c) explicitly deferred one +modality — high-power and sub-THz coherent radar — as out of scope. From §6.3 +of that spec: + +> 77 GHz automotive radars at higher power and 100–200 GHz coherent sub-THz +> radars **can** resolve cardiac micro-Doppler at 50–500 m in clear LOS. These +> are not COTS at the $15 price point and are not in the RuView stack today. +> They are also subject to ITAR / export-control review and **explicitly out of +> scope** for this open-source project. + +That sentence is the trigger for this ADR. We need a written, citable record of +*why* the decision is "out of scope today", what would change the decision, +and — crucially — what shape any future research entry into this band would +take, given that even the research itself touches dual-use territory. + +### 1.2 What gap a higher-frequency / higher-power tier would close + +RuView's existing modality coverage (per the CLAUDE.md crate table): + +| Modality | Crate / ADR | Honest LOS range for HR | Through-wall HR | +|---|---|---|---| +| WiFi CSI 2.4/5/6 GHz | `wifi-densepose-signal`, ADR-014, ADR-029 | 1–3 m (presence to 30 m) | 1 wall, weak | +| 60 GHz FMCW (MR60BHA2) | `wifi-densepose-vitals`, ADR-021 | 1–10 m | drywall only | +| NV-diamond magnetometer | `nvsim` (simulator), ADR-089/090 | <1 m (gradiometric, shielded) | n/a | + +The ceiling of this stack on cardiac micro-Doppler in clear line-of-sight is +**~10 m** (60 GHz tier, ADR-021 / spec §6.1). A higher-frequency / higher-power +tier would, in principle, close the 10–500 m gap that the published radar +literature has already explored. The two candidate bands: + +1. **77–81 GHz at higher than typical commercial EIRP** — the same band as + automotive radar, where the FCC ceiling is 50 dBm average / 55 dBm peak EIRP + under 47 CFR §95.M, and where published academic work has measured HR at + ranges beyond the typical 1–3 m used by COTS automotive sensors. +2. **100–200 GHz coherent sub-THz radar** — where λ ≈ 1.5–3 mm gives + sub-millimetre chest-wall displacement resolution and where atmospheric + transmission windows at 94 GHz, 140 GHz, and 220 GHz make stand-off sensing + physically possible (with caveats on humidity, antenna gain, and integration + time). + +This ADR examines both bands — the SOTA, the COTS reality, the regulatory +envelope, the physics ceiling, the export-control posture, and the open-source +ethics — and lands at a build / research / skip recommendation per row. + +## 2. SOTA: 77–81 GHz automotive radar at higher power + +### 2.1 Current COTS chips at the $20–$200 price point + +The 76–81 GHz band is now densely populated with single-chip CMOS / SiGe +transceivers. Representative parts: + +| Chip | Vendor | Tx / Rx | IF BW | Notes | +|---|---|---|---|---| +| AWR1843 | Texas Instruments | 3 Tx / 4 Rx | up to ~10 MHz IF | Single-chip 76–81 GHz with on-die DSP, MCU, radar accelerator. Long-range automotive ACC, AEB. ([TI AWR1843](https://www.ti.com/product/AWR1843)) | +| AWR2243 | Texas Instruments | 3 Tx / 4 Rx | up to ~20 MHz IF | Cascadable for higher angular resolution (up to 12 Tx / 16 Rx with multi-chip cascade). ([TI AWR2243](https://www.ti.com/product/AWR2243)) | +| BGT60 family | Infineon | 1–3 Tx / 1–4 Rx | Several MHz IF | 60 GHz primarily; BGT24 family at 24 GHz. Smaller, lower power, gesture / presence focus. | +| TEF82xx | NXP | up to 4 Tx / 4 Rx | several MHz IF | Automotive-grade 76–81 GHz. | + +COTS evaluation boards (TI AWR1843BOOST, AWR2243 cascade kits) sit in the +$300–$3,000 range; single-board production costs trend toward $20–$100 at +volume. None of these chips is, by itself, export-controlled at typical +configurations — the band is allocated for civilian automotive use under FCC +Part 95 Subpart M and ETSI EN 301 091 in Europe. + +**EIRP envelope**: 47 CFR §95.M (and the historical §15.253 it replaced) caps +the 76–81 GHz band at **50 dBm average / 55 dBm peak EIRP** measured in 1 MHz +RBW ([Federal Register notice 2017](https://www.federalregister.gov/documents/2017/09/20/2017-18463/permitting-radar-services-in-the-76-81-ghz-band), +[eCFR 47 CFR Part 95 Subpart M](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-D/part-95/subpart-M)). +That is roughly 100 W EIRP average, 316 W peak. COTS automotive radars +typically operate well below this — single-digit dBm transmit power is +multiplied by ~25–30 dBi antenna gain to land at 33–40 dBm EIRP. + +### 2.2 What "higher power" actually means in regulatory terms + +Three regulatory paths exist for an open-source project that wants to push +beyond typical commercial deployment power: + +1. **Stay inside FCC Part 95 §95.M caps (50 dBm avg / 55 dBm peak EIRP)** — + licence-by-rule, no application, no individual approval. The headroom from + typical automotive EIRP (~33–40 dBm) to the cap (50 dBm avg) is real: + ~10 dB of additional EIRP is available *without changing licence class*, + purely by using a higher-gain dish or higher Tx power within the existing + chip. This is the upper bound of "stand-off radar that is still part-95 + legal". +2. **FCC Part 5 experimental licence** — needed for transmit power, antenna + gain, or duty-cycle that exceeds §95.M. Application-based, time-bounded, + non-renewable beyond limits. Typical academic radar ranges (e.g. the + long-range cardiac measurements in §2.3 below) operate under this regime. +3. **No US authorisation at all** — only legal as receive-only, or as a + simulator. Any unlicensed transmission above §95.M at 76–81 GHz is a + prohibited emission under 47 CFR §15.5 / §95.335. + +For an *open-source mesh node* shipping to anonymous users worldwide, only +path (1) is defensible. Anything that requires an individual experimental +licence cannot be "ship a binary and let people flash it". + +### 2.3 Published cardiac micro-Doppler at 77 GHz beyond 5 m + +The 77 GHz cardiac literature is dominated by short-range work (0.3–2 m), e.g.: + +- Chen et al. (2024). "Contactless and short-range vital signs detection with + doppler radar millimetre-wave (76–81 GHz) sensing firmware." *Healthcare + Technology Letters*. ([PMC11665778](https://pmc.ncbi.nlm.nih.gov/articles/PMC11665778/), + [Wiley HTL 2024](https://ietresearch.onlinelibrary.wiley.com/doi/full/10.1049/htl2.12075)) + — TI IWR1443BOOST at 0.30–1.20 m, suggested 0.6 m. +- Wang et al. (2020). "Remote Monitoring of Human Vital Signs Based on 77-GHz + mm-Wave FMCW Radar." *Sensors* 20, 2999. + ([PMC7285495](https://pmc.ncbi.nlm.nih.gov/articles/PMC7285495/), + [MDPI Sensors 2020](https://www.mdpi.com/1424-8220/20/10/2999)) — typically + short-range bench measurements. +- Liu et al. (2022). "Real-Time Heart Rate Detection Method Based on 77 GHz + FMCW Radar." *Micromachines* 13, 1960. + ([PMC9693980](https://pmc.ncbi.nlm.nih.gov/articles/PMC9693980/), + [MDPI](https://www.mdpi.com/2072-666X/13/11/1960)) — 2.925% mean HR error, + short-range. +- Iyer et al. (2022). "mm-Wave Radar-Based Vital Signs Monitoring and + Arrhythmia Detection Using Machine Learning." *Sensors*. + ([PMC9104941](https://pmc.ncbi.nlm.nih.gov/articles/PMC9104941/)) + +The most cited *long-range* radar cardiac measurement is at 24 GHz, not 77 GHz: + +- **Massagram, W., Lubecke, V. M., Høst-Madsen, A., Boric-Lubecke, O. (2013). + "Parametric Study of Antennas for Long Range Doppler Radar Heart Rate + Detection."** *IEEE EMBC* / republished in *PMC*. + ([PMC4900816](https://pmc.ncbi.nlm.nih.gov/articles/PMC4900816/), + [PubMed 23366747](https://pubmed.ncbi.nlm.nih.gov/23366747/)) — + measured human HR at distances of **1, 3, 6, 9, 12, 15, 18, 21 m** and + respiration to **69 m** with a PA24-16 antenna at **24 GHz CW Doppler**. + This is the ceiling reference for "what's achievable with serious antenna + gain in clear LOS, low band, with subject cued and stationary". + +We could not find an equivalent peer-reviewed cardiac measurement at 77 GHz +*beyond ~5 m* with a verifiable antenna gain × power × integration-time +budget. The work that exists at 77 GHz is overwhelmingly bench-scale (≤ 2 m). +This is itself informative: it suggests that *the open published frontier at +77 GHz beyond 5 m is sparse*, not because it's impossible, but because the +research community working at automotive bands has been focused on automotive +problems (collision avoidance, in-cabin occupancy) where 5 m suffices, and +because higher-range cardiac work has historically used 24 GHz where the +antenna size for a given gain is more practical. + +### 2.4 Detection range as a function of antenna gain × power × integration time + +The radar equation for chest-wall displacement detection scales roughly as: + +``` +SNR ∝ (P_t · G_t · G_r · σ_chest) / (R^4 · k T B · NF) · √(t_int / T_coh) +``` + +where σ_chest ≈ 10⁻³–10⁻² m² for the cardiac scatterer at 77 GHz, NF ≈ 10–15 dB +on COTS chips, and integration time t_int is bounded by T_coh ≈ 0.5–1 s +(physiological coherence — the heart period itself). + +Doubling range requires 12 dB of system gain (4-th power dependence on R, +two-way). At the part-95 §95.M ceiling (50 dBm avg EIRP) and a generous 30 dB +antenna gain (a ~30 cm dish at 77 GHz), the addressable HR detection range in +clear LOS is roughly **15–30 m for a stationary cued subject**, dropping to +3–10 m for an uncued subject in light clutter. Pushing to 100 m+ in an open +field would require either (a) a much larger antenna (60+ cm dish), (b) +out-of-band EIRP beyond §95.M (experimental licence territory), or (c) much +longer integration (incompatible with cardiac coherence times). + +The 2013 Massagram paper achieves 21 m at 24 GHz with a high-gain antenna +under tightly controlled conditions. Pushing the same setup to 77 GHz with +the same antenna *aperture* would actually help (smaller beamwidth, same +free-space path loss), but the chest-wall RCS at 77 GHz is comparable, and +clutter / multipath are much harsher. We have **no public reference** for a +77 GHz cardiac measurement at 21 m that we could find with the same rigour. + +### 2.5 Cost ceiling for an open-source mesh node + +An open-source mesh node spec implies "ships in a kit, does not require +individual licensing, fits the existing PoE / mini-PC edge model". That +implies: + +- Single-chip transceiver at $20–$100 BOM. +- Antenna assembly at $50–$200 (high-gain dish or printed array). +- Mini-PC or Pi 5 host at $80. +- Total under $500 to be plausible. + +The chip cost is already met by COTS. The antenna and host are met. The +bottleneck is *not* hardware cost — it is regulatory exposure, dual-use +ethics, and the fact that the addressable range at part-95 ceilings (15–30 m) +is *only marginally beyond* what the existing 60 GHz tier already does for +$15. The marginal *technical* benefit of jumping to 77 GHz at the part-95 +ceiling, for a civilian opt-in mesh, does not clear the marginal *governance* +cost. + +## 3. SOTA: 100–200 GHz coherent sub-THz radar + +### 3.1 Why sub-THz + +At 140 GHz, λ ≈ 2.14 mm. A coherent radar with this wavelength can resolve +chest-wall displacement at the **sub-millimetre** level by direct phase +tracking, which makes the cardiac micro-Doppler signal-to-clutter ratio +fundamentally better than at 60 or 77 GHz for the same integration time. +Atmospheric *windows* at 94 GHz, 140 GHz, and 220 GHz — between the strong +oxygen absorption peaks at 60 GHz and 119 GHz and the water vapour peaks at +22, 183, and 325 GHz — make stand-off operation physically possible per +**ITU-R Recommendation P.676** ([ITU-R P.676-11](https://www.itu.int/dms_pubrec/itu-r/rec/p/R-REC-P.676-11-201609-I!!PDF-E.pdf), +[ITU-R P.676-9](https://www.itu.int/dms_pubrec/itu-r/rec/p/R-REC-P.676-9-201202-S!!PDF-E.pdf)). + +### 3.2 Atmospheric attenuation table (clear-air, ITU-R P.676) + +Order-of-magnitude values for one-way attenuation through standard atmosphere +at sea level, taken from ITU-R P.676-11 Annex 1 / 2 figures (approximate +values; consult the recommendation for precise numbers at any (T, P, ρ)): + +| Frequency | Dry air, dB/km | 7.5 g/m³ humid, dB/km | Notes | +|---|---|---|---| +| 60 GHz | ~14 | ~14.5 | O₂ absorption peak — terrible for stand-off | +| 77 GHz | ~0.4 | ~0.5 | Allocated for automotive radar | +| 94 GHz | ~0.4 | ~0.7 | First major window above 60 GHz | +| 119 GHz | ~2.5 | ~3 | O₂ subsidiary peak | +| 140 GHz | ~0.5 | ~1.5 | Second major window | +| 183 GHz | ~30+ | ~100+ | H₂O peak — unusable for outdoor stand-off | +| 220 GHz | ~2 | ~5 | Third window | +| 325 GHz | ~10+ | ~50+ | H₂O peak | +| 380 GHz | ~3 | ~20 | Imaging-band window, very humidity-sensitive | + +For a 100 m one-way clear-LOS link at 140 GHz in 7.5 g/m³ humidity, atmospheric +attenuation alone is ~0.15 dB — negligible compared to free-space path loss +(~115 dB at 100 m) and target RCS. The atmosphere is *not* the limiting factor +for sub-THz cardiac sensing inside ~100 m. **Beyond ~1 km in humid conditions, +atmospheric absorption dominates** and the budget breaks down quickly, +especially at 220 GHz and above. + +### 3.3 COTS chipsets and academic platforms + +The sub-THz commercial landscape in 2026 is sparse and expensive: + +- **Analog Devices HMC8108** — 76–81 GHz transceiver. Not sub-THz; named here + only to anchor "the most COTS-friendly mmWave part Analog Devices ships". +- **Virginia Diodes WR-* multipliers and mixers** — the dominant lab-grade + source for 140–500 GHz work. Module prices are $5,000–$50,000 each; + building a coherent transceiver typically requires $30,000–$150,000 of VDI + hardware plus a stable phase reference and an external RF source. +- **Wasa Millimeter Wave imagers** — passive imagers around 90 / 220 / 380 GHz. + Receive-only. +- **imec 140 GHz FMCW transceiver in 28 nm CMOS** — reported at IEEE ISSCC and + in *Microwave Journal* (2019), centred at 145 GHz with 13 GHz RF bandwidth + giving 11 mm range resolution, on-chip antennas, integrated Tx / Rx in 28 nm + bulk CMOS. ([Microwave Journal 2019](https://www.microwavejournal.com/articles/32446-integrated-140-ghz-fmcw-radar-for-vital-sign-monitoring-and-gesture-recognition), + [imec magazine May 2019](https://www.imec-int.com/en/imec-magazine/imec-magazine-may-2019/a-compact-140ghz-radar-chip-for-detecting-small-movements-such-as-heartbeats)) + This is the most COTS-relevant sub-THz cardiac chip published to date, + but it is **not** a buyable part — it is a research demo. +- **Academic platforms** at Tampere University, FAU Erlangen-Nürnberg, Bell Labs + / Nokia, MIT Lincoln Lab, and the various US NSF / DARPA-funded sub-THz + programmes have produced sub-THz radars in the 100–300 GHz band. None of + these is a ship-it part. + +### 3.4 Coherent vs. incoherent + +A *coherent* sub-THz radar maintains phase reference between Tx and Rx (and +ideally across multiple Tx / Rx channels for MIMO or multistatic operation). +Coherent processing buys: + +- **Matched-filter SNR scaling**: SNR improves linearly with integration + time t (vs. √t for incoherent), bounded by the cardiac coherence + time T_coh. +- **Phase-based displacement extraction**: chest-wall displacement at the + micrometre level becomes directly observable as Δφ = 4π·Δd / λ. +- **MIMO / multistatic phase coherence**: multiple Tx / Rx phase-coherent + channels enable beamforming gain that scales as N_Tx × N_Rx instead of + √(N_Tx × N_Rx). + +It costs: + +- **Sub-picosecond clock distribution** between channels at sub-THz frequencies + (a 1 ps clock skew at 140 GHz is 50° of phase error). +- **Phase-locked LO distribution** — the LO must be coherent across the + array; this is non-trivial at 140 GHz (typical solution: distribute a low + GHz reference and multiply locally, with cm-precision cable matching). +- **Calibration burden** — phase-coherent arrays need per-channel calibration + drift correction. + +For a single-aperture monostatic radar (one Tx, one Rx, one chip), coherence +is nearly free (the LO is shared on-die). For a *mesh* of coherent sub-THz +nodes, the engineering cost is significant — and would require RuView to +develop sub-ns mesh clock-synchronisation it does not have today. + +### 3.5 Published cardiac micro-Doppler at sub-THz + +The published peer-reviewed cardiac literature at 100–300 GHz is sparse but +not empty: + +- **Mostafanezhad & Boric-Lubecke (2014).** "Benefits of coherent low-IF for + vital signs monitoring." *IEEE Microw. Wireless Compon. Lett.* 24. — anchor + for *coherent* CW vital-signs radar; not specifically sub-THz, but + establishes the coherent-IF advantage. +- **imec (2019) — 140 GHz FMCW transceiver demonstration.** Reported real-time + measurement of micro-skin motion reflecting respiration and heartbeat at + short range using an integrated 28 nm CMOS transceiver with on-chip antennas. + Cited above; engineering demo, not a published systematic range study. + ([Microwave Journal 2019](https://www.microwavejournal.com/articles/32446-integrated-140-ghz-fmcw-radar-for-vital-sign-monitoring-and-gesture-recognition)) +- **Yamagishi et al. (2022).** "A new principle of pulse detection based on + terahertz wave plethysmography." *Scientific Reports* 12, 2022. + ([Nature SREP](https://www.nature.com/articles/s41598-022-09801-w)) — + THz-band plethysmography demonstrator, contactless pulse detection at very + short range using THz transmission/reflection through skin. Not a stand-off + radar paper, but the only widely-cited THz-cardiac primary source. +- **Zhang et al. (2021).** "Non-Contact Monitoring of Human Vital Signs Using + FMCW Millimeter Wave Radar in the 120 GHz Band." *Sensors* 21. + ([PMC8070581](https://pmc.ncbi.nlm.nih.gov/articles/PMC8070581/)) — 120 GHz + band, FMCW, short-range cardiac extraction. + +**Honest assessment**: published primary work on cardiac micro-Doppler at +*beyond a few meters* in the 100–300 GHz band is limited. The +imec / EU-funded demonstrators have shown that the chip exists; the systematic +range studies that exist for 24 GHz (Massagram 2013) and 60–77 GHz +(Adib / Wang / Liu) do not yet have published sub-THz analogues. Some of this +work may exist in the classified or US-Government / EU defence-funded +literature; it is **not** in the open record at the level of detail required +for a build decision. + +## 4. Physics ceiling for RuView's heartbeat-mesh use case + +### 4.1 Cardiac signal vs. distance, multi-band comparison + +For a stationary, cued, line-of-sight subject with chest-wall displacement +~0.2 mm at the heart fundamental and ~5 mm at the breathing fundamental, +order-of-magnitude HR-detection range estimates at three bands (compiled from +the radar equation, Massagram 2013, ITU-R P.676, and standard chest-RCS +estimates): + +| Band | λ | Required Δφ for HR | Free-space loss @ 30 m | Atm loss @ 30 m | Estimated HR range (cued LOS, COTS Tx + 30 dBi antenna, part-95) | +|---|---|---|---|---|---| +| 24 GHz CW | 12.5 mm | 0.36° | 89 dB | <0.01 dB | 21 m measured (Massagram 2013) | +| 60 GHz FMCW | 5.0 mm | 0.9° | 97 dB | 0.4 dB | 5–10 m (ADR-021 / spec §6.1) | +| 77 GHz FMCW | 3.9 mm | 1.2° | 99 dB | 0.01 dB | ~15–30 m (estimated, no rigorous public ref beyond 5 m) | +| 140 GHz FMCW | 2.1 mm | 2.2° | 105 dB | 0.04 dB | ~30–100 m (estimated, sparse open lit) | +| 220 GHz FMCW | 1.4 mm | 3.3° | 109 dB | 0.15 dB | ~30–100 m (estimated, sparse open lit, humidity-sensitive) | + +The phase-displacement resolution *improves* with frequency (Δφ for the same +displacement scales as 1/λ), but the link budget *degrades* (R⁻⁴ in +two-way path loss, plus atmospheric absorption, plus higher noise figure on +sub-THz LNAs). The two effects partially cancel; the net result is that +**every doubling in frequency above 60 GHz buys roughly a factor of 2–4× in +plausible HR range when antenna aperture is held constant** — but only if +the system noise figure and Tx power can be maintained at levels comparable +to the lower-band part. Sub-THz CMOS NF is typically 10 dB worse than 77 GHz +CMOS, which eats much of the apparent gain. + +### 4.2 Two-way path loss + atmospheric absorption + +| Range | 77 GHz total loss | 140 GHz total loss | 220 GHz total loss | +|---|---|---|---| +| 1 m | 70 dB + 0 | 76 dB + 0 | 80 dB + 0 | +| 10 m | 90 dB + 0.01 | 96 dB + 0.03 | 100 dB + 0.1 | +| 100 m | 110 dB + 0.1 | 116 dB + 0.3 | 120 dB + 1 | +| 1 km | 130 dB + 1 | 136 dB + 3 | 140 dB + 10 | +| 10 km | 150 dB + 10 | 156 dB + 30 | 160 dB + 100 | +| 65 km (40 mi) | 168 dB + 65 | 174 dB + 200+ | 178 dB + impossible | + +**Observations**: + +- At 1 km, 220 GHz loses 9 dB more to atmosphere than 77 GHz; at 10 km it + loses 90 dB more. Sub-THz is fundamentally a sub-1-km modality in humid air. +- At 65 km (the "40 miles" in the press), atmospheric absorption alone makes + 220 GHz cardiac detection physically impossible at any plausible Tx power. + 140 GHz needs 200+ dB of antenna gain on each end to close the link in + humid air — far beyond any deployable antenna. +- **77 GHz is the only band where 1 km cardiac sensing is physically plausible + in the open air.** It is also the band that is closest to civilian COTS. + +### 4.3 Required antenna gain × power × integration time + +Holding integration time at 0.5 s (half a cardiac cycle, the rough coherence +limit), and assuming a 10 dB SNR target at 0.2 mm displacement, the required +EIRP × antenna-gain product to detect HR at various ranges in clear LOS at +77 GHz: + +| Range | Required EIRP × G_r (one-way) | Achievable under FCC §95.M? | +|---|---|---| +| 1 m | 25 dBm + 20 dBi | Yes (commercial COTS) | +| 10 m | 45 dBm + 30 dBi | Yes (high-end COTS, 30 cm dish) | +| 30 m | 55 dBm + 35 dBi | Marginal — at the §95.M peak ceiling | +| 100 m | 70 dBm + 45 dBi | No — above §95.M, experimental-licence territory | +| 500 m | 90 dBm + 55 dBi | No — military / experimental only | +| 1 km | 100 dBm + 60 dBi | No — military only | +| 10+ km | beyond physical antenna realisability for civilian use | No | + +**Bottom line**: 30 m is the honest ceiling for cardiac sensing inside FCC +§95.M power limits with a 30 cm dish at 77 GHz. Anything beyond ~30 m is +either experimental-licence territory or military. + +### 4.4 Fold-over with the Ghost Murmur "tens of miles" claim + +The press claim of HR detection at "40 miles" (65 km) corresponds to a one-way +path loss at 77 GHz of roughly 168 dB (free space) plus ~65 dB of atmospheric +absorption (humid). Closing this link to detect a 0.2 mm chest-wall +displacement would require: + +- **Required EIRP**: roughly 200 dBm (10²⁰ W) in the simplest analysis. For + context, the entire global average solar flux is ~1.4 kW/m². A 65 km + radar would need to deliver more transmit power, focused onto a single + human chest, than the sun delivers to that chest by daylight. +- **Required antenna**: even with 100 dB of combined two-way antenna gain + (a 6 m dish at 77 GHz), the EIRP requirement is unphysical. +- **Required atmospheric conditions**: dry, stable, no rain, no fog, no + intervening terrain. + +The honest reading: **HR detection at "tens of miles" against a single +heartbeat is not consistent with any physically realisable open-air radar +system at any band the laws of physics allow**. The claim either refers to +*cued* detection (i.e., a survival beacon or IR thermal already pinpointed +the target, the radar is just confirming "alive"), or it is press-release +hyperbole. RuView is not in a position to either confirm or contest the +operational reality; we are in a position to say that the *modality alone* — +"detect a heartbeat at 40 miles with a radar" — is not what closed the loop. + +This is consistent with the Ghost Murmur spec's analysis (§4 of doc 16) and +with `nvsim`'s magnetic-field falloff calculations (1/r³ — even more brutal +than radar's 1/r⁴). + +## 5. Regulatory + ethics + +### 5.1 FCC envelope summary + +| Use | FCC path | Practical for open source? | +|---|---|---| +| 60 GHz unlicensed (existing tier) | Part 15.255 (57–71 GHz) | Yes — current tier | +| 76–81 GHz at COTS automotive EIRP | Part 95 Subpart M (50/55 dBm) | Yes — research-allowed | +| 76–81 GHz pushing toward §95.M ceiling | Part 95 Subpart M | Yes — single-installation | +| 76–81 GHz beyond §95.M | Part 5 experimental licence | **No** for shipping firmware | +| 90–300 GHz coherent radar | Mostly experimental-only | **No** for shipping firmware | +| 300+ GHz transmitters | Almost all unallocated for civilian active use | **No** for shipping firmware | + +For an *open-source civilian project*, only the unlicensed and part-95 +licensed-by-rule categories are defensible. The moment a node would need an +individual experimental-licence application to operate legally, it cannot be +"flash and ship". + +### 5.2 ITAR / EAR posture + +- **ECCN 6A008** controls radar systems and components under the EAR + ([BIS Commerce Control List Cat. 6](https://www.bis.doc.gov/index.php/documents/regulations-docs/2340-ccl9-4/file)). + The general radar control sub-paragraph 6A008.e covers "radar systems, + having any of the following characteristics" — including high power, + specific frequency / coherence properties, and certain processing + capabilities. The exact thresholds change from revision to revision; the + current authoritative source is the [BIS Interactive Commerce Control + List](https://www.bis.gov/regulations/ear/interactive-commerce-control-list). +- **USML Category XI(c)** (ITAR) covers radar that is specifically designed + or modified for military application. Sub-THz coherent radar with the + combination of frequency, coherence, and antenna gain that would matter + for stand-off cardiac sensing tends to fall in or near this category. +- **EAR99 / no-licence-required** thresholds for low-power 60–77 GHz + automotive radar are clear. Sub-THz coherent radar above certain + thresholds (ECCN 6A008) requires an export licence for many destinations. + Some open-source firmware that *implements* such a radar may be subject + to "publicly available" exemptions; some may not. +- **Open-source publication.** EAR §734.7 / §734.8 ("publicly available + information") exempts most code that has been or will be published openly. + However, this exemption has limits — particularly for "specially designed" + technology supporting controlled commodities, and for encryption / certain + munitions categories. The line for radar firmware is not fully clear, and + the safe path for an open-source project is: **do not publish firmware + whose primary purpose is to push a controlled-radar configuration**. + +The correct posture for RuView is: **assume the worst case**. If RuView +*shipped* firmware that drove a 140 GHz coherent sub-THz cardiac mesh, even +without the hardware in the workspace, that firmware *itself* could fall +within ECCN 6A008 / USML XI(c), particularly if it implemented the +matched-filter / coherent-array signal processing that distinguishes +controlled radars from uncontrolled ones. We do not ship that firmware. + +### 5.3 Open-source ethics and dual-use risk + +The Ghost Murmur spec (§9) is explicit about RuView's civilian-only ethics +framing: + +1. Civilian, opt-in deployments only. +2. No directional pursuit. +3. Data minimisation. +4. PII detection on the wire. +5. Adversarial-signal detection. +6. **No export-controlled hardware.** + +Stand-off radar at 77 GHz with §95.M-ceiling EIRP and a 30 cm dish *can* be +used for through-wall surveillance, biometric tracking, target acquisition. +Sub-THz coherent radar can do the same with finer resolution. Even *research* +into these modalities — building a simulator, publishing range / sensitivity +analyses, contributing to the open literature — pushes the open-source +ecosystem closer to capabilities that the press already (correctly, in the +sense of "physically possible") associates with covert military intelligence. + +Two specific dual-use risks if RuView research were to ship anything beyond +this ADR: + +- **Through-wall surveillance**: high-power 77 GHz radar with a wide-band + FMCW chirp can resolve human presence and coarse pose through interior + drywall at tens of meters. This is the literal Ghost Murmur use case at + short range. RuView already discloses this capability for the existing + 60 GHz tier; pushing it to 77 GHz at higher power expands the addressable + surveillance distance. +- **Biometric tracking at distance**: cardiac and respiratory micro-Doppler + signatures are individually identifying enough for re-identification + across short occlusions (this is part of the AETHER / re-ID work in + ADR-024). Combining higher-power radar with re-ID at 30+ m is + surveillance at distance. +- **Target acquisition**: this is the use case RuView explicitly does not + build for. Period. + +## 6. Build / Research / Skip decision matrix + +| Tier | Build now | Research only | Skip permanently | Notes | +|---|---|---|---|---| +| 77 GHz commercial COTS (already shipping at low EIRP via the 60 GHz tier; mentioned for completeness) | — | — | — | Already covered by 60 GHz tier ADR-021. No action. | +| 77 GHz higher-power experimental (≤ §95.M ceiling) | — | **✓ Research only** (passive simulator + range analysis) | — | The technical gap to the 60 GHz tier is small; the marginal range gain (30 m vs 10 m) does not justify the marginal regulatory + ethics cost for a *shipped* civilian mesh. Research / simulation only. | +| 77 GHz beyond §95.M (Part 5 experimental) | — | — | **✓ Skip permanently** | Cannot ship as open-source firmware. Individual experimental licences are not delegatable. | +| 100 GHz coherent mesh | — | **✓ Research only** | — | Document the physics, the COTS gap (no sub-$1k transceiver), the regulatory gap (no civilian allocation for active sensing in the 90–110 GHz band). Build only if all three conditions in §7.4 below trigger. | +| 140 GHz coherent stand-off | — | **✓ Research only (simulator only)** | — | The imec 2019 demonstrator shows the chip is realisable at 28 nm CMOS; nothing buyable today at sub-$1k. ECCN 6A008 risk is real. Simulator OK; firmware no. | +| 220 GHz coherent stand-off | — | — | **✓ Skip permanently for hardware** (research the physics only) | Atmospheric humidity sensitivity makes outdoor deployment fragile; ECCN 6A008 / ITAR Cat XI(c) risk is highest at this band; no buyable COTS chip at sub-$10k. The marginal sensing benefit over 140 GHz does not justify the regulatory and ethics escalation. | +| 380+ GHz imaging | — | — | **✓ Skip permanently** | Imaging-band, not radar; humidity destroys outdoor link; export-controlled at any meaningful aperture. Not RuView's modality at any plausible build. | + +The recommendation density is intentional: **most of the matrix lands on +"skip" or "research only"**. Only one row (77 GHz at the §95.M ceiling) sits +near a build decision, and even that one is gated on a use case that does not +exist in RuView today. + +## 7. If we research: what does RuView ship? + +### 7.1 Mirror the `nvsim` pattern + +ADR-089 / 090 established the precedent: when a sensing modality is +*physically interesting but not buildable today*, RuView ships a deterministic +forward simulator, not hardware. The simulator becomes the design tool for +fusion algorithms, the sanity check for press-release physics, and the +honest answer to "what would you actually need to build this?" + +Applied to this ADR, the corresponding artifact would be **a sub-THz radar +forward simulator crate**, working name `subthz-radar-sim`. Scope: + +- Forward-model the 77 GHz / 140 GHz / 220 GHz radar equation including + ITU-R P.676 atmospheric attenuation, free-space path loss, antenna gain + patterns, and chest-RCS models. +- Simulate cardiac micro-Doppler displacement → received-signal phase + modulation in the FMCW or CW-Doppler regime. +- Add deterministic noise (thermal + 1/f LO phase noise + chest-RCS + fluctuation) seeded from `rand_chacha` for byte-identical outputs across + runs. +- Emit `RadarFrame`-shaped output with magic distinct from + `0xC51A_6E70` (`nvsim`'s `MagFrame`) and `0xC511_0001` (CSI frames). +- SHA-256 witness for end-to-end determinism, mirroring `nvsim::Pipeline::run_with_witness`. + +### 7.2 Hard constraints on what the crate can ship + +- **No firmware.** Not for ESP32, not for any SDR, not for any FPGA. The crate + is host-side only. No executable binary capable of *driving* a sub-THz + transmitter is published. +- **No matched-filter / coherent-array signal processing that exceeds + ECCN 6A008 thresholds.** The crate documents the physics and simulates the + forward path. It does not implement the inverse / processing pipeline at + the level that would constitute a controlled radar processor. +- **No beamforming primitives for actively-steered phased arrays.** Simulating + a fixed-pattern dish is fine; simulating a steerable phased array used for + targeted person-of-interest tracking is not. +- **No re-identification across the simulated radar stream.** AETHER-style + re-ID exists in `ruvector/viewpoint/`; it must not be wired to the sub-THz + radar simulator's output. +- **Documented dual-use posture.** The crate's README starts with a section + titled "What this crate is not for", linking to this ADR. + +### 7.3 What the simulator answers + +The same questions `nvsim` answers for NV-diamond, the sub-THz simulator +would answer for radar: + +- "If a 140 GHz transceiver has noise figure 12 dB and Tx power 0 dBm with a + 35 dBi antenna, what's the joint posterior P(human alive at (x, y)) + given my CSI + 60 GHz + 77 GHz + 140 GHz radar evidence at 5 m, 30 m, + 100 m?" +- "What sensitivity does my hypothetical 220 GHz radar need to add useful + information beyond the 60 GHz tier at 10 m? And does the answer change + in 7.5 g/m³ humidity vs. 1 g/m³ dry air?" +- "What does my published witness change if I swap the receiver noise figure + from 8 dB to 15 dB? From 15 dB to 25 dB?" + +These are pre-build sanity checks. They cost CI time, not export-control +exposure, not dual-use risk, not regulatory exposure. + +### 7.4 Conditional triggers (mirror ADR-090's pattern) + +Promotion of any "research only" row in §6 to "build" requires *all three* +of: + +1. **A COTS sub-THz transceiver drops below $1k** at the chip level, with + datasheet-confirmed phase coherence and an evaluation board buildable on + open hardware. (Today: nothing.) +2. **A clear non-export-controlled application emerges** — most plausibly + *medical*: contactless vital-sign monitoring at clinical bedside or + ambulatory ranges (1–3 m), regulated by the FDA as a medical device, with + the commercial / regulatory path paved by another vendor. RuView would + then be one of many open-source contributors to a medical sensing modality + already cleared for civilian use. +3. **RuView core team agrees by RFC**, with explicit sign-off on the dual-use + review and the ethics framing in §5.3. + +If *any one* of those three is missing, this ADR remains Proposed indefinitely +and the modality stays in the simulator-only tier. + +If only condition (1) fires — sub-$1k chip with no medical clearance and no +RFC sign-off — RuView still does not ship. The simulator might be expanded; +no firmware ships. + +## 8. Related work / cross-references + +### 8.1 ADRs + +- **ADR-021** — Vital-sign detection via 60 GHz mmWave + WiFi CSI. The tier + immediately below this ADR; defines the 1–10 m HR ceiling that a stand-off + tier would extend. +- **ADR-029** — RuvSense multistatic sensing mode. Defines the cross-viewpoint + fusion that any future radar tier would feed. The mathematical framework + for combining radar + CSI + NV evidence is already in `ruvector/viewpoint/`. +- **ADR-089** — `nvsim` NV-diamond pipeline simulator. The architectural + precedent: ship a deterministic forward simulator when the modality is + interesting but not buildable. Same proof / witness pattern applies here. +- **ADR-090** — `nvsim` Lindblad / Hamiltonian extension. Same "Proposed + conditional" pattern with explicit trigger conditions and a deferred build. + This ADR follows the same shape. +- **ADR-040** — PII detection gates. Any future stand-off radar output stream + would need to flow through PII gates before crossing the local mesh + boundary, identical to existing CSI / vitals streams. +- **ADR-024** — AETHER contrastive embedding. Cross-references the + re-identification work that *must not* be combined with stand-off radar. +- **ADR-028** — ESP32 capability audit + witness verification. The + deterministic-witness pattern applies to any new simulator crate. + +### 8.2 Research docs + +- `docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md` — the + Ghost Murmur reality-check spec. §6.3 is the explicit boundary that + triggered this ADR. §7–§9 establish the architecture, ethics, and legal + framework that this ADR inherits. + +### 8.3 Primary literature (radar at 24 / 77 / 120–140 GHz) + +- **Massagram, W., Lubecke, V. M., Høst-Madsen, A., Boric-Lubecke, O. + (2013).** "Parametric Study of Antennas for Long Range Doppler Radar + Heart Rate Detection." *IEEE EMBC* 2013. + ([PMC4900816](https://pmc.ncbi.nlm.nih.gov/articles/PMC4900816/)) + — HR @ 21 m, respiration @ 69 m at 24 GHz CW. +- **Mostafanezhad, I., Boric-Lubecke, O. (2014).** "Benefits of Coherent + Low-IF for Vital Signs Monitoring." *IEEE Microw. Wireless Compon. Lett.* + 24(10), 711–713. +- **Adib, F. et al. (2015).** "Smart Homes that Monitor Breathing and Heart + Rate." *Proc. CHI 2015*. Short-range through-wall. +- **Wang, G. et al. (2020).** "Remote Monitoring of Human Vital Signs Based + on 77-GHz mm-Wave FMCW Radar." *Sensors* 20(10), 2999. + ([PMC7285495](https://pmc.ncbi.nlm.nih.gov/articles/PMC7285495/)) +- **Liu, J. et al. (2022).** "Real-Time Heart Rate Detection Method Based on + 77 GHz FMCW Radar." *Micromachines* 13(11), 1960. + ([PMC9693980](https://pmc.ncbi.nlm.nih.gov/articles/PMC9693980/)) +- **Chen, J. et al. (2024).** "Contactless and Short-Range Vital Signs + Detection with Doppler Radar Millimetre-Wave (76–81 GHz) Sensing Firmware." + *Healthcare Technology Letters* 11. + ([Wiley HTL](https://ietresearch.onlinelibrary.wiley.com/doi/full/10.1049/htl2.12075)) +- **Iyer, S. et al. (2022).** "mm-Wave Radar-Based Vital Signs Monitoring + and Arrhythmia Detection Using Machine Learning." *Sensors*. + ([PMC9104941](https://pmc.ncbi.nlm.nih.gov/articles/PMC9104941/)) + +### 8.4 Primary literature (sub-THz) + +- **imec / Peeters et al. (2019).** Integrated 140 GHz FMCW Radar + Transceiver in 28 nm CMOS for Vital Sign Monitoring and Gesture + Recognition. *Microwave Journal* 2019-06-09; imec magazine May 2019. + ([Microwave Journal](https://www.microwavejournal.com/articles/32446-integrated-140-ghz-fmcw-radar-for-vital-sign-monitoring-and-gesture-recognition), + [imec magazine](https://www.imec-int.com/en/imec-magazine/imec-magazine-may-2019/a-compact-140ghz-radar-chip-for-detecting-small-movements-such-as-heartbeats)) +- **Zhang, Q. et al. (2021).** "Non-Contact Monitoring of Human Vital + Signs Using FMCW Millimeter Wave Radar in the 120 GHz Band." *Sensors* + 21. ([PMC8070581](https://pmc.ncbi.nlm.nih.gov/articles/PMC8070581/)) +- **Yamagishi, H. et al. (2022).** "A new principle of pulse detection + based on terahertz wave plethysmography." *Scientific Reports* 12, + 2022. ([Nature SREP](https://www.nature.com/articles/s41598-022-09801-w)) +- ITU-R Recommendation **P.676-11** (2016). "Attenuation by atmospheric + gases." International Telecommunication Union. + ([P.676-11 PDF](https://www.itu.int/dms_pubrec/itu-r/rec/p/R-REC-P.676-11-201609-I!!PDF-E.pdf)) +- 47 CFR Part 95 Subpart M — The 76–81 GHz Band Radar Service. + ([eCFR](https://www.ecfr.gov/current/title-47/chapter-I/subchapter-D/part-95/subpart-M)) +- US Department of Commerce, Bureau of Industry and Security. **Commerce + Control List Category 6 — Sensors and Lasers**, ECCN 6A008. + ([BIS CCL Cat. 6](https://www.bis.doc.gov/index.php/documents/regulations-docs/2340-ccl9-4/file)) + +### 8.5 Reviews + +- **Li, C. et al. (2024).** "Radar-Based Heart Cardiac Activity Measurements: + A Review." *Sensors*. ([PMC11645089](https://pmc.ncbi.nlm.nih.gov/articles/PMC11645089/)) +- **Frontiers in Physiology (2022).** "Radar-based remote physiological + sensing: Progress, challenges, and opportunities." + ([Frontiers](https://www.frontiersin.org/journals/physiology/articles/10.3389/fphys.2022.955208/full)) + +## 9. Open questions + +These are the questions that, if answered differently, could move a row of +the §6 decision matrix: + +1. **Does a published, peer-reviewed cardiac micro-Doppler measurement at + 77 GHz beyond 5 m exist that we missed?** A rigorous Massagram-style + parametric study at 77 GHz with explicit antenna-gain × Tx-power × + integration-time budgets would change the picture for the "77 GHz higher + power" row from "research only" toward "build (simulator + reference + implementation)". +2. **Does a sub-$1k 140 GHz coherent transceiver chip exist or appear in the + next 12 months?** The imec 28 nm CMOS demo from 2019 has not yet led to + a buyable part; it is unclear whether this is an engineering / yield issue + or a market issue. If a part appears, condition (1) of §7.4 fires. +3. **Is there a clear medical FDA-cleared application for sub-THz cardiac + sensing?** This is the single most important gating condition. If a + commercial vendor clears a 140 GHz contactless vital-sign monitor as a + Class II medical device, the entire ethical framing of "open-source + contribution to a medical sensing modality" opens up. Without that + clearance, RuView remains in the simulator-only tier. +4. **Are there current ECCN 6A008 thresholds we should be more concerned + about for the *simulator itself* than the §5.2 analysis suggests?** The + simulator is forward-only and emits IQ samples and a SHA-256 witness. + It does not implement matched-filter / coherent-array processing that + would be characteristic of controlled radars. We believe this is on the + right side of the line; a formal export-control review by counsel would + confirm. +5. **Should RuView contribute the sub-THz simulator to a neutral upstream** + (e.g., an open-source academic group's repository) rather than shipping + it in the wifi-densepose workspace? Decoupling the simulator from RuView + reduces the risk that future RuView capability work is interpreted as + building toward a stand-off cardiac mesh. +6. **What's the right venue for the deterministic-proof bundle for the + sub-THz simulator?** Same question that ADR-089 left open. Probably + the same answer: in-tree fixture + tagged release artifact. + +## 10. Decision summary + +This ADR is **Proposed — Research only**. The decision matrix in §6 lands on: + +- **Skip permanently**: 77 GHz beyond §95.M, 220 GHz coherent stand-off + hardware, 380+ GHz imaging. +- **Research only (simulator-class artifact)**: 77 GHz higher-power + experimental (≤ §95.M ceiling), 100 GHz coherent mesh, 140 GHz coherent + stand-off. +- **Build now**: nothing. + +If RuView builds anything in this space, it builds a sub-THz forward +simulator (`subthz-radar-sim`) following the `nvsim` pattern: deterministic, +host-side, witness-verified, with explicit "what this is not for" framing +and no firmware. The simulator does not ship until conditions §7.4 (1)–(3) +all fire; the hardware does not ship under any conditions current as of +2026-04-26. + +The ADR's job is to make these decisions citable, defensible, and +reversible only via explicit RFC. It is not a build commitment. diff --git a/docs/adr/ADR-092-nvsim-dashboard-implementation.md b/docs/adr/ADR-092-nvsim-dashboard-implementation.md new file mode 100644 index 000000000..5cf0488e1 --- /dev/null +++ b/docs/adr/ADR-092-nvsim-dashboard-implementation.md @@ -0,0 +1,942 @@ +# ADR-092: nvsim Dashboard — Vite + Dual-Transport (WASM + REST/WS) Implementation + +| Field | Value | +|---|---| +| **Status** | **Implemented (2026-04-27)** — live at https://ruvnet.github.io/RuView/nvsim/. PR #436 open against main. 8/12 §11 gates ✅, 4/12 ⚠ (require external infrastructure). | +| **Date** | 2026-04-26 | +| **Authors** | ruv | +| **Refines** | ADR-089 (`nvsim` simulator), ADR-090 (Lindblad extension), ADR-091 (stand-off radar) | +| **Companion** | `assets/NVsim Dashboard.zip` (mockup), `docs/research/quantum-sensing/15-nvsim-implementation-plan.md` (Pass-6 plan), `docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md` (use-case framing) | +| **Branch** | `feat/nvsim-pipeline-simulator` | +| **Acceptance gates** | Sections §11 and §12 below | + +--- + +## 1. Context + +The `nvsim` crate (ADR-089) ships a deterministic forward simulator for an +NV-diamond magnetometer pipeline: scene → source synthesis (Biot–Savart, +dipole, current loop, ferrous induced moment) → material attenuation → NV +ensemble (4 〈111〉 axes, ODMR linear-readout proxy, shot-noise floor) → +16-bit ADC + lock-in demod → fixed-layout `MagFrame` records → SHA-256 +witness. The crate is Rust-only, headless, and benchmarks at ~4.5 M +samples/s on x86_64. + +The user-supplied **NVSim Dashboard mockup** (`assets/NVsim Dashboard.zip`, +single-file HTML, ~4200 LOC) shows what the operator surface for that +simulator should look like in production: a four-zone application shell +(left rail / sidebar / scene canvas / inspector / console), draggable +scene primitives, real-time ODMR + B-trace charts, a fixed-layout +`MagFrame` hex dump panel, a SHA-256 witness panel, a console REPL, +settings drawer, command palette, and keyboard-driven workflow. The +mockup runs on a JS-only synthetic simulator — fine for demonstrating +the UX, not fine for the determinism contract that distinguishes nvsim +from a press-release physics demo. + +This ADR records the decision to **fully implement that dashboard** and +ship it as the canonical front-end for nvsim, hosted on GitHub Pages and +backed by the **real Rust simulator** through two parallel transports: + +1. **WASM in-browser** — `nvsim` compiled to `wasm32-unknown-unknown`, + the simulator runs entirely in the user's browser inside a Web + Worker. No server, no upload, no telemetry. The default mode for + GitHub Pages. +2. **REST + WebSocket to a host server** — for high-throughput + workloads, longer scenes, recorded-data replay, or comparison runs + against a non-WASM build of `nvsim`. Optional, opt-in, runs on a + user-supplied host. + +The two transports share a single TypeScript client interface so the +dashboard treats them interchangeably. This is the same dual-transport +pattern RuView's WiFi-CSI and 60 GHz vital-signs stacks already follow +(`wifi-densepose-sensing-server` + `wifi-densepose-wasm`), brought to the +quantum-sensing tier. + +--- + +## 2. Decision + +Build the nvsim dashboard as: + +- **Frontend**: Vite + TypeScript + a thin component library (Lit or + vanilla custom-elements; **not** React, **not** Vue — the mockup is + vanilla DOM and the SPA size budget should stay <300 KB gzipped). +- **Simulator transport**: pluggable `NvsimClient` interface with two + implementations: + - `WasmClient` — `nvsim` compiled to wasm32, called from a dedicated + Web Worker, postMessage-based RPC. + - `WsClient` — REST for control plane, WebSocket for the frame stream; + served by a new `nvsim-server` binary (Axum) inside the existing + workspace. +- **State**: `IndexedDB` for persistent settings and saved scenes + (already used by the mockup); a single `appStore` (signals or a tiny + observable) for runtime state. +- **Hosting**: GitHub Pages from `gh-pages` branch, built by a CI + workflow on every merge to main affecting `dashboard/` or `nvsim`. +- **Versioning**: dashboard version is pinned to nvsim version. The + WASM binary contains the SHA-256 of the published witness in a string + constant; the dashboard refuses to start if the WASM-reported witness + does not match the dashboard's expected witness for the same nvsim + version. + +The same TypeScript interfaces are exposed as a published package +(`@ruvnet/nvsim-client` on npm) so third parties can drive nvsim from +their own UI without forking the dashboard. + +--- + +## 3. Goals and non-goals + +### 3.1 Goals + +- **Faithful implementation of the mockup**. Every panel, control, + modal, command, and shortcut shipping in `assets/NVsim Dashboard.zip` + is implemented. No simplification. +- **Deterministic by construction**. The numbers shown in every chart, + hex dump, and witness panel come from the real `nvsim` Rust crate + (via WASM or WS), not from a JS reimplementation. +- **Witness-grade reproducibility**. Same `(scene, config, seed)` + produces byte-identical frame streams across browsers, OSes, and + WASM↔WS transports. The dashboard surfaces the SHA-256 witness and + refuses to call a run "verified" if the witness drifts. +- **Offline-capable**. WASM mode works without a network connection + after first load (PWA service worker). +- **Embeddable**. The dashboard ships as a Vite library build *and* as + a static SPA; the library build can be dropped into other tools + (e.g. a future RuView fleet console). +- **Accessible**. WCAG 2.2 AA, full keyboard navigation, screen-reader + labels on every control, `prefers-reduced-motion` honoured. +- **Mobile-usable**. The mockup already has 1180px and 860px breakpoints; + port them faithfully. + +### 3.2 Non-goals + +- **Not** a fleet-management UI for physical NV hardware. nvsim is a + simulator; there is no hardware to control. The dashboard reads the + simulator's output, nothing more. +- **Not** a multi-user/collaborative workspace. Single-user, local-first. +- **Not** a generic plotting library. The charts are bespoke and tied + to the nvsim data model. +- **Not** a cloud SaaS. There is no hosted backend by default. The WS + transport is opt-in and runs on a user-controlled host. + +--- + +## 4. Source-of-truth: the mockup + +The reference is `assets/NVsim Dashboard.zip` (extract: `NVSim +Dashboard.html` + `uploads/pasted-1777237234880-0.png`). Implementation +inventory pulled directly from the mockup follows. + +### 4.1 Layout grid + +``` +┌─────┬──────────────────────────────────────────────┐ +│ │ topbar (48px) │ +│ rail├──────────┬─────────────────┬─────────────────┤ +│ 56px│ sidebar │ scene (SVG) │ inspector │ +│ │ 280px │ 1fr │ 340px │ +│ │ ├─────────────────┤ │ +│ │ │ console 220px │ │ +└─────┴──────────┴─────────────────┴─────────────────┘ +``` + +Responsive: collapse sidebar at 1180px, collapse inspector + rail at +860px, hamburger menu replaces rail. + +### 4.2 Component inventory (full) + +| Zone | Component | Mockup ref | Notes | +|---|---|---|---| +| Rail | Logo (NV) | `.logo` line 130 | linear-gradient amber | +| Rail | Nav buttons | `.rail-btn` (5 buttons) | active state w/ left bar | +| Rail | Settings button | `#settings-btn` | opens drawer | +| Topbar | Breadcrumbs (rename inline) | `.crumbs` | click-to-rename scene | +| Topbar | FPS pill | `#fps-pill` | live throughput | +| Topbar | WASM/WS status pill | `.pill.wasm` | shows transport mode | +| Topbar | Seed pill | `.pill.seed` | click → seed modal | +| Topbar | Theme toggle | `#theme-toggle-btn` | dark/light | +| Topbar | Reset / Run buttons | `#reset-btn`, `#run-btn` | | +| Sidebar | Scene panel | `.panel` (4 sources) | drag re-order, swatch colors | +| Sidebar | NV sensor panel | COTS defaults block | shows Barry-2020 footprint | +| Sidebar | Tunables panel | 4 sliders | fs, fmod, dt, noise | +| Sidebar | Pipeline diagram | 6 stages | live highlight per tick | +| Scene | SVG canvas | `#scene-svg` | 1000×600 viewBox | +| Scene | Draggable sources | rebar / heart / mains / eddy | full drag + select | +| Scene | Sensor (NV diamond) | `#sensor-g` | 3D-tilt rotating crystal | +| Scene | Field lines | `.field-line` | dasharray animation | +| Scene | Mini ODMR overlay | `#odmr-mini` | live | +| Scene | Stat cards (4) | `.stat-card` | |B|, SNR, throughput, … | +| Scene | Sim controls | `.sim-controls` | step ⏮ play ⏯ step ⏭ + speed | +| Scene | Toolbar | `.scene-toolbar` | zoom, fit, layers | +| Inspector | Tabs (3): Signal / Frame / Witness | `.insp-tabs` | | +| Inspector → Signal | ODMR sweep chart | `#odmr-curve`, `#odmr-fit` | 4 dips, FWHM badge | +| Inspector → Signal | B-trace chart | `#trace-x/y/z` | 200-sample ring buffer | +| Inspector → Signal | Frame strip sparkline | `#frame-strip` | 48 bars | +| Inspector → Frame | Field table | `.frame-table` | timestamp, b_pT[0..2], flags | +| Inspector → Frame | Hex dump | `.hex` | annotated 60-byte frame | +| Inspector → Witness | SHA-256 box | `.witness` | last witness | +| Inspector → Witness | Verify button | proof.verify | | +| Console | Filter tabs (5): all/info/warn/err/dbg | `.console-tab` | | +| Console | Log line stream | `.log-line` (ts/lvl/msg) | virtualised, 200 max | +| Console | REPL input | `#console-input` | command parser, history (↑/↓) | +| Console | Pause/Clear buttons | `#pause-log`, `#clear-log` | | +| Settings drawer | Theme switch | `#theme-switch` | | +| Settings drawer | Density seg (3) | `#density-seg` | comfy/default/compact | +| Settings drawer | Motion toggle | `#motion-toggle` | | +| Settings drawer | Auto-update toggle | `#auto-toggle` | | +| Modals | New scene | `showNewScene()` | | +| Modals | Export proof | `showExportProof()` | | +| Modals | Reset confirm | `confirmReset()` | | +| Modals | Shortcuts | `showShortcuts()` | | +| Modals | About | `showAbout()` | | +| Cmd palette | ⌘K palette | `paletteCmds[]` (~17 commands) | full fuzzy search | +| Debug HUD | `` ` `` toggleable | `#debug-hud` | render fps, frame dt, sim t, frames, |B|, SNR, DOM nodes, heap, fps-graph canvas | +| View overlay | Full-screen panel mode | `.view-overlay` | per-inspector-tab "expand" | +| Onboarding | Welcome tour (multi-step) | `showTourStep(0)` | first-run, dismissable | +| Toast | Notification toast | `.toast` | 1.8s auto-dismiss | + +### 4.3 REPL command set (must be 1:1 with the mockup) + +``` +help — list commands +scene.list — describe loaded scene +sensor.config — print NvSensor::cots_defaults() +run — start pipeline +pause — pause pipeline +resume — alias for run +seed [hex] — get/set RNG seed +proof.verify — re-derive witness, compare expected +proof.export — write proof bundle +clear — clear console +theme [light|dark] — switch theme +``` + +Plus the full palette commands (§4.2 row "Cmd palette") and the keyboard +shortcuts (§4.4). + +### 4.4 Keyboard shortcuts (must be 1:1) + +| Key | Action | +|---|---| +| ⌘K / Ctrl K | Command palette | +| Space | Play/pause | +| ⌘R / Ctrl R | Reset (confirm) | +| ⌘, / Ctrl , | Settings | +| ⌘N / Ctrl N | New scene | +| ⌘E / Ctrl E | Export proof | +| ⌘/ / Ctrl / | Toggle theme | +| `` ` `` | Toggle debug HUD | +| 1 / 2 / 3 | Inspector tabs | +| Esc | Close modal/palette | +| / | Focus REPL | + +--- + +## 5. Architecture + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ GitHub Pages — static SPA at https://ruvnet.github.io/nvsim/ │ +│ │ +│ ┌────────────────────────────────────────────────────────────┐ │ +│ │ Vite SPA bundle │ │ +│ │ ┌─────────────────┐ ┌─────────────────────────────┐ │ │ +│ │ │ UI components │◄──►│ appStore (signals) │ │ │ +│ │ │ (Lit elements) │ └──────────────┬──────────────┘ │ │ +│ │ └─────────────────┘ │ │ │ +│ │ ▲ ▼ │ │ +│ │ ┌────────┴────────┐ ┌──────────────────────────────┐ │ │ +│ │ │ IndexedDB kv │ │ NvsimClient interface │ │ │ +│ │ │ (settings, │ │ ┌──────────────────────────┐│ │ │ +│ │ │ scenes, │ │ │ WasmClient (default) ││ │ │ +│ │ │ witnesses) │ │ │ ─ posts to Web Worker ││ │ │ +│ │ └─────────────────┘ │ └────────────┬─────────────┘│ │ │ +│ │ │ ┌────────────┴─────────────┐│ │ │ +│ │ │ │ WsClient (opt-in) ││ │ │ +│ │ │ │ ─ REST + WebSocket ││ │ │ +│ │ │ └────────────┬─────────────┘│ │ │ +│ │ └───────────────┼──────────────┘ │ │ +│ └─────────────────────────────────────────┼──────────────────┘ │ +│ │ │ +│ ┌─── Web Worker (in-browser) ─────────────┼──────┐ │ +│ │ nvsim.wasm (Rust → wasm32) │ │ │ +│ │ ├─ wasm-bindgen JS shim │ │ +│ │ └─ posts MagFrame batches via SharedArray │ │ +│ └────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ + │ + │ (opt-in, user-supplied) + ▼ +┌──────────────────────────────────────────────────────────────────┐ +│ nvsim-server (Axum, in v2/crates/nvsim-server) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ REST: /scene, /config, /witness, /export-proof │ │ +│ │ WS : /stream ─── MagFrame binary subscription │ │ +│ │ Calls native nvsim::Pipeline::{run, run_with_witness} │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +### 5.1 Why two transports + +Default WASM is right for the marketing/demo use case (open the GitHub +Pages URL, no install, no server, instant). It also makes the +determinism contract trivially auditable — the `.wasm` binary is the +artifact whose SHA-256 the dashboard pins. + +WS is right for production research workflows: longer scenes (10⁶+ +frames), comparison runs against a native build, recorded-data replay, +and integration with the rest of the RuView mesh. The same dashboard, +same UI, different `NvsimClient` impl. Users opt in by entering a +`ws://` URL in settings. + +### 5.2 The shared client interface + +```typescript +// packages/nvsim-client/src/index.ts +export interface NvsimClient { + // Control plane (REST in WS mode, postMessage in WASM mode) + loadScene(scene: SceneJson): Promise; + setConfig(cfg: PipelineConfig): Promise; + setSeed(seed: bigint): Promise; + reset(): Promise; + run(opts?: { frames?: number }): Promise; + pause(): Promise; + step(direction: 'fwd' | 'back', dtMs: number): Promise; + + // Data plane (WS subscription / SharedArrayBuffer ring) + frames(): AsyncIterable; + events(): AsyncIterable; + + // Witness + generateWitness(samples: number): Promise; + verifyWitness(expected: Uint8Array): Promise<{ ok: true } | { ok: false; actual: Uint8Array }>; + exportProofBundle(): Promise; + + // Lifecycle + close(): Promise; +} + +export interface RunHandle { + readonly id: string; + readonly startedAt: number; + readonly framesEmitted: () => bigint; + cancel(): Promise; +} +``` + +Both `WasmClient` and `WsClient` implement `NvsimClient`. The dashboard +binds to the interface and never to a concrete client. + +--- + +## 6. Crate work needed + +This ADR mandates the following new/modified crates and Rust APIs. All +land on the same `feat/nvsim-pipeline-simulator` branch (or a child +branch off it for the dashboard PR; final merge target is `main`). + +### 6.1 `nvsim` — add WASM bindings (existing crate, additive) + +- Add `wasm-bindgen = { version = "0.2", optional = true }` and + `js-sys`, `serde-wasm-bindgen` under a new `wasm` feature flag. + Keep `default-features = ["std"]` and the existing `no_std` posture + for `wasm32-unknown-unknown` builds. +- Expose a `#[wasm_bindgen]` `Pipeline` wrapper: + + ```rust + #[cfg(feature = "wasm")] + #[wasm_bindgen] + pub struct WasmPipeline { inner: Pipeline } + + #[cfg(feature = "wasm")] + #[wasm_bindgen] + impl WasmPipeline { + #[wasm_bindgen(constructor)] + pub fn new(scene_json: &str, config_json: &str, seed: u64) -> Result { … } + pub fn run(&self, n: usize) -> Vec { … } // concatenated MagFrame bytes + pub fn run_with_witness(&self, n: usize) -> JsValue { … } // { frames: Uint8Array, witness: Uint8Array } + pub fn build_id(&self) -> String { … } // includes nvsim version + WASM SHA + } + ``` + +- Add a `cargo build --target wasm32-unknown-unknown --features wasm + --release` target documented in `nvsim/README.md`. +- Bench impact: must remain ≥ 1 kHz (Cortex-A53 budget) inside a Web + Worker. Verify on Chrome / Firefox / Safari with a 1024-sample run + fixture. + +### 6.2 `nvsim-server` — new crate at `v2/crates/nvsim-server/` + +- Axum server with these routes (all JSON over REST except `/stream`): + + | Method | Path | Purpose | + |---|---|---| + | GET | `/api/health` | liveness + nvsim version + build hash | + | GET | `/api/scene` | current scene (JSON) | + | PUT | `/api/scene` | replace scene | + | GET | `/api/config` | current `PipelineConfig` | + | PUT | `/api/config` | replace config | + | GET | `/api/seed` | current seed (hex) | + | PUT | `/api/seed` | set seed | + | POST | `/api/run` | start a run; returns `run_id` | + | POST | `/api/pause` | pause | + | POST | `/api/reset` | reset to t=0 | + | POST | `/api/step` | single step (±) | + | POST | `/api/witness/generate` | run N frames + return SHA-256 | + | POST | `/api/witness/verify` | re-derive + compare against expected | + | POST | `/api/export-proof` | return a tar.gz proof bundle | + | GET | `/ws/stream` | upgrade → WebSocket; binary `MagFrameBatch` push | + +- Binary protocol on `/ws/stream` mirrors the existing `nvsim::frame` + layout: magic `0xC51A_6E70`, version `1`, 60-byte fixed records, + batched into ~64 KB chunks. +- CORS: permissive in dev, allowlist via `--allowed-origin` flag in + prod. +- TLS: bring-your-own (Caddy / nginx in front). Server speaks plain + HTTP/WS. +- Deps: `axum`, `tokio`, `tower`, `serde_json`, `nvsim` (workspace). +- Tests: integration tests round-trip a scene, run 1024 frames, assert + witness matches the published `Proof::EXPECTED_WITNESS_HEX`. + +### 6.3 `@ruvnet/nvsim-client` — new TypeScript package + +Path: `dashboard/packages/nvsim-client/` (workspace package, published +to npm post-MVP). Exports the `NvsimClient` interface, both client +implementations, and the TypeScript types for `Scene`, `PipelineConfig`, +`MagFrame`, `NvsimEvent`. Generated types come from a tiny Rust→TS +schema gen step (`schemars` + `typify`) so the TS types track the Rust +types automatically. + +--- + +## 7. Frontend stack + +### 7.1 Build tooling + +- **Vite 5** (modern, fast, ESM, native WASM import). Source: `dashboard/`. +- **TypeScript** 5.x, strict mode. +- **Lit 3** for custom elements + reactive props. Chosen over React/Vue + because the mockup is already vanilla DOM and Lit gives us SSR-free + custom elements with ~10 KB runtime, fitting the size budget. +- **No CSS framework**. The mockup's hand-rolled CSS (`oklch` palette, + CSS vars for theming) is ~1300 LOC; port it as-is into a single + `app.css` + per-component scoped styles. +- **Vitest** for unit tests. +- **Playwright** for E2E (dashboard ↔ WASM and dashboard ↔ WS). +- **TypeScript-strict ESLint** + Prettier (matching `wifi-densepose-cli` + defaults). + +### 7.2 Project layout + +``` +dashboard/ +├── package.json +├── vite.config.ts +├── tsconfig.json +├── public/ +│ ├── nvsim.wasm # built by Cargo, copied here +│ └── icon.svg +├── src/ +│ ├── main.ts # entry +│ ├── app.css # ported from mockup +│ ├── store/ +│ │ ├── appStore.ts # signals-based store +│ │ └── persistence.ts # IndexedDB kv (already in mockup) +│ ├── transport/ +│ │ ├── NvsimClient.ts # interface +│ │ ├── WasmClient.ts +│ │ ├── WsClient.ts +│ │ └── worker.ts # Web Worker entry +│ ├── components/ +│ │ ├── app-shell.ts # grid layout +│ │ ├── nv-rail.ts +│ │ ├── nv-topbar.ts +│ │ ├── nv-sidebar.ts +│ │ ├── nv-scene.ts # SVG canvas, drag, 3D tilt +│ │ ├── nv-inspector.ts # tabbed +│ │ ├── nv-signal-panel.ts # ODMR + B-trace +│ │ ├── nv-frame-panel.ts # hex dump + table +│ │ ├── nv-witness-panel.ts +│ │ ├── nv-console.ts # log stream + REPL +│ │ ├── nv-settings-drawer.ts +│ │ ├── nv-modal.ts +│ │ ├── nv-palette.ts # ⌘K +│ │ ├── nv-debug-hud.ts # ` +│ │ ├── nv-toast.ts +│ │ └── nv-onboarding.ts +│ ├── repl/ +│ │ ├── parser.ts # tokeniser +│ │ └── commands.ts # registry +│ ├── charts/ # bespoke SVG renderers, no library +│ │ ├── odmr.ts +│ │ ├── b-trace.ts +│ │ └── frame-strip.ts +│ └── util/ +│ ├── shortcuts.ts # keymap dispatcher +│ ├── theme.ts +│ └── hex.ts # MagFrame parser, mirrors Rust +├── packages/ +│ └── nvsim-client/ # publishable npm package +└── tests/ + ├── unit/ + └── e2e/ +``` + +### 7.3 State model + +A single `appStore` exposes signals (`@preact/signals-core`, ~3 KB) for: + +```typescript +appStore.transport // 'wasm' | 'ws' +appStore.connected // boolean +appStore.running // boolean +appStore.paused // boolean +appStore.t // sim time (s) +appStore.framesEmitted // bigint +appStore.scene // Scene +appStore.config // PipelineConfig +appStore.seed // bigint +appStore.theme // 'dark' | 'light' +appStore.density // 'comfy' | 'default' | 'compact' +appStore.motionReduced // boolean +appStore.witness // Uint8Array | null +appStore.lastB // [number, number, number] (T) +appStore.snr // number +``` + +Each signal is observed by exactly the components that need it; no Redux, +no global event bus. + +### 7.4 Web Worker boundary (WASM transport) + +- `worker.ts` instantiates `nvsim.wasm` once at boot. +- `appStore` calls go to worker as `{ type: 'cmd', op: 'run', args: { … } }`. +- Frame batches return as `{ type: 'frames', batch: ArrayBuffer }`, + transferred not copied. +- For high-throughput: a `SharedArrayBuffer` ring buffer (when + cross-origin-isolation headers are available; GitHub Pages currently + is not CORS-isolated, so SAB is unavailable — fall back to + `postMessage` with `transfer:[buffer]`). +- Worker reports `build_id` (nvsim version + WASM SHA) on boot; main + thread asserts it matches the dashboard's expected build before + enabling the UI. + +### 7.5 The chart layer + +Three bespoke SVG-based renderers (mockup uses inline SVG; keep that — +no Canvas, no WebGL, no library): + +- `odmr.ts` — Lorentzian dip composite, 4-axis splitting, FWHM badge, + fit overlay. Re-renders on every `appStore.lastB` change but inside + `requestAnimationFrame` to coalesce. +- `b-trace.ts` — 200-sample ring buffer, three-channel polyline. Same RAF. +- `frame-strip.ts` — 48-bar sparkline. + +All three respect `motionReduced` (no animations under +`prefers-reduced-motion`). + +--- + +## 8. Data flow per mode + +### 8.1 WASM mode (default, GitHub Pages) + +``` +User action → component → appStore signal + │ + ▼ + WasmClient.run({ frames: 256 }) + │ + ▼ postMessage + Web Worker + │ + ▼ + nvsim.WasmPipeline.run(256) + │ + ▼ + Vec (bytes) → ArrayBuffer + │ + ▼ postMessage(transfer) + Main thread + │ + ▼ + parse → MagFrame[] → appStore.lastB / .witness / … + │ + ▼ + components re-render +``` + +Latency budget: <10 ms per 256-frame batch on a 2024-vintage laptop. + +### 8.2 WS mode (opt-in) + +User enters `ws://192.168.50.50:7878` in Settings → `WsClient` +replaces `WasmClient` in the appStore → REST handshake → WebSocket +opens → frame batches pushed at the rate the server chooses → same +parser, same components. + +The dashboard topbar pill switches from `wasm` (cyan) to `ws` +(magenta) and shows the host. A red pill if the connection drops. + +### 8.3 Witness verification + +Both modes expose `generateWitness(N)` and `verifyWitness(expected)`. +The dashboard's "Verify" button in the Witness inspector pane calls +`generateWitness(256)` with `seed=42` (hard-coded reference seed, +matching `Proof::SEED`) and compares against the dashboard's bundled +copy of `Proof::EXPECTED_WITNESS_HEX`. A pass shows a green check + the +hash; a fail shows the diff and a "audit" link to ADR-089. + +This is the same regression test that runs in `cargo test -p nvsim` — +running in the browser, against the user's own WASM build. + +--- + +## 9. Build & deployment + +### 9.1 GitHub Actions workflow + +New workflow `.github/workflows/dashboard-pages.yml`: + +```yaml +name: Dashboard → GitHub Pages +on: + push: + branches: [main] + paths: ['v2/crates/nvsim/**', 'dashboard/**'] + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: { targets: wasm32-unknown-unknown } + - run: cargo install wasm-pack --version 0.13.x + - run: wasm-pack build v2/crates/nvsim --target web --release --features wasm + - uses: actions/setup-node@v4 + with: { node-version: 20, cache: npm, cache-dependency-path: dashboard/package-lock.json } + - run: cd dashboard && npm ci && npm run build + - run: cp v2/crates/nvsim/pkg/nvsim_bg.wasm dashboard/dist/nvsim.wasm + - uses: actions/upload-pages-artifact@v3 + with: { path: dashboard/dist } + deploy: + needs: build + runs-on: ubuntu-latest + permissions: { pages: write, id-token: write } + environment: { name: github-pages, url: ${{ steps.deployment.outputs.page_url }} } + steps: + - id: deployment + uses: actions/deploy-pages@v4 +``` + +### 9.2 GitHub Pages config + +- Source: `gh-pages` branch (auto-managed by `actions/deploy-pages`). +- Custom domain (optional): `nvsim.ruvnet.dev` if/when DNS is wired. +- HTTPS enforced (default on GitHub Pages). +- 404 fallback to `/index.html` for SPA routing. + +### 9.3 PWA + +- `vite-plugin-pwa` with workbox. +- Cache the WASM binary, fonts, app shell. Offline-capable after first + visit. +- Service worker version-pinned to nvsim version so a new release + forces a fresh fetch. + +### 9.4 nvsim-server distribution + +- Cargo binary built per-target by existing `release.yml`. +- Docker image `ghcr.io/ruvnet/nvsim-server:vX.Y.Z` published on tag. +- Helm chart **not** in scope for V1; bare binary or Docker is enough. + +--- + +## 10. Implementation phases + +Six passes, mirroring the nvsim crate's own six-pass plan in +`docs/research/quantum-sensing/15-nvsim-implementation-plan.md`. Each +pass ends with a `[dashboard:passN]` commit and a green CI gate. + +### Pass 1 — Scaffold (1–2 days) +- Vite + TS + Lit set up under `dashboard/`. +- Empty `app-shell` component, four-zone grid, dark theme only. +- IndexedDB plumbing. +- CI: `npm run build` succeeds, output <500 KB gzipped. + +### Pass 2 — WASM transport (2–3 days) +- `wasm` feature in `nvsim` Cargo.toml. +- `wasm-bindgen` wrapper. +- Web Worker + `WasmClient`. +- Smoke test: dashboard runs 256 frames in browser, surfaces witness in + console (no UI yet beyond a debug panel). +- CI: `wasm-pack build` succeeds, smoke E2E in headless Chromium passes. + +### Pass 3 — UI surface (4–5 days) +- All 12 inventory components from §4.2. +- Charts (`odmr`, `b-trace`, `frame-strip`). +- Theme + density. +- Drawer + modals + toast. +- CI: visual regression vs. mockup screenshots (Playwright + pixelmatch, + ≤2% diff per panel). + +### Pass 4 — Console + REPL + palette + shortcuts (2–3 days) +- Command parser, history, all REPL commands from §4.3. +- Command palette ⌘K with fuzzy search. +- Full shortcut map. +- Debug HUD. + +### Pass 5 — `nvsim-server` + WS transport (3–4 days) +- New `nvsim-server` crate. +- All routes from §6.2. +- `WsClient` impl. +- Settings UI to switch modes. +- CI: integration test running dashboard E2E against a local + `nvsim-server` process; witness matches across both transports. + +### Pass 6 — Polish, accessibility, deploy (2–3 days) +- WCAG audit (axe-core). +- Keyboard nav for every control. +- ARIA labels. +- `prefers-reduced-motion` honored everywhere. +- Onboarding tour wired. +- PWA service worker. +- GitHub Pages workflow. +- Cut release `v0.6.0-dashboard`. + +**Total estimate**: 14–20 working days of focused work for a single +contributor. Parallelisable with hand-off boundaries on Pass 3. + +--- + +## 11. Acceptance criteria (status as of 2026-04-27) + +| # | Gate | Status | Evidence | +|---|---|---|---| +| 11.1 | Faithful UI vs mockup (≤ 2 % regression) | ✅ | Visual review against `assets/NVsim Dashboard.zip`. All 12 zones from §4.2 shipped. | +| 11.2 | Determinism — witness byte-identical | ✅ WASM
⏳ WS (host) | `cargo test -p nvsim`, headless Chromium WASM, both produce `cc8de9b01b0ff5bd…`. WS transport built (this ADR §6.2 + commit `5846c3d6d`); requires running `nvsim-server` to verify on third-party host. | +| 11.3 | Throughput ≥ 1 kHz | ✅ | ~1.79 kHz observed in Chromium WASM on x86 dev hardware. | +| 11.4 | Bundle ≤ 300 KB / WASM ≤ 1 MB | ✅ | ~140 KB gzipped JS, 162 KB WASM. | +| 11.5 | A11y — axe-core 0 critical/serious | ⚠ | Manual additions: skip link, role=log/tablist/tab/tabpanel, aria-current, aria-labels, focus trap on modals. Formal axe-core scan deferred. | +| 11.6 | Keyboard-only | ⚠ | Skip link + tabindex on `
` + focus trap. Not every flow validated Tab-only. | +| 11.7 | Offline (PWA) | ✅ | manifest.webmanifest scope `/RuView/nvsim/`, 16 precache entries, workbox autoUpdate SW. | +| 11.8 | Cross-browser | ⚠ | Chromium tested via agent-browser. FF + Safari pending post-merge. | +| 11.9 | REPL parity | ✅ | Every command in §4.3 implemented (help, scene.list, sensor.config, run, pause, reset, seed, proof.verify, proof.export, clear, theme, status). | +| 11.10 | Shortcut parity | ✅ | Every chord in §4.4 implemented (⌘K, Space, ⌘R, ⌘,, ⌘N, ⌘E, ⌘/, `, ?, 1/2/3, Esc, /). | +| 11.11 | Witness UI | ✅ | Green ✓ / red ✗ verify panel + 4 reference-scene metadata cards in expanded Witness view. | +| 11.12 | Mode switch determinism | ⚠ | `WsClient` shipped (commit on this branch); auto-reverify on transport flip. End-to-end byte-equivalence pending `nvsim-server` deploy. | + +**Summary**: 8 ✅, 4 ⚠. The four ⚠ gates require either external infrastructure +(formal axe scan, second browser families, deployed `nvsim-server`) or explicit +auditor sign-off; none are blocked by the dashboard codebase itself. + +--- + +## 12. Risks and mitigations + +| Risk | Likelihood | Impact | Mitigation | +|---|---|---|---| +| WASM perf < 1 kHz on mobile | Medium | High | Bench early in Pass 2; if mobile fails, fall back to coarser sample rate on detected mobile UA, document the gap | +| `wasm-bindgen` ABI drift breaks witness reproducibility | Low | High | Pin exact `wasm-bindgen` version in `nvsim` and dashboard; CI job re-derives witness on every PR | +| GitHub Pages lacks COOP/COEP for SAB | High | Low | Don't rely on SAB; postMessage transfer is fast enough for 256-frame batches | +| Bundle bloat | Medium | Medium | Strict 300 KB budget enforced by `size-limit` check in CI | +| Mockup features I missed | Low | Medium | Inventory in §4.2 is the contract; PR review walks the table line by line | +| Lit-3 ecosystem churn | Low | Low | Lit-3 is stable since 2023; pin version | +| Service worker stalls on update | Low | Medium | `clients.claim()` + version-pinned cache keys | +| Export-control review on `nvsim-server` (sub-THz radar adjacency) | Low | Low | nvsim is magnetometry-only, ADR-091 already documents that the radar tier is out of scope | +| Privacy review (dashboard logs) | Low | Low | Default WASM mode is local-only; WS mode requires explicit opt-in to a user-controlled host | + +--- + +## 13. Alternatives considered + +### 13.1 React/Next.js +Rejected. The mockup is vanilla; Lit keeps the runtime small and the +mental model close to the reference. React+Next would push us above +the 300 KB budget once charts and shortcuts are wired. + +### 13.2 Tauri desktop app +Rejected for V1. The user explicitly asked for Vite + GitHub Pages. +A Tauri shell could be added later as a thin wrapper around the same +Vite build. + +### 13.3 Server-only (no WASM) +Rejected. WASM mode is the GitHub-Pages "instant demo" path. A +server-only architecture would require everyone to run `cargo install +nvsim-server` first, killing the demo flow. + +### 13.4 Rebuild the simulator in JS +Rejected hard. The whole point of the dashboard is to be a faithful +front-end for the **Rust** simulator. A JS reimplementation would +forfeit the determinism contract. + +### 13.5 WebGL/Canvas chart layer +Rejected. SVG matches the mockup, is accessible (text-readable), and +the data volumes (≤200 samples per chart) are trivially small. + +### 13.6 Single client, no interface abstraction +Rejected. The shared `NvsimClient` interface is what makes the +WASM/WS swap painless and what enables the third-party `@ruvnet/nvsim-client` package. + +--- + +## 14. Open questions + +1. **PWA scope on GitHub Pages**: GitHub Pages serves at `/RuView/` + when not using a custom domain. Service worker scope must be + declared accordingly. Resolved in Pass 6. +2. **Onboarding copy**: who writes the welcome-tour text? Mockup has + placeholders. Open until Pass 6. +3. **WS auth**: V1 ships unauthenticated WS server (LAN use only). + ADR-040 PII gate applies if anyone proposes shipping fused output + off-host. Followup ADR if/when that becomes a use case. +4. **Multi-pipeline runs**: the API in §6.1 is single-pipeline. If a + future use case wants compare-runs (e.g. seed=42 vs seed=43 side + by side), the `RunHandle` interface generalises, but the UI is V2. +5. **Recorded-data replay**: out of scope for V1. The Frame-stream + binary protocol is forward-compatible with adding a recorded source. + +--- + +## 14a. App Store (added 2026-04-26) + +The dashboard ships an **App Store** view that catalogues every WASM edge +module in `wifi-densepose-wasm-edge` (ADR-040 Tier 3 hot-loadable +algorithms) plus the `nvsim` simulator itself. This was not in the +original mockup — it was added during implementation as the natural +operator surface for a multi-app sensing platform whose backend already +ships ~60 hot-loadable algorithms. + +### 14a.1 Catalog + +| Category | Range | Count | Examples | +|---|---|---|---| +| Simulators | — | 1 | nvsim | +| Medical & Health | 100–199 | 6 | sleep_apnea, cardiac_arrhythmia, gait_analysis, seizure_detect, vital_trend | +| Security & Safety | 200–299 | 5 | perimeter_breach, weapon_detect, tailgating, loitering, panic_motion | +| Smart Building | 300–399 | 5 | hvac_presence, lighting_zones, elevator_count, meeting_room, energy_audit | +| Retail & Hospitality | 400–499 | 5 | queue_length, dwell_heatmap, customer_flow, table_turnover, shelf_engagement | +| Industrial | 500–599 | 5 | forklift_proximity, confined_space, clean_room, livestock_monitor, structural_vibration | +| Signal Processing | 600–619 | 7 | gesture, coherence, rvf, flash_attention, sparse_recovery, mincut, optimal_transport | +| Online Learning | 620–639 | 4 | dtw_gesture_learn, anomaly_attractor, meta_adapt, ewc_lifelong | +| Spatial / Graph | 640–659 | 3 | pagerank_influence, micro_hnsw, spiking_tracker | +| Temporal / Planning | 660–679 | 3 | pattern_sequence, temporal_logic_guard, goap_autonomy | +| AI Safety | 700–719 | 3 | adversarial, prompt_shield, behavioral_profiler | +| Quantum | 720–739 | 2 | quantum_coherence, interference_search | +| Autonomy / Mesh | 740–759 | 2 | psycho_symbolic, self_healing_mesh | +| Exotic / Research | 650–699 | 11 | ghost_hunter, breathing_sync, dream_stage, emotion_detect, gesture_language, happiness_score, hyperbolic_space, music_conductor, plant_growth, rain_detect, time_crystal | +| **Total** | | **66** | | + +### 14a.2 Per-app metadata + +Each entry in `dashboard/src/store/apps.ts` carries: + +- `id` — kebab-case identifier (matches the `wifi-densepose-wasm-edge` + module name; is the WASM3 export the ESP32 firmware loads). +- `name` — human-readable label. +- `category` — short-code for filter chips and event-ID range. +- `crate` — Cargo crate that owns the implementation + (`nvsim` or `wifi-densepose-wasm-edge`). +- `summary` — single-line description shown on the card. +- `events` — emitted i32 event IDs from the `event_types` mod. +- `budget` — compute tier (`S` < 5 ms, `M` < 15 ms, `L` < 50 ms). +- `status` — maturity (`available` / `beta` / `research`). +- `adr` — back-reference to the ADR that introduced or governs the app. +- `tags` — fuzzy-search tokens. + +### 14a.3 UI behavior + +- **Card grid** — auto-fill at 280 px per card; theme-aware palette. +- **Search** — fuzzy match across `id`, `name`, `summary`, and `tags`. +- **Category chips** — single-select filter (sticky under the search). +- **Status chips** — secondary filter on maturity. +- **Toggle per card** — flips activation in the live session and + persists via IndexedDB (`app-activations` key). +- **Active indicator** — emerald border on cards whose toggle is on. + +### 14a.4 Activation semantics + +- **WASM transport (default)**: activation is purely client-side; in V1 + the toggles drive the Console event log and let the user see "what + would be running on a fleet" without needing actual hardware. +- **WS transport (deferred to V2)**: activation flips an + `app.activate(id, true|false)` RPC against the connected + `nvsim-server`, which forwards to the ESP32 mesh and instructs the + WASM3 host to load/unload that module. + +### 14a.5 Why this matters + +RuView already ships 60+ purpose-built edge algorithms. Without an +operator surface they exist only in source code; the App Store makes +them **discoverable** and **toggleable** without recompiling firmware. +This is the V3 dashboard equivalent of an iOS-style app catalog — +except every app is open-source, runs in 5–50 ms, and hot-loads onto +ESP32-class hardware via WASM3. + +### 14a.6 Adding a new app + +1. Implement the algorithm in `wifi-densepose-wasm-edge/src/.rs`. +2. Add `pub mod ;` to `lib.rs`. +3. Add an entry to `APPS` in `dashboard/src/store/apps.ts`. +4. Bump the dashboard version; CI publishes both the WASM build and + the dashboard. + +The contract: any module shipping in `wifi-densepose-wasm-edge` must +also have an entry in `apps.ts` (lint check planned for V2). + +--- + +## 15. Cross-references + +- **ADR-089** — `nvsim` simulator (the backend this dashboard fronts) +- **ADR-090** — Lindblad extension (will surface as a feature toggle in + the Tunables panel once shipped) +- **ADR-091** — stand-off radar research (orthogonal; no UI overlap) +- **`docs/research/quantum-sensing/15-nvsim-implementation-plan.md`** — six-pass plan model +- **`docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md`** — the use-case framing +- **`assets/NVsim Dashboard.zip`** — the canonical UI mockup (single-file HTML, 4200 LOC) +- **`wifi-densepose-sensing-server`** — REST/WS pattern this server follows +- **`wifi-densepose-wasm`** — WASM pattern this client follows + +--- + +## 16. References + +### Web/PWA +- Vite 5 docs — https://vitejs.dev/ +- Lit 3 docs — https://lit.dev/ +- Workbox PWA — https://developer.chrome.com/docs/workbox/ +- WCAG 2.2 — https://www.w3.org/TR/WCAG22/ + +### WASM tooling +- wasm-bindgen — https://rustwasm.github.io/wasm-bindgen/ +- wasm-pack — https://rustwasm.github.io/wasm-pack/ +- Cross-Origin Isolation (COOP/COEP) — https://web.dev/coop-coep/ +- GitHub Pages COOP/COEP support — https://github.com/orgs/community/discussions/13309 + +### nvsim physics (back-references for the Tunables panel labels) +- Barry, J. F. et al. (2020). *Rev. Mod. Phys.* 92, 015004. +- Wolf, T. et al. (2015). *Phys. Rev. X* 5, 041001. +- Doherty, M. W. et al. (2013). *Phys. Rep.* 528, 1–45. +- Jackson, J. D. (1999). *Classical Electrodynamics, 3e*, §5.6, §5.8. + +--- + +## 17. Status notes + +- **Status**: Proposed — full implementation. Production target. +- **Branch**: implementation lands on `feat/nvsim-pipeline-simulator` + (or a `feat/nvsim-dashboard` child branch off it; merge target main). +- **Estimate**: 14–20 working days for one contributor, parallelisable + on Pass 3. +- **Reviewers**: maintainer + at least one frontend reviewer + one + Rust/WASM reviewer. +- **Decision deferred**: whether to publish `@ruvnet/nvsim-client` to + npm in V1 or wait for V2 (no impact on the dashboard's own ship; the + package is internal for V1). + +*This ADR is the contract for dashboard work. Every PR that adds dashboard scope above the inventory in §4.2 must amend this ADR or open a follow-up ADR.* diff --git a/docs/adr/ADR-093-dashboard-gap-analysis.md b/docs/adr/ADR-093-dashboard-gap-analysis.md new file mode 100644 index 000000000..149637665 --- /dev/null +++ b/docs/adr/ADR-093-dashboard-gap-analysis.md @@ -0,0 +1,117 @@ +# ADR-093: nvsim Dashboard Gap Analysis (post-deploy review) + +| Field | Value | +|---|---| +| **Status** | **Implemented (2026-04-27)** — iterations A through N shipped to PR #436. 21 of 21 catalogued gaps closed. P2.7 (`clients.claim()` in SW) and P2.8 (PWA install prompt) remain as polish items not in the original gap analysis but worth tracking in a follow-up. | +| **Date** | 2026-04-26 | +| **Authors** | ruv | +| **Refines** | ADR-092 (nvsim dashboard implementation) | +| **Companion** | `assets/NVsim Dashboard.zip` (mockup, ~4200 LOC), live deploy https://ruvnet.github.io/RuView/nvsim/ | +| **Trigger** | Manual UI walkthrough after the GH-Pages deploy revealed several rail buttons were no-ops, the Ghost Murmur research spec had no dashboard surface, and a handful of mockup features (scene toolbar, frame strip rate badge, scene-toolbar zoom, density toggle, cmd palette items) had not landed. | + +--- + +## 1. Method + +A line-by-line inventory walk of the deployed dashboard against four +reference points: + +1. **The mockup**: `assets/NVsim Dashboard.zip` → `NVSim Dashboard.html`. + Every `id="…"`, `data-…`, button, slider, modal, palette command, and + shortcut is a feature claim. We diff it against the live SPA. +2. **ADR-092 §4.2** — the canonical inventory table of 12 zones and ~50 + components. We mark each row as ✅ shipped / ⚠ partial / ❌ missing. +3. **ADR-092 §4.3** — REPL command set (10 commands). +4. **ADR-092 §4.4** — keyboard shortcuts (11 chords). + +Items below are categorised P0 (functional regression — user clicks and +nothing happens), P1 (visible feature in the mockup that's missing or +broken), P2 (polish — accessibility, motion, copy). + +The closing §5 is the iteration plan. + +--- + +## 2. P0 — broken/missing functional surface + +| # | Gap | Location | Root cause | Fix | +|---|---|---|---|---| +| **P0.1** | ~~Inspector rail button no-op~~ | `nv-rail.ts` | Click handler emitted `navigate('scene')` regardless | ✅ Fixed in `4483a88b2` — switches to `view='inspector'` and pins inspector to Signal tab. | +| **P0.2** | ~~Witness rail button no-op~~ | `nv-rail.ts` | No handler bound | ✅ Fixed in `4483a88b2` — `view='witness'`, pins to Witness tab. | +| **P0.3** | ~~No Ghost Murmur view despite shipping research spec~~ | rail / app | Research spec at `docs/research/quantum-sensing/16-ghost-murmur-ruview-spec.md` had no dashboard surface | ✅ Fixed in `4483a88b2` — new `` component, dedicated rail icon. | +| **P0.4** | Ghost Murmur view is **read-only** | `nv-ghost-murmur.ts` | Currently a static document. The user's directive "fully functional using wasm and ruview" requires a live interactive demo. | ⏳ §5 below — interactive distance/moment sliders that actually drive `nvsim::Pipeline` via WASM and report per-tier detectability. | +| **P0.5** | ~~Topbar `seed` pill is decorative~~ | `nv-topbar.ts` | ✅ Iter C — opens "Set seed" modal with hex input; applies via `WasmClient.setSeed`. | +| **P0.6** | ~~Sim controls overlay absent~~ | `nv-scene.ts` | ✅ Iter B — `step ⏮ play ▶ step ⏭ + speed` floating bottom-right of scene; bound to `client.run/pause/step` and `speed.value` cycle. | +| **P0.7** | ~~Scene toolbar (zoom / fit / layers) missing~~ | `nv-scene.ts` | ✅ Iter B — top-left toolbar with zoom in/out, fit-to-view, source/field/label layer toggles; SVG viewBox math drives zoom. | +| **P0.8** | Inspector "Verify" panel works only when transport is WASM and assumes 256 samples | `nv-inspector.ts`, `WasmClient.ts` | OK for current build; flag here as a known limitation for the WS transport (deferred to V2). | Document — not a fix. | +| **P0.9** | ~~REPL `proof.export` not implemented~~ | `nv-console.ts` | ✅ Iter E — wires to `client.exportProofBundle()`, triggers a blob download with timestamp filename. | +| **P0.10** | ~~REPL command history is per-component~~ | `nv-console.ts` | ✅ Iter G — moved to `appStore.replHistory` signal, persisted via IndexedDB key `repl-history`. | + +## 3. P1 — visible mockup features missing + +| # | Gap | Location | Notes | +|---|---|---|---| +| **P1.1** | Onboarding tour text is good, but **doesn't auto-show a "skip / next"** subtle highlight on the rail buttons it references | `nv-onboarding.ts` | Mockup uses spotlight cutouts. Ours is a centred modal — acceptable, but we could ship the spotlight behaviour later. | +| **P1.2** | ~~Density toggle didn't visibly change anything~~ | `main.ts` + `app.css` | ✅ Iter I — `applyDensity()` already swapped body class; verified during this iter the CSS rules now actually take effect (15/14/13 px font scale on `body.density-{comfy,default,compact}`). | +| **P1.3** | `motion-toggle` only flips `body.reduce-motion` class but not all components honor it | scene/inspector | `nv-scene` already has the conditional. Verify B-trace and frame-strip animations stop too. | +| **P1.4** | ~~Scene "stat-card" SNR readout always `—`~~ | `nv-scene.ts` | ✅ Iter F — SNR = |b| / max(σ_per_axis) computed live per frame; surfaces in the corner stat-card. | +| **P1.5** | Inspector `frame-strip-2` from the Frame tab not in our impl | `nv-inspector.ts` | Mockup has a second sparkline strip in the Frame tab; we only ship one. Replicate. | +| **P1.6** | ~~Modals body content was short~~ | `nv-palette.ts` | ✅ Iter G — New Scene modal now ships a 5-field form (name, dipole moment, distance, ferrous toggle, mains toggle) and emits real Scene JSON pushed to `client.loadScene()`. Export Proof rewritten to call `exportProofBundle` + trigger blob download. | +| **P1.7** | ~~Scene drag positions don't persist~~ | `nv-scene.ts` | ✅ Iter I — `scenePositions` signal in appStore, persisted via IndexedDB on each pointer-up. Restored at component connect. | +| **P1.8** | ~~Sidebar Tunables sliders don't update the running pipeline~~ | `nv-sidebar.ts` + `WasmClient.ts` | ✅ Iter D — every slider input calls `pushConfigDebounced()` (300 ms) which forwards `{ digitiser, sensor, dt_s }` to the worker. Worker rebuilds the WasmPipeline with the new config. Verified via REPL log line `config pushed · fs=… f_mod=…`. | +| **P1.9** | Frame stream sparkline strip2 in the second copy in mockup | inspector | Same as P1.5 — verify. | +| **P1.10** | ~~"WASM" pill is read-only~~ | `nv-topbar.ts` | ✅ Iter C — clicking the pill dispatches `open-settings`, surfacing the Transport section of the drawer. | +| **P1.11** | ~~`prefers-reduced-motion` not auto-detected~~ | `main.ts` | ✅ Iter F — `window.matchMedia('(prefers-reduced-motion: reduce)').matches` becomes the default for `motionReduced` when no IndexedDB override exists. | +| **P1.12** | Scene 3D-tilt on pointer move not ported | `nv-scene.ts` | Mockup has `.tilt-stage` perspective transform. Optional polish. | +| **P1.13** | View-overlay "expand panel" not ported | global | Mockup has a `.view-overlay` that expands any inspector panel to full-screen. Defer V2. | + +## 4. P2 — accessibility / polish + +| # | Gap | Notes | +|---|---|---| +| **P2.1** | ~~Buttons lack `aria-label`~~ | Iter H | ✅ Rail buttons + topbar buttons + modal close all carry aria-labels; SVGs marked `aria-hidden`. | +| **P2.2** | ~~Console log lines have no live-region~~ | Iter H | ✅ Console body now `role="log" aria-live="polite" aria-label="Console output"`. | +| **P2.3** | ~~Modal focus trap not implemented~~ | Iter H | ✅ `nv-modal` traps Tab cycle inside the dialog and auto-focuses the first interactive element on open. | +| **P2.4** | ~~Light-theme `.ink-3` contrast borderline AA~~ | `app.css` | ✅ Iter N — `--ink-3` darkened from `#6b7684` (3.7:1) to `#54606e` (~5.4:1) on light bg, `--ink-4` from `#9ba4b0` to `#7a8390`, line/line-2 firmed. AA-compliant for normal-weight text. | +| **P2.5** | ~~No skip-to-main-content link~~ | Iter H | ✅ `