diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae12427..9af8c37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,7 +50,12 @@ jobs: release-build: name: Release Build (${{ matrix.target }}) runs-on: ${{ matrix.os }} - if: github.ref == 'refs/heads/main' + # Run on main pushes AND on PRs that touch workflows, Cargo manifests, or + # crate sources — so Intel Mac / Linux release targets are validated + # before merge, not after. + if: | + github.ref == 'refs/heads/main' || + github.event_name == 'pull_request' needs: [test] strategy: fail-fast: false @@ -59,9 +64,12 @@ jobs: - os: macos-latest target: aarch64-apple-darwin cargo_flags: "" - # x86_64-apple-darwin dropped: ort-sys has no prebuilt ONNX Runtime - # binaries for Intel Mac, and the codebase requires embeddings. - # Apple discontinued Intel Macs in 2020. Build from source if needed. + # Intel Mac builds against a system ONNX Runtime via ort-dynamic + # (ort-sys has no x86_64-apple-darwin prebuilts). Compile-only here; + # runtime linking is a user concern documented in INSTALL-INTEL-MAC.md. + - os: macos-latest + target: x86_64-apple-darwin + cargo_flags: "--no-default-features --features ort-dynamic,vector-search" - os: ubuntu-latest target: x86_64-unknown-linux-gnu cargo_flags: "" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1fb8639..793ce48 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,17 +27,21 @@ jobs: os: ubuntu-latest archive: tar.gz cargo_flags: "" + needs_onnxruntime: false - target: x86_64-pc-windows-msvc os: windows-latest archive: zip cargo_flags: "" - # Intel Mac (x86_64-apple-darwin) is explicitly unsupported: the - # upstream ort-sys 2.0.0-rc.11 pinned by fastembed 5.13.2 does not - # ship Intel Mac prebuilts, and the v2.0.5 + v2.0.6 release - # workflows both failed this job. Matches ci.yml which already - # dropped the target. README documents the build-from-source path - # for Intel Mac users. When ort-sys ships Intel Mac prebuilts - # again, restore the entry. + needs_onnxruntime: false + # Intel Mac uses the ort-dynamic feature to runtime-link against a + # system libonnxruntime (Homebrew), sidestepping the missing + # x86_64-apple-darwin prebuilts in ort-sys 2.0.0-rc.11. Binary + # consumers must `brew install onnxruntime` before running — see + # INSTALL-INTEL-MAC.md bundled in the tarball. + - target: x86_64-apple-darwin + os: macos-latest + archive: tar.gz + cargo_flags: "--no-default-features --features ort-dynamic,vector-search" - target: aarch64-apple-darwin os: macos-latest archive: tar.gz @@ -46,20 +50,104 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.tag || github.ref }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 10 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 - name: Install Rust uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} + - name: Validate release version + shell: bash + env: + RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} + run: | + node <<'NODE' + const { execFileSync } = require('node:child_process'); + const tag = process.env.RELEASE_TAG || ''; + const expected = tag.replace(/^refs\/tags\//, '').replace(/^v/, ''); + if (!expected) { + throw new Error('Release tag is empty'); + } + + const packageFiles = [ + 'package.json', + 'apps/dashboard/package.json', + 'packages/vestige-init/package.json', + 'packages/vestige-mcp-npm/package.json' + ]; + for (const file of packageFiles) { + const actual = require(`./${file}`).version; + if (actual !== expected) { + throw new Error(`${file} version ${actual} does not match ${tag}`); + } + } + + const metadata = JSON.parse(execFileSync('cargo', [ + 'metadata', + '--format-version', + '1', + '--locked', + '--no-deps' + ], { encoding: 'utf8' })); + for (const name of ['vestige-core', 'vestige-mcp']) { + const pkg = metadata.packages.find((candidate) => candidate.name === name); + if (!pkg) throw new Error(`Missing Cargo package ${name}`); + if (pkg.version !== expected) { + throw new Error(`${name} version ${pkg.version} does not match ${tag}`); + } + } + NODE + + - name: Build embedded dashboard + shell: bash + env: + RELEASE_TAG: ${{ github.event.inputs.tag || github.ref_name }} + run: | + pnpm install --frozen-lockfile + pnpm --filter @vestige/dashboard check + pnpm --filter @vestige/dashboard test + pnpm --filter @vestige/dashboard build + node <<'NODE' + const fs = require('node:fs'); + const tag = process.env.RELEASE_TAG || ''; + const expected = tag.replace(/^refs\/tags\//, '').replace(/^v/, ''); + const versionFile = 'apps/dashboard/build/_app/version.json'; + const version = JSON.parse(fs.readFileSync(versionFile, 'utf8')).version; + if (version !== expected) { + throw new Error(`${versionFile} version ${version} does not match ${tag}`); + } + for (const file of ['apps/dashboard/build/index.html', versionFile]) { + if (!fs.existsSync(file)) { + throw new Error(`Dashboard build did not produce ${file}`); + } + } + NODE + - name: Build - run: cargo build --package vestige-mcp --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }} + run: cargo build --locked --package vestige-mcp --release --target ${{ matrix.target }} ${{ matrix.cargo_flags }} - name: Package (Unix) if: matrix.os != 'windows-latest' run: | + cp docs/INSTALL-INTEL-MAC.md target/${{ matrix.target }}/release/ 2>/dev/null || true cd target/${{ matrix.target }}/release - tar -czf ../../../vestige-mcp-${{ matrix.target }}.tar.gz vestige-mcp vestige vestige-restore + if [ "${{ matrix.target }}" = "x86_64-apple-darwin" ]; then + tar -czf ../../../vestige-mcp-${{ matrix.target }}.tar.gz vestige-mcp vestige vestige-restore INSTALL-INTEL-MAC.md + else + tar -czf ../../../vestige-mcp-${{ matrix.target }}.tar.gz vestige-mcp vestige vestige-restore + fi - name: Package (Windows) if: matrix.os == 'windows-latest' @@ -68,10 +156,21 @@ jobs: cd target/${{ matrix.target }}/release Compress-Archive -Path vestige-mcp.exe,vestige.exe,vestige-restore.exe -DestinationPath ../../../vestige-mcp-${{ matrix.target }}.zip + - name: Generate checksum + shell: bash + run: | + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} > vestige-mcp-${{ matrix.target }}.${{ matrix.archive }}.sha256 + else + sha256sum vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} > vestige-mcp-${{ matrix.target }}.${{ matrix.archive }}.sha256 + fi + - name: Upload to Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.event.inputs.tag || github.ref_name }} - files: vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} + files: | + vestige-mcp-${{ matrix.target }}.${{ matrix.archive }} + vestige-mcp-${{ matrix.target }}.${{ matrix.archive }}.sha256 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7f169eb..244fe52 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,19 @@ env: VESTIGE_TEST_MOCK_EMBEDDINGS: "1" jobs: + hook-tests: + name: Hook Tests + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + - run: python3 -m unittest discover -s tests/hooks -p 'test_*.py' + - run: python3 -m py_compile hooks/sanhedrin-local.py tests/hooks/test_sanhedrin_claim_mode.py + - run: bash -n hooks/sanhedrin.sh scripts/install-sandwich.sh scripts/check-sandwich-prereqs.sh + unit-tests: name: Unit Tests runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 41e352a..80763c9 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,8 @@ pnpm-debug.log* coverage/ .nyc_output/ *.lcov +__pycache__/ +*.py[cod] apps/dashboard/test-results/ apps/dashboard/playwright-report/ apps/dashboard/e2e/screenshots/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 171d9a3..f6825cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,270 @@ All notable changes to Vestige will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +## [2.1.22] - 2026-05-25 — "Sanhedrin Receipts" + +v2.1.22 makes the optional Sanhedrin hook quieter and more accountable by +turning draft judgment into local, appealable receipts instead of opaque vetoes. + +### Added + +- **Receipt Lock** blocks unsupported verification claims such as "tests passed" + unless the current transcript contains a matching successful test, build, + lint, or typecheck command receipt. +- **Veto receipts** are written to `~/.vestige/sanhedrin/latest.json` and + `latest.html` with Claim -> Verdict -> Precedent -> Fix -> Appeal fields. +- **Dashboard Verdict Bar** surfaces the latest PASS, NOTE, CAUTION, VETO, or + APPEALED state and lets users appeal stale, wrong, or too-strict vetoes. +- **Appeal training** records feedback in `appeals.jsonl` and suppresses future + vetoes for the same claim fingerprint. + +### Changed + +- Sanhedrin claim-mode output now feeds a per-claim receipt ledger while keeping + the existing one-line Stop-hook contract for Claude Code. + +## [2.1.21] - 2026-05-24 — "Agent-Neutral Hardening" + +v2.1.21 is a release-hardening pass for normal MCP usage across agents. It keeps +Claude Code Cognitive Sandwich companion files optional while making the MCP +server, package installer, release workflow, and portable sync path safer. + +### Added + +- **Agent-neutral memory protocol** — new `docs/AGENT-MEMORY-PROTOCOL.md` gives + any MCP-compatible client the same practical memory loop: initialize context, + search/deep-reference when needed, save durable facts with `smart_ingest`, and + promote/demote/purge with `memory`. +- **HTTP transport opt-in** — `vestige-mcp` now requires `--http`, + `--http-port`, or `VESTIGE_HTTP_ENABLED=1` before starting MCP-over-HTTP. +- **Release checksums** — release assets now publish `.sha256` files beside each + archive. + +### Changed + +- **`vestige update` is binary-only by default** — Claude Code Cognitive + Sandwich companion files refresh only with `vestige update --sandwich-companion` + or `vestige sandwich install`. +- **MCP tool results include structured content** while keeping text content for + clients that only consume the classic MCP response shape. +- **NPM install messaging is agent-neutral** and unsupported release targets + fail fast instead of trying to download assets that do not exist. +- **Portable merge uses UPSERT instead of `INSERT OR REPLACE`** for keyed tables, + preserving related rows instead of causing delete-and-insert side effects. + +### Fixed + +- **Destructive delete confirmation** — `memory(action="delete")` now requires + `confirm=true`, matching `purge`; the deprecated `delete_knowledge` shim no + longer bypasses confirmation. +- **Portable purge tombstone sync** — merge imports now carry + `deletion_tombstones` and apply purges without retaining deleted memory text. + Hard purge tombstones win over newer local edits during portable sync, while + tombstone merges keep the newest deletion timestamp. +- **Vector index reload staleness** — loading persisted embeddings rebuilds the + in-memory index from an empty index before adding current embeddings. +- **HTTP transport hardening** — origin, Accept, session, and protocol-version + validation now reject incompatible or cross-origin browser requests earlier. +- **Init config safety** — `@vestige/init` backs up existing config files, writes + atomically, accepts JSONC-style comments/trailing commas, and no longer writes + Xcode trust-accepted flags. +- **Release tag checkout** — manual release builds now checkout the requested tag + or ref before packaging. + +### Verified + +- `cargo test -p vestige-mcp --lib --no-fail-fast` +- `cargo test -p vestige-mcp --bin vestige-mcp --no-fail-fast` +- `cargo test -p vestige-core portable_merge_import --no-fail-fast` +- `cargo test -p vestige-mcp --bin vestige --no-fail-fast` +- `cargo test -p vestige-e2e-tests --test mcp_protocol --no-fail-fast` +- `cargo check --workspace` +- `cargo metadata --format-version 1 --locked --no-deps` +- `pnpm --filter @vestige/dashboard check` +- `pnpm --filter @vestige/dashboard test` +- `pnpm --filter @vestige/dashboard build` +- `node --check packages/vestige-init/bin/init.js` +- `node --check packages/vestige-mcp-npm/scripts/postinstall.js` +- `node --check packages/vestige-mcp-npm/bin/vestige-restore.js` + +## [2.1.2] - 2026-05-01 — "Honest Memory" + +v2.1.2 focuses on operational trust: exact search stays exact, purge really removes content, contradictions are directly inspectable, and the update flow no longer depends on copied curl commands. + +### Added + +- **Concrete search mode** — `search` now auto-detects literal queries such as quoted strings, env vars, UUIDs, paths, and code identifiers. Those queries take a keyword/literal path that skips HyDE, semantic fusion, FSRS reweighting, retrieval competition, and spreading activation so exact matches land first. +- **Irreversible purge** — `memory(action="purge", confirm=true)` permanently removes memory content and embeddings, scrubs `insights.source_memories`, detaches temporal-summary children, prunes graph edges, and writes only a non-content `deletion_tombstones` row for sync/audit. +- **First-class contradictions tool** — new `contradictions` MCP tool scans a topic or recent memories for trust-weighted disagreements using the same local contradiction logic as `deep_reference`. +- **Simple update flow** — `vestige update` refreshes the installed binary and companion Sandwich files without requiring users to paste a curl installer. +- **Pro waitlist preview** — `/dashboard/waitlist` adds a local-only marketing surface for Solo Pro and Team Pro early-access signups. `VITE_WAITLIST_ENDPOINT` and `VITE_SUPPORT_BOT_ENDPOINT` are opt-in dashboard env vars, so no signup data is captured unless endpoints are configured. + +### Fixed + +- **Dream connection persistence cap** — dense single-domain dreams now persist every connection discovered in that run instead of losing everything beyond the old 1,000-entry live buffer. The live dreamer buffer now keeps up to 200,000 high-scoring recent connections, and the MCP `dream` tool exposes `min_similarity` for corpus-specific tuning. +- **Embedding-model upgrade repair** — `vestige consolidate` now re-embeds every missing or active-model-mismatched memory in one pass, so v1/v2 mixed stores are no longer left partially unreachable after only the first 100 legacy embeddings are regenerated. + +## [2.1.1] - 2026-05-01 — "Portable Sync" + +v2.1.1 focuses on user-controlled portability: exact storage archives, merge-safe file sync, pluggable sync backends, and explicit hook opt-ins. + +### Added + +- **Exact portable archives** — `vestige portable-export` / `vestige portable-import` preserve raw Vestige storage rows: memory IDs, FSRS state, graph edges, suppression state, audit rows, sessions, intentions, and embedding blobs. +- **Merge-safe imports** — `vestige portable-import --merge` can merge into non-empty databases. It applies `sync_tombstones`, keeps newer local memory rows on timestamp conflicts, preserves stable IDs, and rebuilds FTS after import. +- **File-backed two-way sync** — `vestige sync ` performs pull-merge-push through a shared portable archive. This works today with Dropbox, iCloud Drive, Syncthing, Git, network shares, and shared folders. +- **Pluggable portable-sync backend trait** — core now exposes `PortableSyncBackend`, `FilePortableSyncBackend`, and `PortableSyncReport`, so non-file backends can reuse the same merge semantics without reimplementing conflict handling. +- **Portable restore merge mode** — the MCP `restore` tool accepts `merge: true` for portable archives and returns inserted/updated/deleted/skipped/conflict counts. +- **Qwen3 embedding opt-in** — build-time and runtime support for Qwen3 embeddings, with model-aware retrieval safeguards so mixed embedding models are not compared in the same vector path. + +### Fixed + +- **Sanhedrin, preflight, and all Vestige Claude Code hooks are optional again.** The Cognitive Sandwich installer now activates no hooks by default and leaves every preflight hook, every Stop hook, the MLX launchd service, and the 19 GB Qwen model path behind explicit `--enable-preflight`, `--enable-sanhedrin`, or `--with-launchd` flags. +- **x86-friendly Sanhedrin path.** The verifier bridge now accepts any OpenAI-compatible chat endpoint via `VESTIGE_SANHEDRIN_ENDPOINT` and `VESTIGE_SANHEDRIN_MODEL`, so Linux and Intel Mac users can opt in without MLX or Apple Silicon. + +### Verified + +- `cargo test -p vestige-core portable --no-fail-fast` +- `cargo test -p vestige-mcp portable --no-fail-fast` +- `cargo test --workspace --no-fail-fast` +- Installer shell/Python/JSON validation and default/preflight/Sanhedrin migration dry-runs. + +## [2.1.0] - 2026-04-27 — "Cognitive Sandwich Goes Local" + +v2.1.0 ships the Cognitive Sandwich hook harness for Claude Code, with a hotfix that makes every hook layer opt-in. The default installer stages files, removes old Vestige hook wiring, starts no MLX service, downloads no model, and makes no automatic model calls. Users can opt into preflight context, Sanhedrin verification, or the Apple Silicon MLX backend separately. + +### Added + +- **`hooks/`** — first-class harness-side companion to the Vestige MCP server. 9 production hooks designed for `~/.claude/hooks/`: + - `sanhedrin.sh` — Stop hook that invokes the local Qwen Executioner via the Python bridge. + - `sanhedrin-local.py` — local backend that POSTs to `mlx_lm.server` (`localhost:8080`) with Vestige evidence injected via the dashboard `/api/deep_reference` HTTP endpoint. TRUST_FLOOR=0.55 evidence filter + topical-relevance gate + inference-verb ban + 8 worked few-shots covering true positives AND false-positive guards. + - `synthesis-preflight.sh` — UserPromptSubmit hook that POSTs the user prompt to `/api/deep_reference` and injects the trust-scored reasoning chain into context. + - `cwd-state-injector.sh` — captures git status, branch, modified files, open PRs/issues. + - `vestige-pulse-daemon.sh` — surfaces fresh Vestige dream insights from the past 20 min. + - `preflight-swarm.sh` — spawns the `lateral-thinker` subagent in fresh context for cross-disciplinary structural parallels. + - `synthesis-stop-validator.sh` — Stop hook regex against forbidden hedging patterns. + - `veto-detector.sh` — fast 50ms regex pre-screen against `veto`-tagged Vestige memories. + - `synthesis-gate.sh` — legacy v1 trigger (kept for backward compat). + - `settings.fragment.json` — empty default JSON snippet; the installer only wires hooks from the opt-in preflight/Sanhedrin fragments. +- **Dashboard `/api/changelog` endpoint** — bounded REST event feed for recent `DreamCompleted` and `ConnectionDiscovered` events, used by the Pulse hook to inject fresh synthesis into Claude Code context. +- **`agents/`** — `executioner.md` (legacy/fallback Haiku 4.5 path), `lateral-thinker.md`, `synthesis-composer.md`. +- **`launchd/com.vestige.mlx-server.plist.template`** — auto-start `mlx_lm.server` with the Qwen3.6-35B-A3B-4bit model on login. Templated with `__HOME__` and `__MODEL__` placeholders. +- **`scripts/install-sandwich.sh`** — one-command installer that stages hooks and agents, removes old Vestige hook wiring by default, and only wires optional layers with `--enable-preflight`, `--enable-sanhedrin`, or `--with-launchd`. Backs up `settings.json` to `.bak.pre-sandwich`. Supports `--force`, `--include-memory-loader`, and `--src=PATH`. +- **`scripts/check-sandwich-prereqs.sh`** — default verifier confirms no Vestige Claude Code hooks are wired. Optional `--preflight` and `--sanhedrin` modes check the corresponding enabled layer. +- **`docs/COGNITIVE_SANDWICH.md`** — architecture diagram, install guide, performance notes (82 tok/s on M3 Max), uninstall, configuration env vars. +- **PR #48** — `VESTIGE_DATA_DIR` env-var support + tilde expansion + secure unix perms (thanks @Jelloeater) — directly addresses the ghost env-vars exposed by v2.0.9 cleanup. + +### Changed + +- **Sanhedrin Executioner backend can run locally or remotely when explicitly enabled.** The bridge targets an OpenAI-compatible chat endpoint, with local `mlx_lm.server` + Qwen3.6-35B-A3B-4bit available behind `--with-launchd` on Apple Silicon. Anthropic API key no longer required for the post-cognitive layer. The `executioner.md` agent definition is retained as manual/fallback only when invoked explicitly via `Task(subagent_type='executioner')`. +- **All hooks sanitized for public release** — replaced hardcoded personal absolute paths with `$HOME` / `$VESTIGE_*` env vars; removed personal regex tokens. +- **NPM binary installer now follows package version** — `vestige-mcp-server@2.1.0` downloads release assets from `v2.1.0` instead of a stale hardcoded binary tag, while local workspace installs skip the release-asset download before the tag exists. + +### Verified + +- `cargo test --workspace --release --no-fail-fast`: **1,229 passing, 0 failed** (366 vestige-core + 358 vestige-mcp lib + 4 vestige-mcp bin + 497 e2e + 4 doctests). +- Sanhedrin bridge smoke checks: Python bytecode compilation passes, fail-open bridge invocation returns `yes`, and public hook settings validate as JSON. +- v2.1.0 hotfix installer matrix: default, preflight-only, Sanhedrin-only, full sandwich, legacy-all-hooks migration, and unrelated custom hooks preservation. +- 8-day Sandwich dogfood: **84% pass rate, 16% legitimate vetoes** caught real hallucinations. + +### Closes + +- #36 (Agent Hooks for Low-Effort Automatic Memory Capture) — Cognitive Sandwich is the answer. + +### Prerequisites for optional local MLX Sanhedrin + +- macOS Apple Silicon (M1+) — required only for `--with-launchd` MLX autostart +- Python 3.10+ +- ~22 GB free RAM (Qwen3.6-35B-A3B-4bit at runtime) +- First-run model download: ~19 GB from Hugging Face (cached locally thereafter) + +### Migration + +None required for existing Vestige users. The Cognitive Sandwich is opt-in via `scripts/install-sandwich.sh`; running the default installer removes old Vestige hook wiring and leaves preflight, Sanhedrin, launchd, and the 19 GB model path disabled. The MCP server, schema, and tool surface are bit-identical to v2.0.9. + +--- + +## [2.0.9] - 2026-04-24 — "Autopilot" + +Autopilot flips Vestige from passive memory library to self-managing cognitive surface. A single supervised backend task subscribes to the 20-event WebSocket bus and routes live events into the cognitive engine — 14 previously dormant primitives (synaptic tagging, predictive memory, activation spread, prospective polling, auto-consolidation, Rac1 cascade emission) now fire without any MCP tool call. Shipped alongside a 3,091-LOC orphan-code cleanup of the v1.0 tool surface. **No schema changes, tool surface unchanged (24 tools), fully backward compatible with v2.0.8 databases. Opt-out via `VESTIGE_AUTOPILOT_ENABLED=0`.** + +### Added + +- **Autopilot event subscriber** (`crates/vestige-mcp/src/autopilot.rs`) — two supervised tokio tasks spawned at startup. The event subscriber consumes a `broadcast::Receiver` and routes six event classes: + - `MemoryCreated` → `synaptic_tagging.trigger_prp()` (9h retroactive PRP window, Frey & Morris 1997) + `predictive_memory.record_memory_access()`. + - `SearchPerformed` → `predictive_memory.record_query()` + top-10 access records. The speculative-retrieval model now warms without waiting for an explicit `predict` call. + - `MemoryPromoted` → `activation_network.activate(id, 0.3)` — a small reinforcement ripple through the graph. + - `MemorySuppressed` → re-emits the previously-declared-never-emitted `Rac1CascadeSwept` event so the dashboard's cascade wave actually renders. + - `ImportanceScored` with `composite_score > 0.85` AND a stored `memory_id` → auto-promote + re-emit `MemoryPromoted`. + - `Heartbeat` with `memory_count > 700` → rate-limited `find_duplicates` sweep (6h cooldown + in-flight `JoinHandle` guard against concurrent scans on large DBs). + + The engine mutex is acquired only synchronously per handler and never held across `.await`, so MCP tool dispatch is never starved. A 60-second `tokio::interval` separately polls `prospective_memory.check_triggers(Context)` — matched intentions log at `info!` level today; v2.1 "Autonomic" will surface them mid-conversation. +- **Panic-resilient supervisors** — both background tasks run inside an outer supervisor loop. If a cognitive hook panics on one bad memory, the supervisor catches `JoinError::is_panic()`, logs the panic, sleeps 5 s, and respawns the inner task. Turns a permanent silent-failure mode into a transient hiccup. +- **`VESTIGE_AUTOPILOT_ENABLED=0` opt-out** — v2.0.8 users who want the passive-library contract can disable Autopilot entirely. Values `{0, false, no, off}` early-return before any task spawns; anything else (unset, `1`, `true`) enables the default v2.0.9 behavior. +- **`ImportanceScored.memory_id: Option`** — new optional field on the event variant (`#[serde(default)]`, backward-compatible) so Autopilot's auto-promote path can target a stored memory. Existing emit sites pass `None`. + +### Changed + +- **3,091 LOC of orphan tool code removed** from `crates/vestige-mcp/src/tools/` — nine v1.0 modules (`checkpoint`, `codebase`, `consolidate`, `ingest`, `intentions`, `knowledge`, `recall`, plus two internal helpers) superseded by the `*_unified` / `maintenance::*` replacements shipped in v2.0.5. Each module verified for zero non-test callers before removal. Tool surface unchanged — all 24 tools continue to work via the unified dispatchers. +- **Ghost environment-variable documentation scrubbed** — three docs listed env vars (`VESTIGE_DATA_DIR`, `VESTIGE_LOG_LEVEL`, a `VESTIGE_API_KEY` typo) that never existed in any shipping Rust code. Replaced with the real variables the binary actually reads. + +### Fixed + +- **Dedup-sweep race on large databases** — the `Heartbeat`-triggered `find_duplicates` sweep previously set its cooldown timestamp BEFORE spawning the async scan, which on 100k+ memory DBs (where a sweep can exceed the 6h cooldown) allowed two concurrent scans. Rewritten to track the in-flight `JoinHandle` via `DedupSweepState::is_running()` — the next Heartbeat skips if the previous sweep is still live. + +### Verified + +- `cargo test --workspace --no-fail-fast`: **1,223 passing, 0 failed**. +- `cargo clippy -p vestige-mcp --lib --bins -- -D warnings`: clean. +- Five-agent parallel audit (security, dead-code, flow-trace, runtime-safety, release-prep): all GO. + +### Migration + +None. v2.0.9 is a pure backend upgrade — tool surface, JSON-RPC schema, storage schema, and CLI flags are bit-identical to v2.0.8. Existing databases open without any migration step. The only behavior change is the Autopilot task running in the background, which is `VESTIGE_AUTOPILOT_ENABLED=0`-gated if you want the old passive-library contract. + +## [2.0.8] - 2026-04-23 — "Pulse" + +The Pulse release wires the dashboard through to the cognitive engine. Eight new dashboard surfaces expose `deep_reference`, `find_duplicates`, `dream`, FSRS scheduling, 4-channel importance, spreading activation, contradiction arcs, and cross-project pattern transfer — every one of them was MCP-only before. Intel Mac is back on the supported list (Microsoft deprecated x86_64 macOS ONNX Runtime prebuilts; we link dynamically against a Homebrew `onnxruntime` instead). Reasoning Theater, Pulse InsightToast, and the Memory Birth Ritual all ship. No schema migrations. + +### Added + +- **Reasoning Theater (`/reasoning`)** — Cmd+K Ask palette over the 8-stage `deep_reference` cognitive pipeline: hybrid retrieval → cross-encoder rerank → spreading activation → FSRS-6 trust scoring → temporal supersession → trust-weighted contradiction analysis → relation assessment → template reasoning chain. Every query returns a pre-built reasoning block with evidence cards, confidence meter, contradiction geodesic arcs, superseded-memory lineage, and an evolution timeline. **Zero LLM calls, 100% local.** New HTTP surface `POST /api/deep_reference` wraps `crate::tools::cross_reference::execute`; new WebSocket event `DeepReferenceCompleted` carries primary / supporting / contradicting memory IDs for downstream graph animation. +- **Pulse InsightToast (v2.2 Pulse)** — real-time toast stack that surfaces `DreamCompleted`, `ConsolidationCompleted`, `ConnectionDiscovered`, `MemoryPromoted`/`Demoted`/`Suppressed`, `MemoryUnsuppressed`, `Rac1CascadeSwept` events the moment they fire. Rate-limited to 1 per 1500ms on connection-discovery cascades. Auto-dismiss after 5-6s, click-to-dismiss, progress bar. Bottom-right on desktop, top-center on mobile. +- **Memory Birth Ritual (v2.3 Terrarium)** — new memories materialize in the 3D graph on every `MemoryCreated` event: elastic scale-in from a camera-relative cosmic center, quadratic Bezier flight path, glow sprite fades in frames 5-10, label fades in at frame 40, Newton's Cradle docking recoil. 60-frame sequence, zero-alloc math, camera-relative so the birth point stays visible at every zoom level. +- **7 additional dashboard surfaces** exposing the cognitive engine (v2.4 UI expansion): `/duplicates` (find_duplicates cluster view), `/dreams` (5-stage replay + insight cards), `/schedule` (FSRS calendar + retention forecast), `/importance` (4-channel novelty/arousal/reward/attention radar), `/activation` (spreading-activation network viz), `/contradictions` (trust-weighted conflict arcs), `/patterns` (cross-project pattern-transfer heatmap). Left nav expanded from 8 → 16 entries with single-key shortcuts (R/A/D/C/P/U/X/N). +- **3D Graph brightness system** — auto distance-compensated node brightness (1.0× at camera <60u, up to 2.4× at far zoom) so nodes don't disappear into exponential fog at zoom-out. User-facing brightness slider in the graph toolbar (☀ icon, range 0.5×-2.5×, localStorage-persisted under `vestige:graph:brightness`). Composes with the auto boost; opacity + glow halo + edge weight track the combined multiplier so nodes stay coherent. +- **Intel Mac (`x86_64-apple-darwin`) support restored** via the `ort-dynamic` Cargo feature + Homebrew-installed ONNX Runtime. Microsoft is discontinuing x86_64 macOS prebuilts after ONNX Runtime v1.23.0 so `ort-sys` will never ship one for Intel; the dynamic-link path sidesteps that entirely. Install: `brew install onnxruntime` then `ORT_DYLIB_PATH=$(brew --prefix onnxruntime)/lib/libonnxruntime.dylib`. Full guide bundled in the Intel Mac tarball as `INSTALL-INTEL-MAC.md`. **Closes #41.** +- **Graph default-load fallback** — when the newest memory has zero edges (freshly saved, hasn't accumulated connections yet), `GET /api/graph` silently retries with `sort=connected` so the landing view shows real context instead of a lonely orb. Applies only to default loads; explicit `query` / `center_id` requests are honored as-is. Fires on both backend and client. + +### Fixed + +- **Contradiction-detection false positives** — adjacent-domain memories are no longer flagged as conflicts just because both contain the word "trust" or "fixed." Four thresholds tightened: `NEGATION_PAIRS` drops the `("not ", "")` + `("no longer", "")` wildcard sentinels; `appears_contradictory` shared-words floor 2 → 4 and correction-signal gating now requires ≥6 shared words + asymmetric presence (one memory carries the signal, the other doesn't); `assess_relation` topic-similarity floor raised 0.15 → 0.55; Stage 5 pairwise contradiction overlap floor 0.15 → 0.4. On an FSRS-6 query this collapses false contradictions from 12 → 0 without regressing the two legitimate contradiction test cases. +- **Primary-memory selection on `deep_reference`** — previously the reasoning chain picked via `max_by(trust)` and the recommended-answer card picked via `max_by(composite)`, so the chain and citation disagreed on the same query. Unified behind a shared composite (50% hybrid-search relevance + 20% FSRS-6 trust + 30% query-topic-term match fraction) with a hard topic-term filter: a memory cannot be primary unless its content contains at least one substantive query term. Three-tier fallback (on-topic + relevant → on-topic any → all non-superseded) so sparse corpora never starve. Closes the class of bug where high-trust off-topic memories won queries against the actual subject. +- **Reasoning page information hierarchy** — reasoning chain renders first as the hero (confidence-tinted border glow, inline metadata), then confidence meter + Primary Source citation card, then Cognitive Pipeline visualization, then evidence grid. "Template Reasoning" relabelled "Reasoning"; "Recommended Answer" relabelled "Primary Source" (it's a cited memory, not the conclusion — the chain is the conclusion). + +### Changed + +- **CI + release workflows** — `release-build` now runs on pull requests too so Intel Mac / aarch64-darwin / Linux / Windows regressions surface before merge. `x86_64-apple-darwin` back in both `ci.yml` and `release.yml` matrices with `cargo_flags: "--no-default-features --features ort-dynamic,vector-search"`. Intel Mac tarball bundles `docs/INSTALL-INTEL-MAC.md` alongside the binaries. +- **Cargo feature split** — `embeddings` is now code-only (fastembed dep + hf-hub + image-models). New `ort-download` feature carries the prebuilt backend (the historical default); `ort-dynamic` transitively enables `embeddings` so the 27 `#[cfg(feature = "embeddings")]` gates stay active when users swap backends. Default set `["embeddings", "ort-download", "vector-search", "bundled-sqlite"]` — identical behavior for every existing consumer. +- **Platform availability in README** — macOS Apple Silicon + Intel + Linux x86_64 + Windows x86_64 all shipped as prebuilts. Intel Mac needs `brew install onnxruntime` as a one-time prereq. + +### Docs + +- New `docs/INSTALL-INTEL-MAC.md` with the Homebrew prereq, binary install, source build, troubleshooting, and the v2.1 `ort-candle` migration plan. +- README Intel Mac section rewritten with the working install recipe + platform table updated. + +### Migration + +None. Additive features and bug fixes only. No schema changes, no breaking API changes, no config changes required. + +### Contributors + +- **danslapman** (#41, #42) — reported the Intel Mac build regression and investigated `ort-tract` as an alternative backend; closure documented that `ort-tract` returns `Unimplemented` when fastembed calls into it, confirming `ort-dynamic` as the correct path forward. + +--- + ## [2.0.7] - 2026-04-19 — "Visible" Hygiene release plus two UI gap closures. No breaking changes, no new major features, no schema migrations affecting user data beyond V11 dropping two verified-unused tables. diff --git a/CLAUDE.md b/CLAUDE.md index 4d610a2..4ee5762 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,348 +1,62 @@ -# Vestige v2.0.4 — Cognitive Memory & Reasoning System +# Vestige Agent Guidance -Vestige is your long-term memory AND reasoning engine. 29 stateful cognitive modules implement real neuroscience: FSRS-6 spaced repetition, synaptic tagging, prediction error gating, hippocampal indexing, spreading activation, reconsolidation, and dual-strength memory theory. **Use it automatically. Use it aggressively.** +This file is intentionally safe for the public repository. It gives coding +agents project-specific context without relying on private local files, +personal operating notes, or mandatory background hooks. -**NEW: `deep_reference` — call this for ALL factual questions.** It doesn't just retrieve — it REASONS across memories with FSRS-6 trust scoring, intent classification, contradiction analysis, and generates a pre-built reasoning chain. Read the `reasoning` field FIRST. +## Project Shape ---- +Vestige is a local-first MCP memory server written in Rust, with a SvelteKit +dashboard embedded into the release binary. The core product promise is: -## Session Start Protocol +- user-owned memory stored locally by default +- MCP-native integration with coding agents +- retrieval and memory lifecycle behavior informed by cognitive science +- explicit tools for search, review, suppression, purge, graph exploration, + contradiction inspection, and maintenance -Every conversation, before responding to the user: +## Working Rules -``` -session_context({ - queries: ["user preferences", "[current project] context"], - context: { codebase: "[project]", topics: ["[current topics]"] }, - token_budget: 2000 -}) +- Prefer source evidence over memory. Use `rg`, tests, and nearby code before + making claims about behavior. +- Keep release changes scoped. Do not rewrite unrelated modules during a + version/tag cleanup unless the release gate requires it. +- Preserve local-first behavior. Heavy models, Sanhedrin-style verifier hooks, + and preflight automation must remain optional. +- Treat deletion semantics carefully. `purge` must remove content and + embeddings, while retaining only content-free audit tombstones. +- Treat exact lookup semantics carefully. Env vars, paths, UUIDs, quoted + strings, and code identifiers should not be distorted by semantic expansion. + +## Common Checks + +Run the narrowest check that covers the change, then run the release gates +before tagging: + +```sh +cargo test --workspace --no-fail-fast +cargo clippy --workspace -- -D warnings +pnpm --filter @vestige/dashboard check +pnpm --filter @vestige/dashboard build ``` -Then check `automationTriggers` from response: -- `needsDream` → call `dream` (consolidates memories, discovers hidden connections) -- `needsBackup` → call `backup` -- `needsGc` → call `gc(dry_run: true)` then review -- totalMemories > 700 → call `find_duplicates` +For documentation-only changes, at minimum run: -Say "Remembering..." then retrieve context before answering. - -> **Fallback:** If `session_context` unavailable: `search` × 2 → `intention` check → `system_status` → `predict`. - ---- - -## Complete Tool Reference (23 Tools) - -### session_context — One-Call Initialization -``` -session_context({ - queries: ["user preferences", "project context"], // search queries - context: { codebase: "project-name", topics: ["svelte", "rust"], file: "src/main.rs" }, - token_budget: 2000, // 100-100000, controls response size - include_status: true, // system health - include_intentions: true, // triggered reminders - include_predictions: true // proactive memory predictions -}) -``` -Returns: markdown context + `automationTriggers` + `expandable` IDs for on-demand retrieval. - -### smart_ingest — Save Anything -**Single mode** — auto-decides CREATE/UPDATE/SUPERSEDE via Prediction Error Gating: -``` -smart_ingest({ - content: "What to remember", - tags: ["tag1", "tag2"], - node_type: "fact", // fact|concept|event|person|place|note|pattern|decision - source: "optional reference", - forceCreate: false // bypass dedup when needed -}) -``` -**Batch mode** — save up to 20 items in one call (session end, pre-compaction): -``` -smart_ingest({ - items: [ - { content: "Item 1", tags: ["session-end"], node_type: "fact" }, - { content: "Item 2", tags: ["bug-fix"], node_type: "fact" } - ] -}) -``` -Each item runs the full cognitive pipeline: importance scoring → intent detection → synaptic tagging → hippocampal indexing → PE gating → cross-project recording. - -### search — 7-Stage Cognitive Search -``` -search({ - query: "search query", - limit: 10, // 1-100 - min_retention: 0.0, // filter by retention strength - min_similarity: 0.5, // minimum cosine similarity - detail_level: "summary", // brief|summary|full - context_topics: ["rust", "debugging"], // boost topic-matching memories - token_budget: 3000, // 100-100000, truncate to fit - retrieval_mode: "balanced" // precise|balanced|exhaustive (v2.1) -}) -``` -Retrieval modes: `precise` (fast, no activation/competition), `balanced` (default 7-stage pipeline), `exhaustive` (5x overfetch, deep graph traversal, no competition suppression). - -Pipeline: Overfetch → Rerank (cross-encoder) → Temporal boost → Accessibility filter (FSRS-6) → Context match (Tulving 1973) → Competition (Anderson 1994) → Spreading activation. **Every search strengthens the memories it finds (Testing Effect).** - -### memory — Read, Edit, Delete, Promote, Demote -``` -memory({ action: "get", id: "uuid" }) // full node with all FSRS state -memory({ action: "edit", id: "uuid", content: "updated text" }) // preserves FSRS state, regenerates embedding -memory({ action: "delete", id: "uuid" }) -memory({ action: "promote", id: "uuid", reason: "was helpful" }) // +0.20 retrieval, +0.10 retention, 1.5x stability -memory({ action: "demote", id: "uuid", reason: "was wrong" }) // -0.30 retrieval, -0.15 retention, 0.5x stability -memory({ action: "state", id: "uuid" }) // Active/Dormant/Silent/Unavailable + accessibility score -memory({ action: "get_batch", ids: ["uuid1", "uuid2", "uuid3"] }) // retrieve up to 20 full memories at once (v2.1) -``` -Promote/demote does NOT delete — it adjusts ranking. Demoted memories rank lower; alternatives surface instead. -`get_batch` is designed for batch retrieval of expandable overflow IDs from search/session_context. - -### codebase — Code Patterns & Architectural Decisions -``` -codebase({ action: "remember_pattern", name: "Pattern Name", - description: "How it works and when to use it", - files: ["src/file.rs"], codebase: "project-name" }) - -codebase({ action: "remember_decision", decision: "What was decided", - rationale: "Why", alternatives: ["Option A", "Option B"], - files: ["src/file.rs"], codebase: "project-name" }) - -codebase({ action: "get_context", codebase: "project-name", limit: 10 }) -// Returns: patterns, decisions, cross-project insights +```sh +git diff --check ``` -### intention — Prospective Memory (Reminders) -``` -intention({ action: "set", description: "What to do", - trigger: { type: "context", topic: "authentication" }, // fires when discussing auth - priority: "high" }) +## Documentation -intention({ action: "set", description: "Deploy by Friday", - trigger: { type: "time", at: "2026-03-07T17:00:00Z" }, - deadline: "2026-03-07T17:00:00Z" }) +- User setup: `README.md` +- Claude-specific templates: `docs/CLAUDE-SETUP.md` +- Storage and sync behavior: `docs/STORAGE.md` +- Cognitive Sandwich and optional verifier hooks: `docs/COGNITIVE_SANDWICH.md` +- Release history: `CHANGELOG.md` -intention({ action: "set", description: "Check test coverage", - trigger: { type: "context", codebase: "vestige", file_pattern: "*.test.*" } }) +## Public-Repo Hygiene -intention({ action: "check", context: { codebase: "vestige", topics: ["testing"] } }) -intention({ action: "update", id: "uuid", status: "complete" }) -intention({ action: "list", filter_status: "active" }) -``` - -### dream — Memory Consolidation -``` -dream({ memory_count: 50 }) -``` -5-stage cycle: Replay → Cross-reference → Strengthen → Prune → Transfer. Uses Waking SWR tagging (70% tagged + 30% random for diversity). Discovers hidden connections, generates insights, persists new edges to the activation network. - -### explore_connections — Graph Traversal -``` -explore_connections({ action: "associations", from: "uuid", limit: 10 }) -// Spreading activation from a memory — find related memories via graph traversal - -explore_connections({ action: "chain", from: "uuid-A", to: "uuid-B" }) -// Build reasoning path between two memories (A*-like pathfinding) - -explore_connections({ action: "bridges", from: "uuid-A", to: "uuid-B" }) -// Find connecting memories that bridge two concepts -``` - -### predict — Proactive Retrieval -``` -predict({ context: { codebase: "vestige", current_file: "src/main.rs", - current_topics: ["error handling", "rust"] } }) -``` -Returns: predictions with confidence, suggestions, speculative retrievals, top interests. Uses SpeculativeRetriever's learned patterns from access history. - -### importance_score — Should I Save This? -``` -importance_score({ content: "Content to evaluate", - context_topics: ["debugging"], project: "vestige" }) -``` -4-channel model: novelty (0.25), arousal (0.30), reward (0.25), attention (0.20). Composite > 0.6 = save it. - -### find_duplicates — Dedup Memory -``` -find_duplicates({ similarity_threshold: 0.80, limit: 20, tags: ["bug-fix"] }) -``` -Cosine similarity clustering. Returns merge/review suggestions. - -### memory_timeline — Chronological Browse -``` -memory_timeline({ start: "2026-02-01", end: "2026-03-01", - node_type: "decision", tags: ["vestige"], limit: 50, detail_level: "summary" }) -``` - -### memory_changelog — Audit Trail -``` -memory_changelog({ memory_id: "uuid", limit: 20 }) // per-memory history -memory_changelog({ start: "2026-03-01", limit: 20 }) // system-wide -``` - -### memory_health — Retention Dashboard -``` -memory_health() -``` -Returns: avg retention, distribution buckets (0-20%, 20-40%, etc.), trend (improving/declining/stable), recommendation. - -### memory_graph — Visualization Export -``` -memory_graph({ query: "search term", depth: 2, max_nodes: 50 }) -memory_graph({ center_id: "uuid", depth: 3, max_nodes: 100 }) -``` -Returns nodes with force-directed positions + edges with weights. - -### deep_reference — Cognitive Reasoning Engine (v2.0.4) ★ USE THIS FOR ALL FACTUAL QUESTIONS -``` -deep_reference({ query: "What port does the dev server use?" }) -deep_reference({ query: "Should I use prefix caching with vLLM?", depth: 30 }) -``` -**THE killer tool.** 8-stage cognitive reasoning pipeline: -1. Broad retrieval + cross-encoder reranking -2. Spreading activation expansion (finds connected memories search misses) -3. FSRS-6 trust scoring (retention × stability × reps ÷ lapses) -4. Intent classification (FactCheck / Timeline / RootCause / Comparison / Synthesis) -5. Temporal supersession (newer high-trust replaces older) -6. Trust-weighted contradiction analysis (only flags conflicts between strong memories) -7. Relation assessment (Supports / Contradicts / Supersedes / Irrelevant per pair) -8. **Template reasoning chain** — pre-built natural language reasoning the AI validates - -Parameters: `query` (required), `depth` (5-50, default 20). - -Returns: `intent`, `reasoning` (THE KEY FIELD — read this first), `recommended` (highest-trust answer), `evidence` (trust-sorted), `contradictions`, `superseded`, `evolution`, `related_insights`, `confidence`. - -`cross_reference` is a backward-compatible alias that calls `deep_reference`. - -### Maintenance Tools -``` -system_status() // health + stats + warnings + recommendations -consolidate() // FSRS-6 decay cycle + embedding generation -backup() // SQLite backup → ~/.vestige/backups/ -export({ format: "json", tags: ["bug-fix"], since: "2026-01-01" }) -gc({ min_retention: 0.1, dry_run: true }) // garbage collect (dry_run first!) -restore({ path: "/path/to/backup.json" }) -``` - ---- - -## Mandatory Save Gates - -**You MUST NOT proceed past a save gate without executing the save.** - -| Gate | Trigger | Action | -|------|---------|--------| -| **BUG_FIX** | After any error is resolved | `smart_ingest({ content: "BUG FIX: [error]\nRoot cause: [why]\nSolution: [fix]\nFiles: [paths]", tags: ["bug-fix", "project"], node_type: "fact" })` | -| **DECISION** | After any architectural/design choice | `codebase({ action: "remember_decision", decision, rationale, alternatives, files, codebase })` | -| **CODE_CHANGE** | After >20 lines or new pattern | `codebase({ action: "remember_pattern", name, description, files, codebase })` | -| **SESSION_END** | Before stopping or compaction | `smart_ingest({ items: [{ content: "SESSION: [summary]", tags: ["session-end"] }] })` | - ---- - -## Trigger Words — Auto-Save - -| User Says | Action | -|-----------|--------| -| "Remember this" / "Don't forget" | `smart_ingest` immediately | -| "I always..." / "I never..." / "I prefer..." | Save as preference | -| "This is important" | `smart_ingest` + `memory(action="promote")` | -| "Remind me..." / "Next time..." | `intention({ action: "set" })` | - ---- - -## Cognitive Architecture - -### Search Pipeline (7 stages) -1. **Overfetch** — 3x results from hybrid search (0.3 BM25 + 0.7 semantic, nomic-embed-text-v1.5 768D) -2. **Rerank** — Cross-encoder rescoring (Jina Reranker v1 Turbo, 38M params) -3. **Temporal** — Recency + validity window boosting (85% relevance + 15% temporal) -4. **Accessibility** — FSRS-6 retention filter (Active ≥0.7, Dormant ≥0.4, Silent ≥0.1) -5. **Context** — Tulving 1973 encoding specificity (topic overlap → +30% boost) -6. **Competition** — Anderson 1994 retrieval-induced forgetting (winners strengthen, competitors weaken) -7. **Activation** — Spreading activation side effects + predictive model + reconsolidation marking - -### Ingest Pipeline -**Pre:** 4-channel importance scoring (novelty/arousal/reward/attention) + intent detection → auto-tag -**Store:** Prediction Error Gating: similarity >0.92 → UPDATE, 0.75-0.92 → UPDATE/SUPERSEDE, <0.75 → CREATE -**Post:** Synaptic tagging (Frey & Morris 1997, 9h backward + 2h forward) + hippocampal indexing + cross-project recording - -### FSRS-6 (State-of-the-Art Spaced Repetition) -- Retrievability: `R = (1 + factor × t / S)^(-w20)` — 21 trained parameters -- Dual-strength model (Bjork & Bjork 1992): storage strength (grows) + retrieval strength (decays) -- Accessibility = retention×0.5 + retrieval×0.3 + storage×0.2 -- 20-30% more efficient than SM-2 (Anki) - -### 29 Cognitive Modules (stateful, persist across calls) - -**Neuroscience (16):** -ActivationNetwork (Collins & Loftus 1975), SynapticTaggingSystem (Frey & Morris 1997), HippocampalIndex (Teyler & Rudy 2007), ContextMatcher (Tulving 1973), AccessibilityCalculator, CompetitionManager (Anderson 1994), StateUpdateService, ImportanceSignals, NoveltySignal, ArousalSignal, RewardSignal, AttentionSignal, EmotionalMemory (Brown & Kulik 1977), PredictiveMemory, ProspectiveMemory, IntentionParser - -**Advanced (11):** -ImportanceTracker, ReconsolidationManager (Nader — 5min labile window), IntentDetector (9 intent types), ActivityTracker, MemoryDreamer (5-stage consolidation), MemoryChainBuilder (A*-like), MemoryCompressor (30-day min age), CrossProjectLearner (6 pattern types), AdaptiveEmbedder, SpeculativeRetriever (6 trigger types), ConsolidationScheduler - -**Search (2):** Reranker, TemporalSearcher - -### Memory States -- **Active** (retention ≥ 0.7) — easily retrievable -- **Dormant** (≥ 0.4) — retrievable with effort -- **Silent** (≥ 0.1) — difficult, needs cues -- **Unavailable** (< 0.1) — needs reinforcement - -### Connection Types -semantic, temporal, causal, spatial, part_of, user_defined — each with strength (0-1), activation_count, timestamps - ---- - -## Advanced Techniques - -### Cross-Project Intelligence -The CrossProjectLearner tracks patterns across ALL projects (ErrorHandling, AsyncConcurrency, Testing, Architecture, Performance, Security). When you learn a pattern in one project that works, it becomes available in all projects. Use `codebase({ action: "get_context" })` without a codebase filter to get universal patterns. - -### Reconsolidation Window -After any memory is accessed (via search, get, or promote), it enters a 5-minute "labile" state where modifications are enhanced. This is the optimal time to edit memories with new context. The system handles this automatically. - -### Synaptic Tagging (Retroactive Importance) -Memories encoded in the last 9 hours can be retroactively promoted when something important happens. If you fix a critical bug, not only does the fix get saved — related memories from the past 9 hours also get importance boosts. The SynapticTaggingSystem handles this automatically. - -### Dream Insights -Dreams don't just consolidate — they generate new insights by cross-referencing recent memories with older knowledge. The insights can reveal: contradictions between memories, previously unseen patterns, connections across different projects. Always check dream results for `insights_generated`. - -### Token Budget Strategy -Use `token_budget` on search and session_context to control response size. For quick lookups: 500. For deep context: 3000-5000. Results that don't fit go to `expandable` — retrieve them with `memory({ action: "get", id: "..." })`. - -### Detail Levels -- `brief` — id/type/tags/score only (1-2 tokens per result, good for scanning) -- `summary` — 8 fields including content preview (default, balanced) -- `full` — all FSRS state, timestamps, embedding info (for debugging/analysis) - ---- - -## Memory Hygiene - -**Promote** when user confirms helpful, solution worked, info was accurate. -**Demote** when user corrects mistake, info was wrong, led to bad outcome. -**Never save:** secrets, API keys, passwords, temporary debugging state, trivial info. - ---- - -## The One Rule - -**When in doubt, save. The cost of a duplicate is near zero (Prediction Error Gating handles dedup). The cost of lost knowledge is permanent.** - -Memory is retrieval. Searching strengthens memory. Search liberally, save aggressively. - ---- - -## Development - -- **Crate:** `vestige-mcp` v2.0.4, Rust 2024 edition, MSRV 1.91 -- **Tests:** 758 (406 mcp + 352 core), zero warnings -- **Build:** `cargo build --release -p vestige-mcp` (features: `embeddings` + `vector-search`) -- **Build (no embeddings):** `cargo build --release -p vestige-mcp --no-default-features` -- **Bench:** `cargo bench -p vestige-core` -- **Architecture:** `McpServer` → `Arc` + `Arc>` -- **Storage:** SQLite WAL mode, `Mutex` reader/writer split, FTS5 full-text search -- **Embeddings:** nomic-embed-text-v1.5 (768D, 8K context) via fastembed (local ONNX, no API) -- **Vector index:** USearch HNSW (20x faster than FAISS) -- **Binaries:** `vestige-mcp` (MCP server), `vestige` (CLI), `vestige-restore` -- **Dashboard:** SvelteKit 2 + Svelte 5 + Three.js + Tailwind 4, embedded at `/dashboard` -- **Env vars:** `VESTIGE_DASHBOARD_PORT` (default 3927), `VESTIGE_HTTP_PORT` (default 3928), `VESTIGE_HTTP_BIND` (default 127.0.0.1), `VESTIGE_AUTH_TOKEN` (auto-generated), `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` (default 6), `RUST_LOG` +Do not commit private absolute paths, local agent memory paths, unpublished +planning files, real credentials, personal operating notes, or private repo +locations. Example environment variables in docs must be empty placeholders or +obviously fake examples. diff --git a/CLAUDE.md.template b/CLAUDE.md.template index fefd005..e007ea6 100644 --- a/CLAUDE.md.template +++ b/CLAUDE.md.template @@ -88,7 +88,7 @@ Tags: ["decision", "topic-name"] | "Don't forget" | `smart_ingest` with tags: ["important"] | | "I always..." / "I never..." | Save as preference | | "I prefer..." / "I like..." | Save as preference | -| "This is important" | `smart_ingest` + `promote_memory` | +| "This is important" | `smart_ingest` + `memory(action="promote")` | | "Remind me..." | Create `intention` with trigger | | "Next time we..." | Create `intention` with context trigger | | "When I'm working on X..." | Create `intention` with codebase trigger | @@ -115,7 +115,7 @@ Act on feedback immediately — don't ask permission to promote/demote. ### Proactive Health Checks If you notice degraded recall or a user mentions memory issues: -1. Run `health_check` — check overall system status +1. Run `system_status` — check overall system status 2. If `averageRetention < 0.5` → suggest running `consolidate` 3. If `dueForReview > 50` → mention that some memories need review diff --git a/Cargo.lock b/Cargo.lock index 0cc8205..70f3956 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4531,8 +4531,9 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vestige-core" -version = "2.0.7" +version = "2.1.22" dependencies = [ + "candle-core", "chrono", "criterion", "directories", @@ -4566,7 +4567,7 @@ dependencies = [ [[package]] name = "vestige-mcp" -version = "2.0.7" +version = "2.1.22" dependencies = [ "anyhow", "axum", diff --git a/Cargo.toml b/Cargo.toml index b3008f1..5cbe508 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ exclude = [ ] [workspace.package] -version = "2.0.5" +version = "2.1.22" edition = "2024" license = "AGPL-3.0-only" repository = "https://github.com/samvallad33/vestige" diff --git a/README.md b/README.md index 33ab89b..3c18759 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,107 @@ # Vestige -### The cognitive engine that gives AI agents a brain. +### Local cognitive memory for MCP-compatible AI agents. [![GitHub stars](https://img.shields.io/github/stars/samvallad33/vestige?style=social)](https://github.com/samvallad33/vestige) [![Release](https://img.shields.io/github/v/release/samvallad33/vestige)](https://github.com/samvallad33/vestige/releases/latest) -[![Tests](https://img.shields.io/badge/tests-1284%20passing-brightgreen)](https://github.com/samvallad33/vestige/actions) +[![Tests](https://img.shields.io/badge/tests-passing-brightgreen)](https://github.com/samvallad33/vestige/actions) [![License](https://img.shields.io/badge/license-AGPL--3.0-blue)](LICENSE) [![MCP Compatible](https://img.shields.io/badge/MCP-compatible-green)](https://modelcontextprotocol.io) -**Your Agent forgets everything between sessions. Vestige fixes that.** +**Your agent forgets project decisions between sessions. Vestige gives it local, inspectable memory.** -Built on 130 years of memory research — FSRS-6 spaced repetition, prediction error gating, synaptic tagging, spreading activation, memory dreaming — all running in a single Rust binary with a 3D neural visualization dashboard. 100% local. Zero cloud. +Built on proven memory and retrieval ideas — FSRS-6 spaced repetition, prediction error gating, synaptic tagging, spreading activation, and memory consolidation — all running in a single Rust binary with a local dashboard. 100% local. Zero cloud. -[Quick Start](#quick-start) | [Dashboard](#-3d-memory-dashboard) | [How It Works](#-the-cognitive-science-stack) | [Tools](#-24-mcp-tools) | [Docs](docs/) +[Quick Start](#quick-start) | [Dashboard](#-3d-memory-dashboard) | [How It Works](#-the-cognitive-science-stack) | [Tools](#-25-mcp-tools) | [Docs](docs/) --- +## What's New in v2.1.22 "Sanhedrin Receipts" + +v2.1.22 makes the optional Sanhedrin hook accountable enough to trust in daily +agent work. Vetoes now leave local receipts, verification claims need real +command evidence, and users can appeal stale or over-strict blocks from the +dashboard. + +- **Receipt Lock.** Claims like "tests passed", "build is green", or "lint is clean" are blocked unless the current transcript contains a matching successful command receipt. +- **Screenshotable veto receipts.** Sanhedrin writes `~/.vestige/sanhedrin/latest.json` and `latest.html` with Claim -> Verdict -> Precedent -> Fix -> Appeal. +- **Dashboard Verdict Bar.** The dashboard shows PASS, NOTE, CAUTION, VETO, or APPEALED globally, expands into the receipt, and records stale/wrong/too-strict appeals. +- **Claim ledger.** Claim-mode Sanhedrin output now maps every extracted claim into structured JSON instead of treating the whole draft as one blob. +- **Appeal training.** Appeals are saved to `appeals.jsonl` and suppress future vetoes for the same claim fingerprint. + +## What's New in v2.1.21 "Agent-Neutral Hardening" + +v2.1.21 tightens Vestige for normal use across MCP-compatible agents, without +making Claude Code companion tooling part of the default path. + +- **Agent-neutral default.** Stdio MCP remains the default transport; optional HTTP MCP is explicit with `--http`, `--http-port`, or `VESTIGE_HTTP_ENABLED=1`. +- **Safer destructive actions.** `memory(action="delete")` now requires `confirm=true`, matching `purge`, and the legacy `delete_knowledge` shim forwards that confirmation instead of bypassing it. +- **Portable sync repair.** Merge imports preserve purge tombstones, avoid `INSERT OR REPLACE` cascades, rebuild the vector index from a clean state, and write portable archive temp files with private Unix permissions. +- **Release/package cleanup.** Release builds check the embedded dashboard before packaging, publish checksums, and the npm installer rejects targets that do not have release assets. +- **Any-agent memory protocol.** The setup docs now include a short agent-agnostic memory protocol for Claude Code, Codex, Cursor, VS Code, Xcode, JetBrains, Windsurf, and other MCP clients. + +## What's New in v2.1.2 "Honest Memory" + +v2.1.2 makes Vestige easier to trust in everyday work: literal lookups stay literal, purge really removes content, contradictions are inspectable, and updates no longer require a curl reinstall flow. + +- **Concrete search mode.** Quoted strings, env vars, UUIDs, paths, and code identifiers now take a keyword/literal path that skips HyDE, semantic fusion, FSRS reweighting, competition, and spreading activation. Exact things like `OPENAI_API_KEY`, `mlx_lm.server`, and migration IDs land first. +- **Irreversible purge.** `memory(action="purge", confirm=true)` permanently removes memory content and embeddings, scrubs insight JSON references, detaches temporal-summary children, prunes graph edges, and keeps only a non-content deletion tombstone for sync/audit. +- **First-class contradiction inspection.** New `contradictions` MCP tool surfaces trust-weighted disagreements directly instead of hiding them inside `deep_reference`. +- **Simple update flow.** `vestige update` refreshes binaries. Claude Code Cognitive Sandwich companion files are opt-in with `vestige update --sandwich-companion` or `vestige sandwich install`. +- **Pro waitlist preview.** `/dashboard/waitlist` adds a local-first Solo Pro and Team Pro early-access surface. `VITE_WAITLIST_ENDPOINT` and `VITE_SUPPORT_BOT_ENDPOINT` are opt-in dashboard env vars, so no signup data is captured unless endpoints are configured. + +## What's New in v2.1.1 "Portable Sync" + +v2.1.1 focuses on the biggest post-launch ask: move memories between machines without losing cognitive state. It also adds opt-in Qwen3 embeddings for higher-recall local retrieval. + +- **Exact portable archives.** `vestige portable-export` / `vestige portable-import` preserve IDs, FSRS state, graph edges, suppression state, audit rows, and embedding blobs for Vestige-to-Vestige device transfer. +- **Sync-safe merge storage.** `vestige portable-import --merge` and `vestige sync ` merge non-empty databases, apply delete tombstones, keep newer local memories, rebuild FTS, and push through a pluggable portable-sync backend. v2.1.1 ships the file backend for Dropbox, iCloud, Syncthing, Git, and shared folders. +- **Qwen3 embeddings.** Build with `qwen3-embeddings`, set `VESTIGE_EMBEDDING_MODEL=qwen3-0.6b`, and run `vestige consolidate` to re-embed existing memories. `vestige health` reports mixed-model stores before search quality is affected. +- **Model-aware retrieval.** Vestige now avoids comparing Qwen and Nomic vectors in the same search/dedup path. + +## What's New in v2.1.0 "Cognitive Sandwich Goes Local" + +v2.1.0 adds an opt-in Claude Code hook harness around the existing Vestige MCP server. The MCP tool surface and database schema stay backward compatible, while preflight hooks can inject trusted memory context before Claude answers. The heavyweight Sanhedrin verifier is optional and can be enabled separately. + +- **Optional Sanhedrin Executioner.** The post-response verifier is off by default. Users can enable it with an OpenAI-compatible endpoint on x86/Linux/Intel Mac, or add `--with-launchd` on Apple Silicon to run the local MLX Qwen backend. +- **One-command Cognitive Sandwich installer.** `vestige sandwich install` stages hook files and agents by default, removes old Vestige hook wiring, and leaves all Claude Code hook layers plus the 19 GB model path opt-in. +- **Pulse hook backed by `/api/changelog`.** Fresh dream and connection events can be injected into the next Claude Code prompt context without blocking the prompt. +- **`VESTIGE_DATA_DIR` support.** `--data-dir` now has an env-var fallback, tilde expansion, secure directory creation, and clear precedence docs. +- **NPM release wrapper fixed.** `vestige-mcp-server@2.1.0` now downloads binaries from the matching `v2.1.0` GitHub release tag instead of an old hardcoded release. + +## What's New in v2.0.9 "Autopilot" + +Autopilot flips Vestige from passive memory library to **self-managing cognitive surface**. Same 24 MCP tools, zero schema changes — but the moment you upgrade, 14 previously dormant cognitive primitives start firing on live events without any tool call from your client. + +- **One supervised backend task subscribes to the 20-event WebSocket bus** and routes six event classes into the cognitive engine: `MemoryCreated` triggers synaptic-tagging PRP + predictive-access records, `SearchPerformed` warms the speculative-retrieval model, `MemoryPromoted` fires activation spread, `MemorySuppressed` emits the Rac1 cascade wave, high-importance `ImportanceScored` (>0.85) auto-promotes, and `Heartbeat` rate-limit-fires `find_duplicates` on large DBs. **The engine mutex is never held across `.await`, so MCP dispatch is never starved.** +- **Panic-resilient supervisors.** Both background tasks run inside an outer supervisor loop — if one handler panics on a bad memory, the supervisor respawns it in 5 s instead of losing every future event. +- **Fully backward compatible.** No new MCP tools. No schema migration. Existing v2.0.8 databases open without a single step. Opt out with `VESTIGE_AUTOPILOT_ENABLED=0` if you want the passive-library contract back. +- **3,091 LOC of orphan v1.0 tool code removed** — nine superseded modules (`checkpoint`, `codebase`, `consolidate`, `ingest`, `intentions`, `knowledge`, `recall`, plus helpers) verified zero non-test callers before deletion. Tool surface unchanged. + +## What's New in v2.0.8 "Pulse" + +v2.0.8 wires the dashboard through to the cognitive engine. Eight new surfaces expose the reasoning stack visually — every one was MCP-only before. + +- **Reasoning Theater (`/reasoning`)** — `Cmd+K` Ask palette over the 8-stage `deep_reference` pipeline (hybrid retrieval → cross-encoder rerank → spreading activation → FSRS-6 trust → temporal supersession → contradiction analysis → relation assessment → template reasoning chain). Evidence cards, confidence meter, contradiction geodesic arcs, superseded-memory lineage, evolution timeline. **Zero LLM calls, 100% local.** +- **Pulse InsightToast** — real-time toasts for `DreamCompleted`, `ConsolidationCompleted`, `ConnectionDiscovered`, promote/demote/suppress/unsuppress, `Rac1CascadeSwept`. Rate-limited, auto-dismiss, click-to-dismiss. +- **Memory Birth Ritual (Terrarium)** — new memories materialize in the 3D graph on every `MemoryCreated`: elastic scale-in, quadratic Bezier flight path, glow sprite fade-in, Newton's Cradle docking recoil. 60-frame sequence, zero-alloc math. +- **7 more dashboard surfaces** — `/duplicates`, `/dreams`, `/schedule`, `/importance`, `/activation`, `/contradictions`, `/patterns`. Left nav expanded 8 → 16 with single-key shortcuts. +- **Intel Mac (`x86_64-apple-darwin`) support restored** via the `ort-dynamic` Cargo feature + Homebrew `onnxruntime`. Microsoft deprecated x86_64 macOS prebuilts; the dynamic-link path sidesteps that permanently. **Closes #41.** +- **Contradiction-detection false positives eliminated** — four thresholds tightened so adjacent-domain memories no longer flag as conflicts. On an FSRS-6 query this collapses false contradictions 12 → 0 without regressing legitimate test cases. + +## What's New in v2.0.7 "Visible" + +Hygiene release closing two UI gaps and finishing schema cleanup. No breaking changes, no user-data migrations. + +- **`POST /api/memories/{id}/suppress` + `/unsuppress` HTTP endpoints** — dashboard can trigger Anderson 2025 SIF + Rac1 cascade without dropping to raw MCP. `suppressionCount`, `retrievalPenalty`, `reversibleUntil`, `labileWindowHours` all in response. Suppress button joins Promote / Demote / Delete on the Memories page. +- **Uptime in the sidebar footer** — the `Heartbeat` event has carried `uptime_secs` since v2.0.5 but was never rendered. Now shows as `up 3d 4h` / `up 18m` / `up 47s`. +- **`execute_export` panic fix** — unreachable match arm replaced with a clean "unsupported export format" error instead of unwinding through the MCP dispatcher. +- **`predict` surfaces `predict_degraded: true`** on lock poisoning instead of silently returning empty vecs. `memory_changelog` honors `start` / `end` bounds. `intention` check honors `include_snoozed`. +- **Migration V11** — drops dead `knowledge_edges` + `compressed_memories` tables (added speculatively in V4, never used). + ## What's New in v2.0.6 "Composer" v2.0.6 is a polish release that makes the existing cognitive stack finally *feel* alive in the dashboard and stays out of your way on the prompt side. @@ -34,7 +117,7 @@ v2.0.6 is a polish release that makes the existing cognitive stack finally *feel Ebbinghaus 1885 models what happens to memories you don't touch. Anderson 2025 models what happens when you actively want to stop thinking about one. Every other AI memory system implements the first. Vestige is the first to ship the second. -Based on [Anderson et al. 2025](https://www.nature.com/articles/s41583-025-00929-y) (Suppression-Induced Forgetting, *Nat Rev Neurosci*) and [Cervantes-Sandoval et al. 2020](https://pmc.ncbi.nlm.nih.gov/articles/PMC7477079/) (Rac1 synaptic cascade). **24 tools · 29 cognitive modules · 1,292 tests.** +Based on [Anderson et al. 2025](https://www.nature.com/articles/s41583-025-00929-y) (Suppression-Induced Forgetting, *Nat Rev Neurosci*) and [Cervantes-Sandoval et al. 2020](https://pmc.ncbi.nlm.nih.gov/articles/PMC7477079/) (Rac1 synaptic cascade).
Earlier releases (v2.0 "Cognitive Leap" → v2.0.4 "Deep Reference") @@ -54,15 +137,15 @@ Based on [Anderson et al. 2025](https://www.nature.com/articles/s41583-025-00929 ## Quick Start ```bash -# 1. Install (macOS Apple Silicon) -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +# 1. Install +npm install -g vestige-mcp-server@latest -# 2. Connect to Claude Code +# 2. Connect to any MCP-compatible agent +# Claude Code claude mcp add vestige vestige-mcp -s user -# Or connect to Codex -codex mcp add vestige -- /usr/local/bin/vestige-mcp +# Codex +codex mcp add vestige -- vestige-mcp # 3. Test it # "Remember that I prefer TypeScript over JavaScript" @@ -74,18 +157,60 @@ codex mcp add vestige -- /usr/local/bin/vestige-mcp
Other platforms & install methods -**Linux (x86_64):** +**Updating an existing install:** ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-unknown-linux-gnu.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +vestige update ``` -**macOS (Intel) and Windows:** Prebuilt binaries aren't currently shipped for these targets because of upstream toolchain gaps (`ort-sys` lacks Intel Mac prebuilts in the 2.0.0-rc.11 release that `fastembed 5.13.2` is pinned to; `usearch 2.24.0` hit a Windows MSVC compile break tracked as [usearch#746](https://github.com/unum-cloud/usearch/issues/746)). Both build fine from source in the meantime: +`vestige update` updates only the Vestige binaries by default. Use +`vestige update --sandwich-companion` if you also want to refresh optional Claude +Code Cognitive Sandwich companion files. + +**macOS/Linux manual binary install:** +```bash +vestige update --install-dir /usr/local/bin +``` + +**macOS (Intel):** Microsoft is discontinuing x86_64 macOS prebuilts after ONNX Runtime v1.23.0, so Vestige's Intel Mac build links dynamically against a Homebrew-installed ONNX Runtime via the `ort-dynamic` feature. Install with: + +```bash +brew install onnxruntime +npm install -g vestige-mcp-server@latest +echo 'export ORT_DYLIB_PATH="'"$(brew --prefix onnxruntime)"'/lib/libonnxruntime.dylib"' >> ~/.zshrc +source ~/.zshrc +claude mcp add vestige vestige-mcp -s user +``` + +Full Intel Mac guide (build-from-source + troubleshooting): [`docs/INSTALL-INTEL-MAC.md`](docs/INSTALL-INTEL-MAC.md). + +**Windows + Claude Desktop (recommended):** + +Fully quit Claude Desktop from the system tray, then install or update Vestige from PowerShell: + +```powershell +npm install -g vestige-mcp-server@latest +vestige-mcp --version +``` + +Open `%APPDATA%\Claude\claude_desktop_config.json` and point Claude Desktop at the installed MCP command: + +```json +{ + "mcpServers": { + "vestige": { + "command": "vestige-mcp" + } + } +} +``` + +If Claude Desktop cannot find `vestige-mcp`, run `where vestige-mcp` in PowerShell and use the exact `.cmd` path it prints as `command`. Example: `"C:\\Users\\you\\AppData\\Roaming\\npm\\vestige-mcp.cmd"`. Reopen Claude Desktop after saving. Future binary updates use `vestige update`; optional Claude Code companion files require `vestige update --sandwich-companion`. + +**Windows source build:** Prebuilt binaries ship but `usearch 2.24.0` hit an MSVC compile break ([usearch#746](https://github.com/unum-cloud/usearch/issues/746)); we've pinned `=2.23.0` until upstream fixes it. Source builds work with: ```bash git clone https://github.com/samvallad33/vestige && cd vestige cargo build --release -p vestige-mcp -# Binary lands at target/release/vestige-mcp ``` **npm:** @@ -106,7 +231,7 @@ cargo build --release -p vestige-mcp --features metal ## Works Everywhere -Vestige speaks MCP — the universal protocol for AI tools. One brain, every IDE. +Vestige speaks MCP, so any client that can register a stdio MCP server can use it. | IDE | Setup | |-----|-------| @@ -136,7 +261,7 @@ Vestige v2.0 ships with a real-time 3D visualization of your AI's memory. Every **Tech:** SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4 + WebSocket -The dashboard runs automatically at `http://localhost:3927/dashboard` when the MCP server starts. +Run `vestige dashboard` to open `http://localhost:3927/dashboard`, or set `VESTIGE_DASHBOARD_ENABLED=true` to start it with the MCP server. --- @@ -151,7 +276,7 @@ The dashboard runs automatically at `http://localhost:3927/dashboard` when the M │ 15 REST endpoints · WS event broadcast │ ├─────────────────────────────────────────────────────┤ │ MCP Server (stdio JSON-RPC) │ -│ 24 tools · 29 cognitive modules │ +│ 25 tools · 30 cognitive modules │ ├─────────────────────────────────────────────────────┤ │ Cognitive Engine │ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ @@ -218,7 +343,7 @@ This isn't a key-value store with an embedding model bolted on. Vestige implemen --- -## 🛠 24 MCP Tools +## 🛠 25 MCP Tools ### Context Packets | Tool | What It Does | @@ -228,9 +353,9 @@ This isn't a key-value store with an embedding model bolted on. Vestige implemen ### Core Memory | Tool | What It Does | |------|-------------| -| `search` | 7-stage cognitive search — HyDE expansion + keyword + semantic + reranking + temporal + competition + spreading activation | +| `search` | Concrete literal search for exact identifiers, or 7-stage cognitive search — HyDE expansion + keyword + semantic + reranking + temporal + competition + spreading activation | | `smart_ingest` | Intelligent storage with CREATE/UPDATE/SUPERSEDE via Prediction Error Gating. Batch mode for session-end saves | -| `memory` | Get, delete, check state, promote (thumbs up), demote (thumbs down) | +| `memory` | Get, purge content/embeddings, check state, promote (thumbs up), demote (thumbs down), edit | | `codebase` | Remember code patterns and architectural decisions per-project | | `intention` | Prospective memory — "remind me to X when Y happens" | @@ -260,34 +385,28 @@ This isn't a key-value store with an embedding model bolted on. Vestige implemen | `consolidate` | Run FSRS-6 decay cycle (also auto-runs every 6 hours) | | `memory_timeline` | Browse chronologically, grouped by day | | `memory_changelog` | Audit trail of state transitions | -| `backup` / `export` / `gc` | Database backup, JSON export, garbage collection | -| `restore` | Restore from JSON backup | +| `backup` / `export` / `gc` | Database backup, JSON/JSONL/portable export, garbage collection | +| `restore` | Restore from JSON backup or portable archive | ### Deep Reference (v2.0.4) | Tool | What It Does | |------|-------------| | `deep_reference` | **Cognitive reasoning across memories.** 8-stage pipeline: FSRS-6 trust scoring, intent classification, spreading activation, temporal supersession, contradiction analysis, relation assessment, dream insight integration, and algorithmic reasoning chain generation. Returns trust-scored evidence with a pre-built reasoning scaffold. | | `cross_reference` | Backward-compatible alias for `deep_reference`. | +| `contradictions` | **Honest memory inspection.** Scans a topic or recent memories for trust-weighted disagreements using the same local contradiction logic as `deep_reference`. | ### Active Forgetting (v2.0.5) | Tool | What It Does | |------|-------------| -| `suppress` | **Top-down active forgetting** — neuroscience-grounded inhibitory control over retrieval. Distinct from `memory.delete` (destroys the row) and `memory.demote` (one-shot ranking hit). Each call **compounds** a retrieval-score penalty (Anderson 2025 SIF), and a background Rac1 cascade worker fades co-activated neighbors over 72h (Davis 2020). Reversible within a 24-hour labile window via `reverse: true`. **The memory persists** — it is inhibited, not erased. | +| `suppress` | **Top-down active forgetting** — neuroscience-grounded inhibitory control over retrieval. Distinct from `memory(action="purge")`, which permanently removes content/embeddings. Each suppression compounds a retrieval-score penalty (Anderson 2025 SIF), and a background Rac1 cascade worker fades co-activated neighbors over 72h (Davis 2020). Reversible within a 24-hour labile window via `reverse: true`. **The memory persists** — it is inhibited, not erased. | --- ## Make Your AI Use Vestige Automatically -Add this to your `CLAUDE.md`: - -```markdown -## Memory - -At the start of every session: -1. Search Vestige for user preferences and project context -2. Save bug fixes, decisions, and patterns without being asked -3. Create reminders when the user mentions deadlines -``` +Registering the MCP server exposes tools; the agent still needs an instruction +that tells it when to call memory. Use the agent-neutral protocol, then adapt it +to your client-specific instruction file. | You Say | AI Does | |---------|---------| @@ -296,7 +415,7 @@ At the start of every session: | "Remind me..." | Creates a future trigger | | "This is important" | Saves + promotes | -[Full CLAUDE.md templates ->](docs/CLAUDE-SETUP.md) +[Agent memory protocol ->](docs/AGENT-MEMORY-PROTOCOL.md) · [Claude Code template ->](docs/CLAUDE-SETUP.md) --- @@ -305,9 +424,9 @@ At the start of every session: | Metric | Value | |--------|-------| | **Language** | Rust 2024 edition (MSRV 1.91) | -| **Codebase** | 80,000+ lines, 1,292 tests (366 core + 425 mcp + 497 e2e + 4 doctests) | +| **Codebase** | 80,000+ lines with Rust core/MCP/e2e, dashboard, and hook coverage | | **Binary size** | ~20MB | -| **Embeddings** | Nomic Embed Text v1.5 (768d → 256d Matryoshka, 8192 context) | +| **Embeddings** | Nomic Embed Text v1.5 by default (768d -> 256d Matryoshka, 8192 context); Qwen3 0.6B optional | | **Vector search** | USearch HNSW (20x faster than FAISS) | | **Reranker** | Jina Reranker v1 Turbo (38M params, +15-20% precision) | | **Storage** | SQLite + FTS5 (optional SQLCipher encryption) | @@ -315,22 +434,14 @@ At the start of every session: | **Transport** | MCP stdio (JSON-RPC 2.0) + WebSocket | | **Cognitive modules** | 30 stateful (17 neuroscience, 11 advanced, 2 search) | | **First run** | Downloads embedding model (~130MB), then fully offline | -| **Platforms** | macOS ARM + Linux x86_64 (prebuilt). macOS Intel + Windows build from source (upstream toolchain gaps, see install notes). | +| **Platforms** | macOS ARM + Intel + Linux x86_64 + Windows x86_64 (all prebuilt). Intel Mac needs `brew install onnxruntime` — see [install guide](docs/INSTALL-INTEL-MAC.md). | ### Optional Features ```bash -# Metal GPU acceleration (Apple Silicon — faster embedding inference) -cargo build --release -p vestige-mcp --features metal - -# Nomic Embed Text v2 MoE (475M params, 305M active, 8 experts) -cargo build --release -p vestige-mcp --features nomic-v2 - -# Qwen3 Reranker (Candle backend, high-precision cross-encoder) -cargo build --release -p vestige-mcp --features qwen3-reranker - -# SQLCipher encryption -cargo build --release -p vestige-mcp --no-default-features --features encryption,embeddings,vector-search +# Qwen3 embeddings (Candle backend; add metal on Apple Silicon) +cargo build --release -p vestige-mcp --features qwen3-embeddings,metal +VESTIGE_EMBEDDING_MODEL=qwen3-0.6b vestige consolidate ``` --- @@ -344,6 +455,10 @@ vestige stats --states # Cognitive state breakdown vestige health # System health check vestige consolidate # Run memory maintenance vestige restore # Restore from backup +vestige portable-export # Exact cross-device archive +vestige portable-import # Import archive into an empty database +vestige portable-import --merge # Merge archive into this database +vestige sync # Pull/merge/push via file backend vestige dashboard # Open 3D dashboard in browser ``` @@ -384,13 +499,13 @@ First run downloads ~130MB from Hugging Face. If behind a proxy: export HTTPS_PROXY=your-proxy:port ``` -Cache: macOS `~/Library/Caches/com.vestige.core/fastembed` | Linux `~/.cache/vestige/fastembed` +Cache: platform user cache directory first, then `./.fastembed_cache` as a fallback. Override with `FASTEMBED_CACHE_PATH`.
Dashboard not loading -The dashboard starts automatically on port 3927 when the MCP server runs. Check: +Run `vestige dashboard` or set `VESTIGE_DASHBOARD_ENABLED=true`, then check: ```bash curl http://localhost:3927/api/health # Should return {"status":"healthy",...} @@ -413,5 +528,5 @@ AGPL-3.0 — free to use, modify, and self-host. If you offer Vestige as a netwo

Built by @samvallad33
- 80,000+ lines of Rust · 29 cognitive modules · 130 years of memory research · one 22MB binary + 80,000+ lines of Rust · 30 cognitive modules · 130 years of memory research · one 22MB binary

diff --git a/SECURITY.md b/SECURITY.md index cc46721..c130b7b 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| 2.0.x | :white_check_mark: | +| 2.1.x | :white_check_mark: | +| 2.0.x | Critical fixes only | | 1.x | :x: | ## Reporting a Vulnerability @@ -27,13 +28,13 @@ You can expect a response within 48 hours. Vestige is a **local MCP server** designed to run on your machine with your user permissions: -- **Trusted**: The MCP client (Claude Code/Desktop) that connects via stdio +- **Trusted**: The MCP client or local agent that connects via stdio - **Untrusted**: Content passed through MCP tool arguments (validated before use) ### What Vestige Does NOT Do -- ❌ Make network requests (except first-run model download from Hugging Face) -- ❌ Execute shell commands +- ❌ Make network requests during normal memory use, except first-run model download from Hugging Face +- ❌ Require telemetry, hosted memory storage, or a cloud account - ❌ Access files outside its data directory - ❌ Send telemetry or analytics - ❌ Phone home to any server diff --git a/agents/executioner.md b/agents/executioner.md new file mode 100644 index 0000000..7d00615 --- /dev/null +++ b/agents/executioner.md @@ -0,0 +1,62 @@ +--- +name: executioner +description: Optional Sanhedrin fallback verifier. Decomposes a draft into check-worthy claims, checks high-trust durable Vestige evidence, and returns a pass/veto verdict. +tools: mcp__vestige__deep_reference, mcp__vestige__memory, mcp__vestige__search +model: claude-haiku-4-5-20251001 +--- + +# Role + +You are a one-turn verifier. You do not converse. You return exactly one line. + +# Job + +Decompose the draft response into check-worthy claims, verify each claim against +high-trust durable Vestige memory when available, and veto only when the draft +contradicts memory or makes a sensitive user-specific assertion without durable +supporting evidence. + +# Claim Classes + +Check all relevant classes: + +1. `TECHNICAL` — APIs, commands, versions, files, configs, endpoints. +2. `BIOGRAPHICAL` — identity, role, location, employment, education. +3. `FINANCIAL` — costs, revenue, pricing, funding, prizes. +4. `ACHIEVEMENT` — releases, rankings, completions, scores, milestones. +5. `TIMELINE` — dates, durations, ordering, deadlines. +6. `QUANTITATIVE` — counts, percentages, metrics, measurements. +7. `ATTRIBUTION` — who said, decided, agreed, shipped, or committed. +8. `CAUSAL` — claimed causes and effects. +9. `COMPARATIVE` — better, most, fastest, more than, fewer than. +10. `EXISTENTIAL` — whether a file, feature, repo, or artifact exists. +11. `VAGUE-QUANTIFIER` — vague positive claims like "a few wins" or "some prize money". + +# Decision Rules + +- Veto direct contradiction with high-trust memory. +- Veto unsupported positive claims about the user's biography, finances, + achievements, timeline, quantitative results, attribution, or vague + positive outcomes. +- Treat staged/current-turn evidence as context only. It is not durable memory and + cannot satisfy the durable-evidence requirement. +- Do not veto purely stylistic disagreement. +- Do not veto technical claims just because Vestige lacks evidence; the draft + may rely on source files or external docs. +- If evidence is stale or superseded, prefer the newer higher-trust memory. + +# Output + +If the draft passes: + +```text +yes +``` + +If the draft should be rewritten: + +```text +no - [Sanhedrin Veto] [CLASS]: [one-sentence reason under 120 chars] +``` + +Output exactly one line. diff --git a/agents/lateral-thinker.md b/agents/lateral-thinker.md new file mode 100644 index 0000000..9e5efd0 --- /dev/null +++ b/agents/lateral-thinker.md @@ -0,0 +1,51 @@ +--- +name: lateral-thinker +description: Subconscious subagent that surfaces cross-disciplinary structural parallels from the Vestige memory graph. Invoked by the preflight-swarm.sh UserPromptSubmit hook (Pre-Cognitive Triad v2.3 "Thalamus"). Fresh context, Haiku 4.5, Vestige MCP tool access. Outputs a single XML block or EMPTY. +tools: mcp__vestige__search, mcp__vestige__explore_connections, mcp__vestige__memory +model: claude-haiku-4-5-20251001 +--- + +# Identity + +You are the Lateral Thinker, a subconscious subagent in the Vestige OS. You run before the main Claude agent sees the user's prompt. Your only job is to surface a cross-disciplinary structural parallel from the Vestige memory graph that the main agent would miss. + +You do not converse. You do not write code. You do not acknowledge or explain yourself. You output exactly one XML block or the single word EMPTY. + +# Execution Protocol + +1. Read the user prompt. +2. Extract the core structural pattern (race condition / state sync / retry loop / memory leak / schema migration / decoding ambiguity / rate limit / ordering guarantee / cache invalidation / etc). +3. Call `mcp__vestige__explore_connections` with action=`bridges` OR `mcp__vestige__search` to find memories in a completely unrelated domain that share the same structural pattern. Prefer bridges between distant clusters — React UI state ↔ Rust async channel, Python DB lock ↔ Git merge conflict, API retry ↔ neural synaptic reinforcement. +4. If you find a high-confidence mechanical parallel (not a metaphor, a real structural isomorphism), output exactly this XML: + +```xml + + one short noun phrase naming the shared pattern + where the user currently is + the unrelated domain where the pattern also lives + the Vestige node ID of the cross-domain memory, if applicable + one sentence explaining how the unrelated memory informs the current problem mechanically, not metaphorically + +``` + +5. If you cannot find a confident, mechanical, distinct bridge in under three tool calls, output exactly the single word: `EMPTY`. Do not apologize, explain, or converse. + +# Examples of valid epiphanies + +```xml + + stale read after write under weak ordering + React context propagation across portal boundary + PostgreSQL read-committed isolation after uncommitted write + pg-isolation-decision-2f7a + The portal boundary behaves like a snapshot isolation level — state written in the parent is not visible to the portal child until the parent re-renders, analogous to waiting for commit visibility in Postgres. + +``` + +# What NOT to do + +- Do not paraphrase the user's prompt. +- Do not summarize Vestige memory contents as a list. +- Do not say "this reminds me of". +- Do not output analogies that are mere vibes — every bridge must be a concrete mechanical equivalence. +- Do not converse. If you are about to type a sentence that begins with "Here is" or "I found" or "Let me think", stop and emit EMPTY instead. diff --git a/agents/synthesis-composer.md b/agents/synthesis-composer.md new file mode 100644 index 0000000..350d45d --- /dev/null +++ b/agents/synthesis-composer.md @@ -0,0 +1,46 @@ +--- +name: synthesis-composer +description: Optional decision helper that turns Vestige retrievals into concise recommendations. Use for high-stakes technical choices, launches, purchases, submissions, architecture decisions, and tradeoffs where memory evidence may change the answer. +tools: mcp__vestige__deep_reference, mcp__vestige__explore_connections, mcp__vestige__search +model: claude-haiku-4-5-20251001 +--- + +# Role + +You are the Synthesis Composer. Your job is to turn retrieved Vestige evidence +into a decision, not a memory summary. + +# Protocol + +1. Use the smallest Vestige retrieval plan that can materially change the + answer. +2. Search adjacent topics when the decision depends on related history. +3. Convert each useful memory into `fact -> implication -> action`. +4. Surface contradictions, stale evidence, or missing evidence before the + recommendation. +5. If no memory changes the recommendation, say that briefly and proceed from + source evidence. + +# Output Shape + +Use this compact structure: + +```text +Evidence: ... +Implication: ... +Recommendation: ... +``` + +When useful, add: + +```text +Contradictions: ... +Next step: ... +``` + +# Do Not + +- Do not dump memory summaries as the final answer. +- Do not invent hidden evidence. +- Do not claim a memory was checked unless a tool result supports it. +- Do not force a rigid template when the answer is simple. diff --git a/apps/dashboard/.env.example b/apps/dashboard/.env.example new file mode 100644 index 0000000..9b64f17 --- /dev/null +++ b/apps/dashboard/.env.example @@ -0,0 +1,9 @@ +# Optional public waitlist capture endpoint used by /dashboard/waitlist. +# The page POSTs JSON with: name, email, plan, priority, notes, source, createdAt. +# Examples: Formspree, Tally webhook, Buttondown custom endpoint, Supabase Edge Function. +VITE_WAITLIST_ENDPOINT= + +# Optional support bot endpoint used by /dashboard/waitlist. +# The page POSTs JSON with: question, plan, priority, source, and recent history. +# If unset, the page uses the built-in deterministic onboarding FAQ. +VITE_SUPPORT_BOT_ENDPOINT= diff --git a/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css b/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css deleted file mode 100644 index 2cd8654..0000000 --- a/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css +++ /dev/null @@ -1 +0,0 @@ -/*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"JetBrains Mono", "Fira Code", "SF Mono", monospace;--color-amber-400:oklch(82.8% .189 84.429);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-wide:.025em;--tracking-wider:.05em;--leading-relaxed:1.625;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-void:#050510;--color-deep:#10102a;--color-subtle:#2a2a5e;--color-muted:#4a4a7a;--color-dim:#7a7aaa;--color-text:#e0e0ff;--color-bright:#fff;--color-synapse:#6366f1;--color-synapse-glow:#818cf8;--color-dream:#a855f7;--color-dream-glow:#c084fc;--color-memory:#3b82f6;--color-recall:#10b981;--color-decay:#ef4444;--color-warning:#f59e0b}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.top-0{top:calc(var(--spacing) * 0)}.top-3{top:calc(var(--spacing) * 3)}.top-4{top:calc(var(--spacing) * 4)}.right-0{right:calc(var(--spacing) * 0)}.right-4{right:calc(var(--spacing) * 4)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-1\/2{left:50%}.left-4{left:calc(var(--spacing) * 4)}.left-6{left:calc(var(--spacing) * 6)}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-\[-12px\]{margin-top:-12px}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-12{height:calc(var(--spacing) * 12)}.h-16{height:calc(var(--spacing) * 16)}.h-24{height:calc(var(--spacing) * 24)}.h-32{height:calc(var(--spacing) * 32)}.h-40{height:calc(var(--spacing) * 40)}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.min-h-0{min-height:calc(var(--spacing) * 0)}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-12{width:calc(var(--spacing) * 12)}.w-16{width:calc(var(--spacing) * 16)}.w-24{width:calc(var(--spacing) * 24)}.w-96{width:calc(var(--spacing) * 96)}.w-\[90\%\]{width:90%}.w-full{width:100%}.w-px{width:1px}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-64{min-width:calc(var(--spacing) * 64)}.min-w-\[2rem\]{min-width:2rem}.min-w-\[3\.5rem\]{min-width:3.5rem}.flex-1{flex:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.grow{flex-grow:1}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.\!border-decay\/20{border-color:#ef444433!important}@supports (color:color-mix(in lab,red,red)){.\!border-decay\/20{border-color:color-mix(in oklab,var(--color-decay) 20%,transparent)!important}}.\!border-dream\/20{border-color:#a855f733!important}@supports (color:color-mix(in lab,red,red)){.\!border-dream\/20{border-color:color-mix(in oklab,var(--color-dream) 20%,transparent)!important}}.\!border-synapse\/20{border-color:#6366f133!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/20{border-color:color-mix(in oklab,var(--color-synapse) 20%,transparent)!important}}.\!border-synapse\/30{border-color:#6366f14d!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/30{border-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)!important}}.\!border-synapse\/40{border-color:#6366f166!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/40{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)!important}}.border-\[\#A33FFF\]\/40{border-color:#a33fff66}.border-dream\/10{border-color:#a855f71a}@supports (color:color-mix(in lab,red,red)){.border-dream\/10{border-color:color-mix(in oklab,var(--color-dream) 10%,transparent)}}.border-dream\/20{border-color:#a855f733}@supports (color:color-mix(in lab,red,red)){.border-dream\/20{border-color:color-mix(in oklab,var(--color-dream) 20%,transparent)}}.border-dream\/30{border-color:#a855f74d}@supports (color:color-mix(in lab,red,red)){.border-dream\/30{border-color:color-mix(in oklab,var(--color-dream) 30%,transparent)}}.border-dream\/40{border-color:#a855f766}@supports (color:color-mix(in lab,red,red)){.border-dream\/40{border-color:color-mix(in oklab,var(--color-dream) 40%,transparent)}}.border-dream\/50{border-color:#a855f780}@supports (color:color-mix(in lab,red,red)){.border-dream\/50{border-color:color-mix(in oklab,var(--color-dream) 50%,transparent)}}.border-recall\/30{border-color:#10b9814d}@supports (color:color-mix(in lab,red,red)){.border-recall\/30{border-color:color-mix(in oklab,var(--color-recall) 30%,transparent)}}.border-subtle\/15{border-color:#2a2a5e26}@supports (color:color-mix(in lab,red,red)){.border-subtle\/15{border-color:color-mix(in oklab,var(--color-subtle) 15%,transparent)}}.border-subtle\/20{border-color:#2a2a5e33}@supports (color:color-mix(in lab,red,red)){.border-subtle\/20{border-color:color-mix(in oklab,var(--color-subtle) 20%,transparent)}}.border-synapse{border-color:var(--color-synapse)}.border-synapse\/10{border-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.border-synapse\/10{border-color:color-mix(in oklab,var(--color-synapse) 10%,transparent)}}.border-synapse\/30{border-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.border-synapse\/30{border-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)}}.border-synapse\/40{border-color:#6366f166}@supports (color:color-mix(in lab,red,red)){.border-synapse\/40{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)}}.border-transparent{border-color:#0000}.border-warning\/40{border-color:#f59e0b66}@supports (color:color-mix(in lab,red,red)){.border-warning\/40{border-color:color-mix(in oklab,var(--color-warning) 40%,transparent)}}.border-warning\/50{border-color:#f59e0b80}@supports (color:color-mix(in lab,red,red)){.border-warning\/50{border-color:color-mix(in oklab,var(--color-warning) 50%,transparent)}}.border-t-dream{border-top-color:var(--color-dream)}.border-t-synapse{border-top-color:var(--color-synapse)}.border-t-warning{border-top-color:var(--color-warning)}.bg-\[\#A33FFF\]{background-color:#a33fff}.bg-\[\#A33FFF\]\/10{background-color:#a33fff1a}.bg-amber-400{background-color:var(--color-amber-400)}.bg-decay{background-color:var(--color-decay)}.bg-decay\/10{background-color:#ef44441a}@supports (color:color-mix(in lab,red,red)){.bg-decay\/10{background-color:color-mix(in oklab,var(--color-decay) 10%,transparent)}}.bg-decay\/20{background-color:#ef444433}@supports (color:color-mix(in lab,red,red)){.bg-decay\/20{background-color:color-mix(in oklab,var(--color-decay) 20%,transparent)}}.bg-deep{background-color:var(--color-deep)}.bg-dream{background-color:var(--color-dream)}.bg-dream\/5{background-color:#a855f70d}@supports (color:color-mix(in lab,red,red)){.bg-dream\/5{background-color:color-mix(in oklab,var(--color-dream) 5%,transparent)}}.bg-dream\/10{background-color:#a855f71a}@supports (color:color-mix(in lab,red,red)){.bg-dream\/10{background-color:color-mix(in oklab,var(--color-dream) 10%,transparent)}}.bg-dream\/20{background-color:#a855f733}@supports (color:color-mix(in lab,red,red)){.bg-dream\/20{background-color:color-mix(in oklab,var(--color-dream) 20%,transparent)}}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/20{background-color:color-mix(in oklab,var(--color-purple-500) 20%,transparent)}}.bg-recall{background-color:var(--color-recall)}.bg-recall\/10{background-color:#10b9811a}@supports (color:color-mix(in lab,red,red)){.bg-recall\/10{background-color:color-mix(in oklab,var(--color-recall) 10%,transparent)}}.bg-recall\/20{background-color:#10b98133}@supports (color:color-mix(in lab,red,red)){.bg-recall\/20{background-color:color-mix(in oklab,var(--color-recall) 20%,transparent)}}.bg-synapse{background-color:var(--color-synapse)}.bg-synapse\/10{background-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/10{background-color:color-mix(in oklab,var(--color-synapse) 10%,transparent)}}.bg-synapse\/15{background-color:#6366f126}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/15{background-color:color-mix(in oklab,var(--color-synapse) 15%,transparent)}}.bg-synapse\/20{background-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/20{background-color:color-mix(in oklab,var(--color-synapse) 20%,transparent)}}.bg-synapse\/25{background-color:#6366f140}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/25{background-color:color-mix(in oklab,var(--color-synapse) 25%,transparent)}}.bg-transparent{background-color:#0000}.bg-void{background-color:var(--color-void)}.bg-void\/60{background-color:#05051099}@supports (color:color-mix(in lab,red,red)){.bg-void\/60{background-color:color-mix(in oklab,var(--color-void) 60%,transparent)}}.bg-warning\/20{background-color:#f59e0b33}@supports (color:color-mix(in lab,red,red)){.bg-warning\/20{background-color:color-mix(in oklab,var(--color-warning) 20%,transparent)}}.bg-white\/\[0\.02\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.02\]{background-color:color-mix(in oklab,var(--color-white) 2%,transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab,var(--color-white) 3%,transparent)}}.bg-white\/\[0\.04\]{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.04\]{background-color:color-mix(in oklab,var(--color-white) 4%,transparent)}}.bg-white\/\[0\.06\]{background-color:#ffffff0f}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.06\]{background-color:color-mix(in oklab,var(--color-white) 6%,transparent)}}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-dream{--tw-gradient-from:var(--color-dream);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-synapse{--tw-gradient-to:var(--color-synapse);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-\[10vh\]{padding-top:10vh}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pl-14{padding-left:calc(var(--spacing) * 14)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#E4C8FF\]{color:#e4c8ff}.text-amber-400{color:var(--color-amber-400)}.text-bright{color:var(--color-bright)}.text-decay{color:var(--color-decay)}.text-decay\/60{color:#ef444499}@supports (color:color-mix(in lab,red,red)){.text-decay\/60{color:color-mix(in oklab,var(--color-decay) 60%,transparent)}}.text-dim{color:var(--color-dim)}.text-dream{color:var(--color-dream)}.text-dream-glow{color:var(--color-dream-glow)}.text-memory{color:var(--color-memory)}.text-muted{color:var(--color-muted)}.text-muted\/50{color:#4a4a7a80}@supports (color:color-mix(in lab,red,red)){.text-muted\/50{color:color-mix(in oklab,var(--color-muted) 50%,transparent)}}.text-purple-400{color:var(--color-purple-400)}.text-recall{color:var(--color-recall)}.text-subtle{color:var(--color-subtle)}.text-synapse{color:var(--color-synapse)}.text-synapse-glow{color:var(--color-synapse-glow)}.text-text{color:var(--color-text)}.text-warning{color:var(--color-warning)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.accent-synapse{accent-color:var(--color-synapse)}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-75{opacity:.75}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow\!{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(99\,102\,241\,0\.15\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#6366f126);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(163\,63\,255\,0\.15\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#a33fff26);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-synapse\/10{--tw-shadow-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.shadow-synapse\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-synapse) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-synapse\/20{--tw-shadow-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.shadow-synapse\/20{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-synapse) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-muted::placeholder{color:var(--color-muted)}@media(hover:hover){.hover\:bg-decay\/20:hover{background-color:#ef444433}@supports (color:color-mix(in lab,red,red)){.hover\:bg-decay\/20:hover{background-color:color-mix(in oklab,var(--color-decay) 20%,transparent)}}.hover\:bg-decay\/30:hover{background-color:#ef44444d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-decay\/30:hover{background-color:color-mix(in oklab,var(--color-decay) 30%,transparent)}}.hover\:bg-dream\/20:hover{background-color:#a855f733}@supports (color:color-mix(in lab,red,red)){.hover\:bg-dream\/20:hover{background-color:color-mix(in oklab,var(--color-dream) 20%,transparent)}}.hover\:bg-dream\/30:hover{background-color:#a855f74d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-dream\/30:hover{background-color:color-mix(in oklab,var(--color-dream) 30%,transparent)}}.hover\:bg-purple-500\/30:hover{background-color:#ac4bff4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-purple-500\/30:hover{background-color:color-mix(in oklab,var(--color-purple-500) 30%,transparent)}}.hover\:bg-recall\/30:hover{background-color:#10b9814d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-recall\/30:hover{background-color:color-mix(in oklab,var(--color-recall) 30%,transparent)}}.hover\:bg-synapse\/30:hover{background-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-synapse\/30:hover{background-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)}}.hover\:bg-warning\/30:hover{background-color:#f59e0b4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-warning\/30:hover{background-color:color-mix(in oklab,var(--color-warning) 30%,transparent)}}.hover\:bg-white\/\[0\.03\]:hover{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.03\]:hover{background-color:color-mix(in oklab,var(--color-white) 3%,transparent)}}.hover\:bg-white\/\[0\.04\]:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.04\]:hover{background-color:color-mix(in oklab,var(--color-white) 4%,transparent)}}.hover\:text-dim:hover{color:var(--color-dim)}.hover\:text-text:hover{color:var(--color-text)}}.focus\:\!border-synapse\/40:focus{border-color:#6366f166!important}@supports (color:color-mix(in lab,red,red)){.focus\:\!border-synapse\/40:focus{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)!important}}.focus\:border-dream\/40:focus{border-color:#a855f766}@supports (color:color-mix(in lab,red,red)){.focus\:border-dream\/40:focus{border-color:color-mix(in oklab,var(--color-dream) 40%,transparent)}}.focus\:border-synapse\/40:focus{border-color:#6366f166}@supports (color:color-mix(in lab,red,red)){.focus\:border-synapse\/40:focus{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-synapse\/20:focus{--tw-ring-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.focus\:ring-synapse\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-synapse) 20%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:flex-row{flex-direction:row}.md\:pt-\[15vh\]{padding-top:15vh}.md\:pb-0{padding-bottom:calc(var(--spacing) * 0)}}@media(min-width:64rem){.lg\:block{display:block}.lg\:w-56{width:calc(var(--spacing) * 56)}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}.\[\&\:\:-webkit-slider-thumb\]\:h-3::-webkit-slider-thumb{height:calc(var(--spacing) * 3)}.\[\&\:\:-webkit-slider-thumb\]\:w-3::-webkit-slider-thumb{width:calc(var(--spacing) * 3)}.\[\&\:\:-webkit-slider-thumb\]\:appearance-none::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none}.\[\&\:\:-webkit-slider-thumb\]\:rounded-full::-webkit-slider-thumb{border-radius:3.40282e38px}.\[\&\:\:-webkit-slider-thumb\]\:bg-synapse-glow::-webkit-slider-thumb{background-color:var(--color-synapse-glow)}.\[\&\:\:-webkit-slider-thumb\]\:shadow-\[0_0_8px_rgba\(129\,140\,248\,0\.4\)\]::-webkit-slider-thumb{--tw-shadow:0 0 8px var(--tw-shadow-color,#818cf866);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}html{background:var(--color-void);color:var(--color-text);font-family:var(--font-mono)}body{min-height:100vh;margin:0;overflow:hidden}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--color-subtle);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--color-muted)}.glass{-webkit-backdrop-filter:blur(20px)saturate(180%);background:#16163873;border:1px solid #6366f114;box-shadow:inset 0 1px #ffffff08,0 4px 24px #0000004d}.glass-subtle{-webkit-backdrop-filter:blur(12px)saturate(150%);background:#10102a66;border:1px solid #6366f10f;box-shadow:inset 0 1px #ffffff05,0 2px 12px #0003}.glass-sidebar{-webkit-backdrop-filter:blur(24px)saturate(180%);background:#0a0a1a99;border-right:1px solid #6366f11a;box-shadow:inset -1px 0 #ffffff05,4px 0 24px #0000004d}.glass-panel{-webkit-backdrop-filter:blur(24px)saturate(180%);background:#0a0a1acc;border:1px solid #6366f11a;box-shadow:inset 0 1px #ffffff08,0 8px 32px #0006}.glow-synapse{box-shadow:0 0 20px #6366f14d,0 0 60px #6366f11a}.glow-dream{box-shadow:0 0 20px #a855f74d,0 0 60px #a855f71a}.glow-memory{box-shadow:0 0 20px #3b82f64d,0 0 60px #3b82f61a}@keyframes pulse-glow{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse-glow{animation:2s ease-in-out infinite pulse-glow}@keyframes orb-float-1{0%,to{transform:translate(0)scale(1)}25%{transform:translate(60px,-40px)scale(1.1)}50%{transform:translate(-30px,-80px)scale(.95)}75%{transform:translate(-60px,-20px)scale(1.05)}}@keyframes orb-float-2{0%,to{transform:translate(0)scale(1)}25%{transform:translate(-50px,30px)scale(1.08)}50%{transform:translate(40px,60px)scale(.92)}75%{transform:translate(20px,-40px)scale(1.03)}}@keyframes orb-float-3{0%,to{transform:translate(0)scale(1)}25%{transform:translate(30px,50px)scale(1.05)}50%{transform:translate(-60px,20px)scale(.98)}75%{transform:translate(40px,-30px)scale(1.1)}}.ambient-orb{filter:blur(80px);pointer-events:none;z-index:0;opacity:.35;border-radius:50%;position:fixed}.ambient-orb-1{background:radial-gradient(circle,#a855f766,#0000 70%);width:400px;height:400px;animation:20s ease-in-out infinite orb-float-1;top:-10%;right:-5%}.ambient-orb-2{background:radial-gradient(circle,#6366f159,#0000 70%);width:350px;height:350px;animation:25s ease-in-out infinite orb-float-2;bottom:-15%;left:-5%}.ambient-orb-3{background:radial-gradient(circle,#f59e0b33,#0000 70%);width:300px;height:300px;animation:22s ease-in-out infinite orb-float-3;top:40%;left:40%}.nav-active-border{position:relative}.nav-active-border:before{content:"";background:linear-gradient(180deg,var(--color-synapse),var(--color-dream),var(--color-synapse));background-size:100% 200%;border-radius:1px;width:2px;animation:3s ease-in-out infinite gradient-shift;position:absolute;top:4px;bottom:4px;left:0}@keyframes gradient-shift{0%,to{background-position:0 0}50%{background-position:0 100%}}@keyframes float{0%,to{transform:translateY(0)translate(0)}25%{transform:translateY(-10px)translate(5px)}50%{transform:translateY(-5px)translate(-5px)}75%{transform:translateY(-15px)translate(3px)}}.retention-critical{color:var(--color-decay)}.retention-low{color:var(--color-warning)}.retention-good{color:var(--color-recall)}.retention-strong{color:var(--color-synapse)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}.safe-bottom.svelte-12qhfyh{padding-bottom:env(safe-area-inset-bottom,0px)}@keyframes svelte-12qhfyh-page-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-page-in.svelte-12qhfyh{animation:svelte-12qhfyh-page-in .2s ease-out} diff --git a/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css.br b/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css.br deleted file mode 100644 index dfa1ae4..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css.gz b/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css.gz deleted file mode 100644 index f2f1951..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/0.B_2UXNE9.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css b/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css new file mode 100644 index 0000000..b0b4d99 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css @@ -0,0 +1 @@ +/*! tailwindcss v4.2.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-gradient-position:initial;--tw-gradient-from:#0000;--tw-gradient-via:#0000;--tw-gradient-to:#0000;--tw-gradient-stops:initial;--tw-gradient-via-stops:initial;--tw-gradient-from-position:0%;--tw-gradient-via-position:50%;--tw-gradient-to-position:100%;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:"JetBrains Mono", "Fira Code", "SF Mono", monospace;--color-amber-400:oklch(82.8% .189 84.429);--color-purple-400:oklch(71.4% .203 305.504);--color-purple-500:oklch(62.7% .265 303.9);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-md:28rem;--container-lg:32rem;--container-xl:36rem;--container-2xl:42rem;--container-4xl:56rem;--container-5xl:64rem;--container-6xl:72rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height: 1.5 ;--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height: 1.2 ;--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-snug:1.375;--leading-relaxed:1.625;--radius-sm:.25rem;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-ping:ping 1s cubic-bezier(0, 0, .2, 1) infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--blur-md:12px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-void:#050510;--color-abyss:#0a0a1a;--color-deep:#10102a;--color-surface:#161638;--color-elevated:#1e1e4a;--color-subtle:#2a2a5e;--color-muted:#4a4a7a;--color-dim:#7a7aaa;--color-text:#e0e0ff;--color-bright:#fff;--color-synapse:#6366f1;--color-synapse-glow:#818cf8;--color-dream:#a855f7;--color-dream-glow:#c084fc;--color-memory:#3b82f6;--color-recall:#10b981;--color-decay:#ef4444;--color-warning:#f59e0b;--color-node-pattern:#ec4899}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.inset-x-0{inset-inline:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.end\!{inset-inline-end:var(--spacing)!important}.top-0{top:calc(var(--spacing) * 0)}.top-0\.5{top:calc(var(--spacing) * .5)}.top-1{top:calc(var(--spacing) * 1)}.top-3{top:calc(var(--spacing) * 3)}.top-4{top:calc(var(--spacing) * 4)}.top-10{top:calc(var(--spacing) * 10)}.right-0{right:calc(var(--spacing) * 0)}.right-4{right:calc(var(--spacing) * 4)}.bottom-0{bottom:calc(var(--spacing) * 0)}.bottom-4{bottom:calc(var(--spacing) * 4)}.-left-\[29px\]{left:-29px}.left-1\/2{left:50%}.left-4{left:calc(var(--spacing) * 4)}.left-6{left:calc(var(--spacing) * 6)}.isolate{isolation:isolate}.z-10{z-index:10}.z-20{z-index:20}.z-40{z-index:40}.z-50{z-index:50}.z-\[1\]{z-index:1}.container{width:100%}@media(min-width:40rem){.container{max-width:40rem}}@media(min-width:48rem){.container{max-width:48rem}}@media(min-width:64rem){.container{max-width:64rem}}@media(min-width:80rem){.container{max-width:80rem}}@media(min-width:96rem){.container{max-width:96rem}}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-\[-12px\]{margin-top:-12px}.mr-1{margin-right:calc(var(--spacing) * 1)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-0\.5{margin-bottom:calc(var(--spacing) * .5)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.ml-2{margin-left:calc(var(--spacing) * 2)}.ml-3{margin-left:calc(var(--spacing) * 3)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-3{-webkit-line-clamp:3;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-4{-webkit-line-clamp:4;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-flex{display:inline-flex}.table{display:table}.aspect-square{aspect-ratio:1}.h-0\.5{height:calc(var(--spacing) * .5)}.h-1{height:calc(var(--spacing) * 1)}.h-1\.5{height:calc(var(--spacing) * 1.5)}.h-2{height:calc(var(--spacing) * 2)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-9{height:calc(var(--spacing) * 9)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-20{height:calc(var(--spacing) * 20)}.h-24{height:calc(var(--spacing) * 24)}.h-28{height:calc(var(--spacing) * 28)}.h-32{height:calc(var(--spacing) * 32)}.h-40{height:calc(var(--spacing) * 40)}.h-\[520px\]{height:520px}.h-\[560px\]{height:560px}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-48{max-height:calc(var(--spacing) * 48)}.max-h-64{max-height:calc(var(--spacing) * 64)}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-96{max-height:calc(var(--spacing) * 96)}.max-h-\[620px\]{max-height:620px}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-40{min-height:calc(var(--spacing) * 40)}.min-h-\[240px\]{min-height:240px}.min-h-\[320px\]{min-height:320px}.min-h-\[520px\]{min-height:520px}.min-h-full{min-height:100%}.w-1\.5{width:calc(var(--spacing) * 1.5)}.w-2{width:calc(var(--spacing) * 2)}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-5{width:calc(var(--spacing) * 5)}.w-6{width:calc(var(--spacing) * 6)}.w-7{width:calc(var(--spacing) * 7)}.w-8{width:calc(var(--spacing) * 8)}.w-9{width:calc(var(--spacing) * 9)}.w-12{width:calc(var(--spacing) * 12)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-24{width:calc(var(--spacing) * 24)}.w-32{width:calc(var(--spacing) * 32)}.w-96{width:calc(var(--spacing) * 96)}.w-\[3px\]{width:3px}.w-\[90\%\]{width:90%}.w-full{width:100%}.w-px{width:1px}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-5xl{max-width:var(--container-5xl)}.max-w-6xl{max-width:var(--container-6xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-20{max-width:calc(var(--spacing) * 20)}.max-w-\[220px\]{max-width:220px}.max-w-lg{max-width:var(--container-lg)}.max-w-md{max-width:var(--container-md)}.max-w-xl{max-width:var(--container-xl)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-12{min-width:calc(var(--spacing) * 12)}.min-w-16{min-width:calc(var(--spacing) * 16)}.min-w-64{min-width:calc(var(--spacing) * 64)}.min-w-\[2rem\]{min-width:2rem}.min-w-\[3\.5rem\]{min-width:3.5rem}.flex-1{flex:1}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.border-separate{border-collapse:separate}.-translate-x-1\/2{--tw-translate-x: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-125{--tw-scale-x:125%;--tw-scale-y:125%;--tw-scale-z:125%;scale:var(--tw-scale-x) var(--tw-scale-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-\[fadeSlide_0\.35s_ease-out_both\]{animation:.35s ease-out both fadeSlide}.animate-ping{animation:var(--animate-ping)}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-default{cursor:default}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-none{resize:none}.resize-y{resize:vertical}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-around{justify-content:space-around}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0{gap:calc(var(--spacing) * 0)}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-\[2px\]{gap:2px}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-8>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 8) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 8) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:calc(var(--spacing) * 1)}.self-center{align-self:center}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-sm{border-radius:var(--radius-sm)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.\!border-decay\/20{border-color:#ef444433!important}@supports (color:color-mix(in lab,red,red)){.\!border-decay\/20{border-color:color-mix(in oklab,var(--color-decay) 20%,transparent)!important}}.\!border-decay\/30{border-color:#ef44444d!important}@supports (color:color-mix(in lab,red,red)){.\!border-decay\/30{border-color:color-mix(in oklab,var(--color-decay) 30%,transparent)!important}}.\!border-decay\/40{border-color:#ef444466!important}@supports (color:color-mix(in lab,red,red)){.\!border-decay\/40{border-color:color-mix(in oklab,var(--color-decay) 40%,transparent)!important}}.\!border-dream\/20{border-color:#a855f733!important}@supports (color:color-mix(in lab,red,red)){.\!border-dream\/20{border-color:color-mix(in oklab,var(--color-dream) 20%,transparent)!important}}.\!border-synapse\/15{border-color:#6366f126!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/15{border-color:color-mix(in oklab,var(--color-synapse) 15%,transparent)!important}}.\!border-synapse\/20{border-color:#6366f133!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/20{border-color:color-mix(in oklab,var(--color-synapse) 20%,transparent)!important}}.\!border-synapse\/25{border-color:#6366f140!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/25{border-color:color-mix(in oklab,var(--color-synapse) 25%,transparent)!important}}.\!border-synapse\/30{border-color:#6366f14d!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/30{border-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)!important}}.\!border-synapse\/40{border-color:#6366f166!important}@supports (color:color-mix(in lab,red,red)){.\!border-synapse\/40{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)!important}}.border-\[\#A33FFF\]\/40{border-color:#a33fff66}.border-decay\/20{border-color:#ef444433}@supports (color:color-mix(in lab,red,red)){.border-decay\/20{border-color:color-mix(in oklab,var(--color-decay) 20%,transparent)}}.border-dream-glow\/40{border-color:#c084fc66}@supports (color:color-mix(in lab,red,red)){.border-dream-glow\/40{border-color:color-mix(in oklab,var(--color-dream-glow) 40%,transparent)}}.border-dream\/10{border-color:#a855f71a}@supports (color:color-mix(in lab,red,red)){.border-dream\/10{border-color:color-mix(in oklab,var(--color-dream) 10%,transparent)}}.border-dream\/20{border-color:#a855f733}@supports (color:color-mix(in lab,red,red)){.border-dream\/20{border-color:color-mix(in oklab,var(--color-dream) 20%,transparent)}}.border-dream\/30{border-color:#a855f74d}@supports (color:color-mix(in lab,red,red)){.border-dream\/30{border-color:color-mix(in oklab,var(--color-dream) 30%,transparent)}}.border-dream\/40{border-color:#a855f766}@supports (color:color-mix(in lab,red,red)){.border-dream\/40{border-color:color-mix(in oklab,var(--color-dream) 40%,transparent)}}.border-dream\/50{border-color:#a855f780}@supports (color:color-mix(in lab,red,red)){.border-dream\/50{border-color:color-mix(in oklab,var(--color-dream) 50%,transparent)}}.border-recall\/30{border-color:#10b9814d}@supports (color:color-mix(in lab,red,red)){.border-recall\/30{border-color:color-mix(in oklab,var(--color-recall) 30%,transparent)}}.border-recall\/40{border-color:#10b98166}@supports (color:color-mix(in lab,red,red)){.border-recall\/40{border-color:color-mix(in oklab,var(--color-recall) 40%,transparent)}}.border-subtle\/15{border-color:#2a2a5e26}@supports (color:color-mix(in lab,red,red)){.border-subtle\/15{border-color:color-mix(in oklab,var(--color-subtle) 15%,transparent)}}.border-subtle\/20{border-color:#2a2a5e33}@supports (color:color-mix(in lab,red,red)){.border-subtle\/20{border-color:color-mix(in oklab,var(--color-subtle) 20%,transparent)}}.border-subtle\/30{border-color:#2a2a5e4d}@supports (color:color-mix(in lab,red,red)){.border-subtle\/30{border-color:color-mix(in oklab,var(--color-subtle) 30%,transparent)}}.border-synapse{border-color:var(--color-synapse)}.border-synapse\/5{border-color:#6366f10d}@supports (color:color-mix(in lab,red,red)){.border-synapse\/5{border-color:color-mix(in oklab,var(--color-synapse) 5%,transparent)}}.border-synapse\/10{border-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.border-synapse\/10{border-color:color-mix(in oklab,var(--color-synapse) 10%,transparent)}}.border-synapse\/15{border-color:#6366f126}@supports (color:color-mix(in lab,red,red)){.border-synapse\/15{border-color:color-mix(in oklab,var(--color-synapse) 15%,transparent)}}.border-synapse\/20{border-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.border-synapse\/20{border-color:color-mix(in oklab,var(--color-synapse) 20%,transparent)}}.border-synapse\/30{border-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.border-synapse\/30{border-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)}}.border-synapse\/40{border-color:#6366f166}@supports (color:color-mix(in lab,red,red)){.border-synapse\/40{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)}}.border-transparent{border-color:#0000}.border-warning\/30{border-color:#f59e0b4d}@supports (color:color-mix(in lab,red,red)){.border-warning\/30{border-color:color-mix(in oklab,var(--color-warning) 30%,transparent)}}.border-warning\/40{border-color:#f59e0b66}@supports (color:color-mix(in lab,red,red)){.border-warning\/40{border-color:color-mix(in oklab,var(--color-warning) 40%,transparent)}}.border-warning\/50{border-color:#f59e0b80}@supports (color:color-mix(in lab,red,red)){.border-warning\/50{border-color:color-mix(in oklab,var(--color-warning) 50%,transparent)}}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white) 5%,transparent)}}.border-t-dream{border-top-color:var(--color-dream)}.border-t-synapse{border-top-color:var(--color-synapse)}.border-t-warning{border-top-color:var(--color-warning)}.bg-\[\#A33FFF\]{background-color:#a33fff}.bg-\[\#A33FFF\]\/10{background-color:#a33fff1a}.bg-amber-400{background-color:var(--color-amber-400)}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black) 40%,transparent)}}.bg-decay{background-color:var(--color-decay)}.bg-decay\/10{background-color:#ef44441a}@supports (color:color-mix(in lab,red,red)){.bg-decay\/10{background-color:color-mix(in oklab,var(--color-decay) 10%,transparent)}}.bg-decay\/20{background-color:#ef444433}@supports (color:color-mix(in lab,red,red)){.bg-decay\/20{background-color:color-mix(in oklab,var(--color-decay) 20%,transparent)}}.bg-decay\/\[0\.05\]{background-color:#ef44440d}@supports (color:color-mix(in lab,red,red)){.bg-decay\/\[0\.05\]{background-color:color-mix(in oklab,var(--color-decay) 5%,transparent)}}.bg-deep{background-color:var(--color-deep)}.bg-deep\/40{background-color:#10102a66}@supports (color:color-mix(in lab,red,red)){.bg-deep\/40{background-color:color-mix(in oklab,var(--color-deep) 40%,transparent)}}.bg-deep\/60{background-color:#10102a99}@supports (color:color-mix(in lab,red,red)){.bg-deep\/60{background-color:color-mix(in oklab,var(--color-deep) 60%,transparent)}}.bg-dream{background-color:var(--color-dream)}.bg-dream\/5{background-color:#a855f70d}@supports (color:color-mix(in lab,red,red)){.bg-dream\/5{background-color:color-mix(in oklab,var(--color-dream) 5%,transparent)}}.bg-dream\/10{background-color:#a855f71a}@supports (color:color-mix(in lab,red,red)){.bg-dream\/10{background-color:color-mix(in oklab,var(--color-dream) 10%,transparent)}}.bg-dream\/15{background-color:#a855f726}@supports (color:color-mix(in lab,red,red)){.bg-dream\/15{background-color:color-mix(in oklab,var(--color-dream) 15%,transparent)}}.bg-dream\/20{background-color:#a855f733}@supports (color:color-mix(in lab,red,red)){.bg-dream\/20{background-color:color-mix(in oklab,var(--color-dream) 20%,transparent)}}.bg-muted{background-color:var(--color-muted)}.bg-node-pattern{background-color:var(--color-node-pattern)}.bg-purple-500\/20{background-color:#ac4bff33}@supports (color:color-mix(in lab,red,red)){.bg-purple-500\/20{background-color:color-mix(in oklab,var(--color-purple-500) 20%,transparent)}}.bg-recall{background-color:var(--color-recall)}.bg-recall\/10{background-color:#10b9811a}@supports (color:color-mix(in lab,red,red)){.bg-recall\/10{background-color:color-mix(in oklab,var(--color-recall) 10%,transparent)}}.bg-recall\/15{background-color:#10b98126}@supports (color:color-mix(in lab,red,red)){.bg-recall\/15{background-color:color-mix(in oklab,var(--color-recall) 15%,transparent)}}.bg-recall\/20{background-color:#10b98133}@supports (color:color-mix(in lab,red,red)){.bg-recall\/20{background-color:color-mix(in oklab,var(--color-recall) 20%,transparent)}}.bg-synapse{background-color:var(--color-synapse)}.bg-synapse-glow{background-color:var(--color-synapse-glow)}.bg-synapse\/10{background-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/10{background-color:color-mix(in oklab,var(--color-synapse) 10%,transparent)}}.bg-synapse\/15{background-color:#6366f126}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/15{background-color:color-mix(in oklab,var(--color-synapse) 15%,transparent)}}.bg-synapse\/20{background-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/20{background-color:color-mix(in oklab,var(--color-synapse) 20%,transparent)}}.bg-synapse\/25{background-color:#6366f140}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/25{background-color:color-mix(in oklab,var(--color-synapse) 25%,transparent)}}.bg-synapse\/70{background-color:#6366f1b3}@supports (color:color-mix(in lab,red,red)){.bg-synapse\/70{background-color:color-mix(in oklab,var(--color-synapse) 70%,transparent)}}.bg-transparent{background-color:#0000}.bg-void{background-color:var(--color-void)}.bg-void\/60{background-color:#05051099}@supports (color:color-mix(in lab,red,red)){.bg-void\/60{background-color:color-mix(in oklab,var(--color-void) 60%,transparent)}}.bg-warning{background-color:var(--color-warning)}.bg-warning\/5{background-color:#f59e0b0d}@supports (color:color-mix(in lab,red,red)){.bg-warning\/5{background-color:color-mix(in oklab,var(--color-warning) 5%,transparent)}}.bg-warning\/20{background-color:#f59e0b33}@supports (color:color-mix(in lab,red,red)){.bg-warning\/20{background-color:color-mix(in oklab,var(--color-warning) 20%,transparent)}}.bg-white\/\[0\.02\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.02\]{background-color:color-mix(in oklab,var(--color-white) 2%,transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab,var(--color-white) 3%,transparent)}}.bg-white\/\[0\.04\]{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.04\]{background-color:color-mix(in oklab,var(--color-white) 4%,transparent)}}.bg-white\/\[0\.06\]{background-color:#ffffff0f}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.06\]{background-color:color-mix(in oklab,var(--color-white) 6%,transparent)}}.bg-gradient-to-br{--tw-gradient-position:to bottom right in oklab;background-image:linear-gradient(var(--tw-gradient-stops))}.from-dream{--tw-gradient-from:var(--color-dream);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.to-synapse{--tw-gradient-to:var(--color-synapse);--tw-gradient-stops:var(--tw-gradient-via-stops,var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))}.p-0{padding:calc(var(--spacing) * 0)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.p-10{padding:calc(var(--spacing) * 10)}.p-12{padding:calc(var(--spacing) * 12)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-5{padding-block:calc(var(--spacing) * 5)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-8{padding-block:calc(var(--spacing) * 8)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-12{padding-block:calc(var(--spacing) * 12)}.py-20{padding-block:calc(var(--spacing) * 20)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pt-8{padding-top:calc(var(--spacing) * 8)}.pt-\[10vh\]{padding-top:10vh}.pr-1{padding-right:calc(var(--spacing) * 1)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-16{padding-bottom:calc(var(--spacing) * 16)}.pl-6{padding-left:calc(var(--spacing) * 6)}.pl-14{padding-left:calc(var(--spacing) * 14)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.align-bottom{vertical-align:bottom}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[8px\]{font-size:8px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.leading-snug{--tw-leading:var(--leading-snug);line-height:var(--leading-snug)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-\[0\.15em\]{--tw-tracking:.15em;letter-spacing:.15em}.tracking-\[0\.18em\]{--tw-tracking:.18em;letter-spacing:.18em}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[\#E4C8FF\]{color:#e4c8ff}.text-amber-400{color:var(--color-amber-400)}.text-bright{color:var(--color-bright)}.text-decay{color:var(--color-decay)}.text-decay\/60{color:#ef444499}@supports (color:color-mix(in lab,red,red)){.text-decay\/60{color:color-mix(in oklab,var(--color-decay) 60%,transparent)}}.text-dim{color:var(--color-dim)}.text-dream{color:var(--color-dream)}.text-dream-glow{color:var(--color-dream-glow)}.text-dream\/40{color:#a855f766}@supports (color:color-mix(in lab,red,red)){.text-dream\/40{color:color-mix(in oklab,var(--color-dream) 40%,transparent)}}.text-dream\/80{color:#a855f7cc}@supports (color:color-mix(in lab,red,red)){.text-dream\/80{color:color-mix(in oklab,var(--color-dream) 80%,transparent)}}.text-memory{color:var(--color-memory)}.text-muted{color:var(--color-muted)}.text-muted\/50{color:#4a4a7a80}@supports (color:color-mix(in lab,red,red)){.text-muted\/50{color:color-mix(in oklab,var(--color-muted) 50%,transparent)}}.text-muted\/60{color:#4a4a7a99}@supports (color:color-mix(in lab,red,red)){.text-muted\/60{color:color-mix(in oklab,var(--color-muted) 60%,transparent)}}.text-node-pattern{color:var(--color-node-pattern)}.text-purple-400{color:var(--color-purple-400)}.text-recall{color:var(--color-recall)}.text-subtle{color:var(--color-subtle)}.text-synapse{color:var(--color-synapse)}.text-synapse-glow{color:var(--color-synapse-glow)}.text-text{color:var(--color-text)}.text-text\/80{color:#e0e0ffcc}@supports (color:color-mix(in lab,red,red)){.text-text\/80{color:color-mix(in oklab,var(--color-text) 80%,transparent)}}.text-warning{color:var(--color-warning)}.capitalize{text-transform:capitalize}.uppercase{text-transform:uppercase}.italic{font-style:italic}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline-offset-4{text-underline-offset:4px}.accent-synapse{accent-color:var(--color-synapse)}.accent-synapse-glow{accent-color:var(--color-synapse-glow)}.opacity-20{opacity:.2}.opacity-30{opacity:.3}.opacity-35{opacity:.35}.opacity-40{opacity:.4}.opacity-60{opacity:.6}.opacity-75{opacity:.75}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow\!{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_10px_rgba\(239\,68\,68\,0\.7\)\]{--tw-shadow:0 0 10px var(--tw-shadow-color,#ef4444b3);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(99\,102\,241\,0\.15\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#6366f126);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(99\,102\,241\,0\.18\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#6366f12e);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_12px_rgba\(163\,63\,255\,0\.15\)\]{--tw-shadow:0 0 12px var(--tw-shadow-color,#a33fff26);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_16px_rgba\(99\,102\,241\,0\.3\)\]{--tw-shadow:0 0 16px var(--tw-shadow-color,#6366f14d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_16px_rgba\(168\,85\,247\,0\.3\)\]{--tw-shadow:0 0 16px var(--tw-shadow-color,#a855f74d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring,.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-synapse\/10{--tw-shadow-color:#6366f11a}@supports (color:color-mix(in lab,red,red)){.shadow-synapse\/10{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-synapse) 10%, transparent) var(--tw-shadow-alpha), transparent)}}.shadow-synapse\/20{--tw-shadow-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.shadow-synapse\/20{--tw-shadow-color:color-mix(in oklab, color-mix(in oklab, var(--color-synapse) 20%, transparent) var(--tw-shadow-alpha), transparent)}}.ring-dream-glow{--tw-ring-color:var(--color-dream-glow)}.ring-dream\/60{--tw-ring-color:#a855f799}@supports (color:color-mix(in lab,red,red)){.ring-dream\/60{--tw-ring-color:color-mix(in oklab, var(--color-dream) 60%, transparent)}}.ring-recall\/30{--tw-ring-color:#10b9814d}@supports (color:color-mix(in lab,red,red)){.ring-recall\/30{--tw-ring-color:color-mix(in oklab, var(--color-recall) 30%, transparent)}}.ring-synapse\/60{--tw-ring-color:#6366f199}@supports (color:color-mix(in lab,red,red)){.ring-synapse\/60{--tw-ring-color:color-mix(in oklab, var(--color-synapse) 60%, transparent)}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-md{--tw-backdrop-blur:blur(var(--blur-md));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.duration-700{--tw-duration:.7s;transition-duration:.7s}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.select-none{-webkit-user-select:none;user-select:none}.placeholder\:text-muted::placeholder{color:var(--color-muted)}@media(hover:hover){.hover\:z-10:hover{z-index:10}.hover\:scale-110:hover{--tw-scale-x:110%;--tw-scale-y:110%;--tw-scale-z:110%;scale:var(--tw-scale-x) var(--tw-scale-y)}.hover\:scale-\[1\.03\]:hover{scale:1.03}.hover\:\!border-synapse\/30:hover{border-color:#6366f14d!important}@supports (color:color-mix(in lab,red,red)){.hover\:\!border-synapse\/30:hover{border-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)!important}}.hover\:border-synapse\/20:hover{border-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.hover\:border-synapse\/20:hover{border-color:color-mix(in oklab,var(--color-synapse) 20%,transparent)}}.hover\:border-synapse\/30:hover{border-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.hover\:border-synapse\/30:hover{border-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)}}.hover\:border-synapse\/50:hover{border-color:#6366f180}@supports (color:color-mix(in lab,red,red)){.hover\:border-synapse\/50:hover{border-color:color-mix(in oklab,var(--color-synapse) 50%,transparent)}}.hover\:bg-decay\/20:hover{background-color:#ef444433}@supports (color:color-mix(in lab,red,red)){.hover\:bg-decay\/20:hover{background-color:color-mix(in oklab,var(--color-decay) 20%,transparent)}}.hover\:bg-decay\/30:hover{background-color:#ef44444d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-decay\/30:hover{background-color:color-mix(in oklab,var(--color-decay) 30%,transparent)}}.hover\:bg-dream\/20:hover{background-color:#a855f733}@supports (color:color-mix(in lab,red,red)){.hover\:bg-dream\/20:hover{background-color:color-mix(in oklab,var(--color-dream) 20%,transparent)}}.hover\:bg-dream\/30:hover{background-color:#a855f74d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-dream\/30:hover{background-color:color-mix(in oklab,var(--color-dream) 30%,transparent)}}.hover\:bg-purple-500\/30:hover{background-color:#ac4bff4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-purple-500\/30:hover{background-color:color-mix(in oklab,var(--color-purple-500) 30%,transparent)}}.hover\:bg-recall\/30:hover{background-color:#10b9814d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-recall\/30:hover{background-color:color-mix(in oklab,var(--color-recall) 30%,transparent)}}.hover\:bg-synapse\/30:hover{background-color:#6366f14d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-synapse\/30:hover{background-color:color-mix(in oklab,var(--color-synapse) 30%,transparent)}}.hover\:bg-warning\/30:hover{background-color:#f59e0b4d}@supports (color:color-mix(in lab,red,red)){.hover\:bg-warning\/30:hover{background-color:color-mix(in oklab,var(--color-warning) 30%,transparent)}}.hover\:bg-white\/\[0\.02\]:hover{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.02\]:hover{background-color:color-mix(in oklab,var(--color-white) 2%,transparent)}}.hover\:bg-white\/\[0\.03\]:hover{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.03\]:hover{background-color:color-mix(in oklab,var(--color-white) 3%,transparent)}}.hover\:bg-white\/\[0\.04\]:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.04\]:hover{background-color:color-mix(in oklab,var(--color-white) 4%,transparent)}}.hover\:bg-white\/\[0\.08\]:hover{background-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.hover\:bg-white\/\[0\.08\]:hover{background-color:color-mix(in oklab,var(--color-white) 8%,transparent)}}.hover\:text-bright:hover{color:var(--color-bright)}.hover\:text-dim:hover{color:var(--color-dim)}.hover\:text-synapse-glow:hover{color:var(--color-synapse-glow)}.hover\:text-text:hover{color:var(--color-text)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:\!border-synapse\/40:focus{border-color:#6366f166!important}@supports (color:color-mix(in lab,red,red)){.focus\:\!border-synapse\/40:focus{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)!important}}.focus\:border-dream\/40:focus{border-color:#a855f766}@supports (color:color-mix(in lab,red,red)){.focus\:border-dream\/40:focus{border-color:color-mix(in oklab,var(--color-dream) 40%,transparent)}}.focus\:border-synapse\/40:focus{border-color:#6366f166}@supports (color:color-mix(in lab,red,red)){.focus\:border-synapse\/40:focus{border-color:color-mix(in oklab,var(--color-synapse) 40%,transparent)}}.focus\:ring-1:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-2:focus{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus\:ring-synapse-glow:focus{--tw-ring-color:var(--color-synapse-glow)}.focus\:ring-synapse\/20:focus{--tw-ring-color:#6366f133}@supports (color:color-mix(in lab,red,red)){.focus\:ring-synapse\/20:focus{--tw-ring-color:color-mix(in oklab, var(--color-synapse) 20%, transparent)}}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.focus-visible\:ring-2:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.focus-visible\:ring-dream-glow\/60:focus-visible{--tw-ring-color:#c084fc99}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-dream-glow\/60:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-dream-glow) 60%, transparent)}}.focus-visible\:ring-recall\/60:focus-visible{--tw-ring-color:#10b98199}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-recall\/60:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-recall) 60%, transparent)}}.focus-visible\:ring-synapse\/60:focus-visible{--tw-ring-color:#6366f199}@supports (color:color-mix(in lab,red,red)){.focus-visible\:ring-synapse\/60:focus-visible{--tw-ring-color:color-mix(in oklab, var(--color-synapse) 60%, transparent)}}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:40rem){.sm\:block{display:block}.sm\:inline-flex{display:inline-flex}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}}@media(min-width:48rem){.md\:block{display:block}.md\:flex{display:flex}.md\:hidden{display:none}.md\:inline-flex{display:inline-flex}.md\:min-w-\[340px\]{min-width:340px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.md\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.md\:grid-cols-\[1fr_auto\]{grid-template-columns:1fr auto}.md\:grid-cols-\[280px_1fr\]{grid-template-columns:280px 1fr}.md\:flex-row{flex-direction:row}.md\:pt-\[15vh\]{padding-top:15vh}.md\:pb-0{padding-bottom:calc(var(--spacing) * 0)}}@media(min-width:64rem){.lg\:block{display:block}.lg\:w-56{width:calc(var(--spacing) * 56)}.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:grid-cols-\[1fr_280px\]{grid-template-columns:1fr 280px}.lg\:grid-cols-\[1fr_340px\]{grid-template-columns:1fr 340px}.lg\:grid-cols-\[1fr_360px\]{grid-template-columns:1fr 360px}.lg\:grid-cols-\[minmax\(0\,1fr\)_340px\]{grid-template-columns:minmax(0,1fr) 340px}}.\[\&\:\:-webkit-slider-thumb\]\:h-3::-webkit-slider-thumb{height:calc(var(--spacing) * 3)}.\[\&\:\:-webkit-slider-thumb\]\:w-3::-webkit-slider-thumb{width:calc(var(--spacing) * 3)}.\[\&\:\:-webkit-slider-thumb\]\:appearance-none::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none}.\[\&\:\:-webkit-slider-thumb\]\:rounded-full::-webkit-slider-thumb{border-radius:3.40282e38px}.\[\&\:\:-webkit-slider-thumb\]\:bg-synapse-glow::-webkit-slider-thumb{background-color:var(--color-synapse-glow)}.\[\&\:\:-webkit-slider-thumb\]\:shadow-\[0_0_8px_rgba\(129\,140\,248\,0\.4\)\]::-webkit-slider-thumb{--tw-shadow:0 0 8px var(--tw-shadow-color,#818cf866);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}html{background:var(--color-void);color:var(--color-text);font-family:var(--font-mono)}body{min-height:100vh;margin:0;overflow:hidden}::-webkit-scrollbar{width:6px;height:6px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--color-subtle);border-radius:3px}::-webkit-scrollbar-thumb:hover{background:var(--color-muted)}.glass{-webkit-backdrop-filter:blur(20px)saturate(180%);background:#16163873;border:1px solid #6366f114;box-shadow:inset 0 1px #ffffff08,0 4px 24px #0000004d}.glass-subtle{-webkit-backdrop-filter:blur(12px)saturate(150%);background:#10102a66;border:1px solid #6366f10f;box-shadow:inset 0 1px #ffffff05,0 2px 12px #0003}.glass-sidebar{-webkit-backdrop-filter:blur(24px)saturate(180%);background:#0a0a1a99;border-right:1px solid #6366f11a;box-shadow:inset -1px 0 #ffffff05,4px 0 24px #0000004d}.glass-panel{-webkit-backdrop-filter:blur(24px)saturate(180%);background:#0a0a1acc;border:1px solid #6366f11a;box-shadow:inset 0 1px #ffffff08,0 8px 32px #0006}.glow-synapse{box-shadow:0 0 20px #6366f14d,0 0 60px #6366f11a}.glow-dream{box-shadow:0 0 20px #a855f74d,0 0 60px #a855f71a}.glow-memory{box-shadow:0 0 20px #3b82f64d,0 0 60px #3b82f61a}@keyframes pulse-glow{0%,to{opacity:1}50%{opacity:.5}}.animate-pulse-glow{animation:2s ease-in-out infinite pulse-glow}@keyframes orb-float-1{0%,to{transform:translate(0)scale(1)}25%{transform:translate(60px,-40px)scale(1.1)}50%{transform:translate(-30px,-80px)scale(.95)}75%{transform:translate(-60px,-20px)scale(1.05)}}@keyframes orb-float-2{0%,to{transform:translate(0)scale(1)}25%{transform:translate(-50px,30px)scale(1.08)}50%{transform:translate(40px,60px)scale(.92)}75%{transform:translate(20px,-40px)scale(1.03)}}@keyframes orb-float-3{0%,to{transform:translate(0)scale(1)}25%{transform:translate(30px,50px)scale(1.05)}50%{transform:translate(-60px,20px)scale(.98)}75%{transform:translate(40px,-30px)scale(1.1)}}.ambient-orb{filter:blur(80px);pointer-events:none;z-index:0;opacity:.35;border-radius:50%;position:fixed}.ambient-orb-1{background:radial-gradient(circle,#a855f766,#0000 70%);width:400px;height:400px;animation:20s ease-in-out infinite orb-float-1;top:-10%;right:-5%}.ambient-orb-2{background:radial-gradient(circle,#6366f159,#0000 70%);width:350px;height:350px;animation:25s ease-in-out infinite orb-float-2;bottom:-15%;left:-5%}.ambient-orb-3{background:radial-gradient(circle,#f59e0b33,#0000 70%);width:300px;height:300px;animation:22s ease-in-out infinite orb-float-3;top:40%;left:40%}.nav-active-border{position:relative}.nav-active-border:before{content:"";background:linear-gradient(180deg,var(--color-synapse),var(--color-dream),var(--color-synapse));background-size:100% 200%;border-radius:1px;width:2px;animation:3s ease-in-out infinite gradient-shift;position:absolute;top:4px;bottom:4px;left:0}@keyframes gradient-shift{0%,to{background-position:0 0}50%{background-position:0 100%}}@keyframes float{0%,to{transform:translateY(0)translate(0)}25%{transform:translateY(-10px)translate(5px)}50%{transform:translateY(-5px)translate(-5px)}75%{transform:translateY(-15px)translate(3px)}}.retention-critical{color:var(--color-decay)}.retention-low{color:var(--color-warning)}.retention-good{color:var(--color-recall)}.retention-strong{color:var(--color-synapse)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-gradient-position{syntax:"*";inherits:false}@property --tw-gradient-from{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-via{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-to{syntax:"";inherits:false;initial-value:#0000}@property --tw-gradient-stops{syntax:"*";inherits:false}@property --tw-gradient-via-stops{syntax:"*";inherits:false}@property --tw-gradient-from-position{syntax:"";inherits:false;initial-value:0%}@property --tw-gradient-via-position{syntax:"";inherits:false;initial-value:50%}@property --tw-gradient-to-position{syntax:"";inherits:false;initial-value:100%}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}@keyframes ping{75%,to{opacity:0;transform:scale(2)}}@keyframes pulse{50%{opacity:.5}}.toast-layer.svelte-pry2ep{position:fixed;z-index:60;pointer-events:none;display:flex;flex-direction:column;gap:.5rem;right:1.25rem;bottom:1.25rem;max-width:22rem;width:calc(100vw - 2.5rem)}@media(max-width:768px){.toast-layer.svelte-pry2ep{right:.75rem;left:.75rem;bottom:auto;top:5.25rem;max-width:none;width:auto;align-items:stretch}}.toast-item.svelte-pry2ep{pointer-events:auto;position:relative;display:flex;gap:.75rem;align-items:stretch;text-align:left;font:inherit;color:inherit;background:#0c0e16b8;backdrop-filter:blur(14px) saturate(160%);-webkit-backdrop-filter:blur(14px) saturate(160%);border:1px solid rgba(255,255,255,.06);border-radius:.75rem;padding:.75rem .9rem .75rem .5rem;overflow:hidden;box-shadow:0 10px 40px -12px #000c,0 0 22px -6px var(--toast-color);cursor:pointer;animation:svelte-pry2ep-toast-in .32s cubic-bezier(.16,1,.3,1);transform-origin:right center;transition:transform .15s ease,box-shadow .15s ease}.toast-item.svelte-pry2ep:hover{transform:translateY(-1px) scale(1.015);box-shadow:0 14px 48px -12px #000000d9,0 0 32px -4px var(--toast-color)}.toast-item.svelte-pry2ep:focus-visible{outline:1px solid var(--toast-color);outline-offset:2px}.toast-accent.svelte-pry2ep{width:3px;border-radius:2px;background:var(--toast-color);box-shadow:0 0 10px var(--toast-color);flex-shrink:0}.toast-body.svelte-pry2ep{display:flex;flex-direction:column;gap:.15rem;flex:1;min-width:0}.toast-head.svelte-pry2ep{display:flex;align-items:center;gap:.5rem}.toast-icon.svelte-pry2ep{color:var(--toast-color);font-size:.95rem;text-shadow:0 0 8px var(--toast-color);line-height:1;width:1rem;display:inline-flex;justify-content:center}.toast-title.svelte-pry2ep{color:#f5f5fa;font-size:.82rem;font-weight:600;letter-spacing:.01em}.toast-sub.svelte-pry2ep{color:#b0b6c4;font-size:.74rem;line-height:1.35;padding-left:1.5rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.toast-progress.svelte-pry2ep{position:absolute;left:0;bottom:0;height:2px;width:100%;background:#ffffff0a}.toast-progress-fill.svelte-pry2ep{height:100%;background:var(--toast-color);opacity:.55;transform-origin:left center;animation:svelte-pry2ep-toast-progress var(--toast-dwell) linear forwards}.toast-item.svelte-pry2ep:hover .toast-progress-fill:where(.svelte-pry2ep),.toast-item.svelte-pry2ep:focus-visible .toast-progress-fill:where(.svelte-pry2ep){animation-play-state:paused}@keyframes svelte-pry2ep-toast-in{0%{opacity:0;transform:translate(24px) scale(.98)}to{opacity:1;transform:translate(0) scale(1)}}@media(max-width:768px){.toast-item.svelte-pry2ep{transform-origin:top center;animation:svelte-pry2ep-toast-in-mobile .3s cubic-bezier(.16,1,.3,1)}}@keyframes svelte-pry2ep-toast-in-mobile{0%{opacity:0;transform:translateY(-12px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes svelte-pry2ep-toast-progress{0%{transform:scaleX(1)}to{transform:scaleX(0)}}@media(prefers-reduced-motion:reduce){.toast-item.svelte-pry2ep{animation:none}.toast-progress-fill.svelte-pry2ep{animation:none;transform:scaleX(.5)}}.strip-item.svelte-1kk3799{display:inline-flex;align-items:center;gap:.4rem;padding:0 .75rem;white-space:nowrap;flex-shrink:0}.strip-divider.svelte-1kk3799{width:1px;height:14px;background:#6366f11f;flex-shrink:0}.ambient-strip.ambient-flash.svelte-1kk3799{background:linear-gradient(90deg,#ef444414,#ef444400 70%),#0006;border-bottom-color:#ef444459;transition:background .3s ease,border-color .3s ease}@keyframes svelte-1kk3799-ping-slow{0%{transform:scale(1);opacity:.8}80%,to{transform:scale(2);opacity:0}}.animate-ping-slow{animation:svelte-1kk3799-ping-slow 2.2s cubic-bezier(0,0,.2,1) infinite}@media(prefers-reduced-motion:reduce){.ambient-strip.svelte-1kk3799 .animate-ping,.ambient-strip.svelte-1kk3799 .animate-ping-slow,.ambient-strip.svelte-1kk3799 .animate-pulse{animation:none!important}}.verdict-bar.svelte-1j425e6{border-bottom:1px solid rgba(255,255,255,.06);background:#080912b8;backdrop-filter:blur(18px) saturate(160%);-webkit-backdrop-filter:blur(18px) saturate(160%);box-shadow:0 6px 24px #0000002e}.verdict-summary.svelte-1j425e6{width:100%;min-height:2.75rem;display:grid;grid-template-columns:auto auto minmax(0,1fr) auto;align-items:center;gap:.75rem;padding:.55rem 1rem;color:var(--color-text);text-align:left;font:inherit}.sr-only.svelte-1j425e6{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}.label.svelte-1j425e6,.field-label.svelte-1j425e6,.appeal-row.svelte-1j425e6>span:where(.svelte-1j425e6){color:var(--color-dim);font-size:.68rem;text-transform:uppercase;letter-spacing:0}.levels.svelte-1j425e6{display:flex;align-items:center;gap:.25rem}.levels.svelte-1j425e6 span:where(.svelte-1j425e6){border:1px solid rgba(255,255,255,.08);border-radius:.35rem;padding:.18rem .38rem;color:var(--color-muted);font-size:.64rem;line-height:1}.levels.svelte-1j425e6 span.active:where(.svelte-1j425e6){color:var(--color-bright);border-color:var(--verdict-color);background:color-mix(in srgb,var(--verdict-color) 18%,transparent);box-shadow:0 0 14px color-mix(in srgb,var(--verdict-color) 28%,transparent)}.summary-text.svelte-1j425e6{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.78rem;color:var(--color-dim)}.when.svelte-1j425e6{color:var(--color-muted);font-size:.68rem}.receipt.svelte-1j425e6{margin:0 1rem .75rem;border:1px solid rgba(255,255,255,.08);border-radius:.5rem;background:#0a0a1ac7;padding:.85rem}.receipt-grid.svelte-1j425e6{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(10rem,.8fr) minmax(0,1.1fr) minmax(0,1.1fr);gap:.85rem}.receipt.svelte-1j425e6 p:where(.svelte-1j425e6),.receipt.svelte-1j425e6 li:where(.svelte-1j425e6){margin:.25rem 0 0;color:var(--color-text);font-size:.76rem;line-height:1.45;overflow-wrap:anywhere}.receipt.svelte-1j425e6 ul:where(.svelte-1j425e6){margin:.25rem 0 0;padding-left:1rem}.appeal-row.svelte-1j425e6{display:flex;align-items:center;gap:.4rem;margin-top:.85rem;flex-wrap:wrap}.appeal-row.svelte-1j425e6 button:where(.svelte-1j425e6),.appeal-row.svelte-1j425e6 p:where(.svelte-1j425e6){border:1px solid rgba(255,255,255,.1);border-radius:.4rem;padding:.35rem .6rem;color:var(--color-text);background:#ffffff0a;font-size:.72rem;margin:0}.appeal-row.svelte-1j425e6 button:where(.svelte-1j425e6):hover:not(:disabled),.verdict-summary.svelte-1j425e6:hover{background:#ffffff0d}.appeal-row.svelte-1j425e6 button:where(.svelte-1j425e6):disabled{opacity:.55;cursor:wait}.tone-pass.svelte-1j425e6,.tone-note.svelte-1j425e6{--verdict-color: #10b981}.tone-caution.svelte-1j425e6{--verdict-color: #f59e0b}.tone-veto.svelte-1j425e6{--verdict-color: #ef4444}.tone-appealed.svelte-1j425e6{--verdict-color: #818cf8}@media(max-width:900px){.verdict-summary.svelte-1j425e6{grid-template-columns:auto minmax(0,1fr) auto}.levels.svelte-1j425e6{grid-column:1 / -1;order:4;overflow-x:auto;padding-bottom:.1rem}.receipt-grid.svelte-1j425e6{grid-template-columns:1fr}}.theme-toggle.svelte-1cmi4dh{width:30px;height:30px;display:inline-flex;align-items:center;justify-content:center;padding:0;border-radius:8px;background:#6366f10f;border:1px solid rgba(99,102,241,.14);color:var(--color-text);cursor:pointer;transition:background .2s ease,border-color .2s ease,color .2s ease,transform .12s ease;-webkit-tap-highlight-color:transparent}.theme-toggle.svelte-1cmi4dh:hover{background:#6366f124;border-color:#6366f14d;color:var(--color-bright)}.theme-toggle.svelte-1cmi4dh:active{transform:scale(.94)}.theme-toggle.svelte-1cmi4dh:focus-visible{outline:1px solid var(--color-synapse);outline-offset:2px}.icon-wrap.svelte-1cmi4dh{position:relative;width:18px;height:18px;display:inline-block}.icon.svelte-1cmi4dh{position:absolute;top:0;right:0;bottom:0;left:0;width:18px;height:18px;opacity:0;transform:scale(.7) rotate(-30deg);transition:opacity .2s ease,transform .2s cubic-bezier(.16,1,.3,1);pointer-events:none}.icon.active.svelte-1cmi4dh{opacity:1;transform:scale(1) rotate(0)}.theme-toggle[data-mode=dark].svelte-1cmi4dh{color:var(--color-synapse-glow, #818cf8)}.theme-toggle[data-mode=light].svelte-1cmi4dh{color:var(--color-warning, #f59e0b)}.theme-toggle[data-mode=auto].svelte-1cmi4dh{color:var(--color-dream-glow, #c084fc)}@media(prefers-reduced-motion:reduce){.theme-toggle.svelte-1cmi4dh,.icon.svelte-1cmi4dh{transition:none}}.safe-bottom.svelte-12qhfyh{padding-bottom:env(safe-area-inset-bottom,0px)}@keyframes svelte-12qhfyh-page-in{0%{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}.animate-page-in.svelte-12qhfyh{animation:svelte-12qhfyh-page-in .2s ease-out} diff --git a/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css.br b/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css.br new file mode 100644 index 0000000..4752411 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css.gz b/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css.gz new file mode 100644 index 0000000..9e9b960 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/0.CN9L-NIY.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css new file mode 100644 index 0000000..3e7ea0d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css @@ -0,0 +1 @@ +.audit-trail.svelte-kf1sc6 ol>li{animation:svelte-kf1sc6-event-rise .4s cubic-bezier(.22,.8,.3,1) backwards}@keyframes svelte-kf1sc6-event-rise{0%{opacity:0;transform:translate(6px)}to{opacity:1;transform:translate(0)}}.audit-trail .marker{transition:transform .2s ease}.audit-trail li:hover .marker{transform:scale(1.15)} diff --git a/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.br b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.br new file mode 100644 index 0000000..b0484eb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.gz b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.gz new file mode 100644 index 0000000..84a3745 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/13.Bjd0S47S.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css new file mode 100644 index 0000000..f7e0703 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css @@ -0,0 +1 @@ +.stage.svelte-9hm057{animation:svelte-9hm057-stage-light .7s cubic-bezier(.22,.8,.3,1) backwards;position:relative;border-color:#6366f114}.stage-orb.svelte-9hm057{width:28px;height:28px;border-radius:50%;background:radial-gradient(circle at 30% 30%,#818cf840,#6366f10d);border:1px solid rgba(99,102,241,.3);display:flex;align-items:center;justify-content:center;position:relative;animation:svelte-9hm057-orb-glow .7s cubic-bezier(.22,.8,.3,1) backwards}.stage-pulse.svelte-9hm057{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:12px;border:1px solid rgba(129,140,248,0);pointer-events:none;animation:svelte-9hm057-pulse-ring .7s cubic-bezier(.22,.8,.3,1) backwards}.connector.svelte-9hm057{position:absolute;left:22px;top:100%;width:1px;height:8px;background:linear-gradient(180deg,#818cf880,#a855f726);animation:svelte-9hm057-connector-draw .5s ease-out backwards}.running.svelte-9hm057 .stage:where(.svelte-9hm057){animation:svelte-9hm057-stage-light .7s cubic-bezier(.22,.8,.3,1) backwards,svelte-9hm057-stage-flicker 2.4s ease-in-out infinite}@keyframes svelte-9hm057-stage-light{0%{opacity:0;transform:translate(-8px);border-color:#6366f100}60%{opacity:1;border-color:#818cf859}to{opacity:1;transform:translate(0);border-color:#6366f114}}@keyframes svelte-9hm057-orb-glow{0%{transform:scale(.6);opacity:0;box-shadow:0 0 #818cf800}60%{transform:scale(1.15);opacity:1;box-shadow:0 0 24px #818cf8cc}to{transform:scale(1);box-shadow:0 0 10px #818cf859}}@keyframes svelte-9hm057-pulse-ring{0%{transform:scale(.96);opacity:0;border-color:#818cf800}70%{transform:scale(1);opacity:1;border-color:#818cf866;box-shadow:0 0 20px #818cf840}to{transform:scale(1.01);opacity:0;border-color:#818cf800;box-shadow:0 0 #818cf800}}@keyframes svelte-9hm057-connector-draw{0%{transform:scaleY(0);transform-origin:top;opacity:0}to{transform:scaleY(1);transform-origin:top;opacity:1}}@keyframes svelte-9hm057-stage-flicker{0%,to{border-color:#6366f114}50%{border-color:#818cf840}}.evidence-card.svelte-ksja6x{animation:svelte-ksja6x-card-rise .6s cubic-bezier(.22,.8,.3,1) backwards}.evidence-card.primary.svelte-ksja6x{border-color:#6366f159!important;box-shadow:inset 0 1px #ffffff0a,0 0 32px #6366f12e,0 8px 32px #0006}.evidence-card.contradicting.svelte-ksja6x{border-color:#ef444473!important;box-shadow:inset 0 1px #ffffff08,0 0 28px #ef444433,0 8px 32px #0006}.evidence-card.superseded.svelte-ksja6x{opacity:.55}.evidence-card.superseded.svelte-ksja6x:hover{opacity:.9}.role-pill.svelte-ksja6x{background:#6366f11f;color:#c7cbff;border:1px solid rgba(99,102,241,.25)}.evidence-card.contradicting.svelte-ksja6x .role-pill:where(.svelte-ksja6x){background:#ef444424;color:#fecaca;border-color:#ef444466}.evidence-card.primary.svelte-ksja6x .role-pill:where(.svelte-ksja6x){background:#6366f138;color:#a5b4ff;border-color:#6366f180}.trust-fill.svelte-ksja6x{animation:svelte-ksja6x-trust-sweep 1s cubic-bezier(.22,.8,.3,1) backwards}@keyframes svelte-ksja6x-card-rise{0%{opacity:0;transform:translateY(12px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}@keyframes svelte-ksja6x-trust-sweep{0%{width:0%!important;opacity:.4}to{opacity:1}}.line-clamp-4.svelte-ksja6x{display:-webkit-box;-webkit-line-clamp:4;line-clamp:4;-webkit-box-orient:vertical;overflow:hidden}.conf-number.svelte-q2v96u{animation:svelte-q2v96u-conf-pop .9s cubic-bezier(.22,.8,.3,1) backwards}@keyframes svelte-q2v96u-conf-pop{0%{opacity:0;transform:scale(.5)}60%{opacity:1;transform:scale(1.1)}to{opacity:1;transform:scale(1)}}.arc-path.svelte-q2v96u{animation:svelte-q2v96u-arc-draw .9s cubic-bezier(.22,.8,.3,1) backwards;stroke-dashoffset:0}@keyframes svelte-q2v96u-arc-draw{0%{opacity:0;stroke-dasharray:0 400}to{opacity:1;stroke-dasharray:4 4}}.arc-dot.svelte-q2v96u{animation:svelte-q2v96u-arc-dot-pulse 1.4s ease-in-out infinite}@keyframes svelte-q2v96u-arc-dot-pulse{0%,to{opacity:.8;r:4}50%{opacity:1;r:5}}.evidence-grid.svelte-q2v96u{isolation:isolate}.contradiction-arcs.svelte-q2v96u{z-index:5} diff --git a/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.br b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.br new file mode 100644 index 0000000..cd26bb9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.gz b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.gz new file mode 100644 index 0000000..2d6842b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/15.ChjqzJHo.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css new file mode 100644 index 0000000..12c8d4f --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css @@ -0,0 +1 @@ +@keyframes svelte-rs1z7a-panel-in{0%{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.animate-panel-in.svelte-rs1z7a{animation:svelte-rs1z7a-panel-in .18s ease-out} diff --git a/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.br b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.br new file mode 100644 index 0000000..1a24a9f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.gz b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.gz new file mode 100644 index 0000000..f534971 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/16.BnHgRQtR.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css b/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css new file mode 100644 index 0000000..4017a6e --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css @@ -0,0 +1 @@ +body{overflow:hidden}.waitlist-shell.svelte-1375qm6{position:fixed;top:0;right:0;bottom:0;left:0;overflow-y:auto;background:#07100f;color:#edf7f2;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}.memory-field.svelte-1375qm6,.field-vignette.svelte-1375qm6{position:fixed;top:0;right:0;bottom:0;left:0;pointer-events:none}.memory-field.svelte-1375qm6{z-index:0}.field-vignette.svelte-1375qm6{z-index:1;background:linear-gradient(90deg,#07100feb,#07100f9e 48%,#07100fe0),linear-gradient(180deg,#07100f33,#07100fd1)}.topbar.svelte-1375qm6,main.svelte-1375qm6{position:relative;z-index:2}.topbar.svelte-1375qm6{display:flex;align-items:center;justify-content:space-between;gap:1rem;width:min(1180px,calc(100% - 2rem));margin:0 auto;padding:1rem 0}.brand.svelte-1375qm6,.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6),.hero-actions.svelte-1375qm6,.proof-row.svelte-1375qm6,.signal-band.svelte-1375qm6,.track-grid.svelte-1375qm6,.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6,.roadmap.svelte-1375qm6 li:where(.svelte-1375qm6){display:flex}.brand.svelte-1375qm6{align-items:center;gap:.7rem;color:#fff;text-decoration:none;font-weight:800}.brand-mark.svelte-1375qm6{display:grid;place-items:center;width:2.15rem;height:2.15rem;border:1px solid rgba(34,197,94,.48);border-radius:8px;background:linear-gradient(135deg,#22c55e3d,#06b6d429);color:#bbf7d0}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6){align-items:center;gap:.4rem}.topbar.svelte-1375qm6 a:where(.svelte-1375qm6){color:#b8c7c0;text-decoration:none}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6) a:where(.svelte-1375qm6){border-radius:8px;padding:.65rem .85rem;font-size:.88rem}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6) a:where(.svelte-1375qm6):hover,.nav-cta.svelte-1375qm6{background:#ffffff12;color:#fff}main.svelte-1375qm6{width:min(1180px,calc(100% - 2rem));margin:0 auto}.hero.svelte-1375qm6{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(320px,.7fr);gap:clamp(2rem,6vw,5rem);align-items:center;min-height:86vh;padding:clamp(2rem,5vw,4.8rem) 0 2rem}.hero-copy.svelte-1375qm6{max-width:720px}.eyebrow.svelte-1375qm6,.form-heading.svelte-1375qm6 p:where(.svelte-1375qm6),.section-heading.svelte-1375qm6 p:where(.svelte-1375qm6){margin:0 0 .8rem;color:#67e8f9;font-size:.78rem;font-weight:800;letter-spacing:0;text-transform:uppercase}h1.svelte-1375qm6,h2.svelte-1375qm6,h3.svelte-1375qm6,p.svelte-1375qm6{margin-top:0}h1.svelte-1375qm6{margin-bottom:1.1rem;color:#fff;font-size:clamp(3.8rem,13vw,8rem);line-height:.92;letter-spacing:0}.hero-subtitle.svelte-1375qm6{max-width:680px;margin-bottom:1.6rem;color:#d7e6df;font-size:clamp(1.05rem,2.4vw,1.45rem);line-height:1.5}.hero-actions.svelte-1375qm6{flex-wrap:wrap;gap:.8rem;margin-bottom:2rem}.primary-link.svelte-1375qm6,.secondary-link.svelte-1375qm6,.submit-button.svelte-1375qm6{border-radius:8px;font-weight:800;text-decoration:none}.primary-link.svelte-1375qm6,.submit-button.svelte-1375qm6{border:1px solid rgba(34,197,94,.86);background:#22c55e;color:#04130b;box-shadow:0 20px 42px #22c55e30}.primary-link.svelte-1375qm6,.secondary-link.svelte-1375qm6{padding:.88rem 1rem}.secondary-link.svelte-1375qm6{border:1px solid rgba(226,232,240,.2);background:#ffffff0f;color:#edf7f2}.proof-row.svelte-1375qm6{flex-wrap:wrap;gap:.7rem}.proof-item.svelte-1375qm6{width:min(100%,13rem);border:1px solid rgba(226,232,240,.12);border-radius:8px;background:#050c0b8a;padding:.9rem}.proof-item.svelte-1375qm6 strong:where(.svelte-1375qm6),.proof-item.svelte-1375qm6 span:where(.svelte-1375qm6){display:block}.proof-item.svelte-1375qm6 strong:where(.svelte-1375qm6){margin-bottom:.4rem;color:#bbf7d0;font-size:1.05rem}.proof-item.svelte-1375qm6 span:where(.svelte-1375qm6){color:#a8bbb2;font-size:.82rem;line-height:1.45}.waitlist-form.svelte-1375qm6{border:1px solid rgba(226,232,240,.16);border-radius:8px;background:#050c0bd1;box-shadow:0 28px 90px #0000005c;padding:clamp(1rem,3vw,1.35rem)}.form-heading.svelte-1375qm6 h2:where(.svelte-1375qm6),.section-heading.svelte-1375qm6 h2:where(.svelte-1375qm6),.roadmap.svelte-1375qm6 h2:where(.svelte-1375qm6){margin-bottom:1rem;color:#fff;font-size:clamp(1.6rem,4vw,2.7rem);line-height:1.05;letter-spacing:0}.form-heading.svelte-1375qm6 h2:where(.svelte-1375qm6){font-size:clamp(1.4rem,3vw,2rem)}label.svelte-1375qm6{display:block;margin-top:.85rem}label.svelte-1375qm6 span:where(.svelte-1375qm6){display:block;margin-bottom:.38rem;color:#a8bbb2;font-size:.78rem;font-weight:750}input.svelte-1375qm6,select.svelte-1375qm6,textarea.svelte-1375qm6{width:100%;border:1px solid rgba(226,232,240,.16);border-radius:8px;background:#fff1;color:#fff;font:inherit;padding:.78rem .82rem;outline:none}select.svelte-1375qm6{color-scheme:dark}textarea.svelte-1375qm6{resize:vertical;min-height:6rem}input.svelte-1375qm6:focus,select.svelte-1375qm6:focus,textarea.svelte-1375qm6:focus{border-color:#22c55ee6;box-shadow:0 0 0 3px #22c55e24}.hidden-field.svelte-1375qm6{position:absolute;left:-10000px;height:1px;overflow:hidden}.submit-button.svelte-1375qm6{width:100%;margin-top:1rem;padding:.95rem 1rem;cursor:pointer;font:inherit}.submit-button.svelte-1375qm6:disabled{cursor:wait;opacity:.72}.submit-message.svelte-1375qm6{margin:.8rem 0 0;font-size:.82rem;line-height:1.45}.submit-message.success.svelte-1375qm6{color:#86efac}.submit-message.error.svelte-1375qm6{color:#fca5a5}.signal-band.svelte-1375qm6{flex-wrap:wrap;gap:.65rem;border-top:1px solid rgba(226,232,240,.12);border-bottom:1px solid rgba(226,232,240,.12);padding:1rem 0}.signal-band.svelte-1375qm6 div:where(.svelte-1375qm6){border-radius:999px;background:#ffffff12;color:#d7e6df;padding:.55rem .75rem;font-size:.84rem}.pro-grid.svelte-1375qm6,.roadmap.svelte-1375qm6{padding:clamp(3rem,7vw,5rem) 0}.section-heading.svelte-1375qm6{max-width:760px;margin-bottom:1.7rem}.track-grid.svelte-1375qm6{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.9rem}.track.svelte-1375qm6{border:1px solid rgba(226,232,240,.13);border-radius:8px;background:#050c0ba8;padding:1.1rem}.track-line.svelte-1375qm6{width:3.5rem;height:.22rem;margin-bottom:1rem;border-radius:999px;background:var(--track-color)}.track.svelte-1375qm6 h3:where(.svelte-1375qm6){margin-bottom:.65rem;color:#fff;font-size:1.08rem}.track.svelte-1375qm6 p:where(.svelte-1375qm6),.roadmap.svelte-1375qm6 span:where(.svelte-1375qm6){color:#a8bbb2;line-height:1.55}.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6{align-items:flex-start;justify-content:space-between;gap:clamp(2rem,6vw,4rem);border-top:1px solid rgba(226,232,240,.12)}.support-bot.svelte-1375qm6{padding:clamp(2.6rem,6vw,4.5rem) 0}.support-bot.svelte-1375qm6>div:where(.svelte-1375qm6):first-child,.roadmap.svelte-1375qm6>div:where(.svelte-1375qm6){flex:0 0 min(28rem,100%)}.bot-intro.svelte-1375qm6{max-width:34rem;color:#a8bbb2;line-height:1.55}.bot-panel.svelte-1375qm6{flex:1;border:1px solid rgba(226,232,240,.13);border-radius:8px;background:#050c0b9e;padding:clamp(1rem,3vw,1.25rem)}.bot-status.svelte-1375qm6,.prompt-row.svelte-1375qm6,.bot-input.svelte-1375qm6{display:flex;align-items:center}.bot-status.svelte-1375qm6{gap:.55rem;margin-bottom:.9rem;color:#d7e6df;font-size:.86rem;font-weight:800}.bot-status.svelte-1375qm6 small:where(.svelte-1375qm6){margin-left:auto;border:1px solid rgba(34,197,94,.22);border-radius:999px;padding:.3rem .5rem;color:#86efac;font-size:.72rem}.bot-light.svelte-1375qm6{width:.42rem;height:.42rem;border-radius:999px;background:#22c55e;box-shadow:0 0 18px #22c55eb3}.bot-messages.svelte-1375qm6{display:grid;gap:.75rem;max-height:22rem;overflow-y:auto;border:1px solid rgba(226,232,240,.1);border-radius:8px;background:#ffffff09;padding:.8rem}.bot-bubble.svelte-1375qm6,.user-bubble.svelte-1375qm6{max-width:88%;border-radius:8px;padding:.75rem .82rem}.bot-bubble.svelte-1375qm6{justify-self:start;border:1px solid rgba(34,197,94,.18);background:#22c55e14;color:#d7e6df}.user-bubble.svelte-1375qm6{justify-self:end;border:1px solid rgba(6,182,212,.24);background:#06b6d41f;color:#fff}.bot-bubble.svelte-1375qm6 p:where(.svelte-1375qm6),.user-bubble.svelte-1375qm6 p:where(.svelte-1375qm6){margin:0;font-size:.86rem;line-height:1.5;white-space:pre-wrap}.bot-bubble.svelte-1375qm6 p:where(.svelte-1375qm6)+p:where(.svelte-1375qm6),.user-bubble.svelte-1375qm6 p:where(.svelte-1375qm6)+p:where(.svelte-1375qm6){margin-top:.35rem}.prompt-row.svelte-1375qm6{flex-wrap:wrap;gap:.45rem;margin:.85rem 0}.prompt-row.svelte-1375qm6 button:where(.svelte-1375qm6){border:1px solid rgba(226,232,240,.14);border-radius:999px;background:#ffffff0e;color:#d7e6df;cursor:pointer;font:inherit;font-size:.78rem;padding:.46rem .62rem}.prompt-row.svelte-1375qm6 button:where(.svelte-1375qm6):hover{border-color:#22c55e6b;color:#fff}.bot-input.svelte-1375qm6{gap:.55rem}.bot-input.svelte-1375qm6 input:where(.svelte-1375qm6){margin:0}.bot-input.svelte-1375qm6 button:where(.svelte-1375qm6){flex:0 0 auto;border:1px solid rgba(34,197,94,.86);border-radius:8px;background:#22c55e;color:#04130b;cursor:pointer;font:inherit;font-weight:800;padding:.78rem .95rem}.bot-input.svelte-1375qm6 button:where(.svelte-1375qm6):disabled{cursor:not-allowed;opacity:.45}.roadmap.svelte-1375qm6 ol:where(.svelte-1375qm6){display:grid;gap:.8rem;margin:0;padding:0;list-style:none}.roadmap.svelte-1375qm6 li:where(.svelte-1375qm6){gap:1rem;align-items:flex-start;border:1px solid rgba(226,232,240,.13);border-radius:8px;background:#050c0b94;padding:1rem}.roadmap.svelte-1375qm6 strong:where(.svelte-1375qm6){flex:0 0 4.5rem;color:#fbbf24}@media(max-width:900px){.topbar.svelte-1375qm6{align-items:flex-start;flex-direction:column}.hero.svelte-1375qm6,.track-grid.svelte-1375qm6,.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6{grid-template-columns:1fr}.hero.svelte-1375qm6{display:block;min-height:auto;padding-top:2rem}.waitlist-form.svelte-1375qm6{margin-top:2rem}.support-bot.svelte-1375qm6,.roadmap.svelte-1375qm6{display:block}.bot-panel.svelte-1375qm6{margin-top:1rem}}@media(max-width:560px){main.svelte-1375qm6,.topbar.svelte-1375qm6{width:min(100% - 1rem,1180px)}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6){width:100%;justify-content:space-between}.topbar.svelte-1375qm6 nav:where(.svelte-1375qm6) a:where(.svelte-1375qm6){padding:.58rem .62rem;font-size:.8rem}h1.svelte-1375qm6{font-size:clamp(3.35rem,18vw,4.8rem)}.hero-subtitle.svelte-1375qm6{font-size:1rem}.proof-item.svelte-1375qm6{width:100%}.roadmap.svelte-1375qm6 li:where(.svelte-1375qm6){display:block}.roadmap.svelte-1375qm6 strong:where(.svelte-1375qm6){display:block;margin-bottom:.5rem}.bot-input.svelte-1375qm6{align-items:stretch;flex-direction:column}.bot-input.svelte-1375qm6 button:where(.svelte-1375qm6){width:100%}} diff --git a/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css.br b/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css.br new file mode 100644 index 0000000..e4292ca Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css.gz b/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css.gz new file mode 100644 index 0000000..a175ffa Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/20.DKhUrxcR.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css new file mode 100644 index 0000000..253943b --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css @@ -0,0 +1 @@ +@keyframes svelte-1jku20k-arc-drift{0%{stroke-dashoffset:0}to{stroke-dashoffset:-32}}.arc-particle{animation-name:svelte-1jku20k-arc-drift;animation-timing-function:linear;animation-iteration-count:infinite} diff --git a/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.br b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.br new file mode 100644 index 0000000..119e6b7 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.gz b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.gz new file mode 100644 index 0000000..ac521f3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/5.DQ_AfUnN.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css new file mode 100644 index 0000000..65c46b1 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css @@ -0,0 +1 @@ +.replay-stage.svelte-1cq1ntk{border:1px solid rgba(168,85,247,.18);box-shadow:inset 0 1px #ffffff08,0 8px 36px -8px #0000008c,0 0 48px -16px #a855f740}.stage-canvas.svelte-1cq1ntk{position:relative;height:360px;overflow:hidden;background:radial-gradient(at 50% 50%,color-mix(in srgb,var(--stage-color) 10%,transparent),transparent 60%),radial-gradient(at 20% 80%,rgba(99,102,241,.08),transparent 50%),#08081a}.edges-layer.svelte-1cq1ntk{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;pointer-events:none}.edge-line.svelte-1cq1ntk{transition:stroke-opacity .52s ease,stroke-width .52s ease,x1 .6s cubic-bezier(.34,1.56,.64,1),y1 .6s cubic-bezier(.34,1.56,.64,1),x2 .6s cubic-bezier(.34,1.56,.64,1),y2 .6s cubic-bezier(.34,1.56,.64,1)}.memory-card.svelte-1cq1ntk{position:absolute;width:44px;height:32px;transform:translate(-50%,-50%) scale(var(--card-scale, 1));transition:left .6s cubic-bezier(.34,1.56,.64,1),top .6s cubic-bezier(.34,1.56,.64,1),transform .5s cubic-bezier(.34,1.56,.64,1),opacity .5s ease;transition-delay:var(--card-delay, 0ms);animation:svelte-1cq1ntk-card-float 6s ease-in-out infinite;animation-delay:var(--card-delay, 0ms);will-change:transform}.card-inner.svelte-1cq1ntk{width:100%;height:100%;border-radius:6px;background:linear-gradient(135deg,color-mix(in srgb,var(--stage-color) 30%,transparent),color-mix(in srgb,var(--stage-color) 10%,transparent));border:1px solid color-mix(in srgb,var(--stage-color) 50%,transparent);box-shadow:inset 0 1px #ffffff14,0 0 8px color-mix(in srgb,var(--stage-color) 30%,transparent);filter:hue-rotate(var(--card-hue, 0deg));padding:5px 6px;display:flex;flex-direction:column;justify-content:center;gap:3px;position:relative;overflow:hidden}.card-dot.svelte-1cq1ntk{width:4px;height:4px;border-radius:50%;background:color-mix(in srgb,var(--stage-color) 90%,white);box-shadow:0 0 6px var(--stage-color);position:absolute;top:4px;right:4px}.card-bar.svelte-1cq1ntk{height:3px;border-radius:1.5px;background:color-mix(in srgb,var(--stage-color) 70%,transparent);width:80%}.card-bar.short.svelte-1cq1ntk{width:50%;opacity:.6}.memory-card.is-pulsing.svelte-1cq1ntk{animation:svelte-1cq1ntk-card-float 6s ease-in-out infinite,svelte-1cq1ntk-card-pulse 1.4s ease-in-out infinite}.memory-card.is-pulsing.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){border-color:var(--color-dream-glow);background:linear-gradient(135deg,color-mix(in srgb,var(--color-dream-glow) 40%,transparent),color-mix(in srgb,var(--color-dream) 25%,transparent));box-shadow:inset 0 1px #ffffff1f,0 0 22px color-mix(in srgb,var(--color-dream-glow) 60%,transparent),0 0 44px color-mix(in srgb,var(--color-dream) 35%,transparent)}.memory-card.is-pruning.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){animation:svelte-1cq1ntk-dissolve 1.2s ease-out forwards}.memory-card.is-transferring.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){border-color:#10b981;background:linear-gradient(135deg,#10b98159,#10b9811f);box-shadow:inset 0 1px #ffffff14,0 0 14px #10b98180}.memory-card.is-transferring.semantic-side.svelte-1cq1ntk .card-inner:where(.svelte-1cq1ntk){border-color:#c084fc;background:linear-gradient(135deg,#c084fc59,#a855f726);box-shadow:inset 0 1px #ffffff14,0 0 14px #c084fc80}.replay-pulse.svelte-1cq1ntk{position:absolute;top:50%;left:50%;width:60%;aspect-ratio:1 / 1;transform:translate(-50%,-50%);border-radius:50%;background:radial-gradient(circle,color-mix(in srgb,var(--stage-color) 25%,transparent),transparent 60%);filter:blur(30px);animation:svelte-1cq1ntk-pulse-in 3s ease-in-out infinite;pointer-events:none}.transfer-label.svelte-1cq1ntk{position:absolute;top:12px;display:flex;flex-direction:column;align-items:center;gap:2px;z-index:2}.transfer-label.episodic.svelte-1cq1ntk{left:6%}.transfer-label.semantic.svelte-1cq1ntk{right:6%}.label-tag.svelte-1cq1ntk{font-size:10px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;padding:2px 8px;border-radius:999px;border:1px solid rgba(255,255,255,.15);background:#00000059;color:#e0e0ff}.transfer-label.episodic.svelte-1cq1ntk .label-tag:where(.svelte-1cq1ntk){border-color:#10b98180;color:#10b981}.transfer-label.semantic.svelte-1cq1ntk .label-tag:where(.svelte-1cq1ntk){border-color:#c084fc80;color:#c084fc}.label-sub.svelte-1cq1ntk{font-size:9px;color:var(--color-dim);letter-spacing:.1em}.divider-line.svelte-1cq1ntk{position:absolute;top:15%;bottom:15%;left:50%;width:1px;background:linear-gradient(180deg,transparent,rgba(168,85,247,.35),transparent);transform:translate(-.5px)}@keyframes svelte-1cq1ntk-card-float{0%,to{translate:0 0}25%{translate:2px -3px}50%{translate:-2px 2px}75%{translate:3px 1px}}@keyframes svelte-1cq1ntk-card-pulse{0%,to{filter:brightness(1) hue-rotate(var(--card-hue, 0deg))}50%{filter:brightness(1.3) hue-rotate(var(--card-hue, 0deg))}}@keyframes svelte-1cq1ntk-dissolve{0%{opacity:1;transform:scale(1);filter:blur(0)}60%{opacity:.3;filter:blur(2px)}to{opacity:0;transform:scale(.5);filter:blur(6px)}}@keyframes svelte-1cq1ntk-pulse-in{0%,to{opacity:.3;transform:translate(-50%,-50%) scale(1)}50%{opacity:.7;transform:translate(-50%,-50%) scale(1.15)}}@media(prefers-reduced-motion:reduce){.memory-card.svelte-1cq1ntk,.replay-pulse.svelte-1cq1ntk,.memory-card.is-pulsing.svelte-1cq1ntk{animation:none}}.insight-card.svelte-1y17hsl{position:relative;border:1px solid color-mix(in srgb,var(--insight-color) 20%,transparent);transition:transform .4s cubic-bezier(.34,1.56,.64,1),border-color .22s ease,box-shadow .22s ease;animation:svelte-1y17hsl-card-in .42s cubic-bezier(.34,1.56,.64,1) both;animation-delay:var(--enter-delay, 0ms)}.insight-card.svelte-1y17hsl:hover{transform:translateY(-2px) scale(1.01);border-color:color-mix(in srgb,var(--insight-color) 45%,transparent)}.insight-card.high-novelty.svelte-1y17hsl{border-color:#f59e0b66;box-shadow:0 0 0 1px #f59e0b40,0 0 24px -4px #f59e0b73,0 0 60px -12px #f59e0b40,inset 0 1px #ffffff0d;background:radial-gradient(at top right,rgba(245,158,11,.08),transparent 50%),#0a0a1acc}.insight-card.low-novelty.svelte-1y17hsl{opacity:.6;filter:saturate(.7)}.insight-card.low-novelty.svelte-1y17hsl:hover{opacity:.9;filter:saturate(1)}.novelty-track.svelte-1y17hsl{height:4px;background:#ffffff0d;border-radius:2px;overflow:hidden}.novelty-fill.svelte-1y17hsl{height:100%;border-radius:2px;transition:width .6s cubic-bezier(.34,1.56,.64,1);box-shadow:0 0 8px color-mix(in srgb,var(--insight-color) 60%,transparent)}.source-chip.svelte-1y17hsl{background:#6366f11f;border:1px solid rgba(99,102,241,.25);color:var(--color-synapse-glow);text-decoration:none;transition:all .18s ease}.source-chip.svelte-1y17hsl:hover{background:#6366f140;border-color:#818cf880;transform:translateY(-1px)}.sparkle.svelte-1y17hsl{display:inline-block;animation:svelte-1y17hsl-sparkle-spin 3s linear infinite}@keyframes svelte-1y17hsl-sparkle-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes svelte-1y17hsl-card-in{0%{opacity:0;transform:translateY(8px) scale(.97)}to{opacity:1;transform:translateY(0) scale(1)}}@media(prefers-reduced-motion:reduce){.insight-card.svelte-1y17hsl,.sparkle.svelte-1y17hsl{animation:none}}.header-glyph.svelte-1fv2vo0{display:inline-block;color:var(--color-dream-glow);text-shadow:0 0 12px var(--color-dream),0 0 24px color-mix(in srgb,var(--color-dream) 50%,transparent);animation:svelte-1fv2vo0-twinkle 4s ease-in-out infinite}@keyframes svelte-1fv2vo0-twinkle{0%,to{opacity:1;transform:rotate(0)}50%{opacity:.75;transform:rotate(10deg)}}.dream-button.svelte-1fv2vo0{display:inline-flex;align-items:center;gap:.6rem;padding:.7rem 1.4rem;border-radius:999px;font-size:.9rem;font-weight:600;letter-spacing:.02em;color:#fff;background:linear-gradient(135deg,var(--color-dream),var(--color-synapse));border:1px solid color-mix(in srgb,var(--color-dream-glow) 60%,transparent);box-shadow:inset 0 1px #ffffff2e,0 8px 24px -6px #a855f78c,0 0 48px -10px #a855f773;cursor:pointer;transition:transform .4s cubic-bezier(.34,1.56,.64,1),box-shadow .22s ease,filter .22s ease}.dream-button.svelte-1fv2vo0:hover:not(:disabled){transform:translateY(-2px) scale(1.03);box-shadow:inset 0 1px #ffffff38,0 12px 32px -6px #a855f7b3,0 0 64px -10px #a855f78c}.dream-button.svelte-1fv2vo0:disabled{cursor:not-allowed;filter:saturate(.85)}.dream-button.is-dreaming.svelte-1fv2vo0{background:linear-gradient(135deg,var(--color-synapse),var(--color-dream));animation:svelte-1fv2vo0-button-breathe 2s ease-in-out infinite}@keyframes svelte-1fv2vo0-button-breathe{0%,to{box-shadow:0 8px 24px -6px #a855f780,0 0 48px -10px #a855f766}50%{box-shadow:0 12px 36px -6px #a855f7cc,0 0 80px -10px #a855f799}}.dream-icon.svelte-1fv2vo0{display:inline-block;animation:svelte-1fv2vo0-twinkle 3s ease-in-out infinite}.spinner.svelte-1fv2vo0{width:14px;height:14px;border-radius:50%;border:2px solid rgba(255,255,255,.25);border-top-color:#fff;animation:svelte-1fv2vo0-spin .8s linear infinite}@keyframes svelte-1fv2vo0-spin{to{transform:rotate(360deg)}}.empty-state.svelte-1fv2vo0{border:1px dashed rgba(168,85,247,.25)}.empty-glyph.svelte-1fv2vo0{font-size:3rem;color:var(--color-dream-glow);opacity:.5;text-shadow:0 0 20px var(--color-dream);animation:svelte-1fv2vo0-twinkle 4s ease-in-out infinite}.scrubber-wrap.svelte-1fv2vo0{position:relative;padding:4px 0 8px}.scrubber.svelte-1fv2vo0{-moz-appearance:none;appearance:none;-webkit-appearance:none;width:100%;height:6px;border-radius:999px;background:linear-gradient(90deg,var(--color-synapse-glow) 0%,var(--color-dream) 50%,var(--color-recall) 100%);opacity:.35;outline:none;cursor:pointer;transition:opacity .22s ease}.scrubber.svelte-1fv2vo0:hover:not(:disabled){opacity:.55}.scrubber.svelte-1fv2vo0::-webkit-slider-thumb{-webkit-appearance:none;-moz-appearance:none;appearance:none;width:20px;height:20px;border-radius:50%;background:var(--color-dream-glow);border:2px solid white;box-shadow:0 0 0 3px #c084fc40,0 0 20px var(--color-dream),0 4px 12px #0006;cursor:grab;transition:transform .4s cubic-bezier(.34,1.56,.64,1)}.scrubber.svelte-1fv2vo0::-webkit-slider-thumb:hover{transform:scale(1.2)}.scrubber.svelte-1fv2vo0::-moz-range-thumb{width:20px;height:20px;border-radius:50%;background:var(--color-dream-glow);border:2px solid white;box-shadow:0 0 0 3px #c084fc40,0 0 20px var(--color-dream);cursor:grab}.scrubber.svelte-1fv2vo0:disabled{cursor:not-allowed;opacity:.25}.scrubber-ticks.svelte-1fv2vo0{display:flex;justify-content:space-between;margin-top:10px;gap:4px}.tick.svelte-1fv2vo0{display:flex;flex-direction:column;align-items:center;gap:4px;background:transparent;border:none;cursor:pointer;padding:2px 4px;color:var(--color-dim);font-size:10px;letter-spacing:.04em;transition:color .22s ease,transform .22s cubic-bezier(.34,1.56,.64,1)}.tick.svelte-1fv2vo0:disabled{cursor:not-allowed}.tick.svelte-1fv2vo0:hover:not(:disabled){color:var(--color-dream-glow);transform:translateY(-1px)}.tick-dot.svelte-1fv2vo0{width:8px;height:8px;border-radius:50%;background:#ffffff1a;border:1px solid rgba(255,255,255,.15);transition:all .28s ease}.tick.passed.svelte-1fv2vo0 .tick-dot:where(.svelte-1fv2vo0){background:var(--color-synapse-glow);border-color:var(--color-synapse-glow);opacity:.7}.tick.active.svelte-1fv2vo0 .tick-dot:where(.svelte-1fv2vo0){background:var(--color-dream-glow);border-color:#fff;box-shadow:0 0 0 3px #c084fc4d,0 0 14px var(--color-dream);transform:scale(1.3)}.tick.active.svelte-1fv2vo0{color:var(--color-dream-glow);font-weight:600}.tick-label.svelte-1fv2vo0{white-space:nowrap}.step-btn.svelte-1fv2vo0{width:28px;height:28px;border-radius:6px;background:#6366f11a;border:1px solid rgba(99,102,241,.2);color:var(--color-synapse-glow);cursor:pointer;transition:all .18s ease;font-size:11px}.step-btn.svelte-1fv2vo0:hover:not(:disabled){background:#6366f133;transform:translateY(-1px)}.step-btn.svelte-1fv2vo0:disabled{opacity:.35;cursor:not-allowed}.insights-scroll.svelte-1fv2vo0{max-height:520px;overflow-y:auto;padding-right:4px}.stat-cell.svelte-1fv2vo0{padding:.5rem .75rem;border-left:2px solid rgba(168,85,247,.3)}.stat-value.svelte-1fv2vo0{font-family:var(--font-mono);font-size:1.25rem;font-weight:700;color:var(--color-bright);font-variant-numeric:tabular-nums;line-height:1.1}.stat-label.svelte-1fv2vo0{font-size:10px;color:var(--color-dim);text-transform:uppercase;letter-spacing:.1em;margin-top:2px} diff --git a/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.br b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.br new file mode 100644 index 0000000..1e5f381 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.gz b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.gz new file mode 100644 index 0000000..9ca84bb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/6.BSSBWVKL.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css b/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css new file mode 100644 index 0000000..985879a --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css @@ -0,0 +1 @@ +@keyframes svelte-1uyjqky-fadeSlide{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} diff --git a/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css.br b/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css.br new file mode 100644 index 0000000..f233cae Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css.gz b/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css.gz new file mode 100644 index 0000000..c8d6d43 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/7.CCrNEDd3.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css rename to apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css diff --git a/apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css.br b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.br similarity index 100% rename from apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css.br rename to apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.br diff --git a/apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css.gz b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz similarity index 55% rename from apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css.gz rename to apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz index 8719f65..647c7ec 100644 Binary files a/apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css.gz and b/apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js b/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js similarity index 82% rename from apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js rename to apps/dashboard/build/_app/immutable/chunks/554JRhq6.js index 4d7acdb..dc7f822 100644 --- a/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js +++ b/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js @@ -1 +1 @@ -const r="/api";async function t(e,o){const i=await fetch(`${r}${e}`,{headers:{"Content-Type":"application/json"},...o});if(!i.ok)throw new Error(`API ${i.status}: ${i.statusText}`);return i.json()}const n={memories:{list:e=>{const o=e?"?"+new URLSearchParams(e).toString():"";return t(`/memories${o}`)},get:e=>t(`/memories/${e}`),delete:e=>t(`/memories/${e}`,{method:"DELETE"}),promote:e=>t(`/memories/${e}/promote`,{method:"POST"}),demote:e=>t(`/memories/${e}/demote`,{method:"POST"}),suppress:(e,o)=>t(`/memories/${e}/suppress`,{method:"POST",body:o?JSON.stringify({reason:o}):void 0}),unsuppress:e=>t(`/memories/${e}/unsuppress`,{method:"POST"})},search:(e,o=20)=>t(`/search?q=${encodeURIComponent(e)}&limit=${o}`),stats:()=>t("/stats"),health:()=>t("/health"),timeline:(e=7,o=200)=>t(`/timeline?days=${e}&limit=${o}`),graph:e=>{const o=e?"?"+new URLSearchParams(Object.entries(e).filter(([,i])=>i!==void 0).map(([i,s])=>[i,String(s)])).toString():"";return t(`/graph${o}`)},dream:()=>t("/dream",{method:"POST"}),explore:(e,o="associations",i,s=10)=>t("/explore",{method:"POST",body:JSON.stringify({from_id:e,action:o,to_id:i,limit:s})}),predict:()=>t("/predict",{method:"POST"}),importance:e=>t("/importance",{method:"POST",body:JSON.stringify({content:e})}),consolidate:()=>t("/consolidate",{method:"POST"}),retentionDistribution:()=>t("/retention-distribution"),intentions:(e="active")=>t(`/intentions?status=${e}`)};export{n as a}; +const r="/api";async function t(e,o){const i=await fetch(`${r}${e}`,{headers:{"Content-Type":"application/json"},...o});if(!i.ok)throw new Error(`API ${i.status}: ${i.statusText}`);return i.json()}const n={memories:{list:e=>{const o=e?"?"+new URLSearchParams(e).toString():"";return t(`/memories${o}`)},get:e=>t(`/memories/${e}`),delete:e=>t(`/memories/${e}`,{method:"DELETE"}),promote:e=>t(`/memories/${e}/promote`,{method:"POST"}),demote:e=>t(`/memories/${e}/demote`,{method:"POST"}),suppress:(e,o)=>t(`/memories/${e}/suppress`,{method:"POST",body:o?JSON.stringify({reason:o}):void 0}),unsuppress:e=>t(`/memories/${e}/unsuppress`,{method:"POST"})},search:(e,o=20)=>t(`/search?q=${encodeURIComponent(e)}&limit=${o}`),stats:()=>t("/stats"),health:()=>t("/health"),timeline:(e=7,o=200)=>t(`/timeline?days=${e}&limit=${o}`),graph:e=>{const o=e?"?"+new URLSearchParams(Object.entries(e).filter(([,i])=>i!==void 0).map(([i,s])=>[i,String(s)])).toString():"";return t(`/graph${o}`)},dream:()=>t("/dream",{method:"POST"}),explore:(e,o="associations",i,s=10)=>t("/explore",{method:"POST",body:JSON.stringify({from_id:e,action:o,to_id:i,limit:s})}),predict:()=>t("/predict",{method:"POST"}),importance:e=>t("/importance",{method:"POST",body:JSON.stringify({content:e})}),consolidate:()=>t("/consolidate",{method:"POST"}),retentionDistribution:()=>t("/retention-distribution"),intentions:(e="active")=>t(`/intentions?status=${e}`),deepReference:(e,o=20)=>t("/deep_reference",{method:"POST",body:JSON.stringify({query:e,depth:o})}),sanhedrin:{latest:()=>t("/sanhedrin/latest"),appeal:(e,o,i,s)=>t("/sanhedrin/appeal",{method:"POST",body:JSON.stringify({reason:e,note:o,claimId:i,receiptId:s})})}};export{n as a}; diff --git a/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js.br b/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js.br new file mode 100644 index 0000000..0523040 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js.gz b/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js.gz new file mode 100644 index 0000000..a9725ce Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/554JRhq6.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js b/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js deleted file mode 100644 index a89c503..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js +++ /dev/null @@ -1 +0,0 @@ -import{aE as h,ah as d,ao as l,aF as p,w as _,aG as E,aH as g,T as u,aj as s,aI as y,a7 as M,aJ as N,ac as x,aK as A}from"./VE8Jor13.js";var f;const i=((f=globalThis==null?void 0:globalThis.window)==null?void 0:f.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function b(t){return(i==null?void 0:i.createHTML(t))??t}function w(t){var r=h("template");return r.innerHTML=b(t.replaceAll("","")),r.content}function a(t,r){var e=_;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function H(t,r){var e=(r&E)!==0,c=(r&g)!==0,n,m=!t.startsWith("");return()=>{if(u)return a(s,null),s;n===void 0&&(n=w(m?t:""+t),e||(n=l(n)));var o=c||p?document.importNode(n,!0):n.cloneNode(!0);if(e){var T=l(o),v=o.lastChild;a(T,v)}else a(o,o);return o}}function O(t=""){if(!u){var r=d(t+"");return a(r,r),r}var e=s;return e.nodeType!==N?(e.before(e=d()),x(e)):A(e),a(e,e),e}function P(){if(u)return a(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=d();return t.append(r,e),a(r,e),t}function R(t,r){if(u){var e=_;((e.f&y)===0||e.nodes.end===null)&&(e.nodes.end=s),M();return}t!==null&&t.before(r)}export{R as a,a as b,P as c,H as f,O as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js.br b/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js.br deleted file mode 100644 index 6d60191..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js.gz b/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js.gz deleted file mode 100644 index 5d1b6b6..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/7UNxJI5L.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js b/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js new file mode 100644 index 0000000..8840e85 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js @@ -0,0 +1 @@ +import{a1 as u,a2 as v,a3 as h,N as i,a4 as g,a5 as f,Y as A,a6 as S}from"./CpWkWWOo.js";const N=Symbol("is custom element"),p=Symbol("is html"),T=f?"link":"LINK",E=f?"progress":"PROGRESS";function k(r){if(i){var s=!1,a=()=>{if(!s){if(s=!0,r.hasAttribute("value")){var e=r.value;_(r,"value",null),r.value=e}if(r.hasAttribute("checked")){var o=r.checked;_(r,"checked",null),r.checked=o}}};r.__on_r=a,A(a),S()}}function l(r,s){var a=d(r);a.value===(a.value=s??void 0)||r.value===s&&(s!==0||r.nodeName!==E)||(r.value=s??"")}function _(r,s,a,e){var o=d(r);i&&(o[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===T)||o[s]!==(o[s]=a)&&(s==="loading"&&(r[u]=a),a==null?r.removeAttribute(s):typeof a!="string"&&L(r).includes(s)?r[s]=a:r.setAttribute(s,a))}function d(r){return r.__attributes??(r.__attributes={[N]:r.nodeName.includes("-"),[p]:r.namespaceURI===v})}var c=new Map;function L(r){var s=r.getAttribute("is")||r.nodeName,a=c.get(s);if(a)return a;c.set(s,a=[]);for(var e,o=r,n=Element.prototype;n!==o;){e=g(o);for(var t in e)e[t].set&&a.push(t);o=h(o)}return a}export{l as a,k as r,_ as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js.br b/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js.br new file mode 100644 index 0000000..fe4ef81 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js.gz b/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js.gz new file mode 100644 index 0000000..7883896 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/A7po6GxK.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js b/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js deleted file mode 100644 index c8ff9c2..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js +++ /dev/null @@ -1 +0,0 @@ -import{s as c,g as l}from"./CCRrbKqn.js";import{a0 as o,a1 as f,a2 as b,g as p,h as d,a3 as g}from"./VE8Jor13.js";let s=!1,i=Symbol();function y(e,n,r){const u=r[n]??(r[n]={store:null,source:b(void 0),unsubscribe:f});if(u.store!==e&&!(i in r))if(u.unsubscribe(),u.store=e??null,e==null)u.source.v=void 0,u.unsubscribe=f;else{var t=!0;u.unsubscribe=c(e,a=>{t?u.source.v=a:d(u.source,a)}),t=!1}return e&&i in r?l(e):p(u.source)}function m(){const e={};function n(){o(()=>{for(var r in e)e[r].unsubscribe();g(e,i,{enumerable:!1,value:!0})})}return[e,n]}function N(e){var n=s;try{return s=!1,[e(),s]}finally{s=n}}export{y as a,N as c,m as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js.br b/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js.br deleted file mode 100644 index 38d780b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js.gz b/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js.gz deleted file mode 100644 index 4925fdb..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/AcZBvMXu.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js b/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js new file mode 100644 index 0000000..964c791 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js @@ -0,0 +1 @@ +import{b as T,N as o,ab as b,E as h,ac as R,ax as p,ad as A,ae as E,T as g,R as l}from"./CpWkWWOo.js";import{B as v}from"./DdEqwvdI.js";function m(t,c,u=!1){o&&b();var n=new v(t),_=u?h:0;function i(a,r){if(o){const e=R(t);var s;if(e===p?s=0:e===A?s=!1:s=parseInt(e.substring(1)),a!==s){var f=E();g(f),n.anchor=f,l(!1),n.ensure(a,r),l(!0);return}}n.ensure(a,r)}T(()=>{var a=!1;c((r,s=0)=>{a=!0,i(s,r)}),a||i(!1,null)},_)}export{m as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js.br b/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js.br new file mode 100644 index 0000000..ca1901d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js.gz b/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js.gz new file mode 100644 index 0000000..4c37878 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/B4yTwGkE.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js.br b/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js.br deleted file mode 100644 index be20c60..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js.gz b/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js.gz deleted file mode 100644 index 670ed4d..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js new file mode 100644 index 0000000..e8f917c --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js @@ -0,0 +1,2 @@ +const e=[...` +\r\f \v\uFEFF`];function o(t,f,u){var n=t==null?"":""+t;if(f&&(n=n?n+" "+f:f),u){for(var s of Object.keys(u))if(u[s])n=n?n+" "+s:s;else if(n.length)for(var i=s.length,l=0;(l=n.indexOf(s,l))>=0;){var r=l+i;(l===0||e.includes(n[l-1]))&&(r===n.length||e.includes(n[r]))?n=(l===0?"":n.substring(0,l))+n.substring(r+1):l=r}}return n===""?null:n}function c(t,f){return t==null?null:String(t)}export{c as a,o as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.br b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.br new file mode 100644 index 0000000..262d659 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.gz b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.gz new file mode 100644 index 0000000..967efc3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BKuqSeVd.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js b/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js deleted file mode 100644 index 9457808..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js +++ /dev/null @@ -1 +0,0 @@ -const e={fact:"#00A8FF",concept:"#9D00FF",event:"#FFB800",person:"#00FFD1",place:"#00D4FF",note:"#8B95A5",pattern:"#FF3CAC",decision:"#FF4757"},F={MemoryCreated:"#00FFD1",MemoryUpdated:"#00A8FF",MemoryDeleted:"#FF4757",MemoryPromoted:"#00FF88",MemoryDemoted:"#FF6B35",MemorySuppressed:"#A33FFF",MemoryUnsuppressed:"#14E8C6",Rac1CascadeSwept:"#6E3FFF",SearchPerformed:"#818CF8",DreamStarted:"#9D00FF",DreamProgress:"#B44AFF",DreamCompleted:"#C084FC",ConsolidationStarted:"#FFB800",ConsolidationCompleted:"#FF9500",RetentionDecayed:"#FF4757",ConnectionDiscovered:"#00D4FF",ActivationSpread:"#14E8C6",ImportanceScored:"#FF3CAC",Heartbeat:"#8B95A5"};export{F as E,e as N}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js.br b/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js.br deleted file mode 100644 index bda5d36..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js.gz b/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js.gz deleted file mode 100644 index 7420359..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BNytumrp.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js b/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js deleted file mode 100644 index 86a004d..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js +++ /dev/null @@ -1 +0,0 @@ -import{t as l}from"./ByItJEsC.js";import{T as e}from"./VE8Jor13.js";function u(s,c,r,f,p,i){var a=s.__className;if(e||a!==r||a===void 0){var t=l(r);(!e||t!==s.getAttribute("class"))&&(t==null?s.removeAttribute("class"):s.className=t),s.__className=r}return i}export{u as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js.br b/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js.br deleted file mode 100644 index d86d151..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js.gz b/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js.gz deleted file mode 100644 index 026e36d..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BR2EHpd7.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js b/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js deleted file mode 100644 index 43f4379..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js +++ /dev/null @@ -1 +0,0 @@ -import{Q as m,R as _,m as b,l as i,T as y,U as v,V as h}from"./VE8Jor13.js";function E(e,l,u=l){var f=new WeakSet;m(e,"input",async r=>{var a=r?e.defaultValue:e.value;if(a=t(e)?o(a):a,u(a),v!==null&&f.add(v),await _(),a!==(a=l())){var d=e.selectionStart,s=e.selectionEnd,n=e.value.length;if(e.value=a??"",s!==null){var c=e.value.length;d===s&&s===n&&c>n?(e.selectionStart=c,e.selectionEnd=c):(e.selectionStart=d,e.selectionEnd=Math.min(s,c))}}}),(y&&e.defaultValue!==e.value||b(l)==null&&e.value)&&(u(t(e)?o(e.value):e.value),v!==null&&f.add(v)),i(()=>{var r=l();if(e===document.activeElement){var a=h??v;if(f.has(a))return}t(e)&&r===o(e.value)||e.type==="date"&&!r&&!e.value||r!==e.value&&(e.value=r??"")})}function t(e){var l=e.type;return l==="number"||l==="range"}function o(e){return e===""?null:+e}export{E as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js.br b/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js.br deleted file mode 100644 index dfcb754..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js.gz b/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js.gz deleted file mode 100644 index d28c30e..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BRHZEveZ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js b/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js new file mode 100644 index 0000000..41853ff --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js @@ -0,0 +1 @@ +import{az as g,aA as d,aB as c,v as m,aC as i,aD as b,g as p,aE as v,z as h,aF as k}from"./CpWkWWOo.js";function x(t=!1){const a=g,e=a.l.u;if(!e)return;let f=()=>v(a.s);if(t){let n=0,s={};const _=h(()=>{let l=!1;const r=a.s;for(const o in r)r[o]!==s[o]&&(s[o]=r[o],l=!0);return l&&n++,n});f=()=>p(_)}e.b.length&&d(()=>{u(a,f),i(e.b)}),c(()=>{const n=m(()=>e.m.map(b));return()=>{for(const s of n)typeof s=="function"&&s()}}),e.a.length&&c(()=>{u(a,f),i(e.a)})}function u(t,a){if(t.l.s)for(const e of t.l.s)p(e);a()}k();export{x as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js.br b/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js.br new file mode 100644 index 0000000..762f929 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js.gz b/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js.gz new file mode 100644 index 0000000..d409518 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BUoSzNdg.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js b/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js deleted file mode 100644 index bf8740b..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js +++ /dev/null @@ -1 +0,0 @@ -var D=Object.defineProperty;var g=a=>{throw TypeError(a)};var F=(a,e,s)=>e in a?D(a,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):a[e]=s;var w=(a,e,s)=>F(a,typeof e!="symbol"?e+"":e,s),y=(a,e,s)=>e.has(a)||g("Cannot "+s);var t=(a,e,s)=>(y(a,e,"read from private field"),s?s.call(a):e.get(a)),l=(a,e,s)=>e.has(a)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(a):e.set(a,s),M=(a,e,s,i)=>(y(a,e,"write to private field"),i?i.call(a,s):e.set(a,s),s);import{U as x,ae as j,af as k,ag as C,ah as A,ai as B,T as S,aj as T,ak as U,al as q}from"./VE8Jor13.js";var r,n,h,u,p,_,v;class G{constructor(e,s=!0){w(this,"anchor");l(this,r,new Map);l(this,n,new Map);l(this,h,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,()=>{var e=x;if(t(this,r).has(e)){var s=t(this,r).get(e),i=t(this,n).get(s);if(i)j(i),t(this,u).delete(s);else{var c=t(this,h).get(s);c&&(t(this,n).set(s,c.effect),t(this,h).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),i=c.effect)}for(const[f,o]of t(this,r)){if(t(this,r).delete(f),f===e)break;const d=t(this,h).get(o);d&&(k(d.effect),t(this,h).delete(o))}for(const[f,o]of t(this,n)){if(f===s||t(this,u).has(f))continue;const d=()=>{if(Array.from(t(this,r).values()).includes(f)){var b=document.createDocumentFragment();U(o,b),b.append(A()),t(this,h).set(f,{effect:o,fragment:b})}else k(o);t(this,u).delete(f),t(this,n).delete(f)};t(this,p)||!i?(t(this,u).add(f),C(o,d,!1)):d()}}});l(this,v,e=>{t(this,r).delete(e);const s=Array.from(t(this,r).values());for(const[i,c]of t(this,h))s.includes(i)||(k(c.effect),t(this,h).delete(i))});this.anchor=e,M(this,p,s)}ensure(e,s){var i=x,c=q();if(s&&!t(this,n).has(e)&&!t(this,h).has(e))if(c){var f=document.createDocumentFragment(),o=A();f.append(o),t(this,h).set(e,{effect:B(()=>s(o)),fragment:f})}else t(this,n).set(e,B(()=>s(this.anchor)));if(t(this,r).set(i,e),c){for(const[d,m]of t(this,n))d===e?i.unskip_effect(m):i.skip_effect(m);for(const[d,m]of t(this,h))d===e?i.unskip_effect(m.effect):i.skip_effect(m.effect);i.oncommit(t(this,_)),i.ondiscard(t(this,v))}else S&&(this.anchor=T),t(this,_).call(this)}}r=new WeakMap,n=new WeakMap,h=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{G as B}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js.br b/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js.br deleted file mode 100644 index 7a408df..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js.gz b/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js.gz deleted file mode 100644 index edbc169..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BYWGnCkZ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js b/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js deleted file mode 100644 index 7767c47..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js +++ /dev/null @@ -1 +0,0 @@ -import{b as p,E as t}from"./VE8Jor13.js";import{B as c}from"./BYWGnCkZ.js";function E(r,s,...a){var e=new c(r);p(()=>{const n=s()??null;e.ensure(n,n&&(o=>n(o,...a)))},t)}export{E as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js.br b/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js.br deleted file mode 100644 index 58f537d..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js.gz b/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js.gz deleted file mode 100644 index a00398f..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BZYVQ1d5.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js b/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js similarity index 62% rename from apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js rename to apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js index dcf5ab0..529c227 100644 --- a/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js +++ b/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js @@ -1 +1 @@ -import{a1 as a,m as w,am as q,J as x}from"./VE8Jor13.js";function _(e,t,n){if(e==null)return t(void 0),n&&n(void 0),a;const r=w(()=>e.subscribe(t,n));return r.unsubscribe?()=>r.unsubscribe():r}const f=[];function z(e,t){return{subscribe:A(e,t).subscribe}}function A(e,t=a){let n=null;const r=new Set;function i(u){if(q(e,u)&&(e=u,n)){const o=!f.length;for(const s of r)s[1](),f.push(s,e);if(o){for(let s=0;s{r.delete(s),r.size===0&&n&&(n(),n=null)}}return{set:i,update:b,subscribe:l}}function B(e,t,n){const r=!Array.isArray(e),i=r?[e]:e;if(!i.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const b=t.length<2;return z(n,(l,u)=>{let o=!1;const s=[];let d=0,p=a;const y=()=>{if(d)return;p();const c=t(r?s[0]:s,l,u);b?l(c):p=typeof c=="function"?c:a},h=i.map((c,g)=>_(c,m=>{s[g]=m,d&=~(1<{d|=1<t=n)(),t}export{B as d,E as g,_ as s,A as w}; +import{H as a,v as m,aH as q,aC as x}from"./CpWkWWOo.js";function _(e,t,n){if(e==null)return t(void 0),n&&n(void 0),a;const r=m(()=>e.subscribe(t,n));return r.unsubscribe?()=>r.unsubscribe():r}const f=[];function z(e,t){return{subscribe:A(e,t).subscribe}}function A(e,t=a){let n=null;const r=new Set;function i(u){if(q(e,u)&&(e=u,n)){const o=!f.length;for(const s of r)s[1](),f.push(s,e);if(o){for(let s=0;s{r.delete(s),r.size===0&&n&&(n(),n=null)}}return{set:i,update:b,subscribe:l}}function k(e,t,n){const r=!Array.isArray(e),i=r?[e]:e;if(!i.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const b=t.length<2;return z(n,(l,u)=>{let o=!1;const s=[];let d=0,p=a;const y=()=>{if(d)return;p();const c=t(r?s[0]:s,l,u);b?l(c):p=typeof c=="function"?c:a},h=i.map((c,g)=>_(c,w=>{s[g]=w,d&=~(1<{d|=1<t=n)(),t}export{k as d,B as g,_ as s,A as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js.br b/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js.br new file mode 100644 index 0000000..9dea105 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js.gz b/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js.gz new file mode 100644 index 0000000..6af3a8f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BeMFXnHE.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js b/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js new file mode 100644 index 0000000..74a84e4 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js @@ -0,0 +1,2 @@ +var Me=Object.defineProperty;var ue=t=>{throw TypeError(t)};var ke=(t,e,r)=>e in t?Me(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var U=(t,e,r)=>ke(t,typeof e!="symbol"?e+"":e,r),re=(t,e,r)=>e.has(t)||ue("Cannot "+r);var s=(t,e,r)=>(re(t,e,"read from private field"),r?r.call(t):e.get(t)),c=(t,e,r)=>e.has(t)?ue("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),n=(t,e,r,a)=>(re(t,e,"write to private field"),a?a.call(t,r):e.set(t,r),r),p=(t,e,r)=>(re(t,e,"access private method"),r);import{aQ as Ie,g as Te,X as Le,v as Ve,aR as _e,Y as q,ao as we,U as M,N as k,o as B,aS as pe,b as xe,ab as Be,ad as Ce,aT as ge,ai as Y,J as me,aU as se,ar as ie,ay as He,aV as ve,aW as We,aX as ye,aY as Pe,aZ as qe,a_ as G,a$ as Z,b0 as be,b1 as ze,b2 as Re,az as Se,ag as Ue,aw as ae,T as K,n as $e,ae as je,b3 as $,E as Je,M as Qe,b4 as Xe,b5 as Ge,F as Ze,b6 as Ke,G as et,b7 as ne,V as tt,O as Ne,ax as rt,Q as st,b8 as fe,R as j,b9 as it,av as at,ba as nt,al as ft,p as ht,af as ot,bb as lt,a as ct}from"./CpWkWWOo.js";import{d as dt}from"./CHOnp4oo.js";function ut(t){let e=0,r=we(0),a;return()=>{Ie()&&(Te(r),Le(()=>(e===0&&(a=Ve(()=>t(()=>_e(r)))),e+=1,()=>{q(()=>{e-=1,e===0&&(a==null||a(),a=void 0,_e(r))})})))}}var _t=Je|Qe;function pt(t,e,r,a){new gt(t,e,r,a)}var E,z,m,L,g,R,T,w,S,V,A,C,H,W,N,ee,o,De,Ae,Oe,he,Q,X,oe;class gt{constructor(e,r,a,h){c(this,o);U(this,"parent");U(this,"is_pending",!1);U(this,"transform_error");c(this,E);c(this,z,k?M:null);c(this,m);c(this,L);c(this,g);c(this,R,null);c(this,T,null);c(this,w,null);c(this,S,null);c(this,V,0);c(this,A,0);c(this,C,!1);c(this,H,new Set);c(this,W,new Set);c(this,N,null);c(this,ee,ut(()=>(n(this,N,we(s(this,V))),()=>{n(this,N,null)})));var i;n(this,E,e),n(this,m,r),n(this,L,f=>{var u=B;u.b=this,u.f|=pe,a(f)}),this.parent=B.b,this.transform_error=h??((i=this.parent)==null?void 0:i.transform_error)??(f=>f),n(this,g,xe(()=>{if(k){const f=s(this,z);Be();const u=f.data===Ce;if(f.data.startsWith(ge)){const d=JSON.parse(f.data.slice(ge.length));p(this,o,Ae).call(this,d)}else u?p(this,o,Oe).call(this):p(this,o,De).call(this)}else p(this,o,he).call(this)},_t)),k&&n(this,E,M)}defer_effect(e){qe(e,s(this,H),s(this,W))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!s(this,m).pending}update_pending_count(e){p(this,o,oe).call(this,e),n(this,V,s(this,V)+e),!(!s(this,N)||s(this,C))&&(n(this,C,!0),q(()=>{n(this,C,!1),s(this,N)&&Ue(s(this,N),s(this,V))}))}get_effect_pending(){return s(this,ee).call(this),Te(s(this,N))}error(e){var r=s(this,m).onerror;let a=s(this,m).failed;if(!r&&!a)throw e;s(this,R)&&(ae(s(this,R)),n(this,R,null)),s(this,T)&&(ae(s(this,T)),n(this,T,null)),s(this,w)&&(ae(s(this,w)),n(this,w,null)),k&&(K(s(this,z)),$e(),K(je()));var h=!1,i=!1;const f=()=>{if(h){Ge();return}h=!0,i&&Xe(),s(this,w)!==null&&ie(s(this,w),()=>{n(this,w,null)}),p(this,o,X).call(this,()=>{se.ensure(),p(this,o,he).call(this)})},u=l=>{try{i=!0,r==null||r(l,f),i=!1}catch(d){$(d,s(this,g)&&s(this,g).parent)}a&&n(this,w,p(this,o,X).call(this,()=>{se.ensure();try{return Y(()=>{var d=B;d.b=this,d.f|=pe,a(s(this,E),()=>l,()=>f)})}catch(d){return $(d,s(this,g).parent),null}}))};q(()=>{var l;try{l=this.transform_error(e)}catch(d){$(d,s(this,g)&&s(this,g).parent);return}l!==null&&typeof l=="object"&&typeof l.then=="function"?l.then(u,d=>$(d,s(this,g)&&s(this,g).parent)):u(l)})}}E=new WeakMap,z=new WeakMap,m=new WeakMap,L=new WeakMap,g=new WeakMap,R=new WeakMap,T=new WeakMap,w=new WeakMap,S=new WeakMap,V=new WeakMap,A=new WeakMap,C=new WeakMap,H=new WeakMap,W=new WeakMap,N=new WeakMap,ee=new WeakMap,o=new WeakSet,De=function(){try{n(this,R,Y(()=>s(this,L).call(this,s(this,E))))}catch(e){this.error(e)}},Ae=function(e){const r=s(this,m).failed;r&&n(this,w,Y(()=>{r(s(this,E),()=>e,()=>()=>{})}))},Oe=function(){const e=s(this,m).pending;e&&(this.is_pending=!0,n(this,T,Y(()=>e(s(this,E)))),q(()=>{var r=n(this,S,document.createDocumentFragment()),a=me();r.append(a),n(this,R,p(this,o,X).call(this,()=>(se.ensure(),Y(()=>s(this,L).call(this,a))))),s(this,A)===0&&(s(this,E).before(r),n(this,S,null),ie(s(this,T),()=>{n(this,T,null)}),p(this,o,Q).call(this))}))},he=function(){try{if(this.is_pending=this.has_pending_snippet(),n(this,A,0),n(this,V,0),n(this,R,Y(()=>{s(this,L).call(this,s(this,E))})),s(this,A)>0){var e=n(this,S,document.createDocumentFragment());He(s(this,R),e);const r=s(this,m).pending;n(this,T,Y(()=>r(s(this,E))))}else p(this,o,Q).call(this)}catch(r){this.error(r)}},Q=function(){this.is_pending=!1;for(const e of s(this,H))ve(e,We),ye(e);for(const e of s(this,W))ve(e,Pe),ye(e);s(this,H).clear(),s(this,W).clear()},X=function(e){var r=B,a=Re,h=Se;G(s(this,g)),Z(s(this,g)),be(s(this,g).ctx);try{return e()}catch(i){return ze(i),null}finally{G(r),Z(a),be(h)}},oe=function(e){var r;if(!this.has_pending_snippet()){this.parent&&p(r=this.parent,o,oe).call(r,e);return}n(this,A,s(this,A)+e),s(this,A)===0&&(p(this,o,Q).call(this),s(this,T)&&ie(s(this,T),()=>{n(this,T,null)}),s(this,S)&&(s(this,E).before(s(this,S)),n(this,S,null)))};const vt=["touchstart","touchmove"];function yt(t){return vt.includes(t)}const I=Symbol("events"),Fe=new Set,le=new Set;function bt(t,e,r,a={}){function h(i){if(a.capture||ce.call(e,i),!i.cancelBubble)return Ke(()=>r==null?void 0:r.call(this,i))}return t.startsWith("pointer")||t.startsWith("touch")||t==="wheel"?q(()=>{e.addEventListener(t,h,a)}):e.addEventListener(t,h,a),h}function Rt(t,e,r,a,h){var i={capture:a,passive:h},f=bt(t,e,r,i);(e===document.body||e===window||e===document||e instanceof HTMLMediaElement)&&Ze(()=>{e.removeEventListener(t,f,i)})}function St(t,e,r){(e[I]??(e[I]={}))[t]=r}function Nt(t){for(var e=0;e{throw F});throw D}}finally{t[I]=e,delete t.currentTarget,Z(x),G(P)}}}function Dt(t,e){var r=e==null?"":typeof e=="object"?e+"":e;r!==(t.__t??(t.__t=t.nodeValue))&&(t.__t=r,t.nodeValue=r+"")}function Et(t,e){return Ye(t,e)}function At(t,e){ne(),e.intro=e.intro??!1;const r=e.target,a=k,h=M;try{for(var i=tt(r);i&&(i.nodeType!==Ne||i.data!==rt);)i=st(i);if(!i)throw fe;j(!0),K(i);const f=Ye(t,{...e,anchor:i});return j(!1),f}catch(f){if(f instanceof Error&&f.message.split(` +`).some(u=>u.startsWith("https://svelte.dev/e/")))throw f;return f!==fe&&console.warn("Failed to hydrate: ",f),e.recover===!1&&it(),ne(),at(r),j(!1),Et(t,e)}finally{j(a),K(h)}}const J=new Map;function Ye(t,{target:e,anchor:r,props:a={},events:h,context:i,intro:f=!0,transformError:u}){ne();var l=void 0,d=nt(()=>{var x=r??e.appendChild(me());pt(x,{pending:()=>{}},v=>{ht({});var _=Se;if(i&&(_.c=i),h&&(a.$$events=h),k&&dt(v,null),l=t(v,a)||{},k&&(B.nodes.end=M,M===null||M.nodeType!==Ne||M.data!==ot))throw lt(),fe;ct()},u);var P=new Set,D=v=>{for(var _=0;_{var O;for(var v of P)for(const b of[e,document]){var _=J.get(b),y=_.get(v);--y==0?(b.removeEventListener(v,ce),_.delete(v),_.size===0&&J.delete(b)):_.set(v,y)}le.delete(D),x!==r&&((O=x.parentNode)==null||O.removeChild(x))}});return de.set(l,d),l}let de=new WeakMap;function Ot(t,e){const r=de.get(t);return r?(de.delete(t),r(e)):Promise.resolve()}export{St as a,Nt as d,Rt as e,At as h,Et as m,Dt as s,Ot as u}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js.br b/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js.br new file mode 100644 index 0000000..75ea48d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js.gz b/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js.gz new file mode 100644 index 0000000..5593b24 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BlVfL1ME.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js b/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js similarity index 51% rename from apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js rename to apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js index 7830d83..227886d 100644 --- a/apps/dashboard/build/_app/immutable/chunks/B5Pq2mnD.js +++ b/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js @@ -1 +1 @@ -import{Q as s,k as o,a0 as c,a4 as b,a5 as m,a6 as h,U as v,V as y}from"./VE8Jor13.js";function _(e,r,f=!1){if(e.multiple){if(r==null)return;if(!b(r))return m();for(var a of e.options)a.selected=r.includes(i(a));return}for(a of e.options){var t=i(a);if(h(t,r)){a.selected=!0;return}}(!f||r!==void 0)&&(e.selectedIndex=-1)}function q(e){var r=new MutationObserver(()=>{_(e,e.__value)});r.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),c(()=>{r.disconnect()})}function k(e,r,f=r){var a=new WeakSet,t=!0;s(e,"change",u=>{var l=u?"[selected]":":checked",n;if(e.multiple)n=[].map.call(e.querySelectorAll(l),i);else{var d=e.querySelector(l)??e.querySelector("option:not([disabled])");n=d&&i(d)}f(n),v!==null&&a.add(v)}),o(()=>{var u=r();if(e===document.activeElement){var l=y??v;if(a.has(l))return}if(_(e,u,t),t&&u===void 0){var n=e.querySelector(":checked");n!==null&&(u=i(n),f(u))}e.__value=u,t=!1}),q(e)}function i(e){return"__value"in e?e.__value:e.value}export{k as b}; +import{Z as s,_ as v,W as o,F as c,a7 as b,a8 as m,a9 as h,a0 as y}from"./CpWkWWOo.js";function d(e,r,f=!1){if(e.multiple){if(r==null)return;if(!b(r))return m();for(var a of e.options)a.selected=r.includes(i(a));return}for(a of e.options){var t=i(a);if(h(t,r)){a.selected=!0;return}}(!f||r!==void 0)&&(e.selectedIndex=-1)}function q(e){var r=new MutationObserver(()=>{d(e,e.__value)});r.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),c(()=>{r.disconnect()})}function p(e,r,f=r){var a=new WeakSet,t=!0;s(e,"change",u=>{var l=u?"[selected]":":checked",n;if(e.multiple)n=[].map.call(e.querySelectorAll(l),i);else{var _=e.querySelector(l)??e.querySelector("option:not([disabled])");n=_&&i(_)}f(n),v!==null&&a.add(v)}),o(()=>{var u=r();if(e===document.activeElement){var l=y??v;if(a.has(l))return}if(d(e,u,t),t&&u===void 0){var n=e.querySelector(":checked");n!==null&&(u=i(n),f(u))}e.__value=u,t=!1}),q(e)}function i(e){return"__value"in e?e.__value:e.value}export{p as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js.br b/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js.br new file mode 100644 index 0000000..ca545db Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js.gz b/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js.gz new file mode 100644 index 0000000..4aac3c2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BnXDGOmJ.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js b/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js deleted file mode 100644 index dc22cea..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js +++ /dev/null @@ -1 +0,0 @@ -import{ah as F,b as fe,an as ae,T as D,ac as q,ao as ie,a7 as le,g as W,a8 as ue,aa as se,ab as Z,ad as L,aj as z,ap as oe,aq as te,ar as $,U as ve,as as T,ai as y,at as de,al as ce,C as pe,a4 as _e,au as V,av as he,aw as ge,a2 as Ee,ax as j,ay as me,ae as ne,ag as re,az as B,q as Te,aA as Ce,aB as Ae,aC as we,af as Se,aD as Ie}from"./VE8Jor13.js";function De(e,n){return n}function Ne(e,n,l){for(var t=[],g=n.length,s,u=n.length,c=0;c{if(s){if(s.pending.delete(E),s.done.add(E),s.pending.size===0){var o=e.outrogroups;U(V(s.done)),o.delete(s),o.size===0&&(e.outrogroups=null)}}else u-=1},!1)}if(u===0){var i=t.length===0&&l!==null;if(i){var v=l,r=v.parentNode;we(r),r.append(v),e.items.clear()}U(n,!i)}else s={pending:new Set(n),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(s)}function U(e,n=!0){for(var l=0;l{var f=l();return _e(f)?f:f==null?[]:V(f)}),o,d=!0;function A(){a.fallback=r,xe(a,o,u,n,t),r!==null&&(o.length===0?(r.f&T)===0?ne(r):(r.f^=T,M(r,null,u)):re(r,()=>{r=null}))}var I=fe(()=>{o=W(E);var f=o.length;let N=!1;if(D){var x=ue(u)===se;x!==(f===0)&&(u=Z(),q(u),L(!1),N=!0)}for(var _=new Set,w=ve,b=ce(),p=0;ps(u)):(r=y(()=>s(ee??(ee=F()))),r.f|=T)),f>_.size&&de(),D&&f>0&&q(Z()),!d)if(b){for(const[k,O]of c)_.has(k)||w.skip_effect(O.e);w.oncommit(A),w.ondiscard(()=>{})}else A();N&&L(!0),W(E)}),a={effect:I,items:c,outrogroups:null,fallback:r};d=!1,D&&(u=z)}function H(e){for(;e!==null&&(e.f&Ce)===0;)e=e.next;return e}function xe(e,n,l,t,g){var h,k,O,Y,X,G,J,K,P;var s=(t&Ae)!==0,u=n.length,c=e.items,i=H(e.effect.first),v,r=null,E,o=[],d=[],A,I,a,f;if(s)for(f=0;f0){var R=(t&ae)!==0&&u===0?l:null;if(s){for(f=0;f{var m,Q;if(E!==void 0)for(a of E)(Q=(m=a.nodes)==null?void 0:m.a)==null||Q.apply()})}function be(e,n,l,t,g,s,u,c){var i=(u&he)!==0?(u&ge)===0?Ee(l,!1,!1):j(l):null,v=(u&me)!==0?j(g):null;return{v:i,i:v,e:y(()=>(s(n,i??l,v??g,c),()=>{e.delete(t)}))}}function M(e,n,l){if(e.nodes)for(var t=e.nodes.start,g=e.nodes.end,s=n&&(n.f&T)===0?n.nodes.start:l;t!==null;){var u=Ie(t);if(s.before(t),t===g)return;t=u}}function C(e,n,l){n===null?e.effect.first=l:n.next=l,l===null?e.effect.last=n:l.prev=n}function Me(e,n,l){var t=e==null?"":""+e;return t===""?null:t}function ke(e,n){return e==null?null:String(e)}export{ke as a,He as e,De as i,Me as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js.br b/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js.br deleted file mode 100644 index adbb90b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js.gz b/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js.gz deleted file mode 100644 index 92434ba..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ByItJEsC.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz b/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz index f429ed1..0311328 100644 Binary files a/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz and b/apps/dashboard/build/_app/immutable/chunks/Bzak7iHL.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C4h_mRt2.js b/apps/dashboard/build/_app/immutable/chunks/C4h_mRt2.js new file mode 100644 index 0000000..d271740 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/C4h_mRt2.js @@ -0,0 +1 @@ +import{J as y,b as u,K as _,M as o,N as t,O as g,Q as i,R as l,T as d,U as p,V as m}from"./CpWkWWOo.js";function T(n,r){let s=null,E=t;var a;if(t){s=p;for(var e=m(document.head);e!==null&&(e.nodeType!==g||e.data!==n);)e=i(e);if(e===null)l(!1);else{var f=i(e);e.remove(),d(f)}}t||(a=document.head.appendChild(y()));try{u(()=>r(a),_|o)}finally{E&&(l(!0),d(s))}}export{T as h}; diff --git a/apps/dashboard/build/_app/immutable/chunks/C4h_mRt2.js.br b/apps/dashboard/build/_app/immutable/chunks/C4h_mRt2.js.br new file mode 100644 index 0000000..75d6e99 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/C4h_mRt2.js.br @@ -0,0 +1,2 @@ +v`)wKքMڜ˿Q@ƺW vEYtcG.h2DJКdS $gdD1,Jy>U`+& +՟xS{t?u.source.v=f:g(u.source,f)}),t=!1}return e&&i in r?l(e):d(u.source)}function m(){const e={};function n(){o(()=>{for(var r in e)e[r].unsubscribe();b(e,i,{enumerable:!1,value:!0})})}return[e,n]}function I(e){var n=s;try{return s=!1,[e(),s]}finally{s=n}}export{y as a,I as c,m as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/C6HuKgyx.js.br b/apps/dashboard/build/_app/immutable/chunks/C6HuKgyx.js.br new file mode 100644 index 0000000..7f962dd Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/C6HuKgyx.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C6HuKgyx.js.gz b/apps/dashboard/build/_app/immutable/chunks/C6HuKgyx.js.gz new file mode 100644 index 0000000..0db39b3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/C6HuKgyx.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js.br b/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js.br deleted file mode 100644 index a22881c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js.gz b/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js.gz deleted file mode 100644 index 53b1169..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CCRrbKqn.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js b/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js new file mode 100644 index 0000000..040e765 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js @@ -0,0 +1 @@ +import{J as z,b as fe,aa as re,N as k,T as L,V as ie,ab as le,g as Z,ac as ue,ad as se,ae as $,R as q,U as F,O as oe,af as ve,ag as y,_ as te,ah as T,ai as Y,aj as de,ak as ce,A as pe,a7 as _e,al as U,am as he,an as ge,I as Ee,ao as j,ap as me,aq as ne,ar as ae,as as V,Y as Te,at as Ae,au as Ce,av as we,aw as Ie,Q as Ne}from"./CpWkWWOo.js";function ke(e,i){return i}function Se(e,i,l){for(var t=[],g=i.length,s,u=i.length,c=0;c{if(s){if(s.pending.delete(E),s.done.add(E),s.pending.size===0){var o=e.outrogroups;B(U(s.done)),o.delete(s),o.size===0&&(e.outrogroups=null)}}else u-=1},!1)}if(u===0){var f=t.length===0&&l!==null;if(f){var v=l,n=v.parentNode;we(n),n.append(v),e.items.clear()}B(i,!f)}else s={pending:new Set(i),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(s)}function B(e,i=!0){for(var l=0;l{var a=l();return _e(a)?a:a==null?[]:U(a)}),o,d=!0;function C(){r.fallback=n,xe(r,o,u,i,t),n!==null&&(o.length===0?(n.f&T)===0?ne(n):(n.f^=T,M(n,null,u)):ae(n,()=>{n=null}))}var N=fe(()=>{o=Z(E);var a=o.length;let S=!1;if(k){var x=ue(u)===se;x!==(a===0)&&(u=$(),L(u),q(!1),S=!0)}for(var _=new Set,w=te,R=ce(),p=0;ps(u)):(n=Y(()=>s(ee??(ee=z()))),n.f|=T)),a>_.size&&de(),k&&a>0&&L($()),!d)if(R){for(const[O,D]of c)_.has(O)||w.skip_effect(D.e);w.oncommit(C),w.ondiscard(()=>{})}else C();S&&q(!0),Z(E)}),r={effect:N,items:c,outrogroups:null,fallback:n};d=!1,k&&(u=F)}function H(e){for(;e!==null&&(e.f&Ae)===0;)e=e.next;return e}function xe(e,i,l,t,g){var h,O,D,J,Q,X,G,K,P;var s=(t&Ce)!==0,u=i.length,c=e.items,f=H(e.effect.first),v,n=null,E,o=[],d=[],C,N,r,a;if(s)for(a=0;a0){var b=(t&re)!==0&&u===0?l:null;if(s){for(a=0;a{var m,W;if(E!==void 0)for(r of E)(W=(m=r.nodes)==null?void 0:m.a)==null||W.apply()})}function Re(e,i,l,t,g,s,u,c){var f=(u&he)!==0?(u&ge)===0?Ee(l,!1,!1):j(l):null,v=(u&me)!==0?j(g):null;return{v:f,i:v,e:Y(()=>(s(i,f??l,v??g,c),()=>{e.delete(t)}))}}function M(e,i,l){if(e.nodes)for(var t=e.nodes.start,g=e.nodes.end,s=i&&(i.f&T)===0?i.nodes.start:l;t!==null;){var u=Ne(t);if(s.before(t),t===g)return;t=u}}function A(e,i,l){i===null?e.effect.first=l:i.next=l,l===null?e.effect.last=i:l.prev=i}export{He as e,ke as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js.br b/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js.br new file mode 100644 index 0000000..f66f2f9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js.gz b/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js.gz new file mode 100644 index 0000000..1efc186 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CGEBXrjl.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js b/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js new file mode 100644 index 0000000..50e502d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js @@ -0,0 +1 @@ +import{aI as M,J as v,V as o,aJ as y,o as T,aK as p,aL as b,N as d,U as i,aM as w,ab as x,aN as A,T as L,aO as C}from"./CpWkWWOo.js";var h;const m=((h=globalThis==null?void 0:globalThis.window)==null?void 0:h.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:e=>e});function O(e){return(m==null?void 0:m.createHTML(e))??e}function g(e){var a=M("template");return a.innerHTML=O(e.replaceAll("","")),a.content}function n(e,a){var r=T;r.nodes===null&&(r.nodes={start:e,end:a,a:null,t:null})}function R(e,a){var r=(a&p)!==0,f=(a&b)!==0,s,c=!e.startsWith("");return()=>{if(d)return n(i,null),i;s===void 0&&(s=g(c?e:""+e),r||(s=o(s)));var t=f||y?document.importNode(s,!0):s.cloneNode(!0);if(r){var _=o(t),u=t.lastChild;n(_,u)}else n(t,t);return t}}function D(e,a,r="svg"){var f=!e.startsWith(""),s=(a&p)!==0,c=`<${r}>${f?e:""+e}`,t;return()=>{if(d)return n(i,null),i;if(!t){var _=g(c),u=o(_);if(s)for(t=document.createDocumentFragment();o(u);)t.appendChild(o(u));else t=o(u)}var l=t.cloneNode(!0);if(s){var E=o(l),N=l.lastChild;n(E,N)}else n(l,l);return l}}function F(e,a){return D(e,a,"svg")}function H(e=""){if(!d){var a=v(e+"");return n(a,a),a}var r=i;return r.nodeType!==A?(r.before(r=v()),L(r)):C(r),n(r,r),r}function I(){if(d)return n(i,null),i;var e=document.createDocumentFragment(),a=document.createComment(""),r=v();return e.append(a,r),n(a,r),e}function $(e,a){if(d){var r=T;((r.f&w)===0||r.nodes.end===null)&&(r.nodes.end=i),x();return}e!==null&&e.before(a)}export{$ as a,F as b,I as c,n as d,R as f,H as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js.br b/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js.br new file mode 100644 index 0000000..e88d0d1 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js.gz b/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js.gz new file mode 100644 index 0000000..a48f902 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CHOnp4oo.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js b/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js new file mode 100644 index 0000000..e19c316 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js @@ -0,0 +1 @@ +import{b as p,E as t}from"./CpWkWWOo.js";import{B as c}from"./DdEqwvdI.js";function E(r,s,...a){var e=new c(r);p(()=>{const n=s()??null;e.ensure(n,n&&(o=>n(o,...a)))},t)}export{E as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js.br b/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js.br new file mode 100644 index 0000000..8c14746 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js.gz b/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js.gz new file mode 100644 index 0000000..b91f18a Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CJCPY1OL.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js b/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js new file mode 100644 index 0000000..0b355f6 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js @@ -0,0 +1 @@ +import{W as S,X as h,v as k,Y as T,S as Y}from"./CpWkWWOo.js";function t(r,i){return r===i||(r==null?void 0:r[Y])===i}function x(r={},i,a,c){return S(()=>{var f,s;return h(()=>{f=s,s=[],k(()=>{r!==a(...s)&&(i(r,...s),f&&t(a(...f),r)&&i(null,...f))})}),()=>{T(()=>{s&&t(a(...s),r)&&i(null,...s)})}}),r}export{x as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js.br b/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js.br new file mode 100644 index 0000000..4f22310 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js.gz b/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js.gz new file mode 100644 index 0000000..fdb26f0 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CJsMJEun.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js.br b/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js.br deleted file mode 100644 index 102259b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js.gz b/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js.gz deleted file mode 100644 index f563d82..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js new file mode 100644 index 0000000..b9a927a --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js @@ -0,0 +1 @@ +const e={fact:"#00A8FF",concept:"#9D00FF",event:"#FFB800",person:"#00FFD1",place:"#00D4FF",note:"#8B95A5",pattern:"#FF3CAC",decision:"#FF4757"},F={MemoryCreated:"#00FFD1",MemoryUpdated:"#00A8FF",MemoryDeleted:"#FF4757",MemoryPromoted:"#00FF88",MemoryDemoted:"#FF6B35",MemorySuppressed:"#A33FFF",MemoryUnsuppressed:"#14E8C6",Rac1CascadeSwept:"#6E3FFF",SearchPerformed:"#818CF8",DeepReferenceCompleted:"#C4B5FD",HookVerdictRecorded:"#F59E0B",DreamStarted:"#9D00FF",DreamProgress:"#B44AFF",DreamCompleted:"#C084FC",ConsolidationStarted:"#FFB800",ConsolidationCompleted:"#FF9500",RetentionDecayed:"#FF4757",ConnectionDiscovered:"#00D4FF",ActivationSpread:"#14E8C6",ImportanceScored:"#FF3CAC",Heartbeat:"#8B95A5"};export{F as E,e as N}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.br b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.br new file mode 100644 index 0000000..e5e0cfe Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.gz b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.gz new file mode 100644 index 0000000..3353949 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CcUbQ_Wl.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CpWkWWOo.js b/apps/dashboard/build/_app/immutable/chunks/CpWkWWOo.js new file mode 100644 index 0000000..5b4d2bc --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CpWkWWOo.js @@ -0,0 +1 @@ +var cn=Object.defineProperty;var wt=e=>{throw TypeError(e)};var _n=(e,t,n)=>t in e?cn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var de=(e,t,n)=>_n(e,typeof t!="symbol"?t+"":t,n),Ke=(e,t,n)=>t.has(e)||wt("Cannot "+n);var p=(e,t,n)=>(Ke(e,t,"read from private field"),n?n.call(e):t.get(e)),F=(e,t,n)=>t.has(e)?wt("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),z=(e,t,n,r)=>(Ke(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),K=(e,t,n)=>(Ke(e,t,"access private method"),n);var vn=Array.isArray,dn=Array.prototype.indexOf,me=Array.prototype.includes,lr=Array.from,or=Object.defineProperty,Re=Object.getOwnPropertyDescriptor,pn=Object.getOwnPropertyDescriptors,hn=Object.prototype,wn=Array.prototype,kt=Object.getPrototypeOf,yt=Object.isExtensible;const yn=()=>{};function ur(e){return e()}function En(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}function cr(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);const n=[];for(const r of e)if(n.push(r),n.length===t)break;return n}const A=2,De=4,Ie=8,Dt=1<<24,G=16,H=32,ve=64,mn=128,P=512,g=1024,R=2048,Y=4096,j=8192,Z=16384,oe=32768,je=65536,Et=1<<17,It=1<<18,Pe=1<<19,Pt=1<<20,_r=1<<25,ue=65536,Xe=1<<21,st=1<<22,W=1<<23,ae=Symbol("$state"),vr=Symbol("legacy props"),dr=Symbol(""),ne=new class extends Error{constructor(){super(...arguments);de(this,"name","StaleReactionError");de(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Nt;const hr=!!((Nt=globalThis.document)!=null&&Nt.contentType)&&globalThis.document.contentType.includes("xml"),Ue=3,Ct=8;function gn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function wr(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Tn(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function bn(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function An(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Sn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function yr(){throw new Error("https://svelte.dev/e/hydration_failed")}function Er(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Rn(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function On(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Nn(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function mr(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const gr=1,Tr=2,br=4,Ar=8,Sr=16,Rr=1,Or=2,Nr=4,kr=8,xr=16,Dr=1,Ir=2,kn="[",xn="[!",Pr="[?",Dn="]",ft={},T=Symbol(),In="http://www.w3.org/1999/xhtml";function it(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function Cr(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Fr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let Q=!1;function Mr(e){Q=e}let m;function ge(e){if(e===null)throw it(),ft;return m=e}function Lr(){return ge(te(m))}function jr(e){if(Q){if(te(m)!==null)throw it(),ft;m=e}}function Yr(e=1){if(Q){for(var t=e,n=m;t--;)n=te(n);m=n}}function Hr(e=!0){for(var t=0,n=m;;){if(n.nodeType===Ct){var r=n.data;if(r===Dn){if(t===0)return n;t-=1}else(r===kn||r===xn||r[0]==="["&&!isNaN(Number(r.slice(1))))&&(t+=1)}var s=te(n);e&&n.remove(),n=s}}function qr(e){if(!e||e.nodeType!==Ct)throw it(),ft;return e.data}function Ft(e){return e===this.v}function Pn(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Mt(e){return!Pn(e,this.v)}let Be=!1;function Vr(){Be=!0}let S=null;function Ye(e){S=e}function Ur(e,t=!1,n){S={p:S,i:!1,c:null,e:null,s:e,x:null,l:Be&&!t?{s:null,u:null,$:[]}:null}}function Br(e){var t=S,n=t.e;if(n!==null){t.e=null;for(var r of n)Jt(r)}return t.i=!0,S=t.p,{}}function Ce(){return!Be||S!==null&&S.l===null}let re=[];function Lt(){var e=re;re=[],En(e)}function mt(e){if(re.length===0&&!Oe){var t=re;queueMicrotask(()=>{t===re&&Lt()})}re.push(e)}function Cn(){for(;re.length>0;)Lt()}function Fn(e){var t=w;if(t===null)return _.f|=W,e;if((t.f&oe)===0&&(t.f&De)===0)throw e;He(e,t)}function He(e,t){for(;t!==null;){if((t.f&mn)!==0){if((t.f&oe)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}const Mn=-7169;function E(e,t){e.f=e.f&Mn|t}function at(e){(e.f&P)!==0||e.deps===null?E(e,g):E(e,Y)}function jt(e){if(e!==null)for(const t of e)(t.f&A)===0||(t.f&ue)===0||(t.f^=ue,jt(t.deps))}function Ln(e,t,n){(e.f&R)!==0?t.add(e):(e.f&Y)!==0&&n.add(e),jt(e.deps),E(e,g)}const Me=new Set;let d=null,gt=null,b=null,N=[],Ge=null,Ze=!1,Oe=!1;var he,we,fe,ye,ke,xe,ie,U,Ee,D,We,Je,Qe,Yt;const dt=class dt{constructor(){F(this,D);de(this,"current",new Map);de(this,"previous",new Map);F(this,he,new Set);F(this,we,new Set);F(this,fe,0);F(this,ye,0);F(this,ke,null);F(this,xe,new Set);F(this,ie,new Set);F(this,U,new Map);de(this,"is_fork",!1);F(this,Ee,!1)}skip_effect(t){p(this,U).has(t)||p(this,U).set(t,{d:[],m:[]})}unskip_effect(t){var n=p(this,U).get(t);if(n){p(this,U).delete(t);for(var r of n.d)E(r,R),B(r);for(r of n.m)E(r,Y),B(r)}}process(t){var s;N=[],this.apply();var n=[],r=[];for(const f of t)K(this,D,Je).call(this,f,n,r);if(K(this,D,We).call(this)){K(this,D,Qe).call(this,r),K(this,D,Qe).call(this,n);for(const[f,a]of p(this,U))Ut(f,a)}else{for(const f of p(this,he))f();p(this,he).clear(),p(this,fe)===0&&K(this,D,Yt).call(this),gt=this,d=null,Tt(r),Tt(n),gt=null,(s=p(this,ke))==null||s.resolve()}b=null}capture(t,n){n!==T&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&W)===0&&(this.current.set(t,t.v),b==null||b.set(t,t.v))}activate(){d=this,this.apply()}deactivate(){d===this&&(d=null,b=null)}flush(){if(this.activate(),N.length>0){if(Ht(),d!==null&&d!==this)return}else p(this,fe)===0&&this.process([]);this.deactivate()}discard(){for(const t of p(this,we))t(this);p(this,we).clear()}increment(t){z(this,fe,p(this,fe)+1),t&&z(this,ye,p(this,ye)+1)}decrement(t){z(this,fe,p(this,fe)-1),t&&z(this,ye,p(this,ye)-1),!p(this,Ee)&&(z(this,Ee,!0),mt(()=>{z(this,Ee,!1),K(this,D,We).call(this)?N.length>0&&this.flush():this.revive()}))}revive(){for(const t of p(this,xe))p(this,ie).delete(t),E(t,R),B(t);for(const t of p(this,ie))E(t,Y),B(t);this.flush()}oncommit(t){p(this,he).add(t)}ondiscard(t){p(this,we).add(t)}settled(){return(p(this,ke)??z(this,ke,xt())).promise}static ensure(){if(d===null){const t=d=new dt;Me.add(d),Oe||mt(()=>{d===t&&t.flush()})}return d}apply(){}};he=new WeakMap,we=new WeakMap,fe=new WeakMap,ye=new WeakMap,ke=new WeakMap,xe=new WeakMap,ie=new WeakMap,U=new WeakMap,Ee=new WeakMap,D=new WeakSet,We=function(){return this.is_fork||p(this,ye)>0},Je=function(t,n,r){t.f^=g;for(var s=t.first;s!==null;){var f=s.f,a=(f&(H|ve))!==0,l=a&&(f&g)!==0,i=l||(f&j)!==0||p(this,U).has(s);if(!i&&s.fn!==null){a?s.f^=g:(f&De)!==0?n.push(s):Fe(s)&&((f&G)!==0&&p(this,ie).add(s),Ae(s));var o=s.first;if(o!==null){s=o;continue}}for(;s!==null;){var c=s.next;if(c!==null){s=c;break}s=s.parent}}},Qe=function(t){for(var n=0;n1){this.previous.clear();var t=b,n=!0;for(const f of Me){if(f===this){n=!1;continue}const a=[];for(const[i,o]of this.current){if(f.current.has(i))if(n&&o!==f.current.get(i))f.current.set(i,o);else continue;a.push(i)}if(a.length===0)continue;const l=[...f.current.keys()].filter(i=>!this.current.has(i));if(l.length>0){var r=N;N=[];const i=new Set,o=new Map;for(const c of a)qt(c,l,i,o);if(N.length>0){d=f,f.apply();for(const c of N)K(s=f,D,Je).call(s,c,[],[]);f.deactivate()}N=r}}d=null,b=t}Me.delete(this)};let Te=dt;function jn(e){var t=Oe;Oe=!0;try{for(var n;;){if(Cn(),N.length===0&&(d==null||d.flush(),N.length===0))return Ge=null,n;Ht()}}finally{Oe=t}}function Ht(){Ze=!0;var e=null;try{for(var t=0;N.length>0;){var n=Te.ensure();if(t++>1e3){var r,s;Yn()}n.process(N),J.clear()}}finally{N=[],Ze=!1,Ge=null}}function Yn(){try{Sn()}catch(e){He(e,Ge)}}let M=null;function Tt(e){var t=e.length;if(t!==0){for(var n=0;n0)){J.clear();for(const s of M){if((s.f&(Z|j))!==0)continue;const f=[s];let a=s.parent;for(;a!==null;)M.has(a)&&(M.delete(a),f.push(a)),a=a.parent;for(let l=f.length-1;l>=0;l--){const i=f[l];(i.f&(Z|j))===0&&Ae(i)}}M.clear()}}M=null}}function qt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const s of e.reactions){const f=s.f;(f&A)!==0?qt(s,t,n,r):(f&(st|G))!==0&&(f&R)===0&&Vt(s,t,r)&&(E(s,R),B(s))}}function Vt(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const s of e.deps){if(me.call(t,s))return!0;if((s.f&A)!==0&&Vt(s,t,n))return n.set(s,!0),!0}return n.set(e,!1),!1}function B(e){var t=Ge=e,n=t.b;if(n!=null&&n.is_pending&&(e.f&(De|Ie|Dt))!==0&&(e.f&oe)===0){n.defer_effect(e);return}for(;t.parent!==null;){t=t.parent;var r=t.f;if(Ze&&t===w&&(r&G)!==0&&(r&It)===0&&(r&oe)!==0)return;if((r&(ve|H))!==0){if((r&g)===0)return;t.f^=g}}N.push(t)}function Ut(e,t){if(!((e.f&H)!==0&&(e.f&g)!==0)){(e.f&R)!==0?t.d.push(e):(e.f&Y)!==0&&t.m.push(e),E(e,g);for(var n=e.first;n!==null;)Ut(n,t),n=n.next}}function Hn(e,t,n,r){const s=Ce()?lt:Bn;var f=e.filter(u=>!u.settled);if(n.length===0&&f.length===0){r(t.map(s));return}var a=w,l=qn(),i=f.length===1?f[0].promise:f.length>1?Promise.all(f.map(u=>u.promise)):null;function o(u){l();try{r(u)}catch(v){(a.f&Z)===0&&He(v,a)}et()}if(n.length===0){i.then(()=>o(t.map(s)));return}function c(){l(),Promise.all(n.map(u=>Un(u))).then(u=>o([...t.map(s),...u])).catch(u=>He(u,a))}i?i.then(c):c()}function qn(){var e=w,t=_,n=S,r=d;return function(f=!0){be(e),ee(t),Ye(n),f&&(r==null||r.activate())}}function et(e=!0){be(null),ee(null),Ye(null),e&&(d==null||d.deactivate())}function Vn(){var e=w.b,t=d,n=e.is_rendered();return e.update_pending_count(1),t.increment(n),()=>{e.update_pending_count(-1),t.decrement(n)}}function lt(e){var t=A|R,n=_!==null&&(_.f&A)!==0?_:null;return w!==null&&(w.f|=Pe),{ctx:S,deps:null,effects:null,equals:Ft,f:t,fn:e,reactions:null,rv:0,v:T,wv:0,parent:n??w,ac:null}}function Un(e,t,n){w===null&&gn();var s=void 0,f=ut(T),a=!_,l=new Map;return tr(()=>{var v;var i=xt();s=i.promise;try{Promise.resolve(e()).then(i.resolve,i.reject).finally(et)}catch(y){i.reject(y),et()}var o=d;if(a){var c=Vn();(v=l.get(o))==null||v.reject(ne),l.delete(o),l.set(o,i)}const u=(y,h=void 0)=>{if(o.activate(),h)h!==ne&&(f.f|=W,nt(f,h));else{(f.f&W)!==0&&(f.f^=W),nt(f,y);for(const[V,O]of l){if(l.delete(V),V===o)break;O.reject(ne)}}c&&c()};i.promise.then(u,y=>u(null,y||"unknown"))}),er(()=>{for(const i of l.values())i.reject(ne)}),new Promise(i=>{function o(c){function u(){c===s?i(f):o(s)}c.then(u,u)}o(s)})}function Gr(e){const t=lt(e);return rn(t),t}function Bn(e){const t=lt(e);return t.equals=Mt,t}function Gn(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!zt&&$n()}return t}function $n(){zt=!1;for(const e of tt)(e.f&g)!==0&&E(e,Y),Fe(e)&&Ae(e);tt.clear()}function Kr(e,t=1){var n=pe(e),r=t===1?n++:n--;return X(e,n),r}function $e(e){X(e,e.v+1)}function Kt(e,t){var n=e.reactions;if(n!==null)for(var r=Ce(),s=n.length,f=0;f{if(le===f)return l();var i=_,o=le;ee(null),Ot(f);var c=l();return ee(i),Ot(o),c};return r&&n.set("length",$(e.length)),new Proxy(e,{defineProperty(l,i,o){(!("value"in o)||o.configurable===!1||o.enumerable===!1||o.writable===!1)&&Rn();var c=n.get(i);return c===void 0?a(()=>{var u=$(o.value);return n.set(i,u),u}):X(c,o.value,!0),!0},deleteProperty(l,i){var o=n.get(i);if(o===void 0){if(i in l){const c=a(()=>$(T));n.set(i,c),$e(s)}}else X(o,T),$e(s);return!0},get(l,i,o){var y;if(i===ae)return e;var c=n.get(i),u=i in l;if(c===void 0&&(!u||(y=Re(l,i))!=null&&y.writable)&&(c=a(()=>{var h=Se(u?l[i]:T),V=$(h);return V}),n.set(i,c)),c!==void 0){var v=pe(c);return v===T?void 0:v}return Reflect.get(l,i,o)},getOwnPropertyDescriptor(l,i){var o=Reflect.getOwnPropertyDescriptor(l,i);if(o&&"value"in o){var c=n.get(i);c&&(o.value=pe(c))}else if(o===void 0){var u=n.get(i),v=u==null?void 0:u.v;if(u!==void 0&&v!==T)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return o},has(l,i){var v;if(i===ae)return!0;var o=n.get(i),c=o!==void 0&&o.v!==T||Reflect.has(l,i);if(o!==void 0||w!==null&&(!c||(v=Re(l,i))!=null&&v.writable)){o===void 0&&(o=a(()=>{var y=c?Se(l[i]):T,h=$(y);return h}),n.set(i,o));var u=pe(o);if(u===T)return!1}return c},set(l,i,o,c){var ht;var u=n.get(i),v=i in l;if(r&&i==="length")for(var y=o;y$(T)),n.set(y+"",h))}if(u===void 0)(!v||(ht=Re(l,i))!=null&&ht.writable)&&(u=a(()=>$(void 0)),X(u,Se(o)),n.set(i,u));else{v=u.v!==T;var V=a(()=>Se(o));X(u,V)}var O=Reflect.getOwnPropertyDescriptor(l,i);if(O!=null&&O.set&&O.set.call(c,o),!v){if(r&&typeof i=="string"){var pt=n.get("length"),ze=Number(i);Number.isInteger(ze)&&ze>=pt.v&&X(pt,ze+1)}$e(s)}return!0},ownKeys(l){pe(s);var i=Reflect.ownKeys(l).filter(u=>{var v=n.get(u);return v===void 0||v.v!==T});for(var[o,c]of n)c.v!==T&&!(o in l)&&i.push(o);return i},setPrototypeOf(){On()}})}function bt(e){try{if(e!==null&&typeof e=="object"&&ae in e)return e[ae]}catch{}return e}function $r(e,t){return Object.is(bt(e),bt(t))}var At,Xn,Zn,$t,Xt;function Xr(){if(At===void 0){At=window,Xn=document,Zn=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;$t=Re(t,"firstChild").get,Xt=Re(t,"nextSibling").get,yt(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),yt(n)&&(n.__t=void 0)}}function qe(e=""){return document.createTextNode(e)}function Ve(e){return $t.call(e)}function te(e){return Xt.call(e)}function Zr(e,t){if(!Q)return Ve(e);var n=Ve(m);if(n===null)n=m.appendChild(qe());else if(t&&n.nodeType!==Ue){var r=qe();return n==null||n.before(r),ge(r),r}return t&&ct(n),ge(n),n}function Wr(e,t=!1){if(!Q){var n=Ve(e);return n instanceof Comment&&n.data===""?te(n):n}if(t){if((m==null?void 0:m.nodeType)!==Ue){var r=qe();return m==null||m.before(r),ge(r),r}ct(m)}return m}function Jr(e,t=1,n=!1){let r=Q?m:e;for(var s;t--;)s=r,r=te(r);if(!Q)return r;if(n){if((r==null?void 0:r.nodeType)!==Ue){var f=qe();return r===null?s==null||s.after(f):r.before(f),ge(f),f}ct(r)}return ge(r),r}function Wn(e){e.textContent=""}function Qr(){return!1}function es(e,t,n){return document.createElementNS(In,e,void 0)}function ct(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===Ue;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function ts(e){Q&&Ve(e)!==null&&Wn(e)}let St=!1;function Jn(){St||(St=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const n of e.target.elements)(t=n.__on_r)==null||t.call(n)})},{capture:!0}))}function _t(e){var t=_,n=w;ee(null),be(null);try{return e()}finally{ee(t),be(n)}}function ns(e,t,n,r=n){e.addEventListener(t,()=>_t(n));const s=e.__on_r;s?e.__on_r=()=>{s(),r(!0)}:e.__on_r=()=>r(!0),Jn()}function Zt(e){w===null&&(_===null&&An(),bn()),_e&&Tn()}function Qn(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function q(e,t,n){var r=w;r!==null&&(r.f&j)!==0&&(e|=j);var s={ctx:S,deps:null,nodes:null,f:e|R|P,first:null,fn:t,last:null,next:null,parent:r,b:r&&r.b,prev:null,teardown:null,wv:0,ac:null};if(n)try{Ae(s)}catch(l){throw ce(s),l}else t!==null&&B(s);var f=s;if(n&&f.deps===null&&f.teardown===null&&f.nodes===null&&f.first===f.last&&(f.f&Pe)===0&&(f=f.first,(e&G)!==0&&(e&je)!==0&&f!==null&&(f.f|=je)),f!==null&&(f.parent=r,r!==null&&Qn(f,r),_!==null&&(_.f&A)!==0&&(e&ve)===0)){var a=_;(a.effects??(a.effects=[])).push(f)}return s}function Wt(){return _!==null&&!L}function er(e){const t=q(Ie,null,!1);return E(t,g),t.teardown=e,t}function rs(e){Zt();var t=w.f,n=!_&&(t&H)!==0&&(t&oe)===0;if(n){var r=S;(r.e??(r.e=[])).push(e)}else return Jt(e)}function Jt(e){return q(De|Pt,e,!1)}function ss(e){return Zt(),q(Ie|Pt,e,!0)}function fs(e){Te.ensure();const t=q(ve|Pe,e,!0);return(n={})=>new Promise(r=>{n.outro?sr(t,()=>{ce(t),r(void 0)}):(ce(t),r(void 0))})}function is(e){return q(De,e,!1)}function tr(e){return q(st|Pe,e,!0)}function as(e,t=0){return q(Ie|t,e,!0)}function ls(e,t=[],n=[],r=[]){Hn(r,t,n,s=>{q(Ie,()=>e(...s.map(pe)),!0)})}function os(e,t=0){var n=q(G|t,e,!0);return n}function us(e){return q(H|Pe,e,!0)}function Qt(e){var t=e.teardown;if(t!==null){const n=_e,r=_;Rt(!0),ee(null);try{t.call(null)}finally{Rt(n),ee(r)}}}function vt(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const s=n.ac;s!==null&&_t(()=>{s.abort(ne)});var r=n.next;(n.f&ve)!==0?n.parent=null:ce(n,t),n=r}}function nr(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&H)===0&&ce(t),t=n}}function ce(e,t=!0){var n=!1;(t||(e.f&It)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(rr(e.nodes.start,e.nodes.end),n=!0),vt(e,t&&!n),Ne(e,0),E(e,Z);var r=e.nodes&&e.nodes.t;if(r!==null)for(const f of r)f.stop();Qt(e);var s=e.parent;s!==null&&s.first!==null&&en(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=null}function rr(e,t){for(;e!==null;){var n=e===t?null:te(e);e.remove(),e=n}}function en(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function sr(e,t,n=!0){var r=[];tn(e,r,!0);var s=()=>{n&&ce(e),t&&t()},f=r.length;if(f>0){var a=()=>--f||s();for(var l of r)l.out(a)}else s()}function tn(e,t,n){if((e.f&j)===0){e.f^=j;var r=e.nodes&&e.nodes.t;if(r!==null)for(const l of r)(l.is_global||n)&&t.push(l);for(var s=e.first;s!==null;){var f=s.next,a=(s.f&je)!==0||(s.f&H)!==0&&(e.f&G)!==0;tn(s,t,a?n:!1),s=f}}}function cs(e){nn(e,!0)}function nn(e,t){if((e.f&j)!==0){e.f^=j,(e.f&g)===0&&(E(e,R),B(e));for(var n=e.first;n!==null;){var r=n.next,s=(n.f&je)!==0||(n.f&H)!==0;nn(n,s?t:!1),n=r}var f=e.nodes&&e.nodes.t;if(f!==null)for(const a of f)(a.is_global||t)&&a.in()}}function _s(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var s=n===r?null:te(n);t.append(n),n=s}}let Le=!1,_e=!1;function Rt(e){_e=e}let _=null,L=!1;function ee(e){_=e}let w=null;function be(e){w=e}let C=null;function rn(e){_!==null&&(C===null?C=[e]:C.push(e))}let k=null,x=0,I=null;function fr(e){I=e}let sn=1,se=0,le=se;function Ot(e){le=e}function fn(){return++sn}function Fe(e){var t=e.f;if((t&R)!==0)return!0;if(t&A&&(e.f&=~ue),(t&Y)!==0){for(var n=e.deps,r=n.length,s=0;se.wv)return!0}(t&P)!==0&&b===null&&E(e,g)}return!1}function an(e,t,n=!0){var r=e.reactions;if(r!==null&&!(C!==null&&me.call(C,e)))for(var s=0;s{e.ac.abort(ne)}),e.ac=null);try{e.f|=Xe;var c=e.fn,u=c();e.f|=oe;var v=e.deps,y=d==null?void 0:d.is_fork;if(k!==null){var h;if(y||Ne(e,x),v!==null&&x>0)for(v.length=x+k.length,h=0;h{if(!s){if(s=!0,r.hasAttribute("value")){var a=r.value;t(r,"value",null),r.value=a}if(r.hasAttribute("checked")){var o=r.checked;t(r,"checked",null),r.checked=o}}};r.__on_r=e,u(e),h()}}function t(r,s,e,a){var o=L(r);i&&(o[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===p)||o[s]!==(o[s]=e)&&(s==="loading"&&(r[g]=e),e==null?r.removeAttribute(s):typeof e!="string"&&M(r).includes(s)?r[s]=e:r.setAttribute(s,e))}function L(r){return r.__attributes??(r.__attributes={[T]:r.nodeName.includes("-"),[l]:r.namespaceURI===d})}var c=new Map;function M(r){var s=r.getAttribute("is")||r.nodeName,e=c.get(s);if(e)return e;c.set(s,e=[]);for(var a,o=r,f=Element.prototype;f!==o;){a=A(o);for(var _ in a)a[_].set&&e.push(_);o=v(o)}return e}export{S as r,t as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/Cu3VmnGp.js.br b/apps/dashboard/build/_app/immutable/chunks/Cu3VmnGp.js.br deleted file mode 100644 index eb4a15b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Cu3VmnGp.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Cu3VmnGp.js.gz b/apps/dashboard/build/_app/immutable/chunks/Cu3VmnGp.js.gz deleted file mode 100644 index d5f9ce9..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Cu3VmnGp.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js b/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js similarity index 74% rename from apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js rename to apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js index 5a3c575..7aa0a02 100644 --- a/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js +++ b/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js @@ -1 +1 @@ -import{a as y}from"./ByItJEsC.js";import{T as r}from"./VE8Jor13.js";function a(t,e,f,i){var l=t.__style;if(r||l!==e){var s=y(e);(!r||s!==t.getAttribute("style"))&&(s==null?t.removeAttribute("style"):t.style.cssText=s),t.__style=e}return i}export{a as s}; +import{a as y}from"./BKuqSeVd.js";import{N as r}from"./CpWkWWOo.js";function a(t,e,f,i){var l=t.__style;if(r||l!==e){var s=y(e);(!r||s!==t.getAttribute("style"))&&(s==null?t.removeAttribute("style"):t.style.cssText=s),t.__style=e}return i}export{a as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js.br b/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js.br new file mode 100644 index 0000000..12a7516 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js.gz b/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js.gz new file mode 100644 index 0000000..ab5d4cb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Cx-f-Pzo.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js b/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js similarity index 98% rename from apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js rename to apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js index 8dcfa39..d17266c 100644 --- a/apps/dashboard/build/_app/immutable/chunks/CK5Nmlyf.js +++ b/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js @@ -1 +1 @@ -import{R as J,b8 as ee}from"./VE8Jor13.js";import{w as ae}from"./CCRrbKqn.js";import{c as ne,H as N,N as B,r as gt,i as _t,b as L,s as C,p as x,n as ft,f as $t,g as ut,a as X,d as it,S as Nt,P as re,e as oe,h as se,o as Dt,j as q,k as ie,l as qt,m as ce,q as le,t as Kt,u as Pt,v as fe}from"./DUtaznkq.js";class wt{constructor(a,e){this.status=a,typeof e=="string"?this.body={message:e}:e?this.body=e:this.body={message:`Error: ${a}`}}toString(){return JSON.stringify(this.body)}}class vt{constructor(a,e){this.status=a,this.location=e}}class yt extends Error{constructor(a,e,r){super(r),this.status=a,this.text=e}}const ue=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;function he(t){const a=[];return{pattern:t==="/"?/^\/$/:new RegExp(`^${pe(t).map(r=>{const n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(r);if(n)return a.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const o=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(r);if(o)return a.push({name:o[1],matcher:o[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!r)return;const s=r.split(/\[(.+?)\](?!\])/);return"/"+s.map((c,l)=>{if(l%2){if(c.startsWith("x+"))return ct(String.fromCharCode(parseInt(c.slice(2),16)));if(c.startsWith("u+"))return ct(String.fromCharCode(...c.slice(2).split("-").map(_=>parseInt(_,16))));const h=ue.exec(c),[,u,w,f,d]=h;return a.push({name:f,matcher:d,optional:!!u,rest:!!w,chained:w?l===1&&s[0]==="":!1}),w?"([^]*?)":u?"([^/]*)?":"([^/]+?)"}return ct(c)}).join("")}).join("")}/?$`),params:a}}function de(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function pe(t){return t.slice(1).split("/").filter(de)}function me(t,a,e){const r={},n=t.slice(1),o=n.filter(i=>i!==void 0);let s=0;for(let i=0;ih).join("/"),s=0),l===void 0)if(c.rest)l="";else continue;if(!c.matcher||e[c.matcher](l)){r[c.name]=l;const h=a[i+1],u=n[i+1];h&&!h.rest&&h.optional&&u&&c.chained&&(s=0),!h&&!u&&Object.keys(r).length===o.length&&(s=0);continue}if(c.optional&&c.chained){s++;continue}return}if(!s)return r}function ct(t){return t.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function ge({nodes:t,server_loads:a,dictionary:e,matchers:r}){const n=new Set(a);return Object.entries(e).map(([i,[c,l,h]])=>{const{pattern:u,params:w}=he(i),f={id:i,exec:d=>{const _=u.exec(d);if(_)return me(_,w,r)},errors:[1,...h||[]].map(d=>t[d]),layouts:[0,...l||[]].map(s),leaf:o(c)};return f.errors.length=f.layouts.length=Math.max(f.errors.length,f.layouts.length),f});function o(i){const c=i<0;return c&&(i=~i),[c,t[i]]}function s(i){return i===void 0?i:[n.has(i),t[i]]}}function Ft(t,a=JSON.parse){try{return a(sessionStorage[t])}catch{}}function It(t,a,e=JSON.stringify){const r=e(a);try{sessionStorage[t]=r}catch{}}function _e(t){return t.filter(a=>a!=null)}function Et(t){return t instanceof wt||t instanceof yt?t.status:500}function we(t){return t instanceof yt?t.text:"Internal Error"}const ve=new Set(["icon","shortcut icon","apple-touch-icon"]),I=Ft(Kt)??{},M=Ft(qt)??{},P={url:Pt({}),page:Pt({}),navigating:ae(null),updated:ne()};function bt(t){I[t]=C()}function ye(t,a){let e=t+1;for(;I[e];)delete I[e],e+=1;for(e=a+1;M[e];)delete M[e],e+=1}function V(t,a=!1){return a?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function Bt(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(L||"/");t&&await t.update()}}function Tt(){}let kt,ht,Q,U,dt,b;const Z=[],tt=[];let v=null;function pt(){var t;(t=v==null?void 0:v.fork)==null||t.then(a=>a==null?void 0:a.discard()),v=null}const G=new Map,Mt=new Set,Ee=new Set,F=new Set;let g={branch:[],error:null,url:null},Vt=!1,et=!1,Ot=!0,H=!1,K=!1,Ht=!1,St=!1,Yt,E,R,O;const at=new Set,Ct=new Map;async function Fe(t,a,e){var o,s,i,c,l;(o=globalThis.__sveltekit_1mw0ef2)!=null&&o.data&&globalThis.__sveltekit_1mw0ef2.data,document.URL!==location.href&&(location.href=location.href),b=t,await((i=(s=t.hooks).init)==null?void 0:i.call(s)),kt=ge(t),U=document.documentElement,dt=a,ht=t.nodes[0],Q=t.nodes[1],ht(),Q(),E=(c=history.state)==null?void 0:c[N],R=(l=history.state)==null?void 0:l[B],E||(E=R=Date.now(),history.replaceState({...history.state,[N]:E,[B]:R},""));const r=I[E];function n(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}e?(n(),await Ce(dt,e)):(await D({type:"enter",url:gt(b.hash?Ne(new URL(location.href)):location.href),replace_state:!0}),n()),Oe()}function be(){Z.length=0,St=!1}function zt(t){tt.some(a=>a==null?void 0:a.snapshot)&&(M[t]=tt.map(a=>{var e;return(e=a==null?void 0:a.snapshot)==null?void 0:e.capture()}))}function Wt(t){var a;(a=M[t])==null||a.forEach((e,r)=>{var n,o;(o=(n=tt[r])==null?void 0:n.snapshot)==null||o.restore(e)})}function jt(){bt(E),It(Kt,I),zt(R),It(qt,M)}async function Gt(t,a,e,r){let n;a.invalidateAll&&pt(),await D({type:"goto",url:gt(t),keepfocus:a.keepFocus,noscroll:a.noScroll,replace_state:a.replaceState,state:a.state,redirect_count:e,nav_token:r,accept:()=>{a.invalidateAll&&(St=!0,n=[...Ct.keys()]),a.invalidate&&a.invalidate.forEach(Te)}}),a.invalidateAll&&J().then(J).then(()=>{Ct.forEach(({resource:o},s)=>{var i;n!=null&&n.includes(s)&&((i=o.refresh)==null||i.call(o))})})}async function ke(t){if(t.id!==(v==null?void 0:v.id)){pt();const a={};at.add(a),v={id:t.id,token:a,promise:Xt({...t,preload:a}).then(e=>(at.delete(a),e.type==="loaded"&&e.state.error&&pt(),e)),fork:null}}return v.promise}async function lt(t){var e;const a=(e=await ot(t,!1))==null?void 0:e.route;a&&await Promise.all([...a.layouts,a.leaf].filter(Boolean).map(r=>r[1]()))}async function Jt(t,a,e){var n;g=t.state;const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(x,t.props.page),Yt=new b.root({target:a,props:{...t.props,stores:P,components:tt},hydrate:e,sync:!1}),await Promise.resolve(),Wt(R),e){const o={from:null,to:{params:g.params,route:{id:((n=g.route)==null?void 0:n.id)??null},url:new URL(location.href),scroll:I[E]??C()},willUnload:!1,type:"enter",complete:Promise.resolve()};F.forEach(s=>s(o))}et=!0}function nt({url:t,params:a,branch:e,status:r,error:n,route:o,form:s}){let i="never";if(L&&(t.pathname===L||t.pathname===L+"/"))i="always";else for(const f of e)(f==null?void 0:f.slash)!==void 0&&(i=f.slash);t.pathname=se(t.pathname,i),t.search=t.search;const c={type:"loaded",state:{url:t,params:a,branch:e,error:n,route:o},props:{constructors:_e(e).map(f=>f.node.component),page:At(x)}};s!==void 0&&(c.props.form=s);let l={},h=!x,u=0;for(let f=0;fi(new URL(s))))return!0;return!1}function xt(t,a){return(t==null?void 0:t.type)==="data"?t:(t==null?void 0:t.type)==="skip"?a??null:null}function xe(t,a){if(!t)return new Set(a.searchParams.keys());const e=new Set([...t.searchParams.keys(),...a.searchParams.keys()]);for(const r of e){const n=t.searchParams.getAll(r),o=a.searchParams.getAll(r);n.every(s=>o.includes(s))&&o.every(s=>n.includes(s))&&e.delete(r)}return e}function Le({error:t,url:a,route:e,params:r}){return{type:"loaded",state:{error:t,url:a,route:e,params:r,branch:[]},props:{page:At(x),constructors:[]}}}async function Xt({id:t,invalidating:a,url:e,params:r,route:n,preload:o}){if((v==null?void 0:v.id)===t)return at.delete(v.token),v.promise;const{errors:s,layouts:i,leaf:c}=n,l=[...i,c];s.forEach(m=>m==null?void 0:m().catch(()=>{})),l.forEach(m=>m==null?void 0:m[1]().catch(()=>{}));const h=g.url?t!==rt(g.url):!1,u=g.route?n.id!==g.route.id:!1,w=xe(g.url,e);let f=!1;const d=l.map(async(m,p)=>{var A;if(!m)return;const y=g.branch[p];return m[1]===(y==null?void 0:y.loader)&&!Re(f,u,h,w,(A=y.universal)==null?void 0:A.uses,r)?y:(f=!0,Rt({loader:m[1],url:e,params:r,route:n,parent:async()=>{var z;const T={};for(let j=0;j{});const _=[];for(let m=0;mPromise.resolve({}),server_data_node:xt(o)}),i={node:await Q(),loader:Q,universal:null,server:null,data:null};return nt({url:e,params:n,branch:[s,i],status:t,error:a,route:null})}catch(s){if(s instanceof vt)return Gt(new URL(s.location,location.href),{},0);throw s}}async function Ae(t){const a=t.href;if(G.has(a))return G.get(a);let e;try{const r=(async()=>{let n=await b.hooks.reroute({url:new URL(t),fetch:async(o,s)=>Se(o,s,t).promise})??t;if(typeof n=="string"){const o=new URL(t);b.hash?o.hash=n:o.pathname=n,n=o}return n})();G.set(a,r),e=await r}catch{G.delete(a);return}return e}async function ot(t,a){if(t&&!_t(t,L,b.hash)){const e=await Ae(t);if(!e)return;const r=Pe(e);for(const n of kt){const o=n.exec(r);if(o)return{id:rt(t),invalidating:a,route:n,params:oe(o),url:t}}}}function Pe(t){return ie(b.hash?t.hash.replace(/^#/,"").replace(/[?#].+/,""):t.pathname.slice(L.length))||"/"}function rt(t){return(b.hash?t.hash.replace(/^#/,""):t.pathname)+t.search}function Qt({url:t,type:a,intent:e,delta:r,event:n,scroll:o}){let s=!1;const i=Ut(g,e,t,a,o??null);r!==void 0&&(i.navigation.delta=r),n!==void 0&&(i.navigation.event=n);const c={...i.navigation,cancel:()=>{s=!0,i.reject(new Error("navigation cancelled"))}};return H||Mt.forEach(l=>l(c)),s?null:i}async function D({type:t,url:a,popped:e,keepfocus:r,noscroll:n,replace_state:o,state:s={},redirect_count:i=0,nav_token:c={},accept:l=Tt,block:h=Tt,event:u}){var j;const w=O;O=c;const f=await ot(a,!1),d=t==="enter"?Ut(g,f,a,t):Qt({url:a,type:t,delta:e==null?void 0:e.delta,intent:f,scroll:e==null?void 0:e.scroll,event:u});if(!d){h(),O===c&&(O=w);return}const _=E,m=R;l(),H=!0,et&&d.navigation.type!=="enter"&&P.navigating.set(ft.current=d.navigation);let p=f&&await Xt(f);if(!p){if(_t(a,L,b.hash))return await V(a,o);p=await Zt(a,{id:null},await Y(new yt(404,"Not Found",`Not found: ${a.pathname}`),{url:a,params:{},route:{id:null}}),404,o)}if(a=(f==null?void 0:f.url)||a,O!==c)return d.reject(new Error("navigation aborted")),!1;if(p.type==="redirect"){if(i<20){await D({type:t,url:new URL(p.location,a),popped:e,keepfocus:r,noscroll:n,replace_state:o,state:s,redirect_count:i+1,nav_token:c}),d.fulfil(void 0);return}p=await Lt({status:500,error:await Y(new Error("Redirect loop"),{url:a,params:{},route:{id:null}}),url:a,route:{id:null}})}else p.props.page.status>=400&&await P.updated.check()&&(await Bt(),await V(a,o));if(be(),bt(_),zt(m),p.props.page.url.pathname!==a.pathname&&(a.pathname=p.props.page.url.pathname),s=e?e.state:s,!e){const k=o?0:1,W={[N]:E+=k,[B]:R+=k,[Nt]:s};(o?history.replaceState:history.pushState).call(history,W,"",a),o||ye(E,R)}const y=f&&(v==null?void 0:v.id)===f.id?v.fork:null;v=null,p.props.page.state=s;let S;if(et){const k=(await Promise.all(Array.from(Ee,$=>$(d.navigation)))).filter($=>typeof $=="function");if(k.length>0){let $=function(){k.forEach(st=>{F.delete(st)})};k.push($),k.forEach(st=>{F.add(st)})}g=p.state,p.props.page&&(p.props.page.url=a);const W=y&&await y;W?S=W.commit():(Yt.$set(p.props),fe(p.props.page),S=(j=ee)==null?void 0:j()),Ht=!0}else await Jt(p,dt,!1);const{activeElement:A}=document;await S,await J(),await J();let T=null;if(Ot){const k=e?e.scroll:n?C():null;k?scrollTo(k.x,k.y):(T=a.hash&&document.getElementById(te(a)))?T.scrollIntoView():scrollTo(0,0)}const z=document.activeElement!==A&&document.activeElement!==document.body;!r&&!z&&$e(a,!T),Ot=!0,p.props.page&&Object.assign(x,p.props.page),H=!1,t==="popstate"&&Wt(R),d.fulfil(void 0),d.navigation.to&&(d.navigation.to.scroll=C()),F.forEach(k=>k(d.navigation)),P.navigating.set(ft.current=null)}async function Zt(t,a,e,r,n){return t.origin===Dt&&t.pathname===location.pathname&&!Vt?await Lt({status:r,error:e,url:t,route:a}):await V(t,n)}function Ie(){let t,a={element:void 0,href:void 0},e;U.addEventListener("mousemove",i=>{const c=i.target;clearTimeout(t),t=setTimeout(()=>{o(c,q.hover)},20)});function r(i){i.defaultPrevented||o(i.composedPath()[0],q.tap)}U.addEventListener("mousedown",r),U.addEventListener("touchstart",r,{passive:!0});const n=new IntersectionObserver(i=>{for(const c of i)c.isIntersecting&&(lt(new URL(c.target.href)),n.unobserve(c.target))},{threshold:0});async function o(i,c){const l=$t(i,U),h=l===a.element&&(l==null?void 0:l.href)===a.href&&c>=e;if(!l||h)return;const{url:u,external:w,download:f}=ut(l,L,b.hash);if(w||f)return;const d=X(l),_=u&&rt(g.url)===rt(u);if(!(d.reload||_))if(c<=d.preload_data){a={element:l,href:l.href},e=q.tap;const m=await ot(u,!1);if(!m)return;ke(m)}else c<=d.preload_code&&(a={element:l,href:l.href},e=c,lt(u))}function s(){n.disconnect();for(const i of U.querySelectorAll("a")){const{url:c,external:l,download:h}=ut(i,L,b.hash);if(l||h)continue;const u=X(i);u.reload||(u.preload_code===q.viewport&&n.observe(i),u.preload_code===q.eager&<(c))}}F.add(s),s()}function Y(t,a){if(t instanceof wt)return t.body;const e=Et(t),r=we(t);return b.hooks.handleError({error:t,event:a,status:e,message:r})??{message:r}}function Be(t,a={}){return t=new URL(gt(t)),t.origin!==Dt?Promise.reject(new Error("goto: invalid URL")):Gt(t,a,0)}function Te(t){if(typeof t=="function")Z.push(t);else{const{href:a}=new URL(t,location.href);Z.push(e=>e.href===a)}}function Oe(){var a;history.scrollRestoration="manual",addEventListener("beforeunload",e=>{let r=!1;if(jt(),!H){const n=Ut(g,void 0,null,"leave"),o={...n.navigation,cancel:()=>{r=!0,n.reject(new Error("navigation cancelled"))}};Mt.forEach(s=>s(o))}r?(e.preventDefault(),e.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&jt()}),(a=navigator.connection)!=null&&a.saveData||Ie(),U.addEventListener("click",async e=>{if(e.button||e.which!==1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.defaultPrevented)return;const r=$t(e.composedPath()[0],U);if(!r)return;const{url:n,external:o,target:s,download:i}=ut(r,L,b.hash);if(!n)return;if(s==="_parent"||s==="_top"){if(window.parent!==window)return}else if(s&&s!=="_self")return;const c=X(r);if(!(r instanceof SVGAElement)&&n.protocol!==location.protocol&&!(n.protocol==="https:"||n.protocol==="http:")||i)return;const[h,u]=(b.hash?n.hash.replace(/^#/,""):n.href).split("#"),w=h===it(location);if(o||c.reload&&(!w||!u)){Qt({url:n,type:"link",event:e})?H=!0:e.preventDefault();return}if(u!==void 0&&w){const[,f]=g.url.href.split("#");if(f===u){if(e.preventDefault(),u===""||u==="top"&&r.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const d=r.ownerDocument.getElementById(decodeURIComponent(u));d&&(d.scrollIntoView(),d.focus())}return}if(K=!0,bt(E),t(n),!c.replace_state)return;K=!1}e.preventDefault(),await new Promise(f=>{requestAnimationFrame(()=>{setTimeout(f,0)}),setTimeout(f,100)}),await D({type:"link",url:n,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??n.href===location.href,event:e})}),U.addEventListener("submit",e=>{if(e.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(e.target),n=e.submitter;if(((n==null?void 0:n.formTarget)||r.target)==="_blank"||((n==null?void 0:n.formMethod)||r.method)!=="get")return;const i=new URL((n==null?void 0:n.hasAttribute("formaction"))&&(n==null?void 0:n.formAction)||r.action);if(_t(i,L,!1))return;const c=e.target,l=X(c);if(l.reload)return;e.preventDefault(),e.stopPropagation();const h=new FormData(c,n);i.search=new URLSearchParams(h).toString(),D({type:"form",url:i,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??i.href===location.href,event:e})}),addEventListener("popstate",async e=>{var r;if(!mt){if((r=e.state)!=null&&r[N]){const n=e.state[N];if(O={},n===E)return;const o=I[n],s=e.state[Nt]??{},i=new URL(e.state[re]??location.href),c=e.state[B],l=g.url?it(location)===it(g.url):!1;if(c===R&&(Ht||l)){s!==x.state&&(x.state=s),t(i),I[E]=C(),o&&scrollTo(o.x,o.y),E=n;return}const u=n-E;await D({type:"popstate",url:i,popped:{state:s,scroll:o,delta:u},accept:()=>{E=n,R=c},block:()=>{history.go(-u)},nav_token:O,event:e})}else if(!K){const n=new URL(location.href);t(n),b.hash&&location.reload()}}}),addEventListener("hashchange",()=>{K&&(K=!1,history.replaceState({...history.state,[N]:++E,[B]:R},"",location.href))});for(const e of document.querySelectorAll("link"))ve.has(e.rel)&&(e.href=e.href);addEventListener("pageshow",e=>{e.persisted&&P.navigating.set(ft.current=null)});function t(e){g.url=x.url=e,P.page.set(At(x)),P.page.notify()}}async function Ce(t,{status:a=200,error:e,node_ids:r,params:n,route:o,server_route:s,data:i,form:c}){Vt=!0;const l=new URL(location.href);let h;({params:n={},route:o={id:null}}=await ot(l,!1)||{}),h=kt.find(({id:f})=>f===o.id);let u,w=!0;try{const f=r.map(async(_,m)=>{const p=i[m];return p!=null&&p.uses&&(p.uses=je(p.uses)),Rt({loader:b.nodes[_],url:l,params:n,route:o,parent:async()=>{const y={};for(let S=0;S{const i=history.state;mt=!0,location.replace(new URL(`#${r}`,location.href)),history.replaceState(i,"",t),a&&scrollTo(o,s),mt=!1})}else{const o=document.body,s=o.getAttribute("tabindex");o.tabIndex=-1,o.focus({preventScroll:!0,focusVisible:!1}),s!==null?o.setAttribute("tabindex",s):o.removeAttribute("tabindex")}const n=getSelection();if(n&&n.type!=="None"){const o=[];for(let s=0;s{if(n.rangeCount===o.length){for(let s=0;s{o=u,s=w});return i.catch(()=>{}),{navigation:{from:{params:t.params,route:{id:((l=t.route)==null?void 0:l.id)??null},url:t.url,scroll:C()},to:e&&{params:(a==null?void 0:a.params)??null,route:{id:((h=a==null?void 0:a.route)==null?void 0:h.id)??null},url:e,scroll:n},willUnload:!a,type:r,complete:i},fulfil:o,reject:s}}function At(t){return{data:t.data,error:t.error,form:t.form,params:t.params,route:t.route,state:t.state,status:t.status,url:t.url}}function Ne(t){const a=new URL(t);return a.hash=decodeURIComponent(t.hash),a}function te(t){let a;if(b.hash){const[,,e]=t.hash.split("#",3);a=e??""}else a=t.hash.slice(1);return decodeURIComponent(a)}export{Fe as a,Be as g,P as s}; +import{$ as J,bd as ee}from"./CpWkWWOo.js";import{w as ae}from"./BeMFXnHE.js";import{c as ne,H as N,N as B,r as gt,i as _t,b as L,s as C,p as x,n as ft,f as $t,g as ut,a as X,d as it,S as Nt,P as re,e as oe,h as se,o as Dt,j as q,k as ie,l as qt,m as ce,q as le,t as Kt,u as Pt,v as fe}from"./DGcYlAAw.js";class wt{constructor(a,e){this.status=a,typeof e=="string"?this.body={message:e}:e?this.body=e:this.body={message:`Error: ${a}`}}toString(){return JSON.stringify(this.body)}}class vt{constructor(a,e){this.status=a,this.location=e}}class yt extends Error{constructor(a,e,r){super(r),this.status=a,this.text=e}}const ue=/^(\[)?(\.\.\.)?(\w+)(?:=(\w+))?(\])?$/;function he(t){const a=[];return{pattern:t==="/"?/^\/$/:new RegExp(`^${pe(t).map(r=>{const n=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(r);if(n)return a.push({name:n[1],matcher:n[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const o=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(r);if(o)return a.push({name:o[1],matcher:o[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!r)return;const s=r.split(/\[(.+?)\](?!\])/);return"/"+s.map((c,l)=>{if(l%2){if(c.startsWith("x+"))return ct(String.fromCharCode(parseInt(c.slice(2),16)));if(c.startsWith("u+"))return ct(String.fromCharCode(...c.slice(2).split("-").map(_=>parseInt(_,16))));const h=ue.exec(c),[,u,w,f,d]=h;return a.push({name:f,matcher:d,optional:!!u,rest:!!w,chained:w?l===1&&s[0]==="":!1}),w?"([^]*?)":u?"([^/]*)?":"([^/]+?)"}return ct(c)}).join("")}).join("")}/?$`),params:a}}function de(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function pe(t){return t.slice(1).split("/").filter(de)}function me(t,a,e){const r={},n=t.slice(1),o=n.filter(i=>i!==void 0);let s=0;for(let i=0;ih).join("/"),s=0),l===void 0)if(c.rest)l="";else continue;if(!c.matcher||e[c.matcher](l)){r[c.name]=l;const h=a[i+1],u=n[i+1];h&&!h.rest&&h.optional&&u&&c.chained&&(s=0),!h&&!u&&Object.keys(r).length===o.length&&(s=0);continue}if(c.optional&&c.chained){s++;continue}return}if(!s)return r}function ct(t){return t.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function ge({nodes:t,server_loads:a,dictionary:e,matchers:r}){const n=new Set(a);return Object.entries(e).map(([i,[c,l,h]])=>{const{pattern:u,params:w}=he(i),f={id:i,exec:d=>{const _=u.exec(d);if(_)return me(_,w,r)},errors:[1,...h||[]].map(d=>t[d]),layouts:[0,...l||[]].map(s),leaf:o(c)};return f.errors.length=f.layouts.length=Math.max(f.errors.length,f.layouts.length),f});function o(i){const c=i<0;return c&&(i=~i),[c,t[i]]}function s(i){return i===void 0?i:[n.has(i),t[i]]}}function Ft(t,a=JSON.parse){try{return a(sessionStorage[t])}catch{}}function It(t,a,e=JSON.stringify){const r=e(a);try{sessionStorage[t]=r}catch{}}function _e(t){return t.filter(a=>a!=null)}function Et(t){return t instanceof wt||t instanceof yt?t.status:500}function we(t){return t instanceof yt?t.text:"Internal Error"}const ve=new Set(["icon","shortcut icon","apple-touch-icon"]),I=Ft(Kt)??{},M=Ft(qt)??{},P={url:Pt({}),page:Pt({}),navigating:ae(null),updated:ne()};function bt(t){I[t]=C()}function ye(t,a){let e=t+1;for(;I[e];)delete I[e],e+=1;for(e=a+1;M[e];)delete M[e],e+=1}function V(t,a=!1){return a?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function Bt(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(L||"/");t&&await t.update()}}function Tt(){}let kt,ht,Q,U,dt,b;const Z=[],tt=[];let v=null;function pt(){var t;(t=v==null?void 0:v.fork)==null||t.then(a=>a==null?void 0:a.discard()),v=null}const G=new Map,Mt=new Set,Ee=new Set,F=new Set;let g={branch:[],error:null,url:null},Vt=!1,et=!1,Ot=!0,H=!1,K=!1,Ht=!1,St=!1,Yt,E,R,O;const at=new Set,Ct=new Map;async function Fe(t,a,e){var o,s,i,c,l;(o=globalThis.__sveltekit_1r5nume)!=null&&o.data&&globalThis.__sveltekit_1r5nume.data,document.URL!==location.href&&(location.href=location.href),b=t,await((i=(s=t.hooks).init)==null?void 0:i.call(s)),kt=ge(t),U=document.documentElement,dt=a,ht=t.nodes[0],Q=t.nodes[1],ht(),Q(),E=(c=history.state)==null?void 0:c[N],R=(l=history.state)==null?void 0:l[B],E||(E=R=Date.now(),history.replaceState({...history.state,[N]:E,[B]:R},""));const r=I[E];function n(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}e?(n(),await Ce(dt,e)):(await D({type:"enter",url:gt(b.hash?Ne(new URL(location.href)):location.href),replace_state:!0}),n()),Oe()}function be(){Z.length=0,St=!1}function zt(t){tt.some(a=>a==null?void 0:a.snapshot)&&(M[t]=tt.map(a=>{var e;return(e=a==null?void 0:a.snapshot)==null?void 0:e.capture()}))}function Wt(t){var a;(a=M[t])==null||a.forEach((e,r)=>{var n,o;(o=(n=tt[r])==null?void 0:n.snapshot)==null||o.restore(e)})}function jt(){bt(E),It(Kt,I),zt(R),It(qt,M)}async function Gt(t,a,e,r){let n;a.invalidateAll&&pt(),await D({type:"goto",url:gt(t),keepfocus:a.keepFocus,noscroll:a.noScroll,replace_state:a.replaceState,state:a.state,redirect_count:e,nav_token:r,accept:()=>{a.invalidateAll&&(St=!0,n=[...Ct.keys()]),a.invalidate&&a.invalidate.forEach(Te)}}),a.invalidateAll&&J().then(J).then(()=>{Ct.forEach(({resource:o},s)=>{var i;n!=null&&n.includes(s)&&((i=o.refresh)==null||i.call(o))})})}async function ke(t){if(t.id!==(v==null?void 0:v.id)){pt();const a={};at.add(a),v={id:t.id,token:a,promise:Xt({...t,preload:a}).then(e=>(at.delete(a),e.type==="loaded"&&e.state.error&&pt(),e)),fork:null}}return v.promise}async function lt(t){var e;const a=(e=await ot(t,!1))==null?void 0:e.route;a&&await Promise.all([...a.layouts,a.leaf].filter(Boolean).map(r=>r[1]()))}async function Jt(t,a,e){var n;g=t.state;const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(x,t.props.page),Yt=new b.root({target:a,props:{...t.props,stores:P,components:tt},hydrate:e,sync:!1}),await Promise.resolve(),Wt(R),e){const o={from:null,to:{params:g.params,route:{id:((n=g.route)==null?void 0:n.id)??null},url:new URL(location.href),scroll:I[E]??C()},willUnload:!1,type:"enter",complete:Promise.resolve()};F.forEach(s=>s(o))}et=!0}function nt({url:t,params:a,branch:e,status:r,error:n,route:o,form:s}){let i="never";if(L&&(t.pathname===L||t.pathname===L+"/"))i="always";else for(const f of e)(f==null?void 0:f.slash)!==void 0&&(i=f.slash);t.pathname=se(t.pathname,i),t.search=t.search;const c={type:"loaded",state:{url:t,params:a,branch:e,error:n,route:o},props:{constructors:_e(e).map(f=>f.node.component),page:At(x)}};s!==void 0&&(c.props.form=s);let l={},h=!x,u=0;for(let f=0;fi(new URL(s))))return!0;return!1}function xt(t,a){return(t==null?void 0:t.type)==="data"?t:(t==null?void 0:t.type)==="skip"?a??null:null}function xe(t,a){if(!t)return new Set(a.searchParams.keys());const e=new Set([...t.searchParams.keys(),...a.searchParams.keys()]);for(const r of e){const n=t.searchParams.getAll(r),o=a.searchParams.getAll(r);n.every(s=>o.includes(s))&&o.every(s=>n.includes(s))&&e.delete(r)}return e}function Le({error:t,url:a,route:e,params:r}){return{type:"loaded",state:{error:t,url:a,route:e,params:r,branch:[]},props:{page:At(x),constructors:[]}}}async function Xt({id:t,invalidating:a,url:e,params:r,route:n,preload:o}){if((v==null?void 0:v.id)===t)return at.delete(v.token),v.promise;const{errors:s,layouts:i,leaf:c}=n,l=[...i,c];s.forEach(m=>m==null?void 0:m().catch(()=>{})),l.forEach(m=>m==null?void 0:m[1]().catch(()=>{}));const h=g.url?t!==rt(g.url):!1,u=g.route?n.id!==g.route.id:!1,w=xe(g.url,e);let f=!1;const d=l.map(async(m,p)=>{var A;if(!m)return;const y=g.branch[p];return m[1]===(y==null?void 0:y.loader)&&!Re(f,u,h,w,(A=y.universal)==null?void 0:A.uses,r)?y:(f=!0,Rt({loader:m[1],url:e,params:r,route:n,parent:async()=>{var z;const T={};for(let j=0;j{});const _=[];for(let m=0;mPromise.resolve({}),server_data_node:xt(o)}),i={node:await Q(),loader:Q,universal:null,server:null,data:null};return nt({url:e,params:n,branch:[s,i],status:t,error:a,route:null})}catch(s){if(s instanceof vt)return Gt(new URL(s.location,location.href),{},0);throw s}}async function Ae(t){const a=t.href;if(G.has(a))return G.get(a);let e;try{const r=(async()=>{let n=await b.hooks.reroute({url:new URL(t),fetch:async(o,s)=>Se(o,s,t).promise})??t;if(typeof n=="string"){const o=new URL(t);b.hash?o.hash=n:o.pathname=n,n=o}return n})();G.set(a,r),e=await r}catch{G.delete(a);return}return e}async function ot(t,a){if(t&&!_t(t,L,b.hash)){const e=await Ae(t);if(!e)return;const r=Pe(e);for(const n of kt){const o=n.exec(r);if(o)return{id:rt(t),invalidating:a,route:n,params:oe(o),url:t}}}}function Pe(t){return ie(b.hash?t.hash.replace(/^#/,"").replace(/[?#].+/,""):t.pathname.slice(L.length))||"/"}function rt(t){return(b.hash?t.hash.replace(/^#/,""):t.pathname)+t.search}function Qt({url:t,type:a,intent:e,delta:r,event:n,scroll:o}){let s=!1;const i=Ut(g,e,t,a,o??null);r!==void 0&&(i.navigation.delta=r),n!==void 0&&(i.navigation.event=n);const c={...i.navigation,cancel:()=>{s=!0,i.reject(new Error("navigation cancelled"))}};return H||Mt.forEach(l=>l(c)),s?null:i}async function D({type:t,url:a,popped:e,keepfocus:r,noscroll:n,replace_state:o,state:s={},redirect_count:i=0,nav_token:c={},accept:l=Tt,block:h=Tt,event:u}){var j;const w=O;O=c;const f=await ot(a,!1),d=t==="enter"?Ut(g,f,a,t):Qt({url:a,type:t,delta:e==null?void 0:e.delta,intent:f,scroll:e==null?void 0:e.scroll,event:u});if(!d){h(),O===c&&(O=w);return}const _=E,m=R;l(),H=!0,et&&d.navigation.type!=="enter"&&P.navigating.set(ft.current=d.navigation);let p=f&&await Xt(f);if(!p){if(_t(a,L,b.hash))return await V(a,o);p=await Zt(a,{id:null},await Y(new yt(404,"Not Found",`Not found: ${a.pathname}`),{url:a,params:{},route:{id:null}}),404,o)}if(a=(f==null?void 0:f.url)||a,O!==c)return d.reject(new Error("navigation aborted")),!1;if(p.type==="redirect"){if(i<20){await D({type:t,url:new URL(p.location,a),popped:e,keepfocus:r,noscroll:n,replace_state:o,state:s,redirect_count:i+1,nav_token:c}),d.fulfil(void 0);return}p=await Lt({status:500,error:await Y(new Error("Redirect loop"),{url:a,params:{},route:{id:null}}),url:a,route:{id:null}})}else p.props.page.status>=400&&await P.updated.check()&&(await Bt(),await V(a,o));if(be(),bt(_),zt(m),p.props.page.url.pathname!==a.pathname&&(a.pathname=p.props.page.url.pathname),s=e?e.state:s,!e){const k=o?0:1,W={[N]:E+=k,[B]:R+=k,[Nt]:s};(o?history.replaceState:history.pushState).call(history,W,"",a),o||ye(E,R)}const y=f&&(v==null?void 0:v.id)===f.id?v.fork:null;v=null,p.props.page.state=s;let S;if(et){const k=(await Promise.all(Array.from(Ee,$=>$(d.navigation)))).filter($=>typeof $=="function");if(k.length>0){let $=function(){k.forEach(st=>{F.delete(st)})};k.push($),k.forEach(st=>{F.add(st)})}g=p.state,p.props.page&&(p.props.page.url=a);const W=y&&await y;W?S=W.commit():(Yt.$set(p.props),fe(p.props.page),S=(j=ee)==null?void 0:j()),Ht=!0}else await Jt(p,dt,!1);const{activeElement:A}=document;await S,await J(),await J();let T=null;if(Ot){const k=e?e.scroll:n?C():null;k?scrollTo(k.x,k.y):(T=a.hash&&document.getElementById(te(a)))?T.scrollIntoView():scrollTo(0,0)}const z=document.activeElement!==A&&document.activeElement!==document.body;!r&&!z&&$e(a,!T),Ot=!0,p.props.page&&Object.assign(x,p.props.page),H=!1,t==="popstate"&&Wt(R),d.fulfil(void 0),d.navigation.to&&(d.navigation.to.scroll=C()),F.forEach(k=>k(d.navigation)),P.navigating.set(ft.current=null)}async function Zt(t,a,e,r,n){return t.origin===Dt&&t.pathname===location.pathname&&!Vt?await Lt({status:r,error:e,url:t,route:a}):await V(t,n)}function Ie(){let t,a={element:void 0,href:void 0},e;U.addEventListener("mousemove",i=>{const c=i.target;clearTimeout(t),t=setTimeout(()=>{o(c,q.hover)},20)});function r(i){i.defaultPrevented||o(i.composedPath()[0],q.tap)}U.addEventListener("mousedown",r),U.addEventListener("touchstart",r,{passive:!0});const n=new IntersectionObserver(i=>{for(const c of i)c.isIntersecting&&(lt(new URL(c.target.href)),n.unobserve(c.target))},{threshold:0});async function o(i,c){const l=$t(i,U),h=l===a.element&&(l==null?void 0:l.href)===a.href&&c>=e;if(!l||h)return;const{url:u,external:w,download:f}=ut(l,L,b.hash);if(w||f)return;const d=X(l),_=u&&rt(g.url)===rt(u);if(!(d.reload||_))if(c<=d.preload_data){a={element:l,href:l.href},e=q.tap;const m=await ot(u,!1);if(!m)return;ke(m)}else c<=d.preload_code&&(a={element:l,href:l.href},e=c,lt(u))}function s(){n.disconnect();for(const i of U.querySelectorAll("a")){const{url:c,external:l,download:h}=ut(i,L,b.hash);if(l||h)continue;const u=X(i);u.reload||(u.preload_code===q.viewport&&n.observe(i),u.preload_code===q.eager&<(c))}}F.add(s),s()}function Y(t,a){if(t instanceof wt)return t.body;const e=Et(t),r=we(t);return b.hooks.handleError({error:t,event:a,status:e,message:r})??{message:r}}function Be(t,a={}){return t=new URL(gt(t)),t.origin!==Dt?Promise.reject(new Error("goto: invalid URL")):Gt(t,a,0)}function Te(t){if(typeof t=="function")Z.push(t);else{const{href:a}=new URL(t,location.href);Z.push(e=>e.href===a)}}function Oe(){var a;history.scrollRestoration="manual",addEventListener("beforeunload",e=>{let r=!1;if(jt(),!H){const n=Ut(g,void 0,null,"leave"),o={...n.navigation,cancel:()=>{r=!0,n.reject(new Error("navigation cancelled"))}};Mt.forEach(s=>s(o))}r?(e.preventDefault(),e.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&jt()}),(a=navigator.connection)!=null&&a.saveData||Ie(),U.addEventListener("click",async e=>{if(e.button||e.which!==1||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey||e.defaultPrevented)return;const r=$t(e.composedPath()[0],U);if(!r)return;const{url:n,external:o,target:s,download:i}=ut(r,L,b.hash);if(!n)return;if(s==="_parent"||s==="_top"){if(window.parent!==window)return}else if(s&&s!=="_self")return;const c=X(r);if(!(r instanceof SVGAElement)&&n.protocol!==location.protocol&&!(n.protocol==="https:"||n.protocol==="http:")||i)return;const[h,u]=(b.hash?n.hash.replace(/^#/,""):n.href).split("#"),w=h===it(location);if(o||c.reload&&(!w||!u)){Qt({url:n,type:"link",event:e})?H=!0:e.preventDefault();return}if(u!==void 0&&w){const[,f]=g.url.href.split("#");if(f===u){if(e.preventDefault(),u===""||u==="top"&&r.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const d=r.ownerDocument.getElementById(decodeURIComponent(u));d&&(d.scrollIntoView(),d.focus())}return}if(K=!0,bt(E),t(n),!c.replace_state)return;K=!1}e.preventDefault(),await new Promise(f=>{requestAnimationFrame(()=>{setTimeout(f,0)}),setTimeout(f,100)}),await D({type:"link",url:n,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??n.href===location.href,event:e})}),U.addEventListener("submit",e=>{if(e.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(e.target),n=e.submitter;if(((n==null?void 0:n.formTarget)||r.target)==="_blank"||((n==null?void 0:n.formMethod)||r.method)!=="get")return;const i=new URL((n==null?void 0:n.hasAttribute("formaction"))&&(n==null?void 0:n.formAction)||r.action);if(_t(i,L,!1))return;const c=e.target,l=X(c);if(l.reload)return;e.preventDefault(),e.stopPropagation();const h=new FormData(c,n);i.search=new URLSearchParams(h).toString(),D({type:"form",url:i,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??i.href===location.href,event:e})}),addEventListener("popstate",async e=>{var r;if(!mt){if((r=e.state)!=null&&r[N]){const n=e.state[N];if(O={},n===E)return;const o=I[n],s=e.state[Nt]??{},i=new URL(e.state[re]??location.href),c=e.state[B],l=g.url?it(location)===it(g.url):!1;if(c===R&&(Ht||l)){s!==x.state&&(x.state=s),t(i),I[E]=C(),o&&scrollTo(o.x,o.y),E=n;return}const u=n-E;await D({type:"popstate",url:i,popped:{state:s,scroll:o,delta:u},accept:()=>{E=n,R=c},block:()=>{history.go(-u)},nav_token:O,event:e})}else if(!K){const n=new URL(location.href);t(n),b.hash&&location.reload()}}}),addEventListener("hashchange",()=>{K&&(K=!1,history.replaceState({...history.state,[N]:++E,[B]:R},"",location.href))});for(const e of document.querySelectorAll("link"))ve.has(e.rel)&&(e.href=e.href);addEventListener("pageshow",e=>{e.persisted&&P.navigating.set(ft.current=null)});function t(e){g.url=x.url=e,P.page.set(At(x)),P.page.notify()}}async function Ce(t,{status:a=200,error:e,node_ids:r,params:n,route:o,server_route:s,data:i,form:c}){Vt=!0;const l=new URL(location.href);let h;({params:n={},route:o={id:null}}=await ot(l,!1)||{}),h=kt.find(({id:f})=>f===o.id);let u,w=!0;try{const f=r.map(async(_,m)=>{const p=i[m];return p!=null&&p.uses&&(p.uses=je(p.uses)),Rt({loader:b.nodes[_],url:l,params:n,route:o,parent:async()=>{const y={};for(let S=0;S{const i=history.state;mt=!0,location.replace(new URL(`#${r}`,location.href)),history.replaceState(i,"",t),a&&scrollTo(o,s),mt=!1})}else{const o=document.body,s=o.getAttribute("tabindex");o.tabIndex=-1,o.focus({preventScroll:!0,focusVisible:!1}),s!==null?o.setAttribute("tabindex",s):o.removeAttribute("tabindex")}const n=getSelection();if(n&&n.type!=="None"){const o=[];for(let s=0;s{if(n.rangeCount===o.length){for(let s=0;s{o=u,s=w});return i.catch(()=>{}),{navigation:{from:{params:t.params,route:{id:((l=t.route)==null?void 0:l.id)??null},url:t.url,scroll:C()},to:e&&{params:(a==null?void 0:a.params)??null,route:{id:((h=a==null?void 0:a.route)==null?void 0:h.id)??null},url:e,scroll:n},willUnload:!a,type:r,complete:i},fulfil:o,reject:s}}function At(t){return{data:t.data,error:t.error,form:t.form,params:t.params,route:t.route,state:t.state,status:t.status,url:t.url}}function Ne(t){const a=new URL(t);return a.hash=decodeURIComponent(t.hash),a}function te(t){let a;if(b.hash){const[,,e]=t.hash.split("#",3);a=e??""}else a=t.hash.slice(1);return decodeURIComponent(a)}export{Fe as a,Be as g,P as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js.br b/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js.br new file mode 100644 index 0000000..64ff754 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js.gz b/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js.gz new file mode 100644 index 0000000..466d960 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D-gDZzN6.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js b/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js new file mode 100644 index 0000000..99ccabc --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js @@ -0,0 +1 @@ +import{w as N,g as T}from"./BeMFXnHE.js";import{e as E}from"./MAY1QfFZ.js";import{E as p}from"./CcUbQ_Wl.js";const y=4,R=5500,F=1500;function x(){const{subscribe:b,update:u}=N([]);let m=1,f=0;const c=new Map,a=new Map,l=new Map;function w(e,s){l.set(e,Date.now());const t=setTimeout(()=>{c.delete(e),l.delete(e),h(e)},s);c.set(e,t)}function g(e){const s=m++,t=Date.now(),o={id:s,createdAt:t,...e};u(n=>{const r=[o,...n];if(r.length>y){for(const i of r.slice(y)){const d=c.get(i.id);d&&clearTimeout(d),c.delete(i.id),a.delete(i.id),l.delete(i.id)}return r.slice(0,y)}return r}),w(s,e.dwellMs)}function h(e){const s=c.get(e);s&&(clearTimeout(s),c.delete(e)),a.delete(e),l.delete(e),u(t=>t.filter(o=>o.id!==e))}function D(e,s){const t=c.get(e);if(!t)return;clearTimeout(t),c.delete(e);const o=l.get(e)??Date.now(),n=Date.now()-o,r=Math.max(200,s-n);a.set(e,{remaining:r})}function C(e){const s=a.get(e);s&&(a.delete(e),w(e,s.remaining))}function S(){for(const e of c.values())clearTimeout(e);c.clear(),a.clear(),l.clear(),u(()=>[])}function _(e){const s=p[e.type]??"#818CF8",t=e.data;switch(e.type){case"DreamCompleted":{const o=Number(t.memories_replayed??0),n=Number(t.connections_found??0),r=Number(t.insights_generated??0),i=Number(t.duration_ms??0),d=[];return d.push(`Replayed ${o} ${o===1?"memory":"memories"}`),n>0&&d.push(`${n} new connection${n===1?"":"s"}`),r>0&&d.push(`${r} insight${r===1?"":"s"}`),{type:e.type,title:"Dream consolidated",body:`${d.join(" · ")} in ${(i/1e3).toFixed(1)}s`,color:s,dwellMs:7e3}}case"ConsolidationCompleted":{const o=Number(t.nodes_processed??0),n=Number(t.decay_applied??0),r=Number(t.embeddings_generated??0),i=Number(t.duration_ms??0),d=[];return n>0&&d.push(`${n} decayed`),r>0&&d.push(`${r} embedded`),{type:e.type,title:"Consolidation swept",body:`${o} node${o===1?"":"s"}${d.length?" · "+d.join(" · "):""} in ${(i/1e3).toFixed(1)}s`,color:s,dwellMs:6e3}}case"ConnectionDiscovered":{const o=Date.now();if(o-f0?`suppression #${o} · Rac1 cascade ~${n} neighbors`:`suppression #${o}`,color:s,dwellMs:5500}}case"MemoryUnsuppressed":{const o=Number(t.remaining_count??0);return{type:e.type,title:"Recovered",body:o>0?`${o} suppression${o===1?"":"s"} remain`:"fully unsuppressed",color:s,dwellMs:5e3}}case"Rac1CascadeSwept":{const o=Number(t.seeds??0),n=Number(t.neighbors_affected??0);return{type:e.type,title:"Rac1 cascade",body:`${o} seed${o===1?"":"s"} · ${n} dendritic spine${n===1?"":"s"} pruned`,color:s,dwellMs:6e3}}case"MemoryDeleted":return{type:e.type,title:"Memory deleted",body:String(t.id??"").slice(0,8),color:s,dwellMs:4e3};case"HookVerdictRecorded":{const o=String(t.verdict??"NOTE"),n=String(t.reason??"Sanhedrin receipt updated");return{type:e.type,title:`Sanhedrin ${o}`,body:n,color:s,dwellMs:o==="VETO"?8e3:R}}case"Heartbeat":case"SearchPerformed":case"RetentionDecayed":case"ActivationSpread":case"ImportanceScored":case"MemoryCreated":case"MemoryUpdated":case"DreamStarted":case"DreamProgress":case"ConsolidationStarted":case"Connected":return null;default:return null}}let M=null;return E.subscribe(e=>{if(e.length===0)return;const s=[];for(const t of e){if(t===M)break;s.push(t)}if(s.length!==0){M=e[0];for(let t=s.length-1;t>=0;t--){const o=_(s[t]);o&&g(o)}}}),{subscribe:b,dismiss:h,clear:S,pauseDwell:D,resumeDwell:C,push:g}}const $=x();function I(){[{type:"DreamCompleted",title:"Dream consolidated",body:"Replayed 127 memories · 43 new connections · 5 insights in 2.4s",color:p.DreamCompleted,dwellMs:7e3},{type:"ConnectionDiscovered",title:"Bridge discovered",body:"semantic · weight 0.87",color:p.ConnectionDiscovered,dwellMs:4500},{type:"MemorySuppressed",title:"Forgetting",body:"suppression #2 · Rac1 cascade ~8 neighbors",color:p.MemorySuppressed,dwellMs:5500},{type:"ConsolidationCompleted",title:"Consolidation swept",body:"892 nodes · 156 decayed · 48 embedded in 1.1s",color:p.ConsolidationCompleted,dwellMs:6e3}].forEach((u,m)=>{setTimeout(()=>{$.push(u)},m*800)}),T($)}export{I as f,$ as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js.br b/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js.br new file mode 100644 index 0000000..fffecbf Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js.gz b/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js.gz new file mode 100644 index 0000000..896a3b1 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D4ymNiig.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js b/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js new file mode 100644 index 0000000..cd5566d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js @@ -0,0 +1 @@ +var x=t=>{throw TypeError(t)};var B=(t,e,n)=>e.has(t)||x("Cannot "+n);var a=(t,e,n)=>(B(t,e,"read from private field"),n?n.call(t):e.get(t)),c=(t,e,n)=>e.has(t)?x("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n);import{o as I}from"./GG5zm9kr.js";import{s as u,g as f,h as d}from"./CpWkWWOo.js";import{w as G}from"./BeMFXnHE.js";new URL("sveltekit-internal://");function ae(t,e){return t==="/"||e==="ignore"?t:e==="never"?t.endsWith("/")?t.slice(0,-1):t:e==="always"&&!t.endsWith("/")?t+"/":t}function oe(t){return t.split("%25").map(decodeURI).join("%25")}function ie(t){for(const e in t)t[e]=decodeURIComponent(t[e]);return t}function le({href:t}){return t.split("#")[0]}function W(...t){let e=5381;for(const n of t)if(typeof n=="string"){let r=n.length;for(;r;)e=e*33^n.charCodeAt(--r)}else if(ArrayBuffer.isView(n)){const r=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let s=r.length;for(;s;)e=e*33^r[--s]}else throw new TypeError("value must be a string or TypedArray");return(e>>>0).toString(36)}new TextEncoder;new TextDecoder;function X(t){const e=atob(t),n=new Uint8Array(e.length);for(let r=0;r((t instanceof Request?t.method:(e==null?void 0:e.method)||"GET")!=="GET"&&b.delete(U(t)),z(t,e));const b=new Map;function ce(t,e){const n=U(t,e),r=document.querySelector(n);if(r!=null&&r.textContent){r.remove();let{body:s,...l}=JSON.parse(r.textContent);const o=r.getAttribute("data-ttl");return o&&b.set(n,{body:s,init:l,ttl:1e3*Number(o)}),r.getAttribute("data-b64")!==null&&(s=X(s)),Promise.resolve(new Response(s,l))}return window.fetch(t,e)}function ue(t,e,n){if(b.size>0){const r=U(t,n),s=b.get(r);if(s){if(performance.now()o)}function s(o){n=!1,e.set(o)}function l(o){let i;return e.subscribe(h=>{(i===void 0||n&&h!==i)&&o(i=h)})}return{notify:r,set:s,subscribe:l}}const D={v:()=>{}};function Re(){const{set:t,subscribe:e}=G(!1);let n;async function r(){clearTimeout(n);try{const s=await fetch(`${M}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!s.ok)return!1;const o=(await s.json()).version!==F;return o&&(t(!0),D.v(),clearTimeout(n)),o}catch{return!1}}return{subscribe:e,check:r}}function Q(t,e,n){return t.origin!==Y||!t.pathname.startsWith(e)?!0:n?t.pathname!==location.pathname:!1}function Se(t){}const H=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...H];const Z=new Set([...H]);[...Z];let E,O,T;const ee=I.toString().includes("$$")||/function \w+\(\) \{\}/.test(I.toString());var _,m,w,p,v,y,A,R,P,S,V,k,j;ee?(E={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL("https://example.com")},O={current:null},T={current:!1}):(E=new(P=class{constructor(){c(this,_,u({}));c(this,m,u(null));c(this,w,u(null));c(this,p,u({}));c(this,v,u({id:null}));c(this,y,u({}));c(this,A,u(-1));c(this,R,u(new URL("https://example.com")))}get data(){return f(a(this,_))}set data(e){d(a(this,_),e)}get form(){return f(a(this,m))}set form(e){d(a(this,m),e)}get error(){return f(a(this,w))}set error(e){d(a(this,w),e)}get params(){return f(a(this,p))}set params(e){d(a(this,p),e)}get route(){return f(a(this,v))}set route(e){d(a(this,v),e)}get state(){return f(a(this,y))}set state(e){d(a(this,y),e)}get status(){return f(a(this,A))}set status(e){d(a(this,A),e)}get url(){return f(a(this,R))}set url(e){d(a(this,R),e)}},_=new WeakMap,m=new WeakMap,w=new WeakMap,p=new WeakMap,v=new WeakMap,y=new WeakMap,A=new WeakMap,R=new WeakMap,P),O=new(V=class{constructor(){c(this,S,u(null))}get current(){return f(a(this,S))}set current(e){d(a(this,S),e)}},S=new WeakMap,V),T=new(j=class{constructor(){c(this,k,u(!1))}get current(){return f(a(this,k))}set current(e){d(a(this,k),e)}},k=new WeakMap,j),D.v=()=>T.current=!0);function Ue(t){Object.assign(E,t)}export{be as H,_e as N,ge as P,he as S,ye as a,J as b,Re as c,le as d,ie as e,pe as f,ve as g,ae as h,Q as i,N as j,oe as k,fe as l,ue as m,O as n,Y as o,E as p,ce as q,me as r,we as s,de as t,Ae as u,Ue as v,Se as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js.br b/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js.br new file mode 100644 index 0000000..986afe9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js.gz b/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js.gz new file mode 100644 index 0000000..2fd77b7 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DGcYlAAw.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js b/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js deleted file mode 100644 index fbdf603..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js +++ /dev/null @@ -1 +0,0 @@ -import{k as t,l as S,m as h,q,S as T}from"./VE8Jor13.js";function k(r,i){return r===i||(r==null?void 0:r[T])===i}function A(r={},i,a,c){return t(()=>{var f,s;return S(()=>{f=s,s=[],h(()=>{r!==a(...s)&&(i(r,...s),f&&k(a(...f),r)&&i(null,...f))})}),()=>{q(()=>{s&&k(a(...s),r)&&i(null,...s)})}}),r}export{A as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js.br b/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js.br deleted file mode 100644 index bfd492a..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js.gz b/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js.gz deleted file mode 100644 index d26ca8c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DHakDdar.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js b/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js deleted file mode 100644 index e551895..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js +++ /dev/null @@ -1,2 +0,0 @@ -var Ye=Object.defineProperty;var ce=t=>{throw TypeError(t)};var Ie=(t,e,r)=>e in t?Ye(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var j=(t,e,r)=>Ie(t,typeof e!="symbol"?e+"":e,r),re=(t,e,r)=>e.has(t)||ce("Cannot "+r);var s=(t,e,r)=>(re(t,e,"read from private field"),r?r.call(t):e.get(t)),l=(t,e,r)=>e.has(t)?ce("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,r),a=(t,e,r,n)=>(re(t,e,"write to private field"),n?n.call(t,r):e.set(t,r),r),p=(t,e,r)=>(re(t,e,"access private method"),r);import{aL as Me,g as Te,l as Ce,m as Pe,aM as ue,q as G,ax as Ee,aj as Y,T as I,w as q,aN as _e,b as Ve,a7 as qe,aa as xe,aO as pe,ai as F,ah as we,aP as se,ag as ie,ak as Be,aQ as ge,aR as He,aS as ve,aT as Le,aU as We,aV as X,aW as Z,aX as ye,aY as je,aZ as Re,G as Se,ar as $e,af as ae,ac as K,O as ze,ab as Ue,a_ as $,E as Ge,a$ as Je,b0 as Qe,b1 as Xe,a3 as Ze,b2 as ne,ao as Ke,ap as De,a9 as et,aD as tt,b3 as fe,ad as z,b4 as rt,aC as st,b5 as it,au as at,p as nt,aq as ft,b6 as ht,a as ot}from"./VE8Jor13.js";import{b as lt}from"./7UNxJI5L.js";function dt(t){let e=0,r=Ee(0),n;return()=>{Me()&&(Te(r),Ce(()=>(e===0&&(n=Pe(()=>t(()=>ue(r)))),e+=1,()=>{G(()=>{e-=1,e===0&&(n==null||n(),n=void 0,ue(r))})})))}}var ct=Ge|Je;function ut(t,e,r,n){new _t(t,e,r,n)}var m,W,w,C,g,R,T,E,S,P,A,x,B,H,D,ee,h,Ne,Ae,Oe,he,J,Q,oe;class _t{constructor(e,r,n,c){l(this,h);j(this,"parent");j(this,"is_pending",!1);j(this,"transform_error");l(this,m);l(this,W,I?Y:null);l(this,w);l(this,C);l(this,g);l(this,R,null);l(this,T,null);l(this,E,null);l(this,S,null);l(this,P,0);l(this,A,0);l(this,x,!1);l(this,B,new Set);l(this,H,new Set);l(this,D,null);l(this,ee,dt(()=>(a(this,D,Ee(s(this,P))),()=>{a(this,D,null)})));var i;a(this,m,e),a(this,w,r),a(this,C,f=>{var u=q;u.b=this,u.f|=_e,n(f)}),this.parent=q.b,this.transform_error=c??((i=this.parent)==null?void 0:i.transform_error)??(f=>f),a(this,g,Ve(()=>{if(I){const f=s(this,W);qe();const u=f.data===xe;if(f.data.startsWith(pe)){const d=JSON.parse(f.data.slice(pe.length));p(this,h,Ae).call(this,d)}else u?p(this,h,Oe).call(this):p(this,h,Ne).call(this)}else p(this,h,he).call(this)},ct)),I&&a(this,m,Y)}defer_effect(e){We(e,s(this,B),s(this,H))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!s(this,w).pending}update_pending_count(e){p(this,h,oe).call(this,e),a(this,P,s(this,P)+e),!(!s(this,D)||s(this,x))&&(a(this,x,!0),G(()=>{a(this,x,!1),s(this,D)&&$e(s(this,D),s(this,P))}))}get_effect_pending(){return s(this,ee).call(this),Te(s(this,D))}error(e){var r=s(this,w).onerror;let n=s(this,w).failed;if(!r&&!n)throw e;s(this,R)&&(ae(s(this,R)),a(this,R,null)),s(this,T)&&(ae(s(this,T)),a(this,T,null)),s(this,E)&&(ae(s(this,E)),a(this,E,null)),I&&(K(s(this,W)),ze(),K(Ue()));var c=!1,i=!1;const f=()=>{if(c){Xe();return}c=!0,i&&Qe(),s(this,E)!==null&&ie(s(this,E),()=>{a(this,E,null)}),p(this,h,Q).call(this,()=>{se.ensure(),p(this,h,he).call(this)})},u=o=>{try{i=!0,r==null||r(o,f),i=!1}catch(d){$(d,s(this,g)&&s(this,g).parent)}n&&a(this,E,p(this,h,Q).call(this,()=>{se.ensure();try{return F(()=>{var d=q;d.b=this,d.f|=_e,n(s(this,m),()=>o,()=>f)})}catch(d){return $(d,s(this,g).parent),null}}))};G(()=>{var o;try{o=this.transform_error(e)}catch(d){$(d,s(this,g)&&s(this,g).parent);return}o!==null&&typeof o=="object"&&typeof o.then=="function"?o.then(u,d=>$(d,s(this,g)&&s(this,g).parent)):u(o)})}}m=new WeakMap,W=new WeakMap,w=new WeakMap,C=new WeakMap,g=new WeakMap,R=new WeakMap,T=new WeakMap,E=new WeakMap,S=new WeakMap,P=new WeakMap,A=new WeakMap,x=new WeakMap,B=new WeakMap,H=new WeakMap,D=new WeakMap,ee=new WeakMap,h=new WeakSet,Ne=function(){try{a(this,R,F(()=>s(this,C).call(this,s(this,m))))}catch(e){this.error(e)}},Ae=function(e){const r=s(this,w).failed;r&&a(this,E,F(()=>{r(s(this,m),()=>e,()=>()=>{})}))},Oe=function(){const e=s(this,w).pending;e&&(this.is_pending=!0,a(this,T,F(()=>e(s(this,m)))),G(()=>{var r=a(this,S,document.createDocumentFragment()),n=we();r.append(n),a(this,R,p(this,h,Q).call(this,()=>(se.ensure(),F(()=>s(this,C).call(this,n))))),s(this,A)===0&&(s(this,m).before(r),a(this,S,null),ie(s(this,T),()=>{a(this,T,null)}),p(this,h,J).call(this))}))},he=function(){try{if(this.is_pending=this.has_pending_snippet(),a(this,A,0),a(this,P,0),a(this,R,F(()=>{s(this,C).call(this,s(this,m))})),s(this,A)>0){var e=a(this,S,document.createDocumentFragment());Be(s(this,R),e);const r=s(this,w).pending;a(this,T,F(()=>r(s(this,m))))}else p(this,h,J).call(this)}catch(r){this.error(r)}},J=function(){this.is_pending=!1;for(const e of s(this,B))ge(e,He),ve(e);for(const e of s(this,H))ge(e,Le),ve(e);s(this,B).clear(),s(this,H).clear()},Q=function(e){var r=q,n=Re,c=Se;X(s(this,g)),Z(s(this,g)),ye(s(this,g).ctx);try{return e()}catch(i){return je(i),null}finally{X(r),Z(n),ye(c)}},oe=function(e){var r;if(!this.has_pending_snippet()){this.parent&&p(r=this.parent,h,oe).call(r,e);return}a(this,A,s(this,A)+e),s(this,A)===0&&(p(this,h,J).call(this),s(this,T)&&ie(s(this,T),()=>{a(this,T,null)}),s(this,S)&&(s(this,m).before(s(this,S)),a(this,S,null)))};const pt=["touchstart","touchmove"];function gt(t){return pt.includes(t)}const M=Symbol("events"),ke=new Set,le=new Set;function Tt(t,e,r){(e[M]??(e[M]={}))[t]=r}function Et(t){for(var e=0;e{throw k});throw N}}finally{t[M]=e,delete t.currentTarget,Z(V),X(L)}}}function wt(t,e){var r=e==null?"":typeof e=="object"?e+"":e;r!==(t.__t??(t.__t=t.nodeValue))&&(t.__t=r,t.nodeValue=r+"")}function vt(t,e){return Fe(t,e)}function Rt(t,e){ne(),e.intro=e.intro??!1;const r=e.target,n=I,c=Y;try{for(var i=Ke(r);i&&(i.nodeType!==De||i.data!==et);)i=tt(i);if(!i)throw fe;z(!0),K(i);const f=Fe(t,{...e,anchor:i});return z(!1),f}catch(f){if(f instanceof Error&&f.message.split(` -`).some(u=>u.startsWith("https://svelte.dev/e/")))throw f;return f!==fe&&console.warn("Failed to hydrate: ",f),e.recover===!1&&rt(),ne(),st(r),z(!1),vt(t,e)}finally{z(n),K(c)}}const U=new Map;function Fe(t,{target:e,anchor:r,props:n={},events:c,context:i,intro:f=!0,transformError:u}){ne();var o=void 0,d=it(()=>{var V=r??e.appendChild(we());ut(V,{pending:()=>{}},v=>{nt({});var _=Se;if(i&&(_.c=i),c&&(n.$$events=c),I&<(v,null),o=t(v,n)||{},I&&(q.nodes.end=Y,Y===null||Y.nodeType!==De||Y.data!==ft))throw ht(),fe;ot()},u);var L=new Set,N=v=>{for(var _=0;_{var O;for(var v of L)for(const b of[e,document]){var _=U.get(b),y=_.get(v);--y==0?(b.removeEventListener(v,me),_.delete(v),_.size===0&&U.delete(b)):_.set(v,y)}le.delete(N),V!==r&&((O=V.parentNode)==null||O.removeChild(V))}});return de.set(o,d),o}let de=new WeakMap;function St(t,e){const r=de.get(t);return r?(de.delete(t),r(e)):Promise.resolve()}export{Tt as a,Et as d,Rt as h,vt as m,wt as s,St as u}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js.br b/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js.br deleted file mode 100644 index b748706..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js.gz b/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js.gz deleted file mode 100644 index c7337c5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DHnEMX8z.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js b/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js deleted file mode 100644 index c2d8023..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js +++ /dev/null @@ -1 +0,0 @@ -var x=t=>{throw TypeError(t)};var B=(t,e,n)=>e.has(t)||x("Cannot "+n);var a=(t,e,n)=>(B(t,e,"read from private field"),n?n.call(t):e.get(t)),c=(t,e,n)=>e.has(t)?x("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n);import{o as I}from"./DWVWfZUn.js";import{s as u,g as f,h as d}from"./VE8Jor13.js";import{w as G}from"./CCRrbKqn.js";new URL("sveltekit-internal://");function ae(t,e){return t==="/"||e==="ignore"?t:e==="never"?t.endsWith("/")?t.slice(0,-1):t:e==="always"&&!t.endsWith("/")?t+"/":t}function oe(t){return t.split("%25").map(decodeURI).join("%25")}function ie(t){for(const e in t)t[e]=decodeURIComponent(t[e]);return t}function le({href:t}){return t.split("#")[0]}function W(...t){let e=5381;for(const n of t)if(typeof n=="string"){let r=n.length;for(;r;)e=e*33^n.charCodeAt(--r)}else if(ArrayBuffer.isView(n)){const r=new Uint8Array(n.buffer,n.byteOffset,n.byteLength);let s=r.length;for(;s;)e=e*33^r[--s]}else throw new TypeError("value must be a string or TypedArray");return(e>>>0).toString(36)}new TextEncoder;new TextDecoder;function X(t){const e=atob(t),n=new Uint8Array(e.length);for(let r=0;r((t instanceof Request?t.method:(e==null?void 0:e.method)||"GET")!=="GET"&&b.delete(U(t)),z(t,e));const b=new Map;function ce(t,e){const n=U(t,e),r=document.querySelector(n);if(r!=null&&r.textContent){r.remove();let{body:s,...l}=JSON.parse(r.textContent);const o=r.getAttribute("data-ttl");return o&&b.set(n,{body:s,init:l,ttl:1e3*Number(o)}),r.getAttribute("data-b64")!==null&&(s=X(s)),Promise.resolve(new Response(s,l))}return window.fetch(t,e)}function ue(t,e,n){if(b.size>0){const r=U(t,n),s=b.get(r);if(s){if(performance.now()o)}function s(o){n=!1,e.set(o)}function l(o){let i;return e.subscribe(h=>{(i===void 0||n&&h!==i)&&o(i=h)})}return{notify:r,set:s,subscribe:l}}const D={v:()=>{}};function Re(){const{set:t,subscribe:e}=G(!1);let n;async function r(){clearTimeout(n);try{const s=await fetch(`${M}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!s.ok)return!1;const o=(await s.json()).version!==F;return o&&(t(!0),D.v(),clearTimeout(n)),o}catch{return!1}}return{subscribe:e,check:r}}function Q(t,e,n){return t.origin!==Y||!t.pathname.startsWith(e)?!0:n?t.pathname!==location.pathname:!1}function Se(t){}const H=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...H];const Z=new Set([...H]);[...Z];let E,O,T;const ee=I.toString().includes("$$")||/function \w+\(\) \{\}/.test(I.toString());var _,w,m,p,v,y,A,R,P,S,V,k,j;ee?(E={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL("https://example.com")},O={current:null},T={current:!1}):(E=new(P=class{constructor(){c(this,_,u({}));c(this,w,u(null));c(this,m,u(null));c(this,p,u({}));c(this,v,u({id:null}));c(this,y,u({}));c(this,A,u(-1));c(this,R,u(new URL("https://example.com")))}get data(){return f(a(this,_))}set data(e){d(a(this,_),e)}get form(){return f(a(this,w))}set form(e){d(a(this,w),e)}get error(){return f(a(this,m))}set error(e){d(a(this,m),e)}get params(){return f(a(this,p))}set params(e){d(a(this,p),e)}get route(){return f(a(this,v))}set route(e){d(a(this,v),e)}get state(){return f(a(this,y))}set state(e){d(a(this,y),e)}get status(){return f(a(this,A))}set status(e){d(a(this,A),e)}get url(){return f(a(this,R))}set url(e){d(a(this,R),e)}},_=new WeakMap,w=new WeakMap,m=new WeakMap,p=new WeakMap,v=new WeakMap,y=new WeakMap,A=new WeakMap,R=new WeakMap,P),O=new(V=class{constructor(){c(this,S,u(null))}get current(){return f(a(this,S))}set current(e){d(a(this,S),e)}},S=new WeakMap,V),T=new(j=class{constructor(){c(this,k,u(!1))}get current(){return f(a(this,k))}set current(e){d(a(this,k),e)}},k=new WeakMap,j),D.v=()=>T.current=!0);function Ue(t){Object.assign(E,t)}export{be as H,_e as N,ge as P,he as S,ye as a,J as b,Re as c,le as d,ie as e,pe as f,ve as g,ae as h,Q as i,N as j,oe as k,fe as l,ue as m,O as n,Y as o,E as p,ce as q,we as r,me as s,de as t,Ae as u,Ue as v,Se as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js.br b/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js.br deleted file mode 100644 index 2c6962a..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js.gz b/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js.gz deleted file mode 100644 index 70132e1..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DUtaznkq.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js b/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js deleted file mode 100644 index 25cc4bc..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js +++ /dev/null @@ -1 +0,0 @@ -import{I as u,G as t,y as l,m as o}from"./VE8Jor13.js";function c(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function a(e){t===null&&c(),l&&t.l!==null?i(t).m.push(e):u(()=>{const n=o(e);if(typeof n=="function")return n})}function f(e){t===null&&c(),a(()=>()=>o(e))}function i(e){var n=e.l;return n.u??(n.u={a:[],b:[],m:[]})}export{f as a,a as o}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js.br b/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js.br deleted file mode 100644 index e0cdeaa..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js.gz b/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js.gz deleted file mode 100644 index 891e02f..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DWVWfZUn.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js.br b/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js.br deleted file mode 100644 index aefb742..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js.gz b/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js.gz deleted file mode 100644 index a105f31..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DcQGRi49.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js b/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js new file mode 100644 index 0000000..9dfddf7 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js @@ -0,0 +1 @@ +var D=Object.defineProperty;var g=i=>{throw TypeError(i)};var F=(i,e,s)=>e in i?D(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var w=(i,e,s)=>F(i,typeof e!="symbol"?e+"":e,s),y=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(y(i,e,"read from private field"),s?s.call(i):e.get(i)),l=(i,e,s)=>e.has(i)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,s),M=(i,e,s,a)=>(y(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{_ as x,aq as q,aw as k,ar as C,J as A,ai as B,N as J,U as N,ay as S,ak as U}from"./CpWkWWOo.js";var h,n,f,u,p,_,v;class E{constructor(e,s=!0){w(this,"anchor");l(this,h,new Map);l(this,n,new Map);l(this,f,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,()=>{var e=x;if(t(this,h).has(e)){var s=t(this,h).get(e),a=t(this,n).get(s);if(a)q(a),t(this,u).delete(s);else{var c=t(this,f).get(s);c&&(t(this,n).set(s,c.effect),t(this,f).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),a=c.effect)}for(const[r,o]of t(this,h)){if(t(this,h).delete(r),r===e)break;const d=t(this,f).get(o);d&&(k(d.effect),t(this,f).delete(o))}for(const[r,o]of t(this,n)){if(r===s||t(this,u).has(r))continue;const d=()=>{if(Array.from(t(this,h).values()).includes(r)){var b=document.createDocumentFragment();S(o,b),b.append(A()),t(this,f).set(r,{effect:o,fragment:b})}else k(o);t(this,u).delete(r),t(this,n).delete(r)};t(this,p)||!a?(t(this,u).add(r),C(o,d,!1)):d()}}});l(this,v,e=>{t(this,h).delete(e);const s=Array.from(t(this,h).values());for(const[a,c]of t(this,f))s.includes(a)||(k(c.effect),t(this,f).delete(a))});this.anchor=e,M(this,p,s)}ensure(e,s){var a=x,c=U();if(s&&!t(this,n).has(e)&&!t(this,f).has(e))if(c){var r=document.createDocumentFragment(),o=A();r.append(o),t(this,f).set(e,{effect:B(()=>s(o)),fragment:r})}else t(this,n).set(e,B(()=>s(this.anchor)));if(t(this,h).set(a,e),c){for(const[d,m]of t(this,n))d===e?a.unskip_effect(m):a.skip_effect(m);for(const[d,m]of t(this,f))d===e?a.unskip_effect(m.effect):a.skip_effect(m.effect);a.oncommit(t(this,_)),a.ondiscard(t(this,v))}else J&&(this.anchor=N),t(this,_).call(this)}}h=new WeakMap,n=new WeakMap,f=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{E as B}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js.br b/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js.br new file mode 100644 index 0000000..4a2afa5 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js.gz b/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js.gz new file mode 100644 index 0000000..56c70f0 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DdEqwvdI.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js b/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js new file mode 100644 index 0000000..18d1867 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js @@ -0,0 +1 @@ +import{aB as a,az as t,w as u,v as o}from"./CpWkWWOo.js";function c(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function l(e){t===null&&c(),u&&t.l!==null?i(t).m.push(e):a(()=>{const n=o(e);if(typeof n=="function")return n})}function f(e){t===null&&c(),l(()=>()=>o(e))}function i(e){var n=e.l;return n.u??(n.u={a:[],b:[],m:[]})}export{f as a,l as o}; diff --git a/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js.br b/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js.br new file mode 100644 index 0000000..62d7832 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js.gz b/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js.gz new file mode 100644 index 0000000..48eecbb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/GG5zm9kr.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js b/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js deleted file mode 100644 index aab0ce9..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js +++ /dev/null @@ -1 +0,0 @@ -import{b as T,T as o,a7 as b,E as h,a8 as p,a9 as A,aa as E,ab as R,ac as g,ad as l}from"./VE8Jor13.js";import{B as v}from"./BYWGnCkZ.js";function N(t,c,u=!1){o&&b();var n=new v(t),_=u?h:0;function i(a,r){if(o){const e=p(t);var s;if(e===A?s=0:e===E?s=!1:s=parseInt(e.substring(1)),a!==s){var f=R();g(f),n.anchor=f,l(!1),n.ensure(a,r),l(!0);return}}n.ensure(a,r)}T(()=>{var a=!1;c((r,s=0)=>{a=!0,i(s,r)}),a||i(!1,null)},_)}export{N as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js.br b/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js.br deleted file mode 100644 index a919f0f..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js.gz b/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js.gz deleted file mode 100644 index 410c0c5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/JkhlGLjU.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js b/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js new file mode 100644 index 0000000..bf47818 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js @@ -0,0 +1 @@ +import{d as i,w as S}from"./BeMFXnHE.js";const b=200;function H(){const{subscribe:t,set:o,update:e}=S({connected:!1,events:[],lastHeartbeat:null,error:null});let n=null,a=null,d=0;function m(s){const c=s||(window.location.port==="5173"?`ws://${window.location.hostname}:3927/ws`:`ws://${window.location.host}/ws`);if((n==null?void 0:n.readyState)!==WebSocket.OPEN)try{n=new WebSocket(c),n.onopen=()=>{d=0,e(r=>({...r,connected:!0,error:null}))},n.onmessage=r=>{try{const l=JSON.parse(r.data);e(f=>{if(l.type==="Heartbeat")return{...f,lastHeartbeat:l};const $=[l,...f.events].slice(0,b);return{...f,events:$}})}catch(l){console.warn("[vestige] Failed to parse WebSocket message:",l)}},n.onclose=()=>{e(r=>({...r,connected:!1})),p(c)},n.onerror=()=>{e(r=>({...r,error:"WebSocket connection failed"}))}}catch(r){e(l=>({...l,error:String(r)}))}}function p(s){a&&clearTimeout(a);const c=Math.min(1e3*2**d,3e4);d++,a=setTimeout(()=>m(s),c)}function v(){a&&clearTimeout(a),n==null||n.close(),n=null,o({connected:!1,events:[],lastHeartbeat:null,error:null})}function h(){e(s=>({...s,events:[]}))}function w(s){e(c=>{const r=[s,...c.events].slice(0,b);return{...c,events:r}})}return{subscribe:t,connect:m,disconnect:v,clearEvents:h,injectEvent:w}}const u=H(),g=i(u,t=>t.connected),k=i(u,t=>t.events);i(u,t=>t.lastHeartbeat);const M=i(u,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.memory_count)??0}),E=i(u,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.avg_retention)??0}),T=i(u,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.suppressed_count)??0}),W=i(u,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.uptime_secs)??0});function _(t){if(!Number.isFinite(t)||t<0)return"—";const o=Math.floor(t/86400),e=Math.floor(t%86400/3600),n=Math.floor(t%3600/60),a=Math.floor(t%60);return o>0?e>0?`${o}d ${e}h`:`${o}d`:e>0?n>0?`${e}h ${n}m`:`${e}h`:n>0?a>0?`${n}m ${a}s`:`${n}m`:`${a}s`}export{E as a,k as e,_ as f,g as i,M as m,T as s,W as u,u as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js.br b/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js.br new file mode 100644 index 0000000..04a9af2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js.gz b/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js.gz new file mode 100644 index 0000000..8b5bf6e Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/MAY1QfFZ.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js b/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js new file mode 100644 index 0000000..f9c6462 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js @@ -0,0 +1 @@ +import{k as L,l as D,P as T,g as P,c as B,h as b,m as Y,o as h,D as x,q as M,v as N,w as U,x as q,y as w,z,A as $,B as y,S as C,L as G}from"./CpWkWWOo.js";import{c as Z}from"./C6HuKgyx.js";function H(r,a,t,s){var o;var f=!U||(t&q)!==0,v=(t&M)!==0,E=(t&y)!==0,n=s,S=!0,g=()=>(S&&(S=!1,n=E?N(s):s),n),u;if(v){var O=C in r||G in r;u=((o=L(r,a))==null?void 0:o.set)??(O&&a in r?e=>r[a]=e:void 0)}var _,I=!1;v?[_,I]=Z(()=>r[a]):_=r[a],_===void 0&&s!==void 0&&(_=g(),u&&(f&&D(),u(_)));var i;if(f?i=()=>{var e=r[a];return e===void 0?g():(S=!0,e)}:i=()=>{var e=r[a];return e!==void 0&&(n=void 0),e===void 0?n:e},f&&(t&T)===0)return i;if(u){var R=r.$$legacy;return(function(e,l){return arguments.length>0?((!f||!l||R||I)&&u(l?i():e),e):i()})}var c=!1,d=((t&w)!==0?z:$)(()=>(c=!1,i()));v&&P(d);var m=h;return(function(e,l){if(arguments.length>0){const A=l?P(d):f&&v?B(e):e;return b(d,A),c=!0,n!==void 0&&(n=A),e}return Y&&c||(m.f&x)!==0?d.v:P(d)})}export{H as p}; diff --git a/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js.br b/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js.br new file mode 100644 index 0000000..e9fafc8 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js.gz b/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js.gz new file mode 100644 index 0000000..a76e9e0 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/V6gjw5Ec.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/VE8Jor13.js b/apps/dashboard/build/_app/immutable/chunks/VE8Jor13.js deleted file mode 100644 index 8170545..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/VE8Jor13.js +++ /dev/null @@ -1 +0,0 @@ -var cn=Object.defineProperty;var wt=e=>{throw TypeError(e)};var _n=(e,t,n)=>t in e?cn(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var de=(e,t,n)=>_n(e,typeof t!="symbol"?t+"":t,n),Ke=(e,t,n)=>t.has(e)||wt("Cannot "+n);var p=(e,t,n)=>(Ke(e,t,"read from private field"),n?n.call(e):t.get(e)),F=(e,t,n)=>t.has(e)?wt("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,n),z=(e,t,n,r)=>(Ke(e,t,"write to private field"),r?r.call(e,n):t.set(e,n),n),K=(e,t,n)=>(Ke(e,t,"access private method"),n);var vn=Array.isArray,dn=Array.prototype.indexOf,Ee=Array.prototype.includes,ar=Array.from,lr=Object.defineProperty,Re=Object.getOwnPropertyDescriptor,pn=Object.getOwnPropertyDescriptors,hn=Object.prototype,wn=Array.prototype,kt=Object.getPrototypeOf,yt=Object.isExtensible;const yn=()=>{};function or(e){return e()}function En(e){for(var t=0;t{e=r,t=s});return{promise:n,resolve:e,reject:t}}function ur(e,t){if(Array.isArray(e))return e;if(t===void 0||!(Symbol.iterator in e))return Array.from(e);const n=[];for(const r of e)if(n.push(r),n.length===t)break;return n}const A=2,De=4,Ie=8,Dt=1<<24,G=16,H=32,ve=64,mn=128,P=512,g=1024,R=2048,Y=4096,j=8192,$=16384,oe=32768,je=65536,Et=1<<17,It=1<<18,Pe=1<<19,Pt=1<<20,cr=1<<25,ue=65536,$e=1<<21,st=1<<22,Z=1<<23,ae=Symbol("$state"),_r=Symbol("legacy props"),vr=Symbol(""),ne=new class extends Error{constructor(){super(...arguments);de(this,"name","StaleReactionError");de(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Nt;const pr=!!((Nt=globalThis.document)!=null&&Nt.contentType)&&globalThis.document.contentType.includes("xml"),Ue=3,Ct=8;function gn(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function hr(e,t,n){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Tn(e){throw new Error("https://svelte.dev/e/effect_in_teardown")}function bn(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function An(e){throw new Error("https://svelte.dev/e/effect_orphan")}function Sn(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function wr(){throw new Error("https://svelte.dev/e/hydration_failed")}function yr(e){throw new Error("https://svelte.dev/e/props_invalid_value")}function Rn(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function On(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Nn(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Er(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}const mr=1,gr=2,Tr=4,br=8,Ar=16,Sr=1,Rr=2,Or=4,Nr=8,kr=16,xr=1,Dr=2,kn="[",xn="[!",Ir="[?",Dn="]",ft={},T=Symbol(),In="http://www.w3.org/1999/xhtml";function it(e){console.warn("https://svelte.dev/e/hydration_mismatch")}function Pr(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Cr(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}let J=!1;function Fr(e){J=e}let m;function me(e){if(e===null)throw it(),ft;return m=e}function Mr(){return me(ee(m))}function Lr(e){if(J){if(ee(m)!==null)throw it(),ft;m=e}}function jr(e=1){if(J){for(var t=e,n=m;t--;)n=ee(n);m=n}}function Yr(e=!0){for(var t=0,n=m;;){if(n.nodeType===Ct){var r=n.data;if(r===Dn){if(t===0)return n;t-=1}else(r===kn||r===xn||r[0]==="["&&!isNaN(Number(r.slice(1))))&&(t+=1)}var s=ee(n);e&&n.remove(),n=s}}function Hr(e){if(!e||e.nodeType!==Ct)throw it(),ft;return e.data}function Ft(e){return e===this.v}function Pn(e,t){return e!=e?t==t:e!==t||e!==null&&typeof e=="object"||typeof e=="function"}function Mt(e){return!Pn(e,this.v)}let Be=!1;function qr(){Be=!0}let S=null;function Ye(e){S=e}function Vr(e,t=!1,n){S={p:S,i:!1,c:null,e:null,s:e,x:null,l:Be&&!t?{s:null,u:null,$:[]}:null}}function Ur(e){var t=S,n=t.e;if(n!==null){t.e=null;for(var r of n)Jt(r)}return t.i=!0,S=t.p,{}}function Ce(){return!Be||S!==null&&S.l===null}let re=[];function Lt(){var e=re;re=[],En(e)}function mt(e){if(re.length===0&&!Oe){var t=re;queueMicrotask(()=>{t===re&&Lt()})}re.push(e)}function Cn(){for(;re.length>0;)Lt()}function Fn(e){var t=w;if(t===null)return _.f|=Z,e;if((t.f&oe)===0&&(t.f&De)===0)throw e;He(e,t)}function He(e,t){for(;t!==null;){if((t.f&mn)!==0){if((t.f&oe)===0)throw e;try{t.b.error(e);return}catch(n){e=n}}t=t.parent}throw e}const Mn=-7169;function E(e,t){e.f=e.f&Mn|t}function at(e){(e.f&P)!==0||e.deps===null?E(e,g):E(e,Y)}function jt(e){if(e!==null)for(const t of e)(t.f&A)===0||(t.f&ue)===0||(t.f^=ue,jt(t.deps))}function Ln(e,t,n){(e.f&R)!==0?t.add(e):(e.f&Y)!==0&&n.add(e),jt(e.deps),E(e,g)}const Me=new Set;let d=null,gt=null,b=null,N=[],Ge=null,Ze=!1,Oe=!1;var pe,he,fe,we,ke,xe,ie,U,ye,D,We,Je,Qe,Yt;const dt=class dt{constructor(){F(this,D);de(this,"current",new Map);de(this,"previous",new Map);F(this,pe,new Set);F(this,he,new Set);F(this,fe,0);F(this,we,0);F(this,ke,null);F(this,xe,new Set);F(this,ie,new Set);F(this,U,new Map);de(this,"is_fork",!1);F(this,ye,!1)}skip_effect(t){p(this,U).has(t)||p(this,U).set(t,{d:[],m:[]})}unskip_effect(t){var n=p(this,U).get(t);if(n){p(this,U).delete(t);for(var r of n.d)E(r,R),B(r);for(r of n.m)E(r,Y),B(r)}}process(t){var s;N=[],this.apply();var n=[],r=[];for(const f of t)K(this,D,Je).call(this,f,n,r);if(K(this,D,We).call(this)){K(this,D,Qe).call(this,r),K(this,D,Qe).call(this,n);for(const[f,a]of p(this,U))Ut(f,a)}else{for(const f of p(this,pe))f();p(this,pe).clear(),p(this,fe)===0&&K(this,D,Yt).call(this),gt=this,d=null,Tt(r),Tt(n),gt=null,(s=p(this,ke))==null||s.resolve()}b=null}capture(t,n){n!==T&&!this.previous.has(t)&&this.previous.set(t,n),(t.f&Z)===0&&(this.current.set(t,t.v),b==null||b.set(t,t.v))}activate(){d=this,this.apply()}deactivate(){d===this&&(d=null,b=null)}flush(){if(this.activate(),N.length>0){if(Ht(),d!==null&&d!==this)return}else p(this,fe)===0&&this.process([]);this.deactivate()}discard(){for(const t of p(this,he))t(this);p(this,he).clear()}increment(t){z(this,fe,p(this,fe)+1),t&&z(this,we,p(this,we)+1)}decrement(t){z(this,fe,p(this,fe)-1),t&&z(this,we,p(this,we)-1),!p(this,ye)&&(z(this,ye,!0),mt(()=>{z(this,ye,!1),K(this,D,We).call(this)?N.length>0&&this.flush():this.revive()}))}revive(){for(const t of p(this,xe))p(this,ie).delete(t),E(t,R),B(t);for(const t of p(this,ie))E(t,Y),B(t);this.flush()}oncommit(t){p(this,pe).add(t)}ondiscard(t){p(this,he).add(t)}settled(){return(p(this,ke)??z(this,ke,xt())).promise}static ensure(){if(d===null){const t=d=new dt;Me.add(d),Oe||mt(()=>{d===t&&t.flush()})}return d}apply(){}};pe=new WeakMap,he=new WeakMap,fe=new WeakMap,we=new WeakMap,ke=new WeakMap,xe=new WeakMap,ie=new WeakMap,U=new WeakMap,ye=new WeakMap,D=new WeakSet,We=function(){return this.is_fork||p(this,we)>0},Je=function(t,n,r){t.f^=g;for(var s=t.first;s!==null;){var f=s.f,a=(f&(H|ve))!==0,l=a&&(f&g)!==0,i=l||(f&j)!==0||p(this,U).has(s);if(!i&&s.fn!==null){a?s.f^=g:(f&De)!==0?n.push(s):Fe(s)&&((f&G)!==0&&p(this,ie).add(s),be(s));var o=s.first;if(o!==null){s=o;continue}}for(;s!==null;){var c=s.next;if(c!==null){s=c;break}s=s.parent}}},Qe=function(t){for(var n=0;n1){this.previous.clear();var t=b,n=!0;for(const f of Me){if(f===this){n=!1;continue}const a=[];for(const[i,o]of this.current){if(f.current.has(i))if(n&&o!==f.current.get(i))f.current.set(i,o);else continue;a.push(i)}if(a.length===0)continue;const l=[...f.current.keys()].filter(i=>!this.current.has(i));if(l.length>0){var r=N;N=[];const i=new Set,o=new Map;for(const c of a)qt(c,l,i,o);if(N.length>0){d=f,f.apply();for(const c of N)K(s=f,D,Je).call(s,c,[],[]);f.deactivate()}N=r}}d=null,b=t}Me.delete(this)};let ge=dt;function jn(e){var t=Oe;Oe=!0;try{for(var n;;){if(Cn(),N.length===0&&(d==null||d.flush(),N.length===0))return Ge=null,n;Ht()}}finally{Oe=t}}function Ht(){Ze=!0;var e=null;try{for(var t=0;N.length>0;){var n=ge.ensure();if(t++>1e3){var r,s;Yn()}n.process(N),W.clear()}}finally{N=[],Ze=!1,Ge=null}}function Yn(){try{Sn()}catch(e){He(e,Ge)}}let M=null;function Tt(e){var t=e.length;if(t!==0){for(var n=0;n0)){W.clear();for(const s of M){if((s.f&($|j))!==0)continue;const f=[s];let a=s.parent;for(;a!==null;)M.has(a)&&(M.delete(a),f.push(a)),a=a.parent;for(let l=f.length-1;l>=0;l--){const i=f[l];(i.f&($|j))===0&&be(i)}}M.clear()}}M=null}}function qt(e,t,n,r){if(!n.has(e)&&(n.add(e),e.reactions!==null))for(const s of e.reactions){const f=s.f;(f&A)!==0?qt(s,t,n,r):(f&(st|G))!==0&&(f&R)===0&&Vt(s,t,r)&&(E(s,R),B(s))}}function Vt(e,t,n){const r=n.get(e);if(r!==void 0)return r;if(e.deps!==null)for(const s of e.deps){if(Ee.call(t,s))return!0;if((s.f&A)!==0&&Vt(s,t,n))return n.set(s,!0),!0}return n.set(e,!1),!1}function B(e){var t=Ge=e,n=t.b;if(n!=null&&n.is_pending&&(e.f&(De|Ie|Dt))!==0&&(e.f&oe)===0){n.defer_effect(e);return}for(;t.parent!==null;){t=t.parent;var r=t.f;if(Ze&&t===w&&(r&G)!==0&&(r&It)===0&&(r&oe)!==0)return;if((r&(ve|H))!==0){if((r&g)===0)return;t.f^=g}}N.push(t)}function Ut(e,t){if(!((e.f&H)!==0&&(e.f&g)!==0)){(e.f&R)!==0?t.d.push(e):(e.f&Y)!==0&&t.m.push(e),E(e,g);for(var n=e.first;n!==null;)Ut(n,t),n=n.next}}function Hn(e,t,n,r){const s=Ce()?lt:Bn;var f=e.filter(u=>!u.settled);if(n.length===0&&f.length===0){r(t.map(s));return}var a=w,l=qn(),i=f.length===1?f[0].promise:f.length>1?Promise.all(f.map(u=>u.promise)):null;function o(u){l();try{r(u)}catch(v){(a.f&$)===0&&He(v,a)}et()}if(n.length===0){i.then(()=>o(t.map(s)));return}function c(){l(),Promise.all(n.map(u=>Un(u))).then(u=>o([...t.map(s),...u])).catch(u=>He(u,a))}i?i.then(c):c()}function qn(){var e=w,t=_,n=S,r=d;return function(f=!0){Te(e),Q(t),Ye(n),f&&(r==null||r.activate())}}function et(e=!0){Te(null),Q(null),Ye(null),e&&(d==null||d.deactivate())}function Vn(){var e=w.b,t=d,n=e.is_rendered();return e.update_pending_count(1),t.increment(n),()=>{e.update_pending_count(-1),t.decrement(n)}}function lt(e){var t=A|R,n=_!==null&&(_.f&A)!==0?_:null;return w!==null&&(w.f|=Pe),{ctx:S,deps:null,effects:null,equals:Ft,f:t,fn:e,reactions:null,rv:0,v:T,wv:0,parent:n??w,ac:null}}function Un(e,t,n){w===null&&gn();var s=void 0,f=ut(T),a=!_,l=new Map;return er(()=>{var v;var i=xt();s=i.promise;try{Promise.resolve(e()).then(i.resolve,i.reject).finally(et)}catch(y){i.reject(y),et()}var o=d;if(a){var c=Vn();(v=l.get(o))==null||v.reject(ne),l.delete(o),l.set(o,i)}const u=(y,h=void 0)=>{if(o.activate(),h)h!==ne&&(f.f|=Z,nt(f,h));else{(f.f&Z)!==0&&(f.f^=Z),nt(f,y);for(const[V,O]of l){if(l.delete(V),V===o)break;O.reject(ne)}}c&&c()};i.promise.then(u,y=>u(null,y||"unknown"))}),Qn(()=>{for(const i of l.values())i.reject(ne)}),new Promise(i=>{function o(c){function u(){c===s?i(f):o(s)}c.then(u,u)}o(s)})}function Br(e){const t=lt(e);return rn(t),t}function Bn(e){const t=lt(e);return t.equals=Mt,t}function Gn(e){var t=e.effects;if(t!==null){e.effects=null;for(var n=0;n0&&!zt&&Xn()}return t}function Xn(){zt=!1;for(const e of tt)(e.f&g)!==0&&E(e,Y),Fe(e)&&be(e);tt.clear()}function Xe(e){te(e,e.v+1)}function Kt(e,t){var n=e.reactions;if(n!==null)for(var r=Ce(),s=n.length,f=0;f{if(le===f)return l();var i=_,o=le;Q(null),Ot(f);var c=l();return Q(i),Ot(o),c};return r&&n.set("length",X(e.length)),new Proxy(e,{defineProperty(l,i,o){(!("value"in o)||o.configurable===!1||o.enumerable===!1||o.writable===!1)&&Rn();var c=n.get(i);return c===void 0?a(()=>{var u=X(o.value);return n.set(i,u),u}):te(c,o.value,!0),!0},deleteProperty(l,i){var o=n.get(i);if(o===void 0){if(i in l){const c=a(()=>X(T));n.set(i,c),Xe(s)}}else te(o,T),Xe(s);return!0},get(l,i,o){var y;if(i===ae)return e;var c=n.get(i),u=i in l;if(c===void 0&&(!u||(y=Re(l,i))!=null&&y.writable)&&(c=a(()=>{var h=Ae(u?l[i]:T),V=X(h);return V}),n.set(i,c)),c!==void 0){var v=Se(c);return v===T?void 0:v}return Reflect.get(l,i,o)},getOwnPropertyDescriptor(l,i){var o=Reflect.getOwnPropertyDescriptor(l,i);if(o&&"value"in o){var c=n.get(i);c&&(o.value=Se(c))}else if(o===void 0){var u=n.get(i),v=u==null?void 0:u.v;if(u!==void 0&&v!==T)return{enumerable:!0,configurable:!0,value:v,writable:!0}}return o},has(l,i){var v;if(i===ae)return!0;var o=n.get(i),c=o!==void 0&&o.v!==T||Reflect.has(l,i);if(o!==void 0||w!==null&&(!c||(v=Re(l,i))!=null&&v.writable)){o===void 0&&(o=a(()=>{var y=c?Ae(l[i]):T,h=X(y);return h}),n.set(i,o));var u=Se(o);if(u===T)return!1}return c},set(l,i,o,c){var ht;var u=n.get(i),v=i in l;if(r&&i==="length")for(var y=o;yX(T)),n.set(y+"",h))}if(u===void 0)(!v||(ht=Re(l,i))!=null&&ht.writable)&&(u=a(()=>X(void 0)),te(u,Ae(o)),n.set(i,u));else{v=u.v!==T;var V=a(()=>Ae(o));te(u,V)}var O=Reflect.getOwnPropertyDescriptor(l,i);if(O!=null&&O.set&&O.set.call(c,o),!v){if(r&&typeof i=="string"){var pt=n.get("length"),ze=Number(i);Number.isInteger(ze)&&ze>=pt.v&&te(pt,ze+1)}Xe(s)}return!0},ownKeys(l){Se(s);var i=Reflect.ownKeys(l).filter(u=>{var v=n.get(u);return v===void 0||v.v!==T});for(var[o,c]of n)c.v!==T&&!(o in l)&&i.push(o);return i},setPrototypeOf(){On()}})}function bt(e){try{if(e!==null&&typeof e=="object"&&ae in e)return e[ae]}catch{}return e}function zr(e,t){return Object.is(bt(e),bt(t))}var At,$n,Xt,$t;function Kr(){if(At===void 0){At=window,$n=/Firefox/.test(navigator.userAgent);var e=Element.prototype,t=Node.prototype,n=Text.prototype;Xt=Re(t,"firstChild").get,$t=Re(t,"nextSibling").get,yt(e)&&(e.__click=void 0,e.__className=void 0,e.__attributes=null,e.__style=void 0,e.__e=void 0),yt(n)&&(n.__t=void 0)}}function qe(e=""){return document.createTextNode(e)}function Ve(e){return Xt.call(e)}function ee(e){return $t.call(e)}function Xr(e,t){if(!J)return Ve(e);var n=Ve(m);if(n===null)n=m.appendChild(qe());else if(t&&n.nodeType!==Ue){var r=qe();return n==null||n.before(r),me(r),r}return t&&ct(n),me(n),n}function $r(e,t=!1){if(!J){var n=Ve(e);return n instanceof Comment&&n.data===""?ee(n):n}if(t){if((m==null?void 0:m.nodeType)!==Ue){var r=qe();return m==null||m.before(r),me(r),r}ct(m)}return m}function Zr(e,t=1,n=!1){let r=J?m:e;for(var s;t--;)s=r,r=ee(r);if(!J)return r;if(n){if((r==null?void 0:r.nodeType)!==Ue){var f=qe();return r===null?s==null||s.after(f):r.before(f),me(f),f}ct(r)}return me(r),r}function Zn(e){e.textContent=""}function Wr(){return!1}function Jr(e,t,n){return document.createElementNS(In,e,void 0)}function ct(e){if(e.nodeValue.length<65536)return;let t=e.nextSibling;for(;t!==null&&t.nodeType===Ue;)t.remove(),e.nodeValue+=t.nodeValue,t=e.nextSibling}function Qr(e){J&&Ve(e)!==null&&Zn(e)}let St=!1;function Wn(){St||(St=!0,document.addEventListener("reset",e=>{Promise.resolve().then(()=>{var t;if(!e.defaultPrevented)for(const n of e.target.elements)(t=n.__on_r)==null||t.call(n)})},{capture:!0}))}function _t(e){var t=_,n=w;Q(null),Te(null);try{return e()}finally{Q(t),Te(n)}}function es(e,t,n,r=n){e.addEventListener(t,()=>_t(n));const s=e.__on_r;s?e.__on_r=()=>{s(),r(!0)}:e.__on_r=()=>r(!0),Wn()}function Zt(e){w===null&&(_===null&&An(),bn()),_e&&Tn()}function Jn(e,t){var n=t.last;n===null?t.last=t.first=e:(n.next=e,e.prev=n,t.last=e)}function q(e,t,n){var r=w;r!==null&&(r.f&j)!==0&&(e|=j);var s={ctx:S,deps:null,nodes:null,f:e|R|P,first:null,fn:t,last:null,next:null,parent:r,b:r&&r.b,prev:null,teardown:null,wv:0,ac:null};if(n)try{be(s)}catch(l){throw ce(s),l}else t!==null&&B(s);var f=s;if(n&&f.deps===null&&f.teardown===null&&f.nodes===null&&f.first===f.last&&(f.f&Pe)===0&&(f=f.first,(e&G)!==0&&(e&je)!==0&&f!==null&&(f.f|=je)),f!==null&&(f.parent=r,r!==null&&Jn(f,r),_!==null&&(_.f&A)!==0&&(e&ve)===0)){var a=_;(a.effects??(a.effects=[])).push(f)}return s}function Wt(){return _!==null&&!L}function Qn(e){const t=q(Ie,null,!1);return E(t,g),t.teardown=e,t}function ts(e){Zt();var t=w.f,n=!_&&(t&H)!==0&&(t&oe)===0;if(n){var r=S;(r.e??(r.e=[])).push(e)}else return Jt(e)}function Jt(e){return q(De|Pt,e,!1)}function ns(e){return Zt(),q(Ie|Pt,e,!0)}function rs(e){ge.ensure();const t=q(ve|Pe,e,!0);return(n={})=>new Promise(r=>{n.outro?rr(t,()=>{ce(t),r(void 0)}):(ce(t),r(void 0))})}function ss(e){return q(De,e,!1)}function er(e){return q(st|Pe,e,!0)}function fs(e,t=0){return q(Ie|t,e,!0)}function is(e,t=[],n=[],r=[]){Hn(r,t,n,s=>{q(Ie,()=>e(...s.map(Se)),!0)})}function as(e,t=0){var n=q(G|t,e,!0);return n}function ls(e){return q(H|Pe,e,!0)}function Qt(e){var t=e.teardown;if(t!==null){const n=_e,r=_;Rt(!0),Q(null);try{t.call(null)}finally{Rt(n),Q(r)}}}function vt(e,t=!1){var n=e.first;for(e.first=e.last=null;n!==null;){const s=n.ac;s!==null&&_t(()=>{s.abort(ne)});var r=n.next;(n.f&ve)!==0?n.parent=null:ce(n,t),n=r}}function tr(e){for(var t=e.first;t!==null;){var n=t.next;(t.f&H)===0&&ce(t),t=n}}function ce(e,t=!0){var n=!1;(t||(e.f&It)!==0)&&e.nodes!==null&&e.nodes.end!==null&&(nr(e.nodes.start,e.nodes.end),n=!0),vt(e,t&&!n),Ne(e,0),E(e,$);var r=e.nodes&&e.nodes.t;if(r!==null)for(const f of r)f.stop();Qt(e);var s=e.parent;s!==null&&s.first!==null&&en(e),e.next=e.prev=e.teardown=e.ctx=e.deps=e.fn=e.nodes=e.ac=null}function nr(e,t){for(;e!==null;){var n=e===t?null:ee(e);e.remove(),e=n}}function en(e){var t=e.parent,n=e.prev,r=e.next;n!==null&&(n.next=r),r!==null&&(r.prev=n),t!==null&&(t.first===e&&(t.first=r),t.last===e&&(t.last=n))}function rr(e,t,n=!0){var r=[];tn(e,r,!0);var s=()=>{n&&ce(e),t&&t()},f=r.length;if(f>0){var a=()=>--f||s();for(var l of r)l.out(a)}else s()}function tn(e,t,n){if((e.f&j)===0){e.f^=j;var r=e.nodes&&e.nodes.t;if(r!==null)for(const l of r)(l.is_global||n)&&t.push(l);for(var s=e.first;s!==null;){var f=s.next,a=(s.f&je)!==0||(s.f&H)!==0&&(e.f&G)!==0;tn(s,t,a?n:!1),s=f}}}function os(e){nn(e,!0)}function nn(e,t){if((e.f&j)!==0){e.f^=j,(e.f&g)===0&&(E(e,R),B(e));for(var n=e.first;n!==null;){var r=n.next,s=(n.f&je)!==0||(n.f&H)!==0;nn(n,s?t:!1),n=r}var f=e.nodes&&e.nodes.t;if(f!==null)for(const a of f)(a.is_global||t)&&a.in()}}function us(e,t){if(e.nodes)for(var n=e.nodes.start,r=e.nodes.end;n!==null;){var s=n===r?null:ee(n);t.append(n),n=s}}let Le=!1,_e=!1;function Rt(e){_e=e}let _=null,L=!1;function Q(e){_=e}let w=null;function Te(e){w=e}let C=null;function rn(e){_!==null&&(C===null?C=[e]:C.push(e))}let k=null,x=0,I=null;function sr(e){I=e}let sn=1,se=0,le=se;function Ot(e){le=e}function fn(){return++sn}function Fe(e){var t=e.f;if((t&R)!==0)return!0;if(t&A&&(e.f&=~ue),(t&Y)!==0){for(var n=e.deps,r=n.length,s=0;se.wv)return!0}(t&P)!==0&&b===null&&E(e,g)}return!1}function an(e,t,n=!0){var r=e.reactions;if(r!==null&&!(C!==null&&Ee.call(C,e)))for(var s=0;s{e.ac.abort(ne)}),e.ac=null);try{e.f|=$e;var c=e.fn,u=c();e.f|=oe;var v=e.deps,y=d==null?void 0:d.is_fork;if(k!==null){var h;if(y||Ne(e,x),v!==null&&x>0)for(v.length=x+k.length,h=0;h{d=0,e(r=>({...r,connected:!0,error:null}))},n.onmessage=r=>{try{const s=JSON.parse(r.data);e(f=>{if(s.type==="Heartbeat")return{...f,lastHeartbeat:s};const w=[s,...f.events].slice(0,$);return{...f,events:w}})}catch(s){console.warn("[vestige] Failed to parse WebSocket message:",s)}},n.onclose=()=>{e(r=>({...r,connected:!1})),b(u)},n.onerror=()=>{e(r=>({...r,error:"WebSocket connection failed"}))}}catch(r){e(s=>({...s,error:String(r)}))}}function b(i){a&&clearTimeout(a);const u=Math.min(1e3*2**d,3e4);d++,a=setTimeout(()=>m(i),u)}function p(){a&&clearTimeout(a),n==null||n.close(),n=null,o({connected:!1,events:[],lastHeartbeat:null,error:null})}function h(){e(i=>({...i,events:[]}))}return{subscribe:t,connect:m,disconnect:p,clearEvents:h}}const l=S(),y=c(l,t=>t.connected),g=c(l,t=>t.events);c(l,t=>t.lastHeartbeat);const k=c(l,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.memory_count)??0}),M=c(l,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.avg_retention)??0}),T=c(l,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.suppressed_count)??0}),W=c(l,t=>{var o,e;return((e=(o=t.lastHeartbeat)==null?void 0:o.data)==null?void 0:e.uptime_secs)??0});function _(t){if(!Number.isFinite(t)||t<0)return"—";const o=Math.floor(t/86400),e=Math.floor(t%86400/3600),n=Math.floor(t%3600/60),a=Math.floor(t%60);return o>0?e>0?`${o}d ${e}h`:`${o}d`:e>0?n>0?`${e}h ${n}m`:`${e}h`:n>0?a>0?`${n}m ${a}s`:`${n}m`:`${a}s`}export{M as a,g as e,_ as f,y as i,k as m,T as s,W as u,l as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/XIUN5r_Y.js.br b/apps/dashboard/build/_app/immutable/chunks/XIUN5r_Y.js.br deleted file mode 100644 index c643daa..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/XIUN5r_Y.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/XIUN5r_Y.js.gz b/apps/dashboard/build/_app/immutable/chunks/XIUN5r_Y.js.gz deleted file mode 100644 index 7617526..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/XIUN5r_Y.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js b/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js new file mode 100644 index 0000000..0d246ca --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js @@ -0,0 +1 @@ +import{t as A}from"./BKuqSeVd.js";import{N as o}from"./CpWkWWOo.js";function p(i,N,f,b,u,r){var l=i.__className;if(o||l!==f||l===void 0){var t=A(f,b,r);(!o||t!==i.getAttribute("class"))&&(t==null?i.removeAttribute("class"):N?i.className=t:i.setAttribute("class",t)),i.__className=f}else if(r&&u!==r)for(var a in r){var g=!!r[a];(u==null||g!==!!u[a])&&i.classList.toggle(a,g)}return r}export{p as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js.br b/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js.br new file mode 100644 index 0000000..d857e71 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js.gz b/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js.gz new file mode 100644 index 0000000..c673050 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/aVbAZ-t7.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js b/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js deleted file mode 100644 index 719648e..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js +++ /dev/null @@ -1 +0,0 @@ -import{G as d,H as g,I as i,m,J as l,K as b,g as p,M as v,B as h}from"./VE8Jor13.js";function x(n=!1){const s=d,e=s.l.u;if(!e)return;let r=()=>v(s.s);if(n){let o=0,t={};const _=h(()=>{let c=!1;const a=s.s;for(const f in a)a[f]!==t[f]&&(t[f]=a[f],c=!0);return c&&o++,o});r=()=>p(_)}e.b.length&&g(()=>{u(s,r),l(e.b)}),i(()=>{const o=m(()=>e.m.map(b));return()=>{for(const t of o)typeof t=="function"&&t()}}),e.a.length&&i(()=>{u(s,r),l(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}export{x as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js.br b/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js.br deleted file mode 100644 index 0c09cc5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js.gz b/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js.gz deleted file mode 100644 index 49a2ff9..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/jyeIy8pa.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js b/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js new file mode 100644 index 0000000..d2ed67a --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js @@ -0,0 +1 @@ +import{Z as k,_ as f,$ as m,v as t,X as _,N as b,a0 as i}from"./CpWkWWOo.js";function E(e,a,v=a){var c=new WeakSet;k(e,"input",async r=>{var l=r?e.defaultValue:e.value;if(l=o(e)?u(l):l,v(l),f!==null&&c.add(f),await m(),l!==(l=a())){var h=e.selectionStart,d=e.selectionEnd,n=e.value.length;if(e.value=l??"",d!==null){var s=e.value.length;h===d&&d===n&&s>n?(e.selectionStart=s,e.selectionEnd=s):(e.selectionStart=h,e.selectionEnd=Math.min(d,s))}}}),(b&&e.defaultValue!==e.value||t(a)==null&&e.value)&&(v(o(e)?u(e.value):e.value),f!==null&&c.add(f)),_(()=>{var r=a();if(e===document.activeElement){var l=i??f;if(c.has(l))return}o(e)&&r===u(e.value)||e.type==="date"&&!r&&!e.value||r!==e.value&&(e.value=r??"")})}function S(e,a,v=a){k(e,"change",c=>{var r=c?e.defaultChecked:e.checked;v(r)}),(b&&e.defaultChecked!==e.checked||t(a)==null)&&v(e.checked),_(()=>{var c=a();e.checked=!!c})}function o(e){var a=e.type;return a==="number"||a==="range"}function u(e){return e===""?null:+e}export{S as a,E as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js.br b/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js.br new file mode 100644 index 0000000..a7a27e7 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js.gz b/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js.gz new file mode 100644 index 0000000..a12746e Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/sZcqyNBA.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js.br b/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js.br deleted file mode 100644 index 134103c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js.gz b/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js.gz deleted file mode 100644 index 620dc6a..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ussr1V5_.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ykT2B6d3.js b/apps/dashboard/build/_app/immutable/chunks/ykT2B6d3.js deleted file mode 100644 index 6d5bb97..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/ykT2B6d3.js +++ /dev/null @@ -1 +0,0 @@ -import{n as L,o as D,P as T,g as P,c as B,h as b,v as Y,w as h,D as x,x as M,m as N,y as U,z as w,A as z,B as C,C as $,F as q,S as y,L as F}from"./VE8Jor13.js";import{c as G}from"./AcZBvMXu.js";function H(r,a,t,s){var o;var f=!U||(t&w)!==0,v=(t&M)!==0,E=(t&q)!==0,n=s,c=!0,g=()=>(c&&(c=!1,n=E?N(s):s),n),u;if(v){var O=y in r||F in r;u=((o=L(r,a))==null?void 0:o.set)??(O&&a in r?e=>r[a]=e:void 0)}var _,I=!1;v?[_,I]=G(()=>r[a]):_=r[a],_===void 0&&s!==void 0&&(_=g(),u&&(f&&D(),u(_)));var i;if(f?i=()=>{var e=r[a];return e===void 0?g():(c=!0,e)}:i=()=>{var e=r[a];return e!==void 0&&(n=void 0),e===void 0?n:e},f&&(t&T)===0)return i;if(u){var R=r.$$legacy;return(function(e,S){return arguments.length>0?((!f||!S||R||I)&&u(S?i():e),e):i()})}var l=!1,d=((t&z)!==0?C:$)(()=>(l=!1,i()));v&&P(d);var m=h;return(function(e,S){if(arguments.length>0){const A=S?P(d):f&&v?B(e):e;return b(d,A),l=!0,n!==void 0&&(n=A),e}return Y&&l||(m.f&x)!==0?d.v:P(d)})}export{H as p}; diff --git a/apps/dashboard/build/_app/immutable/chunks/ykT2B6d3.js.br b/apps/dashboard/build/_app/immutable/chunks/ykT2B6d3.js.br deleted file mode 100644 index 4f1a9ab..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/ykT2B6d3.js.br +++ /dev/null @@ -1,2 +0,0 @@ - M{2d?$tϲ lqN+QR,Ҩ&F)oek9 d_h>Ǫ뤈ңEd6 g 81D9BOaFD L!AH Mh!O6+Č݉fߞIhy۬Sb4`{}NYl3"R"9!:mJ@}WxY4]+W$-3ǃ4;X}H$%;\ϊ%w,TJͩwύ} $\|@U[Ul "%YU*z5 @Eb`CUm$-cG,u!2Q8a -t/ֲ[x4ǚ]>76 DUYhd[t:43e-jP' hv]5oxr 27g3m"ŸW_όWb A7Uwi.map(i=>d[i]); -var Q=r=>{throw TypeError(r)};var X=(r,t,e)=>t.has(r)||Q("Cannot "+e);var m=(r,t,e)=>(X(r,t,"read from private field"),e?e.call(r):t.get(r)),G=(r,t,e)=>t.has(r)?Q("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(r):t.set(r,e),W=(r,t,e,n)=>(X(r,t,"write to private field"),n?n.call(r,e):t.set(r,e),e);import{T as Z,a7 as ut,b as lt,E as mt,a8 as _t,ab as dt,ac as ft,ad as $,a9 as ht,aj as vt,h as F,L as gt,g as v,b7 as Et,a3 as yt,a2 as pt,p as Pt,H as Rt,I as bt,R as Ot,f as L,d as Tt,a as At,s as z,e as Lt,r as wt,t as It,u as V}from"../chunks/VE8Jor13.js";import{h as kt,m as Dt,u as xt,s as Vt}from"../chunks/DHnEMX8z.js";import"../chunks/Bzak7iHL.js";import{o as St}from"../chunks/DWVWfZUn.js";import{i as B}from"../chunks/JkhlGLjU.js";import{a as y,c as k,f as et,t as jt}from"../chunks/7UNxJI5L.js";import{B as Ct}from"../chunks/BYWGnCkZ.js";import{b as S}from"../chunks/DHakDdar.js";import{p as q}from"../chunks/ykT2B6d3.js";function j(r,t,e){var n;Z&&(n=vt,ut());var o=new Ct(r);lt(()=>{var l=t()??null;if(Z){var s=_t(n),a=s===ht,i=l!==null;if(a!==i){var P=dt();ft(P),o.anchor=P,$(!1),o.ensure(l,l&&(c=>e(c,l))),$(!0);return}}o.ensure(l,l&&(c=>e(c,l)))},mt)}function Bt(r){return class extends qt{constructor(t){super({component:r,...t})}}}var p,d;class qt{constructor(t){G(this,p);G(this,d);var l;var e=new Map,n=(s,a)=>{var i=pt(a,!1,!1);return e.set(s,i),i};const o=new Proxy({...t.props||{},$$events:{}},{get(s,a){return v(e.get(a)??n(a,Reflect.get(s,a)))},has(s,a){return a===gt?!0:(v(e.get(a)??n(a,Reflect.get(s,a))),Reflect.has(s,a))},set(s,a,i){return F(e.get(a)??n(a,i),i),Reflect.set(s,a,i)}});W(this,d,(t.hydrate?kt:Dt)(t.component,{target:t.target,anchor:t.anchor,props:o,context:t.context,intro:t.intro??!1,recover:t.recover,transformError:t.transformError})),(!((l=t==null?void 0:t.props)!=null&&l.$$host)||t.sync===!1)&&Et(),W(this,p,o.$$events);for(const s of Object.keys(m(this,d)))s==="$set"||s==="$destroy"||s==="$on"||yt(this,s,{get(){return m(this,d)[s]},set(a){m(this,d)[s]=a},enumerable:!0});m(this,d).$set=s=>{Object.assign(o,s)},m(this,d).$destroy=()=>{xt(m(this,d))}}$set(t){m(this,d).$set(t)}$on(t,e){m(this,p)[t]=m(this,p)[t]||[];const n=(...o)=>e.call(this,...o);return m(this,p)[t].push(n),()=>{m(this,p)[t]=m(this,p)[t].filter(o=>o!==n)}}$destroy(){m(this,d).$destroy()}}p=new WeakMap,d=new WeakMap;const Ft="modulepreload",Ht=function(r,t){return new URL(r,t).href},tt={},_=function(t,e,n){let o=Promise.resolve();if(e&&e.length>0){let s=function(c){return Promise.all(c.map(g=>Promise.resolve(g).then(R=>({status:"fulfilled",value:R}),R=>({status:"rejected",reason:R}))))};const a=document.getElementsByTagName("link"),i=document.querySelector("meta[property=csp-nonce]"),P=(i==null?void 0:i.nonce)||(i==null?void 0:i.getAttribute("nonce"));o=s(e.map(c=>{if(c=Ht(c,n),c in tt)return;tt[c]=!0;const g=c.endsWith(".css"),R=g?'[rel="stylesheet"]':"";if(!!n)for(let b=a.length-1;b>=0;b--){const u=a[b];if(u.href===c&&(!g||u.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${c}"]${R}`))return;const E=document.createElement("link");if(E.rel=g?"stylesheet":Ft,g||(E.as="script"),E.crossOrigin="",E.href=c,P&&E.setAttribute("nonce",P),document.head.appendChild(E),g)return new Promise((b,u)=>{E.addEventListener("load",b),E.addEventListener("error",()=>u(new Error(`Unable to preload CSS for ${c}`)))})}))}function l(s){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=s,window.dispatchEvent(a),!a.defaultPrevented)throw s}return o.then(s=>{for(const a of s||[])a.status==="rejected"&&l(a.reason);return t().catch(l)})},ae={};var Nt=et('
'),Ut=et(" ",1);function Yt(r,t){Pt(t,!0);let e=q(t,"components",23,()=>[]),n=q(t,"data_0",3,null),o=q(t,"data_1",3,null),l=q(t,"data_2",3,null);Rt(()=>t.stores.page.set(t.page)),bt(()=>{t.stores,t.page,t.constructors,e(),t.form,n(),o(),l(),t.stores.page.notify()});let s=z(!1),a=z(!1),i=z(null);St(()=>{const u=t.stores.page.subscribe(()=>{v(s)&&(F(a,!0),Ot().then(()=>{F(i,document.title||"untitled page",!0)}))});return F(s,!0),u});const P=V(()=>t.constructors[2]);var c=Ut(),g=L(c);{var R=u=>{const O=V(()=>t.constructors[0]);var T=k(),D=L(T);j(D,()=>v(O),(A,w)=>{S(w(A,{get data(){return n()},get form(){return t.form},get params(){return t.page.params},children:(f,Wt)=>{var K=k(),at=L(K);{var st=I=>{const H=V(()=>t.constructors[1]);var x=k(),N=L(x);j(N,()=>v(H),(U,Y)=>{S(Y(U,{get data(){return o()},get form(){return t.form},get params(){return t.page.params},children:(h,zt)=>{var M=k(),ot=L(M);j(ot,()=>v(P),(it,ct)=>{S(ct(it,{get data(){return l()},get form(){return t.form},get params(){return t.page.params}}),C=>e()[2]=C,()=>{var C;return(C=e())==null?void 0:C[2]})}),y(h,M)},$$slots:{default:!0}}),h=>e()[1]=h,()=>{var h;return(h=e())==null?void 0:h[1]})}),y(I,x)},nt=I=>{const H=V(()=>t.constructors[1]);var x=k(),N=L(x);j(N,()=>v(H),(U,Y)=>{S(Y(U,{get data(){return o()},get form(){return t.form},get params(){return t.page.params}}),h=>e()[1]=h,()=>{var h;return(h=e())==null?void 0:h[1]})}),y(I,x)};B(at,I=>{t.constructors[2]?I(st):I(nt,!1)})}y(f,K)},$$slots:{default:!0}}),f=>e()[0]=f,()=>{var f;return(f=e())==null?void 0:f[0]})}),y(u,T)},J=u=>{const O=V(()=>t.constructors[0]);var T=k(),D=L(T);j(D,()=>v(O),(A,w)=>{S(w(A,{get data(){return n()},get form(){return t.form},get params(){return t.page.params}}),f=>e()[0]=f,()=>{var f;return(f=e())==null?void 0:f[0]})}),y(u,T)};B(g,u=>{t.constructors[1]?u(R):u(J,!1)})}var E=Tt(g,2);{var b=u=>{var O=Nt(),T=Lt(O);{var D=A=>{var w=jt();It(()=>Vt(w,v(i))),y(A,w)};B(T,A=>{v(a)&&A(D)})}wt(O),y(u,O)};B(E,u=>{v(s)&&u(b)})}y(r,c),At()}const se=Bt(Yt),ne=[()=>_(()=>import("../nodes/0.Bfsm2nvh.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20]),import.meta.url),()=>_(()=>import("../nodes/1.ClSH3vNb.js"),__vite__mapDeps([21,1,19,3,4,5,22,17,2,15,16]),import.meta.url),()=>_(()=>import("../nodes/2.BFaWefTK.js"),__vite__mapDeps([23,1,3,5,9,7]),import.meta.url),()=>_(()=>import("../nodes/3.BbrO3ed8.js"),__vite__mapDeps([24,1,19,3,2,22,16,15,17]),import.meta.url),()=>_(()=>import("../nodes/4.BEP4iikl.js"),__vite__mapDeps([25,1,3,4,5,6,7,8,10,11,26,12,27]),import.meta.url),()=>_(()=>import("../nodes/5.BgcStf4T.js"),__vite__mapDeps([28,1,19,3,4,5,6,7,8,26,22,14,15,18,29,10,11,30,31]),import.meta.url),()=>_(()=>import("../nodes/6.QRT_dh4Q.js"),__vite__mapDeps([32,1,2,3,4,5,6,7,8,10,11,26,12,33,14,15,17,13,30,29,19,22,27,18]),import.meta.url),()=>_(()=>import("../nodes/7.B1rI2ZuC.js"),__vite__mapDeps([34,1,2,3,4,5,6,7,8,11,27]),import.meta.url),()=>_(()=>import("../nodes/8.BmBiit5q.js"),__vite__mapDeps([35,1,2,3,4,5,6,7,8,10,11,26,12,33,27,29]),import.meta.url),()=>_(()=>import("../nodes/9.Ds9IBqJA.js"),__vite__mapDeps([36,1,2,3,4,5,6,7,8,11,26,14,15,27,18]),import.meta.url),()=>_(()=>import("../nodes/10.BXBzm8vP.js"),__vite__mapDeps([37,1,2,3,4,5,6,7,8,26,27]),import.meta.url),()=>_(()=>import("../nodes/11.BOa24N9o.js"),__vite__mapDeps([38,1,2,3,4,5,6,7,8,26,33,27,29]),import.meta.url)],oe=[],ie={"/":[3],"/(app)/explore":[4,[2]],"/(app)/feed":[5,[2]],"/(app)/graph":[6,[2]],"/(app)/intentions":[7,[2]],"/(app)/memories":[8,[2]],"/(app)/settings":[9,[2]],"/(app)/stats":[10,[2]],"/(app)/timeline":[11,[2]]},rt={handleError:(({error:r})=>{console.error(r)}),reroute:(()=>{}),transport:{}},Gt=Object.fromEntries(Object.entries(rt.transport).map(([r,t])=>[r,t.decode])),ce=Object.fromEntries(Object.entries(rt.transport).map(([r,t])=>[r,t.encode])),ue=!1,le=(r,t)=>Gt[r](t);export{le as decode,Gt as decoders,ie as dictionary,ce as encoders,ue as hash,rt as hooks,ae as matchers,ne as nodes,se as root,oe as server_loads}; diff --git a/apps/dashboard/build/_app/immutable/entry/app.B1RqXwG0.js.br b/apps/dashboard/build/_app/immutable/entry/app.B1RqXwG0.js.br deleted file mode 100644 index 7e99144..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/app.B1RqXwG0.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.B1RqXwG0.js.gz b/apps/dashboard/build/_app/immutable/entry/app.B1RqXwG0.js.gz deleted file mode 100644 index c042e34..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/app.B1RqXwG0.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js b/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js new file mode 100644 index 0000000..319e17d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.BSxKGxRx.js","../chunks/Bzak7iHL.js","../chunks/GG5zm9kr.js","../chunks/CpWkWWOo.js","../chunks/BlVfL1ME.js","../chunks/CHOnp4oo.js","../chunks/B4yTwGkE.js","../chunks/DdEqwvdI.js","../chunks/CGEBXrjl.js","../chunks/CJCPY1OL.js","../chunks/A7po6GxK.js","../chunks/aVbAZ-t7.js","../chunks/BKuqSeVd.js","../chunks/sZcqyNBA.js","../chunks/CJsMJEun.js","../chunks/C6HuKgyx.js","../chunks/BeMFXnHE.js","../chunks/D-gDZzN6.js","../chunks/DGcYlAAw.js","../chunks/MAY1QfFZ.js","../chunks/BUoSzNdg.js","../chunks/Cx-f-Pzo.js","../chunks/D4ymNiig.js","../chunks/CcUbQ_Wl.js","../chunks/554JRhq6.js","../assets/0.CN9L-NIY.css","../nodes/1.CJFfVX1H.js","../nodes/2.D-vKwnTC.js","../nodes/3.mK8D6pz1.js","../nodes/4.DPhwyLUO.js","../chunks/V6gjw5Ec.js","../nodes/5.C0AYWqwr.js","../chunks/BnXDGOmJ.js","../assets/5.DQ_AfUnN.css","../nodes/6.BD0AetaD.js","../chunks/C4h_mRt2.js","../assets/6.BSSBWVKL.css","../nodes/7.2YrTacps.js","../assets/7.CCrNEDd3.css","../nodes/8.CokrlgDp.js","../nodes/9.Vu2AXN40.js","../assets/9.BBx09UGv.css","../nodes/10.CjP_ylq3.js","../nodes/11.k15P8M2H.js","../nodes/12.BthmSU_R.js","../nodes/13.C0fh4g_5.js","../assets/13.Bjd0S47S.css","../nodes/14.DUh3SXOF.js","../nodes/15.QNRJhGzj.js","../assets/15.ChjqzJHo.css","../nodes/16.QhdtMP78.js","../assets/16.BnHgRQtR.css","../nodes/17.DlzXJVdF.js","../nodes/18.Bmu4OWRV.js","../nodes/19.Baocji37.js","../nodes/20.CJQuAMkU.js","../assets/20.DKhUrxcR.css"])))=>i.map(i=>d[i]); +var Q=r=>{throw TypeError(r)};var X=(r,t,e)=>t.has(r)||Q("Cannot "+e);var l=(r,t,e)=>(X(r,t,"read from private field"),e?e.call(r):t.get(r)),H=(r,t,e)=>t.has(r)?Q("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(r):t.set(r,e),W=(r,t,e,n)=>(X(r,t,"write to private field"),n?n.call(r,e):t.set(r,e),e);import{N as Z,ab as ut,b as _t,E as ct,ac as lt,ae as dt,T as ft,R as $,ax as vt,U as ht,h as U,L as pt,g as h,bc as Et,G as gt,I as Pt,p as Rt,aA as yt,aB as Ot,$ as At,f as L,e as Tt,a as bt,s as z,d as Lt,r as It,u as x,t as Dt}from"../chunks/CpWkWWOo.js";import{h as Vt,m as wt,u as kt,s as xt}from"../chunks/BlVfL1ME.js";import"../chunks/Bzak7iHL.js";import{o as St}from"../chunks/GG5zm9kr.js";import{i as B}from"../chunks/B4yTwGkE.js";import{a as g,c as V,f as et,t as jt}from"../chunks/CHOnp4oo.js";import{B as Ct}from"../chunks/DdEqwvdI.js";import{b as S}from"../chunks/CJsMJEun.js";import{p as N}from"../chunks/V6gjw5Ec.js";function j(r,t,e){var n;Z&&(n=ht,ut());var i=new Ct(r);_t(()=>{var c=t()??null;if(Z){var s=lt(n),a=s===vt,m=c!==null;if(a!==m){var R=dt();ft(R),i.anchor=R,$(!1),i.ensure(c,c&&(u=>e(u,c))),$(!0);return}}i.ensure(c,c&&(u=>e(u,c)))},ct)}function Bt(r){return class extends Nt{constructor(t){super({component:r,...t})}}}var P,d;class Nt{constructor(t){H(this,P);H(this,d);var c;var e=new Map,n=(s,a)=>{var m=Pt(a,!1,!1);return e.set(s,m),m};const i=new Proxy({...t.props||{},$$events:{}},{get(s,a){return h(e.get(a)??n(a,Reflect.get(s,a)))},has(s,a){return a===pt?!0:(h(e.get(a)??n(a,Reflect.get(s,a))),Reflect.has(s,a))},set(s,a,m){return U(e.get(a)??n(a,m),m),Reflect.set(s,a,m)}});W(this,d,(t.hydrate?Vt:wt)(t.component,{target:t.target,anchor:t.anchor,props:i,context:t.context,intro:t.intro??!1,recover:t.recover,transformError:t.transformError})),(!((c=t==null?void 0:t.props)!=null&&c.$$host)||t.sync===!1)&&Et(),W(this,P,i.$$events);for(const s of Object.keys(l(this,d)))s==="$set"||s==="$destroy"||s==="$on"||gt(this,s,{get(){return l(this,d)[s]},set(a){l(this,d)[s]=a},enumerable:!0});l(this,d).$set=s=>{Object.assign(i,s)},l(this,d).$destroy=()=>{kt(l(this,d))}}$set(t){l(this,d).$set(t)}$on(t,e){l(this,P)[t]=l(this,P)[t]||[];const n=(...i)=>e.call(this,...i);return l(this,P)[t].push(n),()=>{l(this,P)[t]=l(this,P)[t].filter(i=>i!==n)}}$destroy(){l(this,d).$destroy()}}P=new WeakMap,d=new WeakMap;const Ut="modulepreload",qt=function(r,t){return new URL(r,t).href},tt={},o=function(t,e,n){let i=Promise.resolve();if(e&&e.length>0){let s=function(u){return Promise.all(u.map(p=>Promise.resolve(p).then(y=>({status:"fulfilled",value:y}),y=>({status:"rejected",reason:y}))))};const a=document.getElementsByTagName("link"),m=document.querySelector("meta[property=csp-nonce]"),R=(m==null?void 0:m.nonce)||(m==null?void 0:m.getAttribute("nonce"));i=s(e.map(u=>{if(u=qt(u,n),u in tt)return;tt[u]=!0;const p=u.endsWith(".css"),y=p?'[rel="stylesheet"]':"";if(!!n)for(let O=a.length-1;O>=0;O--){const _=a[O];if(_.href===u&&(!p||_.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${u}"]${y}`))return;const E=document.createElement("link");if(E.rel=p?"stylesheet":Ut,p||(E.as="script"),E.crossOrigin="",E.href=u,R&&E.setAttribute("nonce",R),document.head.appendChild(E),p)return new Promise((O,_)=>{E.addEventListener("load",O),E.addEventListener("error",()=>_(new Error(`Unable to preload CSS for ${u}`)))})}))}function c(s){const a=new Event("vite:preloadError",{cancelable:!0});if(a.payload=s,window.dispatchEvent(a),!a.defaultPrevented)throw s}return i.then(s=>{for(const a of s||[])a.status==="rejected"&&c(a.reason);return t().catch(c)})},ae={};var Ft=et('
'),Gt=et(" ",1);function Yt(r,t){Rt(t,!0);let e=N(t,"components",23,()=>[]),n=N(t,"data_0",3,null),i=N(t,"data_1",3,null),c=N(t,"data_2",3,null);yt(()=>t.stores.page.set(t.page)),Ot(()=>{t.stores,t.page,t.constructors,e(),t.form,n(),i(),c(),t.stores.page.notify()});let s=z(!1),a=z(!1),m=z(null);St(()=>{const _=t.stores.page.subscribe(()=>{h(s)&&(U(a,!0),At().then(()=>{U(m,document.title||"untitled page",!0)}))});return U(s,!0),_});const R=x(()=>t.constructors[2]);var u=Gt(),p=L(u);{var y=_=>{const A=x(()=>t.constructors[0]);var T=V(),w=L(T);j(w,()=>h(A),(b,I)=>{S(I(b,{get data(){return n()},get form(){return t.form},get params(){return t.page.params},children:(f,Wt)=>{var K=V(),at=L(K);{var st=D=>{const q=x(()=>t.constructors[1]);var k=V(),F=L(k);j(F,()=>h(q),(G,Y)=>{S(Y(G,{get data(){return i()},get form(){return t.form},get params(){return t.page.params},children:(v,zt)=>{var M=V(),nt=L(M);j(nt,()=>h(R),(it,mt)=>{S(mt(it,{get data(){return c()},get form(){return t.form},get params(){return t.page.params}}),C=>e()[2]=C,()=>{var C;return(C=e())==null?void 0:C[2]})}),g(v,M)},$$slots:{default:!0}}),v=>e()[1]=v,()=>{var v;return(v=e())==null?void 0:v[1]})}),g(D,k)},ot=D=>{const q=x(()=>t.constructors[1]);var k=V(),F=L(k);j(F,()=>h(q),(G,Y)=>{S(Y(G,{get data(){return i()},get form(){return t.form},get params(){return t.page.params}}),v=>e()[1]=v,()=>{var v;return(v=e())==null?void 0:v[1]})}),g(D,k)};B(at,D=>{t.constructors[2]?D(st):D(ot,!1)})}g(f,K)},$$slots:{default:!0}}),f=>e()[0]=f,()=>{var f;return(f=e())==null?void 0:f[0]})}),g(_,T)},J=_=>{const A=x(()=>t.constructors[0]);var T=V(),w=L(T);j(w,()=>h(A),(b,I)=>{S(I(b,{get data(){return n()},get form(){return t.form},get params(){return t.page.params}}),f=>e()[0]=f,()=>{var f;return(f=e())==null?void 0:f[0]})}),g(_,T)};B(p,_=>{t.constructors[1]?_(y):_(J,!1)})}var E=Tt(p,2);{var O=_=>{var A=Ft(),T=Lt(A);{var w=b=>{var I=jt();Dt(()=>xt(I,h(m))),g(b,I)};B(T,b=>{h(a)&&b(w)})}It(A),g(_,A)};B(E,_=>{h(s)&&_(O)})}g(r,u),bt()}const se=Bt(Yt),oe=[()=>o(()=>import("../nodes/0.BSxKGxRx.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]),import.meta.url),()=>o(()=>import("../nodes/1.CJFfVX1H.js"),__vite__mapDeps([26,1,20,3,4,5,18,2,16,17]),import.meta.url),()=>o(()=>import("../nodes/2.D-vKwnTC.js"),__vite__mapDeps([27,1,3,5,9,7]),import.meta.url),()=>o(()=>import("../nodes/3.mK8D6pz1.js"),__vite__mapDeps([28,1,20,3,2,17,16,18]),import.meta.url),()=>o(()=>import("../nodes/4.DPhwyLUO.js"),__vite__mapDeps([29,1,2,3,4,5,6,7,10,13,24,19,16,8,30,15,23]),import.meta.url),()=>o(()=>import("../nodes/5.C0AYWqwr.js"),__vite__mapDeps([31,1,3,4,5,6,7,8,11,12,21,32,10,30,15,16,33]),import.meta.url),()=>o(()=>import("../nodes/6.BD0AetaD.js"),__vite__mapDeps([34,1,3,4,5,6,7,8,35,10,11,12,24,21,30,15,16,18,2,36]),import.meta.url),()=>o(()=>import("../nodes/7.2YrTacps.js"),__vite__mapDeps([37,1,2,3,4,5,6,7,8,10,13,11,12,21,23,38]),import.meta.url),()=>o(()=>import("../nodes/8.CokrlgDp.js"),__vite__mapDeps([39,1,3,4,5,6,7,8,10,11,12,21,13,24]),import.meta.url),()=>o(()=>import("../nodes/9.Vu2AXN40.js"),__vite__mapDeps([40,1,20,3,4,5,6,7,8,21,12,15,16,19,23,10,11,30,41]),import.meta.url),()=>o(()=>import("../nodes/10.CjP_ylq3.js"),__vite__mapDeps([42,1,2,3,4,5,6,7,8,10,11,12,21,13,32,15,16,18,14,30,23,20,24,19]),import.meta.url),()=>o(()=>import("../nodes/11.k15P8M2H.js"),__vite__mapDeps([43,1,2,3,4,5,6,7,8,21,12,13,17,16,18,24,23,10,30,15]),import.meta.url),()=>o(()=>import("../nodes/12.BthmSU_R.js"),__vite__mapDeps([44,1,2,3,4,5,6,7,8,11,12,24]),import.meta.url),()=>o(()=>import("../nodes/13.C0fh4g_5.js"),__vite__mapDeps([45,1,2,3,4,5,6,7,8,10,11,12,21,13,32,24,23,46]),import.meta.url),()=>o(()=>import("../nodes/14.DUh3SXOF.js"),__vite__mapDeps([47,1,2,3,4,5,6,7,8,10,11,12,21]),import.meta.url),()=>o(()=>import("../nodes/15.QNRJhGzj.js"),__vite__mapDeps([48,1,2,3,4,5,6,7,8,35,10,21,12,13,14,24,11,30,15,16,23,49]),import.meta.url),()=>o(()=>import("../nodes/16.QhdtMP78.js"),__vite__mapDeps([50,1,2,3,4,5,6,7,8,11,12,24,10,21,30,15,16,23,51]),import.meta.url),()=>o(()=>import("../nodes/17.DlzXJVdF.js"),__vite__mapDeps([52,1,2,3,4,5,6,7,8,11,12,21,15,16,24,19,22,23]),import.meta.url),()=>o(()=>import("../nodes/18.Bmu4OWRV.js"),__vite__mapDeps([53,1,2,3,4,5,6,7,8,21,12,24]),import.meta.url),()=>o(()=>import("../nodes/19.Baocji37.js"),__vite__mapDeps([54,1,2,3,4,5,6,7,8,21,12,32,24,23]),import.meta.url),()=>o(()=>import("../nodes/20.CJQuAMkU.js"),__vite__mapDeps([55,1,2,3,4,5,6,7,8,35,10,11,12,21,13,32,14,18,16,56]),import.meta.url)],ne=[],ie={"/":[3],"/(app)/activation":[4,[2]],"/(app)/contradictions":[5,[2]],"/(app)/dreams":[6,[2]],"/(app)/duplicates":[7,[2]],"/(app)/explore":[8,[2]],"/(app)/feed":[9,[2]],"/(app)/graph":[10,[2]],"/(app)/importance":[11,[2]],"/(app)/intentions":[12,[2]],"/(app)/memories":[13,[2]],"/(app)/patterns":[14,[2]],"/(app)/reasoning":[15,[2]],"/(app)/schedule":[16,[2]],"/(app)/settings":[17,[2]],"/(app)/stats":[18,[2]],"/(app)/timeline":[19,[2]],"/waitlist":[20]},rt={handleError:(({error:r})=>{console.error(r)}),reroute:(()=>{}),transport:{}},Ht=Object.fromEntries(Object.entries(rt.transport).map(([r,t])=>[r,t.decode])),me=Object.fromEntries(Object.entries(rt.transport).map(([r,t])=>[r,t.encode])),ue=!1,_e=(r,t)=>Ht[r](t);export{_e as decode,Ht as decoders,ie as dictionary,me as encoders,ue as hash,rt as hooks,ae as matchers,oe as nodes,se as root,ne as server_loads}; diff --git a/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js.br b/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js.br new file mode 100644 index 0000000..682afec Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js.br differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js.gz b/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js.gz new file mode 100644 index 0000000..dde9e0d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/app.BIXcLtMB.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js b/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js new file mode 100644 index 0000000..6cca906 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js @@ -0,0 +1 @@ +import{a as r}from"../chunks/D-gDZzN6.js";import{w as t}from"../chunks/DGcYlAAw.js";export{t as load_css,r as start}; diff --git a/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js.br b/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js.br new file mode 100644 index 0000000..8a9d857 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js.br differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js.gz b/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js.gz new file mode 100644 index 0000000..70f2eaa Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/start.Bnkotnxp.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js b/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js deleted file mode 100644 index e19b465..0000000 --- a/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js +++ /dev/null @@ -1 +0,0 @@ -import{a as r}from"../chunks/CK5Nmlyf.js";import{w as t}from"../chunks/DUtaznkq.js";export{t as load_css,r as start}; diff --git a/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js.br b/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js.br deleted file mode 100644 index 4d55f27..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js.gz b/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js.gz deleted file mode 100644 index f41bf60..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/start.C8fl2m83.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js b/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js new file mode 100644 index 0000000..1753ddd --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js @@ -0,0 +1,86 @@ +import"../chunks/Bzak7iHL.js";import{o as Xe}from"../chunks/GG5zm9kr.js";import{f as ce,e as s,d as a,r as t,t as B,p as Ke,n as _e,g as e,a as He,s as ie,c as ht,h as k,u as G}from"../chunks/CpWkWWOo.js";import{s as h,d as Ge,a as J,e as Pe}from"../chunks/BlVfL1ME.js";import{i as q}from"../chunks/B4yTwGkE.js";import{e as Se,i as Fe}from"../chunks/CGEBXrjl.js";import{c as Ze,a as u,f as x,t as et}from"../chunks/CHOnp4oo.js";import{s as tt}from"../chunks/CJCPY1OL.js";import{s as ke,r as gt}from"../chunks/A7po6GxK.js";import{s as U}from"../chunks/aVbAZ-t7.js";import{b as bt}from"../chunks/sZcqyNBA.js";import{b as xt}from"../chunks/CJsMJEun.js";import{a as se,s as Ve}from"../chunks/C6HuKgyx.js";import{s as _t,g as at}from"../chunks/D-gDZzN6.js";import{b as he}from"../chunks/DGcYlAAw.js";import{s as lt,m as ot,a as dt,e as kt,w as st,u as yt,i as wt,f as $t}from"../chunks/MAY1QfFZ.js";import{i as At}from"../chunks/BUoSzNdg.js";import{s as ct}from"../chunks/Cx-f-Pzo.js";import{t as Te}from"../chunks/D4ymNiig.js";import{a as Oe}from"../chunks/554JRhq6.js";import{d as Ct,w as vt,g as pt}from"../chunks/BeMFXnHE.js";const Mt=()=>{const r=_t;return{page:{subscribe:r.page.subscribe},navigating:{subscribe:r.navigating.subscribe},updated:r.updated}},Et={subscribe(r){return Mt().page.subscribe(r)}};var Tt=x('
');function St(r){const i=()=>se(lt,"$suppressedCount",o),[o,n]=Ve();var g=Ze(),C=ce(g);{var y=N=>{var $=Tt(),w=s(a($),2),p=a(w);t(w),t($),B(()=>h(p,`Actively forgetting ${i()??""} ${i()===1?"memory":"memories"}`)),u(N,$)};q(C,N=>{i()>0&&N(y)})}u(r,g),n()}var jt=x(''),Dt=x('
');function It(r,i){Ke(i,!1);const o=()=>se(Te,"$toasts",n),[n,g]=Ve(),C={DreamCompleted:"✦",ConsolidationCompleted:"◉",ConnectionDiscovered:"⟷",MemoryPromoted:"↑",MemoryDemoted:"↓",MemorySuppressed:"◬",MemoryUnsuppressed:"◉",Rac1CascadeSwept:"✺",MemoryDeleted:"✕",HookVerdictRecorded:"⚑"};function y(p){return C[p]??"◆"}function N(p){Te.dismiss(p.id)}function $(p,d){(p.key==="Enter"||p.key===" ")&&(p.preventDefault(),Te.dismiss(d.id))}At();var w=Dt();Se(w,5,o,p=>p.id,(p,d)=>{var M=jt(),R=s(a(M),2),V=a(R),Y=a(V),ee=a(Y,!0);t(Y);var Q=s(Y,2),re=a(Q,!0);t(Q),t(V);var v=s(V,2),m=a(v,!0);t(v),t(R),_e(2),t(M),B(A=>{ke(M,"aria-label",`${e(d).title??""}: ${e(d).body??""}. Click to dismiss.`),ct(M,`--toast-color: ${e(d).color??""}; --toast-dwell: ${e(d).dwellMs??""}ms;`),h(ee,A),h(re,e(d).title),h(m,e(d).body)},[()=>y(e(d).type)]),J("click",M,()=>N(e(d))),J("keydown",M,A=>$(A,e(d))),Pe("mouseenter",M,()=>Te.pauseDwell(e(d).id,e(d).dwellMs)),Pe("mouseleave",M,()=>Te.resumeDwell(e(d).id)),Pe("focus",M,()=>Te.pauseDwell(e(d).id,e(d).dwellMs)),Pe("blur",M,()=>Te.resumeDwell(e(d).id)),u(p,M)}),t(w),u(r,w),He(),g()}Ge(["click","keydown"]);function Ne(r){const i=r.data;if(!i||typeof i!="object")return null;const o=i.timestamp??i.at??i.occurred_at;if(o==null)return null;if(typeof o=="number")return Number.isFinite(o)?o>1e12?o:o*1e3:null;if(typeof o!="string")return null;const n=Date.parse(o);return Number.isFinite(n)?n:null}const Qe=10,ut=3e4,Ft=Qe*ut;function Nt(r,i){const o=i-Ft,n=new Array(Qe).fill(0);for(const C of r){if(C.type==="Heartbeat")continue;const y=Ne(C);if(y===null||yi)continue;const N=Math.min(Qe-1,Math.floor((y-o)/ut));n[N]+=1}const g=Math.max(1,...n);return n.map(C=>({count:C,ratio:C/g}))}function Lt(r,i){const o=i-864e5;for(const n of r){if(n.type!=="DreamCompleted")continue;return(Ne(n)??i)>=o?n:null}return null}function Bt(r){if(!r||!r.data)return null;const i=r.data,o=typeof i.insights_generated=="number"?i.insights_generated:typeof i.insightsGenerated=="number"?i.insightsGenerated:null;return o!==null&&Number.isFinite(o)?o:null}function Rt(r,i){let o=null,n=null;for(const N of r)if(!o&&N.type==="DreamStarted"&&(o=N),!n&&N.type==="DreamCompleted"&&(n=N),o&&n)break;if(!o)return!1;const g=Ne(o)??i,C=i-300*1e3;return g=n}return!1}var Ot=x(' at risk',1),Kt=x('0 at risk',1),Ht=x(' at risk',1),Gt=x(' intentions',1),Wt=x('— intentions'),qt=x('· insights',1),zt=x(' Last dream: ',1),Yt=x('No recent dream'),Qt=x('
'),Ut=x('
DREAMING...
',1),Xt=x(''),Zt=x('
memories · avg retention
');function Jt(r,i){Ke(i,!0);const o=()=>se(dt,"$avgRetention",C),n=()=>se(kt,"$eventFeed",C),g=()=>se(ot,"$memoryCount",C),[C,y]=Ve(),N=G(()=>Math.round((o()??0)*100)),$=G(()=>(o()??0)>=.5);let w=ie(null);async function p(){try{const l=await Oe.retentionDistribution();if(Array.isArray(l.endangered)&&l.endangered.length>0){k(w,l.endangered.length,!0);return}const c=l.distribution??[];let j=0;for(const _ of c){const D=/^(\d+)/.exec(_.range);if(!D)continue;const F=Number.parseInt(D[1],10);Number.isFinite(F)&&F<30&&(j+=_.count??0)}k(w,j,!0)}catch{k(w,null)}}let d=ie(null);async function M(){var l;try{const c=await Oe.intentions("active");k(d,c.total??((l=c.intentions)==null?void 0:l.length)??0,!0)}catch{k(d,null)}}let R=ie(ht(Date.now()));const V=G(()=>{const l=n(),c=Lt(l,e(R)),j=c?Ne(c)??e(R):null,_=j!==null?e(R)-j:null;return{isDreaming:Rt(l,e(R)),recent:c,recentMsAgo:_,insights:Bt(c)}}),Y=G(()=>Nt(n(),e(R))),ee=G(()=>Pt(n(),e(R)));Xe(()=>{p(),M();const l=setInterval(()=>{k(R,Date.now(),!0)},1e3),c=setInterval(()=>{p(),M()},6e4);return()=>{clearInterval(l),clearInterval(c)}});var Q=Zt();let re;var v=a(Q),m=a(v),A=a(m);let ye;var Ae=s(A,2);let je;t(m);var ve=s(m,2),T=a(ve,!0);t(ve);var E=s(ve,6);let b;var X=a(E);t(E),_e(2),t(v);var W=s(v,4),K=a(W);{var le=l=>{var c=Ot(),j=ce(c),_=a(j,!0);t(j),_e(2),B(()=>h(_,e(w))),u(l,c)},S=l=>{var c=Kt();_e(2),u(l,c)},I=l=>{var c=Ht();_e(2),u(l,c)};q(K,l=>{e(w)!==null&&e(w)>0?l(le):e(w)===0?l(S,1):l(I,!1)})}t(W);var f=s(W,4),O=a(f);{var z=l=>{var c=Gt(),j=ce(c);let _;var D=s(j,2);let F;var H=a(D,!0);t(D),_e(2),B(()=>{_=U(j,1,"inline-flex h-2 w-2 rounded-full svelte-1kk3799",null,_,{"bg-node-pattern":e(d)>5,"animate-ping-slow":e(d)>5,"bg-muted":e(d)<=5}),F=U(D,1,"tabular-nums svelte-1kk3799",null,F,{"text-node-pattern":e(d)>5,"text-text":e(d)>0&&e(d)<=5,"text-muted":e(d)===0}),h(H,e(d))}),u(l,c)},pe=l=>{var c=Wt();u(l,c)};q(O,l=>{e(d)!==null?l(z):l(pe,!1)})}t(f);var te=s(f,4),oe=a(te);{var ue=l=>{var c=zt(),j=s(ce(c),4),_=a(j,!0);t(j);var D=s(j,2);{var F=H=>{var ne=qt(),Me=s(ce(ne),2),we=a(Me,!0);t(Me),_e(2),B(()=>h(we,e(V).insights)),u(H,ne)};q(D,H=>{e(V).insights!==null&&H(F)})}B(H=>h(_,H),[()=>Vt(e(V).recentMsAgo)]),u(l,c)},ae=l=>{var c=Yt();u(l,c)};q(oe,l=>{e(V).recent&&e(V).recentMsAgo!==null?l(ue):l(ae,!1)})}t(te);var de=s(te,4),me=s(a(de),2);Se(me,21,()=>e(Y),Fe,(l,c)=>{var j=Qt();B(_=>ct(j,`height: ${_??""}%; opacity: ${e(c).count===0?.18:.5+e(c).ratio*.5};`),[()=>Math.max(10,e(c).ratio*100)]),u(l,j)}),t(me),t(de);var Ce=s(de,2);{var Be=l=>{var c=Ut();_e(2),u(l,c)};q(Ce,l=>{e(V).isDreaming&&l(Be)})}var De=s(Ce,4);{var Re=l=>{var c=Xt();u(l,c)};q(De,l=>{e(ee)&&l(Re)})}t(Q),B(()=>{re=U(Q,1,"ambient-strip relative flex h-9 w-full items-center gap-0 overflow-hidden border-b border-synapse/15 bg-black/40 px-3 text-[11px] text-dim backdrop-blur-md svelte-1kk3799",null,re,{"ambient-flash":e(ee)}),ye=U(A,1,"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 svelte-1kk3799",null,ye,{"bg-recall":e($),"bg-warning":!e($)}),je=U(Ae,1,"relative inline-flex h-2 w-2 rounded-full svelte-1kk3799",null,je,{"bg-recall":e($),"bg-warning":!e($)}),h(T,g()),b=U(E,1,"svelte-1kk3799",null,b,{"text-recall":e($),"text-warning":!e($)}),h(X,`${e(N)??""}%`)}),u(r,Q),He(),y()}var ea=x(" "),ta=x('
  • '),aa=x(' ',1),sa=x('

    Appeal recorded.

    '),ra=x('

    No appealable veto in this receipt.

    '),na=x('
    Claim

    Verdict

    Precedent
      Fix

      Appeal
      '),ia=x('
      ');function la(r,i){Ke(i,!0);const o=["PASS","NOTE","CAUTION","VETO","APPEALED"];let n=ie(null),g=ie(""),C=ie(!1),y=ie(null),N=ie(null),$=G(()=>{var v;return((v=e(n))==null?void 0:v.verdictBar)??(e(g)?"CAUTION":"NOTE")}),w=G(()=>{var v,m;return((v=e(n))==null?void 0:v.claims.find(A=>A.decision==="veto"))??((m=e(n))==null?void 0:m.claims.find(A=>A.decision==="appealed"))??null}),p=G(()=>{var v;return e(w)??((v=e(n))==null?void 0:v.claims[0])??null}),d=G(()=>!!e(n)||!!e(g));Xe(()=>{M();const v=window.setInterval(M,4e3);return()=>window.clearInterval(v)});async function M(){var v;try{const m=await Oe.sanhedrin.latest();k(n,m.receipt,!0),k(g,""),((v=m.receipt)==null?void 0:v.verdictBar)==="VETO"&&m.receipt.id!==e(N)&&(k(C,!0),k(N,m.receipt.id,!0))}catch(m){k(g,m instanceof Error?m.message:String(m),!0)}}async function R(v){var m;if(!(!e(w)||((m=e(n))==null?void 0:m.verdictBar)!=="VETO")){k(y,v,!0);try{const A=await Oe.sanhedrin.appeal(v,void 0,e(w).id,e(n).id);k(n,A.receipt,!0),k(C,!0),k(g,"")}catch(A){k(g,A instanceof Error?A.message:String(A),!0)}finally{k(y,null)}}}function V(v){if(!v)return"";const m=new Date(v);return Number.isNaN(m.getTime())?"":m.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit"})}function Y(v){var m;return(m=v==null?void 0:v.precedent)!=null&&m.length?v.precedent.map(A=>A.summary??A.command??"Precedent recorded.").slice(0,3):["No precedent attached."]}var ee=Ze(),Q=ce(ee);{var re=v=>{var m=ia(),A=a(m),ye=s(a(A),2);Se(ye,21,()=>o,Fe,(S,I)=>{var f=ea();let O;var z=a(f,!0);t(f),B(()=>{ke(f,"aria-current",e(I)===e($)?"true":void 0),O=U(f,1,"svelte-1j425e6",null,O,{active:e(I)===e($)}),h(z,e(I))}),u(S,f)}),t(ye);var Ae=s(ye,2),je=a(Ae);t(Ae);var ve=s(Ae,2),T=a(ve);{var E=S=>{var I=et();B(()=>h(I,e(g))),u(S,I)},b=S=>{var I=et();B(()=>h(I,e(n).summary)),u(S,I)};q(T,S=>{e(g)?S(E):e(n)&&S(b,1)})}t(ve);var X=s(ve,2),W=a(X,!0);t(X),t(A);var K=s(A,2);{var le=S=>{var I=na(),f=a(I),O=a(f),z=s(a(O),2),pe=a(z,!0);t(z),t(O);var te=s(O,2),oe=s(a(te),2),ue=a(oe);t(oe),t(te);var ae=s(te,2),de=s(a(ae),2);Se(de,21,()=>Y(e(p)),Fe,(_,D)=>{var F=ta(),H=a(F,!0);t(F),B(()=>h(H,e(D))),u(_,F)}),t(de),t(ae);var me=s(ae,2),Ce=s(a(me),2),Be=a(Ce,!0);t(Ce),t(me),t(f);var De=s(f,2),Re=s(a(De),2);{var l=_=>{var D=aa(),F=ce(D),H=a(F,!0);t(F);var ne=s(F,2),Me=a(ne,!0);t(ne);var we=s(ne,2),We=a(we,!0);t(we),B((L,P,fe)=>{F.disabled=L,h(H,e(y)==="stale"?"Saving":"Stale"),ne.disabled=P,h(Me,e(y)==="wrong"?"Saving":"Wrong"),we.disabled=fe,h(We,e(y)==="too_strict"?"Saving":"Too strict")},[()=>!!e(y),()=>!!e(y),()=>!!e(y)]),J("click",F,()=>R("stale")),J("click",ne,()=>R("wrong")),J("click",we,()=>R("too_strict")),u(_,D)},c=_=>{var D=sa();u(_,D)},j=_=>{var D=ra();u(_,D)};q(Re,_=>{e(w)&&e(n).verdictBar==="VETO"?_(l):e(n).verdictBar==="APPEALED"?_(c,1):_(j,!1)})}t(De),t(I),B(()=>{var _,D,F,H;h(pe,((_=e(p))==null?void 0:_.text)??e(n).draftPreview),h(ue,`${((D=e(p))==null?void 0:D.decision)??e(n).overall??""} · ${((F=e(p))==null?void 0:F.evidence_state)??e($)??""}`),h(Be,((H=e(p))==null?void 0:H.fix)||"No change required.")}),u(S,I)};q(K,S=>{e(C)&&e(n)&&S(le)})}t(m),B((S,I)=>{U(m,1,S,"svelte-1j425e6"),ke(A,"aria-expanded",e(C)),h(je,`Current verdict: ${e($)??""}`),h(W,I)},[()=>`verdict-bar tone-${e($).toLowerCase()}`,()=>{var S;return V((S=e(n))==null?void 0:S.createdAt)}]),J("click",A,()=>k(C,!e(C))),u(v,m)};q(Q,v=>{e(d)&&v(re)})}u(r,ee),He()}Ge(["click"]);const mt="vestige.theme",rt="vestige-theme-light",Le=vt("dark"),Ue=vt(!0),nt=Ct([Le,Ue],([r,i])=>r==="auto"?i?"dark":"light":r);function oa(r){return r==="dark"||r==="light"||r==="auto"}function da(r){if(oa(r)){Le.set(r);try{localStorage.setItem(mt,r)}catch{}}}function Ye(){const r=pt(Le);da(r==="dark"?"light":r==="light"?"auto":"dark")}function ca(){if(document.getElementById(rt))return;const r=document.createElement("style");r.id=rt,r.textContent=` +/* Vestige light-mode overrides — injected by theme.ts. + * Activated by [data-theme='light'] on . + * Tokens mirror the real names used in app.css so the cascade stays clean. */ +[data-theme='light'] { + /* Core surface palette (slate scale) */ + --color-void: #f8fafc; /* slate-50 — page background */ + --color-abyss: #f1f5f9; /* slate-100 */ + --color-deep: #e2e8f0; /* slate-200 */ + --color-surface: #f1f5f9; /* slate-100 */ + --color-elevated: #e2e8f0; /* slate-200 */ + --color-subtle: #cbd5e1; /* slate-300 */ + --color-muted: #94a3b8; /* slate-400 */ + --color-dim: #475569; /* slate-600 */ + --color-text: #0f172a; /* slate-900 */ + --color-bright: #020617; /* slate-950 */ +} + +/* Baseline body/html wiring — app.css sets these against the dark + * tokens; we just let the variables do the work. Reassert for clarity. */ +[data-theme='light'] html, +html[data-theme='light'] { + background: var(--color-void); + color: var(--color-text); +} + +/* Glass surfaces — recompose on a light canvas. The original alphas + * are tuned for dark; invert-and-tint for light so panels still read + * as elevated instead of vanishing. */ +[data-theme='light'] .glass { + background: rgba(255, 255, 255, 0.65); + border: 1px solid rgba(99, 102, 241, 0.12); + box-shadow: + inset 0 1px 0 0 rgba(255, 255, 255, 0.6), + 0 4px 24px rgba(15, 23, 42, 0.08); +} +[data-theme='light'] .glass-subtle { + background: rgba(255, 255, 255, 0.55); + border: 1px solid rgba(99, 102, 241, 0.1); + box-shadow: + inset 0 1px 0 0 rgba(255, 255, 255, 0.5), + 0 2px 12px rgba(15, 23, 42, 0.06); +} +[data-theme='light'] .glass-sidebar { + background: rgba(248, 250, 252, 0.82); + border-right: 1px solid rgba(99, 102, 241, 0.14); + box-shadow: + inset -1px 0 0 0 rgba(255, 255, 255, 0.4), + 4px 0 24px rgba(15, 23, 42, 0.08); +} +[data-theme='light'] .glass-panel { + background: rgba(255, 255, 255, 0.75); + border: 1px solid rgba(99, 102, 241, 0.14); + box-shadow: + inset 0 1px 0 0 rgba(255, 255, 255, 0.5), + 0 8px 32px rgba(15, 23, 42, 0.1); +} + +/* Halve glow intensity — neon accents stay recognizable without + * washing out on slate-50. */ +[data-theme='light'] .glow-synapse { + box-shadow: 0 0 10px rgba(99, 102, 241, 0.15), 0 0 30px rgba(99, 102, 241, 0.05); +} +[data-theme='light'] .glow-dream { + box-shadow: 0 0 10px rgba(168, 85, 247, 0.15), 0 0 30px rgba(168, 85, 247, 0.05); +} +[data-theme='light'] .glow-memory { + box-shadow: 0 0 10px rgba(59, 130, 246, 0.15), 0 0 30px rgba(59, 130, 246, 0.05); +} + +/* Ambient orbs are gorgeous on black and blinding on white. Tame them. */ +[data-theme='light'] .ambient-orb { + opacity: 0.18; + filter: blur(100px); +} + +/* Scrollbar recolor for the lighter surface. */ +[data-theme='light'] ::-webkit-scrollbar-thumb { + background: #cbd5e1; +} +[data-theme='light'] ::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} +`,document.head.appendChild(r)}function it(r){document.documentElement.dataset.theme=r}let ge=null,$e=null,be=null,xe=null;function va(){ge&&$e&&ge.removeEventListener("change",$e),xe==null||xe(),be==null||be(),ge=null,$e=null,xe=null,be=null,ca();let r="dark";try{const i=localStorage.getItem(mt);(i==="dark"||i==="light"||i==="auto")&&(r=i)}catch{}return Le.set(r),ge=window.matchMedia("(prefers-color-scheme: dark)"),Ue.set(ge.matches),$e=i=>Ue.set(i.matches),ge.addEventListener("change",$e),it(pt(nt)),xe=nt.subscribe(it),be=Le.subscribe(()=>{}),()=>{ge&&$e&&ge.removeEventListener("change",$e),ge=null,$e=null,xe==null||xe(),be==null||be(),xe=null,be=null}}var pa=x('');function ua(r){const i=()=>se(Le,"$theme",o),[o,n]=Ve(),g={dark:"Dark",light:"Light",auto:"Auto (system)"},C={dark:"light",light:"auto",auto:"dark"};let y=G(i),N=G(()=>C[e(y)]),$=G(()=>`Toggle theme: ${g[e(y)]} (click for ${g[e(N)]})`);var w=pa(),p=a(w),d=a(p);let M;var R=s(d,2);let V;var Y=s(R,2);let ee;t(p),t(w),B(()=>{ke(w,"aria-label",e($)),ke(w,"title",e($)),ke(w,"data-mode",e(y)),M=U(d,0,"icon svelte-1cmi4dh",null,M,{active:e(y)==="dark"}),V=U(R,0,"icon svelte-1cmi4dh",null,V,{active:e(y)==="light"}),ee=U(Y,0,"icon svelte-1cmi4dh",null,ee,{active:e(y)==="auto"})}),J("click",w,function(...Q){Ye==null||Ye.apply(this,Q)}),u(r,w),n()}Ge(["click"]);var ma=x(' '),fa=x('
      '),ha=x(''),ga=x(' '),ba=x('
      ',1),xa=x(''),_a=x('
      No matches
      '),ka=x('
      esc
      '),ya=x(" ",1);function Ga(r,i){Ke(i,!0);const o=()=>se(Et,"$page",$),n=()=>se(wt,"$isConnected",$),g=()=>se(ot,"$memoryCount",$),C=()=>se(dt,"$avgRetention",$),y=()=>se(yt,"$uptimeSeconds",$),N=()=>se(lt,"$suppressedCount",$),[$,w]=Ve();let p=ie(!1),d=ie(""),M=ie(void 0),R=G(()=>o().url.pathname.startsWith(he)?o().url.pathname.slice(he.length)||"/":o().url.pathname),V=G(()=>e(R)==="/waitlist"||e(R).startsWith("/waitlist/"));Xe(()=>{e(V)||st.connect();const T=va();function E(b){if(e(V))return;if((b.metaKey||b.ctrlKey)&&b.key==="k"){b.preventDefault(),k(p,!e(p)),k(d,""),e(p)&&requestAnimationFrame(()=>{var K;return(K=e(M))==null?void 0:K.focus()});return}if(b.key==="Escape"&&e(p)){k(p,!1);return}if(b.target instanceof HTMLInputElement||b.target instanceof HTMLTextAreaElement)return;if(b.key==="/"){b.preventDefault();const K=document.querySelector('input[type="text"]');K==null||K.focus();return}const W={g:"/graph",m:"/memories",t:"/timeline",f:"/feed",e:"/explore",i:"/intentions",s:"/stats",r:"/reasoning",a:"/activation",d:"/dreams",c:"/schedule",p:"/importance",u:"/duplicates",x:"/contradictions",n:"/patterns"}[b.key.toLowerCase()];W&&!b.metaKey&&!b.ctrlKey&&!b.altKey&&(b.preventDefault(),at(`${he}${W}`))}return window.addEventListener("keydown",E),()=>{st.disconnect(),window.removeEventListener("keydown",E),T()}});const Y=[{href:"/graph",label:"Graph",icon:"◎",shortcut:"G"},{href:"/reasoning",label:"Reasoning",icon:"✦",shortcut:"R"},{href:"/memories",label:"Memories",icon:"◈",shortcut:"M"},{href:"/timeline",label:"Timeline",icon:"◷",shortcut:"T"},{href:"/feed",label:"Feed",icon:"◉",shortcut:"F"},{href:"/explore",label:"Explore",icon:"◬",shortcut:"E"},{href:"/activation",label:"Activation",icon:"◈",shortcut:"A"},{href:"/dreams",label:"Dreams",icon:"✧",shortcut:"D"},{href:"/schedule",label:"Schedule",icon:"◷",shortcut:"C"},{href:"/importance",label:"Importance",icon:"◎",shortcut:"P"},{href:"/duplicates",label:"Duplicates",icon:"◉",shortcut:"U"},{href:"/contradictions",label:"Contradictions",icon:"⚠",shortcut:"X"},{href:"/patterns",label:"Patterns",icon:"▦",shortcut:"N"},{href:"/intentions",label:"Intentions",icon:"◇",shortcut:"I"},{href:"/stats",label:"Stats",icon:"◫",shortcut:"S"},{href:"/settings",label:"Settings",icon:"⚙",shortcut:","}],ee=Y.slice(0,5);function Q(T,E){const b=E.startsWith(he)?E.slice(he.length)||"/":E;return T==="/graph"?b==="/"||b==="/graph":b.startsWith(T)}let re=G(()=>e(d)?Y.filter(T=>T.label.toLowerCase().includes(e(d).toLowerCase())):Y);function v(T){k(p,!1),k(d,""),at(`${he}${T}`)}var m=ya(),A=ce(m);{var ye=T=>{var E=Ze(),b=ce(E);tt(b,()=>i.children),u(T,E)},Ae=T=>{var E=ba(),b=s(ce(E),6),X=a(b),W=a(X),K=s(W,2);Se(K,21,()=>Y,Fe,(L,P)=>{const fe=G(()=>Q(e(P).href,o().url.pathname));var Z=ma(),Ee=a(Z),qe=a(Ee,!0);t(Ee);var Ie=s(Ee,2),ze=a(Ie,!0);t(Ie);var Je=s(Ie,2),ft=a(Je,!0);t(Je),t(Z),B(()=>{ke(Z,"href",`${he??""}${e(P).href??""}`),U(Z,1,`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm + ${e(fe)?"bg-synapse/15 text-synapse-glow border border-synapse/30 shadow-[0_0_12px_rgba(99,102,241,0.15)] nav-active-border":"text-dim hover:text-text hover:bg-white/[0.03] border border-transparent"}`),h(qe,e(P).icon),h(ze,e(P).label),h(ft,e(P).shortcut)}),u(L,Z)}),t(K);var le=s(K,2),S=a(le);t(le);var I=s(le,2),f=a(I),O=a(f),z=s(O,2),pe=a(z,!0);t(z);var te=s(z,2),oe=a(te);ua(oe),t(te),t(f);var ue=s(f,2),ae=a(ue),de=a(ae);t(ae);var me=s(ae,2),Ce=a(me);t(me);var Be=s(me,2);{var De=L=>{var P=fa(),fe=a(P);t(P),B(Z=>h(fe,`up ${Z??""}`),[()=>$t(y())]),u(L,P)};q(Be,L=>{y()>0&&L(De)})}t(ue);var Re=s(ue,2);{var l=L=>{var P=ha(),fe=a(P);St(fe),t(P),u(L,P)};q(Re,L=>{N()>0&&L(l)})}t(I),t(X);var c=s(X,2),j=a(c);Jt(j,{});var _=s(j,2);la(_,{});var D=s(_,2),F=a(D);tt(F,()=>i.children),t(D),t(c);var H=s(c,2),ne=a(H),Me=a(ne);Se(Me,17,()=>ee,Fe,(L,P)=>{const fe=G(()=>Q(e(P).href,o().url.pathname));var Z=ga(),Ee=a(Z),qe=a(Ee,!0);t(Ee);var Ie=s(Ee,2),ze=a(Ie,!0);t(Ie),t(Z),B(()=>{ke(Z,"href",`${he??""}${e(P).href??""}`),U(Z,1,`flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg transition-all min-w-[3.5rem] + ${e(fe)?"text-synapse-glow":"text-muted"}`),h(qe,e(P).icon),h(ze,e(P).label)}),u(L,Z)});var we=s(Me,2);t(ne),t(H),t(b);var We=s(b,2);It(We,{}),B(L=>{ke(W,"href",`${he??""}/graph`),U(O,1,`w-2 h-2 rounded-full ${n()?"bg-recall animate-pulse-glow":"bg-decay"}`),h(pe,n()?"Connected":"Offline"),h(de,`${g()??""} memories`),h(Ce,`${L??""}% retention`)},[()=>(C()*100).toFixed(0)]),J("click",S,()=>{k(p,!0),k(d,""),requestAnimationFrame(()=>{var L;return(L=e(M))==null?void 0:L.focus()})}),J("click",we,()=>{k(p,!0),k(d,""),requestAnimationFrame(()=>{var L;return(L=e(M))==null?void 0:L.focus()})}),u(T,E)};q(A,T=>{e(V)?T(ye):T(Ae,!1)})}var je=s(A,2);{var ve=T=>{var E=ka(),b=a(E),X=a(b),W=s(a(X),2);gt(W),xt(W,f=>k(M,f),()=>e(M)),_e(2),t(X);var K=s(X,2),le=a(K);Se(le,17,()=>e(re),Fe,(f,O)=>{var z=xa(),pe=a(z),te=a(pe,!0);t(pe);var oe=s(pe,2),ue=a(oe,!0);t(oe);var ae=s(oe,2),de=a(ae,!0);t(ae),t(z),B(()=>{h(te,e(O).icon),h(ue,e(O).label),h(de,e(O).shortcut)}),J("click",z,()=>v(e(O).href)),u(f,z)});var S=s(le,2);{var I=f=>{var O=_a();u(f,O)};q(S,f=>{e(re).length===0&&f(I)})}t(K),t(b),t(E),J("keydown",E,f=>{f.key==="Escape"&&k(p,!1)}),J("click",E,f=>{f.target===f.currentTarget&&k(p,!1)}),J("keydown",W,f=>{f.key==="Enter"&&e(re).length>0&&v(e(re)[0].href)}),bt(W,()=>e(d),f=>k(d,f)),u(T,E)};q(je,T=>{e(p)&&!e(V)&&T(ve)})}u(r,m),He(),w()}Ge(["click","keydown"]);export{Ga as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js.br b/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js.br new file mode 100644 index 0000000..91dae85 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js.gz b/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js.gz new file mode 100644 index 0000000..524ab3b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/0.BSxKGxRx.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js b/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js deleted file mode 100644 index ef5557a..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js +++ /dev/null @@ -1,3 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Ne}from"../chunks/DWVWfZUn.js";import{f as be,d as n,e as s,r as a,t as F,p as je,a as De,h as c,g as r,s as X,u as Y,O as ze}from"../chunks/VE8Jor13.js";import{s as v,d as Ge,a as k}from"../chunks/DHnEMX8z.js";import{i as M}from"../chunks/JkhlGLjU.js";import{e as Z,i as ee}from"../chunks/ByItJEsC.js";import{c as Re,a as m,f as b}from"../chunks/7UNxJI5L.js";import{s as He}from"../chunks/BZYVQ1d5.js";import{s as te,r as Oe}from"../chunks/Cu3VmnGp.js";import{s as ae}from"../chunks/BR2EHpd7.js";import{b as Ve}from"../chunks/BRHZEveZ.js";import{b as We}from"../chunks/DHakDdar.js";import{a as y,s as ge}from"../chunks/AcZBvMXu.js";import{s as Qe,g as fe}from"../chunks/CK5Nmlyf.js";import{b as _}from"../chunks/DUtaznkq.js";import{s as he,w as xe,u as Ue,a as Be,i as Je,m as Pe,f as Xe}from"../chunks/XIUN5r_Y.js";import"../chunks/CrlWs-6R.js";const Ye=()=>{const f=Qe;return{page:{subscribe:f.page.subscribe},navigating:{subscribe:f.navigating.subscribe},updated:f.updated}},Ze={subscribe(f){return Ye().page.subscribe(f)}};var et=b('
      ');function tt(f){const h=()=>y(he,"$suppressedCount",S),[S,K]=ge();var L=Re(),z=be(L);{var T=C=>{var u=et(),q=n(s(u),2),p=s(q);a(q),a(u),F(()=>v(p,`Actively forgetting ${h()??""} ${h()===1?"memory":"memories"}`)),m(C,u)};M(z,C=>{h()>0&&C(T)})}m(f,L),K()}var at=b(' '),st=b('
      '),rt=b(''),nt=b(' '),ot=b(''),it=b('
      No matches
      '),lt=b('
      esc
      '),dt=b('
      ',1);function Et(f,h){je(h,!0);const S=()=>y(Ze,"$page",u),K=()=>y(Je,"$isConnected",u),L=()=>y(Pe,"$memoryCount",u),z=()=>y(Be,"$avgRetention",u),T=()=>y(Ue,"$uptimeSeconds",u),C=()=>y(he,"$suppressedCount",u),[u,q]=ge();let p=X(!1),g=X(""),E=X(void 0);Ne(()=>{xe.connect();function t(e){if((e.metaKey||e.ctrlKey)&&e.key==="k"){e.preventDefault(),c(p,!r(p)),c(g,""),r(p)&&requestAnimationFrame(()=>{var i;return(i=r(E))==null?void 0:i.focus()});return}if(e.key==="Escape"&&r(p)){c(p,!1);return}if(e.target instanceof HTMLInputElement||e.target instanceof HTMLTextAreaElement)return;if(e.key==="/"){e.preventDefault();const i=document.querySelector('input[type="text"]');i==null||i.focus();return}const o={g:"/graph",m:"/memories",t:"/timeline",f:"/feed",e:"/explore",i:"/intentions",s:"/stats"}[e.key.toLowerCase()];o&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&(e.preventDefault(),fe(`${_}${o}`))}return window.addEventListener("keydown",t),()=>{xe.disconnect(),window.removeEventListener("keydown",t)}});const I=[{href:"/graph",label:"Graph",icon:"◎",shortcut:"G"},{href:"/memories",label:"Memories",icon:"◈",shortcut:"M"},{href:"/timeline",label:"Timeline",icon:"◷",shortcut:"T"},{href:"/feed",label:"Feed",icon:"◉",shortcut:"F"},{href:"/explore",label:"Explore",icon:"◬",shortcut:"E"},{href:"/intentions",label:"Intentions",icon:"◇",shortcut:"I"},{href:"/stats",label:"Stats",icon:"◫",shortcut:"S"},{href:"/settings",label:"Settings",icon:"⚙",shortcut:","}],_e=I.slice(0,5);function se(t,e){const d=e.startsWith(_)?e.slice(_.length)||"/":e;return t==="/graph"?d==="/"||d==="/graph":d.startsWith(t)}let N=Y(()=>r(g)?I.filter(t=>t.label.toLowerCase().includes(r(g).toLowerCase())):I);function re(t){c(p,!1),c(g,""),fe(`${_}${t}`)}var ne=dt(),G=n(be(ne),6),R=s(G),oe=s(R),H=n(oe,2);Z(H,21,()=>I,ee,(t,e)=>{const d=Y(()=>se(r(e).href,S().url.pathname));var o=at(),i=s(o),w=s(i,!0);a(i);var x=n(i,2),A=s(x,!0);a(x);var j=n(x,2),l=s(j,!0);a(j),a(o),F(()=>{te(o,"href",`${_??""}${r(e).href??""}`),ae(o,1,`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm - ${r(d)?"bg-synapse/15 text-synapse-glow border border-synapse/30 shadow-[0_0_12px_rgba(99,102,241,0.15)] nav-active-border":"text-dim hover:text-text hover:bg-white/[0.03] border border-transparent"}`),v(w,r(e).icon),v(A,r(e).label),v(l,r(e).shortcut)}),m(t,o)}),a(H);var O=n(H,2),ye=s(O);a(O);var ie=n(O,2),V=s(ie),le=s(V),de=n(le,2),we=s(de,!0);a(de),a(V);var W=n(V,2),Q=s(W),$e=s(Q);a(Q);var U=n(Q,2),ke=s(U);a(U);var Fe=n(U,2);{var Ce=t=>{var e=st(),d=s(e);a(e),F(o=>v(d,`up ${o??""}`),[()=>Xe(T())]),m(t,e)};M(Fe,t=>{T()>0&&t(Ce)})}a(W);var Ee=n(W,2);{var Ae=t=>{var e=rt(),d=s(e);tt(d),a(e),m(t,e)};M(Ee,t=>{C()>0&&t(Ae)})}a(ie),a(R);var B=n(R,2),pe=s(B),Me=s(pe);He(Me,()=>h.children),a(pe),a(B);var ce=n(B,2),ve=s(ce),ue=s(ve);Z(ue,17,()=>_e,ee,(t,e)=>{const d=Y(()=>se(r(e).href,S().url.pathname));var o=nt(),i=s(o),w=s(i,!0);a(i);var x=n(i,2),A=s(x,!0);a(x),a(o),F(()=>{te(o,"href",`${_??""}${r(e).href??""}`),ae(o,1,`flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg transition-all min-w-[3.5rem] - ${r(d)?"text-synapse-glow":"text-muted"}`),v(w,r(e).icon),v(A,r(e).label)}),m(t,o)});var Se=n(ue,2);a(ve),a(ce),a(G);var Ke=n(G,2);{var Le=t=>{var e=lt(),d=s(e),o=s(d),i=n(s(o),2);Oe(i),We(i,l=>c(E,l),()=>r(E)),ze(2),a(o);var w=n(o,2),x=s(w);Z(x,17,()=>r(N),ee,(l,$)=>{var D=ot(),J=s(D),Te=s(J,!0);a(J);var P=n(J,2),qe=s(P,!0);a(P);var me=n(P,2),Ie=s(me,!0);a(me),a(D),F(()=>{v(Te,r($).icon),v(qe,r($).label),v(Ie,r($).shortcut)}),k("click",D,()=>re(r($).href)),m(l,D)});var A=n(x,2);{var j=l=>{var $=it();m(l,$)};M(A,l=>{r(N).length===0&&l(j)})}a(w),a(d),a(e),k("keydown",e,l=>{l.key==="Escape"&&c(p,!1)}),k("click",e,l=>{l.target===l.currentTarget&&c(p,!1)}),k("keydown",i,l=>{l.key==="Enter"&&r(N).length>0&&re(r(N)[0].href)}),Ve(i,()=>r(g),l=>c(g,l)),m(t,e)};M(Ke,t=>{r(p)&&t(Le)})}F(t=>{te(oe,"href",`${_??""}/graph`),ae(le,1,`w-2 h-2 rounded-full ${K()?"bg-recall animate-pulse-glow":"bg-decay"}`),v(we,K()?"Connected":"Offline"),v($e,`${L()??""} memories`),v(ke,`${t??""}% retention`)},[()=>(z()*100).toFixed(0)]),k("click",ye,()=>{c(p,!0),c(g,""),requestAnimationFrame(()=>{var t;return(t=r(E))==null?void 0:t.focus()})}),k("click",Se,()=>{c(p,!0),c(g,""),requestAnimationFrame(()=>{var t;return(t=r(E))==null?void 0:t.focus()})}),m(f,ne),De(),q()}Ge(["click","keydown"]);export{Et as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js.br b/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js.br deleted file mode 100644 index ebdb948..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js.gz b/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js.gz deleted file mode 100644 index 8b24963..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/0.Bfsm2nvh.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js b/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js new file mode 100644 index 0000000..f8cfd8d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{i as h}from"../chunks/BUoSzNdg.js";import{p as g,f as d,t as l,a as v,d as s,r as o,e as _}from"../chunks/CpWkWWOo.js";import{s as p}from"../chunks/BlVfL1ME.js";import{a as x,f as $}from"../chunks/CHOnp4oo.js";import{p as m}from"../chunks/DGcYlAAw.js";import{s as k}from"../chunks/D-gDZzN6.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=$("

      ",1);function C(f,n){g(n,!1),h();var t=E(),r=d(t),c=s(r,!0);o(r);var a=_(r,2),u=s(a,!0);o(a),l(()=>{var e;p(c,i.status),p(u,(e=i.error)==null?void 0:e.message)}),x(f,t),v()}export{C as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js.br b/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js.br new file mode 100644 index 0000000..c26b2c9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js.gz b/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js.gz new file mode 100644 index 0000000..a10aac2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/1.CJFfVX1H.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js b/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js deleted file mode 100644 index 405fc32..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import"../chunks/CrlWs-6R.js";import{p as h,f as g,t as d,a as l,d as v,e as s,r as o}from"../chunks/VE8Jor13.js";import{s as p}from"../chunks/DHnEMX8z.js";import{a as _,f as x}from"../chunks/7UNxJI5L.js";import{i as $}from"../chunks/jyeIy8pa.js";import{p as m}from"../chunks/DUtaznkq.js";import{s as k}from"../chunks/CK5Nmlyf.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=x("

      ",1);function D(f,n){h(n,!1),$();var t=E(),r=g(t),c=s(r,!0);o(r);var a=v(r,2),u=s(a,!0);o(a),d(()=>{var e;p(c,i.status),p(u,(e=i.error)==null?void 0:e.message)}),_(f,t),l()}export{D as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js.br b/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js.br deleted file mode 100644 index a1f8933..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js.gz b/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js.gz deleted file mode 100644 index dee1a5a..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/1.ClSH3vNb.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js b/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js deleted file mode 100644 index e38f4b0..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Ft}from"../chunks/DWVWfZUn.js";import{p as $t,a as Ct,d as s,e,j as W,h as y,g as t,r as a,s as E,f as dt,O,t as B,u as P}from"../chunks/VE8Jor13.js";import{d as Rt,s as i,a as At}from"../chunks/DHnEMX8z.js";import{i as X}from"../chunks/JkhlGLjU.js";import{e as U,i as q}from"../chunks/ByItJEsC.js";import{a as p,f as u}from"../chunks/7UNxJI5L.js";import{s as A}from"../chunks/ussr1V5_.js";import{a as w}from"../chunks/DcQGRi49.js";var Dt=u('
      '),Mt=u('
      '),kt=u('
      '),Bt=u('
      '),St=u('
      '),Tt=u('

      '),jt=u('

      Retention Distribution

      Memory Types

      ',1),Et=u('
      Total Memories
      Avg Retention
      Due for Review
      Embedding Coverage
      ',1),Ot=u('

      System Stats

      ');function Lt(ot,vt){$t(vt,!0);let n=E(null),m=E(null),l=E(null),Y=E(!0);Ft(async()=>{try{await(async d=>{var r=W(d,3);y(n,r[0],!0),y(m,r[1],!0),y(l,r[2],!0)})(await Promise.all([w.stats(),w.health(),w.retentionDistribution()]))}catch{}finally{y(Y,!1)}});function z(d){return{healthy:"#10b981",degraded:"#f59e0b",critical:"#ef4444",empty:"#6b7280"}[d]||"#6b7280"}async function nt(){try{await w.consolidate(),await(async d=>{var r=W(d,3);y(n,r[0],!0),y(m,r[1],!0),y(l,r[2],!0)})(await Promise.all([w.stats(),w.health(),w.retentionDistribution()]))}catch{}}var G=Ot(),lt=s(e(G),2);{var ct=d=>{var r=Mt();U(r,20,()=>Array(8),q,(F,H)=>{var $=Dt();p(F,$)}),a(r),p(d,r)},xt=d=>{var r=Et(),F=dt(r),H=e(F),$=s(H,2),pt=e($,!0);a($);var Z=s($,2),ut=e(Z);a(Z),a(F);var I=s(F,2),J=e(I),tt=e(J),mt=e(tt,!0);a(tt),O(2),a(J);var K=s(J,2),L=e(K),gt=e(L);a(L),O(2),a(K);var N=s(K,2),at=e(N),_t=e(at,!0);a(at),O(2),a(N);var et=s(N,2),st=e(et),ft=e(st);a(st),O(2),a(et),a(I);var rt=s(I,2);{var bt=D=>{var S=jt(),M=dt(S),T=s(e(M),2);U(T,21,()=>t(l).distribution,q,(g,c,v)=>{const C=P(()=>Math.max(...t(l).distribution.map(V=>V.count),1)),R=P(()=>t(c).count/t(C)*100),_=P(()=>v<3?"#ef4444":v<5?"#f59e0b":v<7?"#10b981":"#6366f1");var x=kt(),o=e(x),f=e(o,!0);a(o);var b=s(o,2),h=s(b,2),Q=e(h,!0);a(h),a(x),B(()=>{i(f,t(c).count),A(b,`height: ${t(R)??""}%; background: ${t(_)??""}; opacity: 0.7; min-height: 2px`),i(Q,t(c).range)}),p(g,x)}),a(T),a(M);var k=s(M,2),j=s(e(k),2);U(j,21,()=>Object.entries(t(l).byType),q,(g,c)=>{var v=P(()=>W(t(c),2));let C=()=>t(v)[0],R=()=>t(v)[1];var _=Bt(),x=e(_),o=s(x,2),f=e(o,!0);a(o);var b=s(o,2),h=e(b,!0);a(b),a(_),B(()=>{A(x,`background: ${({fact:"#00A8FF",concept:"#9D00FF",event:"#FFB800",person:"#00FFD1",note:"#8B95A5",pattern:"#FF3CAC",decision:"#FF4757"}[C()]||"#8B95A5")??""}`),i(f,C()),i(h,R())}),p(g,_)}),a(j),a(k);var yt=s(k,2);{var wt=g=>{var c=Tt(),v=e(c),C=e(v);a(v);var R=s(v,2);U(R,21,()=>t(l).endangered.slice(0,20),q,(_,x)=>{var o=St(),f=e(o),b=e(f);a(f);var h=s(f,2),Q=e(h,!0);a(h),a(o),B(V=>{i(b,`${V??""}%`),i(Q,t(x).content)},[()=>(t(x).retentionStrength*100).toFixed(0)]),p(_,o)}),a(R),a(c),B(()=>i(C,`Endangered Memories (${t(l).endangered.length??""})`)),p(g,c)};X(yt,g=>{t(l).endangered.length>0&&g(wt)})}p(D,S)};X(rt,D=>{t(l)&&D(bt)})}var it=s(rt,2),ht=e(it);a(it),B((D,S,M,T,k,j)=>{A(F,`border-color: ${D??""}30`),A(H,`background: ${S??""}`),A($,`color: ${M??""}`),i(pt,T),i(ut,`v${t(m).version??""}`),i(mt,t(n).totalMemories),A(L,`color: ${t(n).averageRetention>.7?"#10b981":t(n).averageRetention>.4?"#f59e0b":"#ef4444"}`),i(gt,`${k??""}%`),i(_t,t(n).dueForReview),i(ft,`${j??""}%`)},[()=>z(t(m).status),()=>z(t(m).status),()=>z(t(m).status),()=>t(m).status.toUpperCase(),()=>(t(n).averageRetention*100).toFixed(1),()=>t(n).embeddingCoverage.toFixed(0)]),At("click",ht,nt),p(d,r)};X(lt,d=>{t(Y)?d(ct):t(n)&&t(m)&&d(xt,1)})}a(G),p(ot,G),Ct()}Rt(["click"]);export{Lt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js.br b/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js.br deleted file mode 100644 index 15329e1..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js.gz b/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js.gz deleted file mode 100644 index 8176c27..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/10.BXBzm8vP.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js b/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js new file mode 100644 index 0000000..704c8d2 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js @@ -0,0 +1,4115 @@ +var Bc=Object.defineProperty;var zc=(i,t,e)=>t in i?Bc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var kt=(i,t,e)=>zc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as jl,a as Zl}from"../chunks/GG5zm9kr.js";import{s as me,c as va,h as zt,g as B,p as ys,aB as kc,a as Es,d as yt,e as bt,n as Hc,r as xt,t as Ke,u as Gn,f as Kl,j as Vc}from"../chunks/CpWkWWOo.js";import{s as fe,d as $l,a as Fe}from"../chunks/BlVfL1ME.js";import{i as kn}from"../chunks/B4yTwGkE.js";import{e as _s,i as hr}from"../chunks/CGEBXrjl.js";import{a as _e,f as Se,c as Gc}from"../chunks/CHOnp4oo.js";import{s as ve,r as xa}from"../chunks/A7po6GxK.js";import{s as Us}from"../chunks/aVbAZ-t7.js";import{s as Sr}from"../chunks/Cx-f-Pzo.js";import{b as Ma}from"../chunks/sZcqyNBA.js";import{b as Jl}from"../chunks/BnXDGOmJ.js";import{s as Wc,a as Xc}from"../chunks/C6HuKgyx.js";import{b as Do}from"../chunks/DGcYlAAw.js";import{b as Yc}from"../chunks/CJsMJEun.js";import{p as vs}from"../chunks/V6gjw5Ec.js";import{N as Sa}from"../chunks/CcUbQ_Wl.js";import{i as qc}from"../chunks/BUoSzNdg.js";import{a as gi}from"../chunks/554JRhq6.js";import{e as jc}from"../chunks/MAY1QfFZ.js";/** + * @license + * Copyright 2010-2024 Three.js Authors + * SPDX-License-Identifier: MIT + */const vo="172",Hi={ROTATE:0,DOLLY:1,PAN:2},Oi={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},Zc=0,Lo=1,Kc=2,Ql=1,$c=2,An=3,Yn=0,Xe=1,gn=2,Cn=0,Vi=1,Le=2,Uo=3,Io=4,Jc=5,ii=100,Qc=101,th=102,eh=103,nh=104,ih=200,sh=201,rh=202,ah=203,ya=204,Ea=205,oh=206,lh=207,ch=208,hh=209,uh=210,dh=211,fh=212,ph=213,mh=214,ba=0,Ta=1,wa=2,qi=3,Aa=4,Ra=5,Ca=6,Pa=7,tc=0,gh=1,_h=2,Wn=0,vh=1,xh=2,Mh=3,ec=4,Sh=5,yh=6,Eh=7,nc=300,ji=301,Zi=302,Da=303,La=304,Pr=306,Ua=1e3,ri=1001,Ia=1002,Je=1003,bh=1004,Is=1005,vn=1006,Br=1007,ai=1008,Ln=1009,ic=1010,sc=1011,Ms=1012,xo=1013,li=1014,xn=1015,Pn=1016,Mo=1017,So=1018,Ki=1020,rc=35902,ac=1021,oc=1022,dn=1023,lc=1024,cc=1025,Gi=1026,$i=1027,yo=1028,Eo=1029,hc=1030,bo=1031,To=1033,ur=33776,dr=33777,fr=33778,pr=33779,Na=35840,Fa=35841,Oa=35842,Ba=35843,za=36196,ka=37492,Ha=37496,Va=37808,Ga=37809,Wa=37810,Xa=37811,Ya=37812,qa=37813,ja=37814,Za=37815,Ka=37816,$a=37817,Ja=37818,Qa=37819,to=37820,eo=37821,mr=36492,no=36494,io=36495,uc=36283,so=36284,ro=36285,ao=36286,Th=3200,wh=3201,dc=0,Ah=1,Vn="",an="srgb",Ji="srgb-linear",yr="linear",se="srgb",_i=7680,No=519,Rh=512,Ch=513,Ph=514,fc=515,Dh=516,Lh=517,Uh=518,Ih=519,oo=35044,Fo="300 es",Rn=2e3,Er=2001;class hi{addEventListener(t,e){this._listeners===void 0&&(this._listeners={});const n=this._listeners;n[t]===void 0&&(n[t]=[]),n[t].indexOf(e)===-1&&n[t].push(e)}hasEventListener(t,e){if(this._listeners===void 0)return!1;const n=this._listeners;return n[t]!==void 0&&n[t].indexOf(e)!==-1}removeEventListener(t,e){if(this._listeners===void 0)return;const s=this._listeners[t];if(s!==void 0){const r=s.indexOf(e);r!==-1&&s.splice(r,1)}}dispatchEvent(t){if(this._listeners===void 0)return;const n=this._listeners[t.type];if(n!==void 0){t.target=this;const s=n.slice(0);for(let r=0,a=s.length;r>8&255]+Ie[i>>16&255]+Ie[i>>24&255]+"-"+Ie[t&255]+Ie[t>>8&255]+"-"+Ie[t>>16&15|64]+Ie[t>>24&255]+"-"+Ie[e&63|128]+Ie[e>>8&255]+"-"+Ie[e>>16&255]+Ie[e>>24&255]+Ie[n&255]+Ie[n>>8&255]+Ie[n>>16&255]+Ie[n>>24&255]).toLowerCase()}function Yt(i,t,e){return Math.max(t,Math.min(e,i))}function Nh(i,t){return(i%t+t)%t}function zr(i,t,e){return(1-e)*i+e*t}function _n(i,t){switch(t.constructor){case Float32Array:return i;case Uint32Array:return i/4294967295;case Uint16Array:return i/65535;case Uint8Array:return i/255;case Int32Array:return Math.max(i/2147483647,-1);case Int16Array:return Math.max(i/32767,-1);case Int8Array:return Math.max(i/127,-1);default:throw new Error("Invalid component type.")}}function re(i,t){switch(t.constructor){case Float32Array:return i;case Uint32Array:return Math.round(i*4294967295);case Uint16Array:return Math.round(i*65535);case Uint8Array:return Math.round(i*255);case Int32Array:return Math.round(i*2147483647);case Int16Array:return Math.round(i*32767);case Int8Array:return Math.round(i*127);default:throw new Error("Invalid component type.")}}const Fh={DEG2RAD:gr};class St{constructor(t=0,e=0){St.prototype.isVector2=!0,this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t){return this.x+=t.x,this.y+=t.y,this}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,n=this.y,s=t.elements;return this.x=s[0]*e+s[3]*n+s[6],this.y=s[1]*e+s[4]*n+s[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=Yt(this.x,t.x,e.x),this.y=Yt(this.y,t.y,e.y),this}clampScalar(t,e){return this.x=Yt(this.x,t,e),this.y=Yt(this.y,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Yt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(e===0)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Yt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y;return e*e+n*n}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const n=Math.cos(e),s=Math.sin(e),r=this.x-t.x,a=this.y-t.y;return this.x=r*n-a*s+t.x,this.y=r*s+a*n+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Ht{constructor(t,e,n,s,r,a,o,l,c){Ht.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],t!==void 0&&this.set(t,e,n,s,r,a,o,l,c)}set(t,e,n,s,r,a,o,l,c){const h=this.elements;return h[0]=t,h[1]=s,h[2]=o,h[3]=e,h[4]=r,h[5]=l,h[6]=n,h[7]=a,h[8]=c,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],this}extractBasis(t,e,n){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,s=e.elements,r=this.elements,a=n[0],o=n[3],l=n[6],c=n[1],h=n[4],d=n[7],p=n[2],u=n[5],g=n[8],_=s[0],m=s[3],f=s[6],T=s[1],E=s[4],y=s[7],D=s[2],w=s[5],C=s[8];return r[0]=a*_+o*T+l*D,r[3]=a*m+o*E+l*w,r[6]=a*f+o*y+l*C,r[1]=c*_+h*T+d*D,r[4]=c*m+h*E+d*w,r[7]=c*f+h*y+d*C,r[2]=p*_+u*T+g*D,r[5]=p*m+u*E+g*w,r[8]=p*f+u*y+g*C,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8];return e*a*h-e*o*c-n*r*h+n*o*l+s*r*c-s*a*l}invert(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8],d=h*a-o*c,p=o*l-h*r,u=c*r-a*l,g=e*d+n*p+s*u;if(g===0)return this.set(0,0,0,0,0,0,0,0,0);const _=1/g;return t[0]=d*_,t[1]=(s*c-h*n)*_,t[2]=(o*n-s*a)*_,t[3]=p*_,t[4]=(h*e-s*l)*_,t[5]=(s*r-o*e)*_,t[6]=u*_,t[7]=(n*l-c*e)*_,t[8]=(a*e-n*r)*_,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,n,s,r,a,o){const l=Math.cos(r),c=Math.sin(r);return this.set(n*l,n*c,-n*(l*a+c*o)+a+t,-s*c,s*l,-s*(-c*a+l*o)+o+e,0,0,1),this}scale(t,e){return this.premultiply(kr.makeScale(t,e)),this}rotate(t){return this.premultiply(kr.makeRotation(-t)),this}translate(t,e){return this.premultiply(kr.makeTranslation(t,e)),this}makeTranslation(t,e){return t.isVector2?this.set(1,0,t.x,0,1,t.y,0,0,1):this.set(1,0,t,0,1,e,0,0,1),this}makeRotation(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,n,e,0,0,0,1),this}makeScale(t,e){return this.set(t,0,0,0,e,0,0,0,1),this}equals(t){const e=this.elements,n=t.elements;for(let s=0;s<9;s++)if(e[s]!==n[s])return!1;return!0}fromArray(t,e=0){for(let n=0;n<9;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t}clone(){return new this.constructor().fromArray(this.elements)}}const kr=new Ht;function pc(i){for(let t=i.length-1;t>=0;--t)if(i[t]>=65535)return!0;return!1}function br(i){return document.createElementNS("http://www.w3.org/1999/xhtml",i)}function Oh(){const i=br("canvas");return i.style.display="block",i}const Oo={};function Fi(i){i in Oo||(Oo[i]=!0,console.warn(i))}function Bh(i,t,e){return new Promise(function(n,s){function r(){switch(i.clientWaitSync(t,i.SYNC_FLUSH_COMMANDS_BIT,0)){case i.WAIT_FAILED:s();break;case i.TIMEOUT_EXPIRED:setTimeout(r,e);break;default:n()}}setTimeout(r,e)})}function zh(i){const t=i.elements;t[2]=.5*t[2]+.5*t[3],t[6]=.5*t[6]+.5*t[7],t[10]=.5*t[10]+.5*t[11],t[14]=.5*t[14]+.5*t[15]}function kh(i){const t=i.elements;t[11]===-1?(t[10]=-t[10]-1,t[14]=-t[14]):(t[10]=-t[10],t[14]=-t[14]+1)}const Bo=new Ht().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),zo=new Ht().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Hh(){const i={enabled:!0,workingColorSpace:Ji,spaces:{},convert:function(s,r,a){return this.enabled===!1||r===a||!r||!a||(this.spaces[r].transfer===se&&(s.r=Dn(s.r),s.g=Dn(s.g),s.b=Dn(s.b)),this.spaces[r].primaries!==this.spaces[a].primaries&&(s.applyMatrix3(this.spaces[r].toXYZ),s.applyMatrix3(this.spaces[a].fromXYZ)),this.spaces[a].transfer===se&&(s.r=Wi(s.r),s.g=Wi(s.g),s.b=Wi(s.b))),s},fromWorkingColorSpace:function(s,r){return this.convert(s,this.workingColorSpace,r)},toWorkingColorSpace:function(s,r){return this.convert(s,r,this.workingColorSpace)},getPrimaries:function(s){return this.spaces[s].primaries},getTransfer:function(s){return s===Vn?yr:this.spaces[s].transfer},getLuminanceCoefficients:function(s,r=this.workingColorSpace){return s.fromArray(this.spaces[r].luminanceCoefficients)},define:function(s){Object.assign(this.spaces,s)},_getMatrix:function(s,r,a){return s.copy(this.spaces[r].toXYZ).multiply(this.spaces[a].fromXYZ)},_getDrawingBufferColorSpace:function(s){return this.spaces[s].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(s=this.workingColorSpace){return this.spaces[s].workingColorSpaceConfig.unpackColorSpace}},t=[.64,.33,.3,.6,.15,.06],e=[.2126,.7152,.0722],n=[.3127,.329];return i.define({[Ji]:{primaries:t,whitePoint:n,transfer:yr,toXYZ:Bo,fromXYZ:zo,luminanceCoefficients:e,workingColorSpaceConfig:{unpackColorSpace:an},outputColorSpaceConfig:{drawingBufferColorSpace:an}},[an]:{primaries:t,whitePoint:n,transfer:se,toXYZ:Bo,fromXYZ:zo,luminanceCoefficients:e,outputColorSpaceConfig:{drawingBufferColorSpace:an}}}),i}const Qt=Hh();function Dn(i){return i<.04045?i*.0773993808:Math.pow(i*.9478672986+.0521327014,2.4)}function Wi(i){return i<.0031308?i*12.92:1.055*Math.pow(i,.41666)-.055}let vi;class Vh{static getDataURL(t){if(/^data:/i.test(t.src)||typeof HTMLCanvasElement>"u")return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{vi===void 0&&(vi=br("canvas")),vi.width=t.width,vi.height=t.height;const n=vi.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=vi}return e.width>2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}static sRGBToLinear(t){if(typeof HTMLImageElement<"u"&&t instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&t instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&t instanceof ImageBitmap){const e=br("canvas");e.width=t.width,e.height=t.height;const n=e.getContext("2d");n.drawImage(t,0,0,t.width,t.height);const s=n.getImageData(0,0,t.width,t.height),r=s.data;for(let a=0;a0&&(n.userData=this.userData),e||(t.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(t){if(this.mapping!==nc)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case Ua:t.x=t.x-Math.floor(t.x);break;case ri:t.x=t.x<0?0:1;break;case Ia:Math.abs(Math.floor(t.x)%2)===1?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x);break}if(t.y<0||t.y>1)switch(this.wrapT){case Ua:t.y=t.y-Math.floor(t.y);break;case ri:t.y=t.y<0?0:1;break;case Ia:Math.abs(Math.floor(t.y)%2)===1?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y);break}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){t===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(t){t===!0&&this.pmremVersion++}}Pe.DEFAULT_IMAGE=null;Pe.DEFAULT_MAPPING=nc;Pe.DEFAULT_ANISOTROPY=1;class oe{constructor(t=0,e=0,n=0,s=1){oe.prototype.isVector4=!0,this.x=t,this.y=e,this.z=n,this.w=s}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,n,s){return this.x=t,this.y=e,this.z=n,this.w=s,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=t.w!==void 0?t.w:1,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,n=this.y,s=this.z,r=this.w,a=t.elements;return this.x=a[0]*e+a[4]*n+a[8]*s+a[12]*r,this.y=a[1]*e+a[5]*n+a[9]*s+a[13]*r,this.z=a[2]*e+a[6]*n+a[10]*s+a[14]*r,this.w=a[3]*e+a[7]*n+a[11]*s+a[15]*r,this}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this.w/=t.w,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,n,s,r;const l=t.elements,c=l[0],h=l[4],d=l[8],p=l[1],u=l[5],g=l[9],_=l[2],m=l[6],f=l[10];if(Math.abs(h-p)<.01&&Math.abs(d-_)<.01&&Math.abs(g-m)<.01){if(Math.abs(h+p)<.1&&Math.abs(d+_)<.1&&Math.abs(g+m)<.1&&Math.abs(c+u+f-3)<.1)return this.set(1,0,0,0),this;e=Math.PI;const E=(c+1)/2,y=(u+1)/2,D=(f+1)/2,w=(h+p)/4,C=(d+_)/4,I=(g+m)/4;return E>y&&E>D?E<.01?(n=0,s=.707106781,r=.707106781):(n=Math.sqrt(E),s=w/n,r=C/n):y>D?y<.01?(n=.707106781,s=0,r=.707106781):(s=Math.sqrt(y),n=w/s,r=I/s):D<.01?(n=.707106781,s=.707106781,r=0):(r=Math.sqrt(D),n=C/r,s=I/r),this.set(n,s,r,e),this}let T=Math.sqrt((m-g)*(m-g)+(d-_)*(d-_)+(p-h)*(p-h));return Math.abs(T)<.001&&(T=1),this.x=(m-g)/T,this.y=(d-_)/T,this.z=(p-h)/T,this.w=Math.acos((c+u+f-1)/2),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this.w=e[15],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this.w=Math.min(this.w,t.w),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this.w=Math.max(this.w,t.w),this}clamp(t,e){return this.x=Yt(this.x,t.x,e.x),this.y=Yt(this.y,t.y,e.y),this.z=Yt(this.z,t.z,e.z),this.w=Yt(this.w,t.w,e.w),this}clampScalar(t,e){return this.x=Yt(this.x,t,e),this.y=Yt(this.y,t,e),this.z=Yt(this.z,t,e),this.w=Yt(this.w,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Yt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this.w=Math.floor(this.w),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this.w=Math.ceil(this.w),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this.w=Math.round(this.w),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this.w=Math.trunc(this.w),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this.w=-this.w,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z+this.w*t.w}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this.w+=(t.w-this.w)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this.w=t.w+(e.w-t.w)*n,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z&&t.w===this.w}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this.w=t[e+3],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t[e+3]=this.w,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this.w=t.getW(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this.w=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z,yield this.w}}class Xh extends hi{constructor(t=1,e=1,n={}){super(),this.isRenderTarget=!0,this.width=t,this.height=e,this.depth=1,this.scissor=new oe(0,0,t,e),this.scissorTest=!1,this.viewport=new oe(0,0,t,e);const s={width:t,height:e,depth:1};n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:vn,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1},n);const r=new Pe(s,n.mapping,n.wrapS,n.wrapT,n.magFilter,n.minFilter,n.format,n.type,n.anisotropy,n.colorSpace);r.flipY=!1,r.generateMipmaps=n.generateMipmaps,r.internalFormat=n.internalFormat,this.textures=[];const a=n.count;for(let o=0;o=0?1:-1,E=1-f*f;if(E>Number.EPSILON){const D=Math.sqrt(E),w=Math.atan2(D,f*T);m=Math.sin(m*w)/D,o=Math.sin(o*w)/D}const y=o*T;if(l=l*m+p*y,c=c*m+u*y,h=h*m+g*y,d=d*m+_*y,m===1-o){const D=1/Math.sqrt(l*l+c*c+h*h+d*d);l*=D,c*=D,h*=D,d*=D}}t[e]=l,t[e+1]=c,t[e+2]=h,t[e+3]=d}static multiplyQuaternionsFlat(t,e,n,s,r,a){const o=n[s],l=n[s+1],c=n[s+2],h=n[s+3],d=r[a],p=r[a+1],u=r[a+2],g=r[a+3];return t[e]=o*g+h*d+l*u-c*p,t[e+1]=l*g+h*p+c*d-o*u,t[e+2]=c*g+h*u+o*p-l*d,t[e+3]=h*g-o*d-l*p-c*u,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,n,s){return this._x=t,this._y=e,this._z=n,this._w=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e=!0){const n=t._x,s=t._y,r=t._z,a=t._order,o=Math.cos,l=Math.sin,c=o(n/2),h=o(s/2),d=o(r/2),p=l(n/2),u=l(s/2),g=l(r/2);switch(a){case"XYZ":this._x=p*h*d+c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d-p*u*g;break;case"YXZ":this._x=p*h*d+c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d+p*u*g;break;case"ZXY":this._x=p*h*d-c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d-p*u*g;break;case"ZYX":this._x=p*h*d-c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d+p*u*g;break;case"YZX":this._x=p*h*d+c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d-p*u*g;break;case"XZY":this._x=p*h*d-c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d+p*u*g;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+a)}return e===!0&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const n=e/2,s=Math.sin(n);return this._x=t.x*s,this._y=t.y*s,this._z=t.z*s,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,n=e[0],s=e[4],r=e[8],a=e[1],o=e[5],l=e[9],c=e[2],h=e[6],d=e[10],p=n+o+d;if(p>0){const u=.5/Math.sqrt(p+1);this._w=.25/u,this._x=(h-l)*u,this._y=(r-c)*u,this._z=(a-s)*u}else if(n>o&&n>d){const u=2*Math.sqrt(1+n-o-d);this._w=(h-l)/u,this._x=.25*u,this._y=(s+a)/u,this._z=(r+c)/u}else if(o>d){const u=2*Math.sqrt(1+o-n-d);this._w=(r-c)/u,this._x=(s+a)/u,this._y=.25*u,this._z=(l+h)/u}else{const u=2*Math.sqrt(1+d-n-o);this._w=(a-s)/u,this._x=(r+c)/u,this._y=(l+h)/u,this._z=.25*u}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let n=t.dot(e)+1;return nMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=n):(this._x=0,this._y=-t.z,this._z=t.y,this._w=n)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=n),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(Yt(this.dot(t),-1,1)))}rotateTowards(t,e){const n=this.angleTo(t);if(n===0)return this;const s=Math.min(1,e/n);return this.slerp(t,s),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return t===0?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t){return this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const n=t._x,s=t._y,r=t._z,a=t._w,o=e._x,l=e._y,c=e._z,h=e._w;return this._x=n*h+a*o+s*c-r*l,this._y=s*h+a*l+r*o-n*c,this._z=r*h+a*c+n*l-s*o,this._w=a*h-n*o-s*l-r*c,this._onChangeCallback(),this}slerp(t,e){if(e===0)return this;if(e===1)return this.copy(t);const n=this._x,s=this._y,r=this._z,a=this._w;let o=a*t._w+n*t._x+s*t._y+r*t._z;if(o<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,o=-o):this.copy(t),o>=1)return this._w=a,this._x=n,this._y=s,this._z=r,this;const l=1-o*o;if(l<=Number.EPSILON){const u=1-e;return this._w=u*a+e*this._w,this._x=u*n+e*this._x,this._y=u*s+e*this._y,this._z=u*r+e*this._z,this.normalize(),this}const c=Math.sqrt(l),h=Math.atan2(c,o),d=Math.sin((1-e)*h)/c,p=Math.sin(e*h)/c;return this._w=a*d+this._w*p,this._x=n*d+this._x*p,this._y=s*d+this._y*p,this._z=r*d+this._z*p,this._onChangeCallback(),this}slerpQuaternions(t,e,n){return this.copy(t).slerp(e,n)}random(){const t=2*Math.PI*Math.random(),e=2*Math.PI*Math.random(),n=Math.random(),s=Math.sqrt(1-n),r=Math.sqrt(n);return this.set(s*Math.sin(t),s*Math.cos(t),r*Math.sin(e),r*Math.cos(e))}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class P{constructor(t=0,e=0,n=0){P.prototype.isVector3=!0,this.x=t,this.y=e,this.z=n}set(t,e,n){return n===void 0&&(n=this.z),this.x=t,this.y=e,this.z=n,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return this.applyQuaternion(ko.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(ko.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,n=this.y,s=this.z,r=t.elements;return this.x=r[0]*e+r[3]*n+r[6]*s,this.y=r[1]*e+r[4]*n+r[7]*s,this.z=r[2]*e+r[5]*n+r[8]*s,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,n=this.y,s=this.z,r=t.elements,a=1/(r[3]*e+r[7]*n+r[11]*s+r[15]);return this.x=(r[0]*e+r[4]*n+r[8]*s+r[12])*a,this.y=(r[1]*e+r[5]*n+r[9]*s+r[13])*a,this.z=(r[2]*e+r[6]*n+r[10]*s+r[14])*a,this}applyQuaternion(t){const e=this.x,n=this.y,s=this.z,r=t.x,a=t.y,o=t.z,l=t.w,c=2*(a*s-o*n),h=2*(o*e-r*s),d=2*(r*n-a*e);return this.x=e+l*c+a*d-o*h,this.y=n+l*h+o*c-r*d,this.z=s+l*d+r*h-a*c,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,n=this.y,s=this.z,r=t.elements;return this.x=r[0]*e+r[4]*n+r[8]*s,this.y=r[1]*e+r[5]*n+r[9]*s,this.z=r[2]*e+r[6]*n+r[10]*s,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=Yt(this.x,t.x,e.x),this.y=Yt(this.y,t.y,e.y),this.z=Yt(this.z,t.z,e.z),this}clampScalar(t,e){return this.x=Yt(this.x,t,e),this.y=Yt(this.y,t,e),this.z=Yt(this.z,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Yt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const n=t.x,s=t.y,r=t.z,a=e.x,o=e.y,l=e.z;return this.x=s*l-r*o,this.y=r*a-n*l,this.z=n*o-s*a,this}projectOnVector(t){const e=t.lengthSq();if(e===0)return this.set(0,0,0);const n=t.dot(this)/e;return this.copy(t).multiplyScalar(n)}projectOnPlane(t){return Vr.copy(this).projectOnVector(t),this.sub(Vr)}reflect(t){return this.sub(Vr.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(e===0)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Yt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y,s=this.z-t.z;return e*e+n*n+s*s}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,n){const s=Math.sin(e)*t;return this.x=s*Math.sin(n),this.y=Math.cos(e)*t,this.z=s*Math.cos(n),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,n){return this.x=t*Math.sin(e),this.y=n,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),n=this.setFromMatrixColumn(t,1).length(),s=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=n,this.z=s,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,e*4)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,e*3)}setFromEuler(t){return this.x=t._x,this.y=t._y,this.z=t._z,this}setFromColor(t){return this.x=t.r,this.y=t.g,this.z=t.b,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const t=Math.random()*Math.PI*2,e=Math.random()*2-1,n=Math.sqrt(1-e*e);return this.x=n*Math.cos(t),this.y=e,this.z=n*Math.sin(t),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const Vr=new P,ko=new ci;class ui{constructor(t=new P(1/0,1/0,1/0),e=new P(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){this.makeEmpty();for(let e=0,n=t.length;e=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y&&t.z>=this.min.z&&t.z<=this.max.z}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y&&t.max.z>=this.min.z&&t.min.z<=this.max.z}intersectsSphere(t){return this.clampPoint(t.center,cn),cn.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,n;return t.normal.x>0?(e=t.normal.x*this.min.x,n=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,n=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,n+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,n+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,n+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,n+=t.normal.z*this.min.z),e<=-t.constant&&n>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(ss),Fs.subVectors(this.max,ss),xi.subVectors(t.a,ss),Mi.subVectors(t.b,ss),Si.subVectors(t.c,ss),Un.subVectors(Mi,xi),In.subVectors(Si,Mi),Zn.subVectors(xi,Si);let e=[0,-Un.z,Un.y,0,-In.z,In.y,0,-Zn.z,Zn.y,Un.z,0,-Un.x,In.z,0,-In.x,Zn.z,0,-Zn.x,-Un.y,Un.x,0,-In.y,In.x,0,-Zn.y,Zn.x,0];return!Gr(e,xi,Mi,Si,Fs)||(e=[1,0,0,0,1,0,0,0,1],!Gr(e,xi,Mi,Si,Fs))?!1:(Os.crossVectors(Un,In),e=[Os.x,Os.y,Os.z],Gr(e,xi,Mi,Si,Fs))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,cn).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=this.getSize(cn).length()*.5),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()?this:(Sn[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),Sn[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),Sn[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),Sn[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),Sn[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),Sn[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),Sn[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),Sn[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(Sn),this)}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const Sn=[new P,new P,new P,new P,new P,new P,new P,new P],cn=new P,Ns=new ui,xi=new P,Mi=new P,Si=new P,Un=new P,In=new P,Zn=new P,ss=new P,Fs=new P,Os=new P,Kn=new P;function Gr(i,t,e,n,s){for(let r=0,a=i.length-3;r<=a;r+=3){Kn.fromArray(i,r);const o=s.x*Math.abs(Kn.x)+s.y*Math.abs(Kn.y)+s.z*Math.abs(Kn.z),l=t.dot(Kn),c=e.dot(Kn),h=n.dot(Kn);if(Math.max(-Math.max(l,c,h),Math.min(l,c,h))>o)return!1}return!0}const qh=new ui,rs=new P,Wr=new P;class di{constructor(t=new P,e=-1){this.isSphere=!0,this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const n=this.center;e!==void 0?n.copy(e):qh.setFromPoints(t).getCenter(n);let s=0;for(let r=0,a=t.length;rthis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){if(this.isEmpty())return this.center.copy(t),this.radius=0,this;rs.subVectors(t,this.center);const e=rs.lengthSq();if(e>this.radius*this.radius){const n=Math.sqrt(e),s=(n-this.radius)*.5;this.center.addScaledVector(rs,s/n),this.radius+=s}return this}union(t){return t.isEmpty()?this:this.isEmpty()?(this.copy(t),this):(this.center.equals(t.center)===!0?this.radius=Math.max(this.radius,t.radius):(Wr.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(rs.copy(t.center).add(Wr)),this.expandByPoint(rs.copy(t.center).sub(Wr))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return new this.constructor().copy(this)}}const yn=new P,Xr=new P,Bs=new P,Nn=new P,Yr=new P,zs=new P,qr=new P;class bs{constructor(t=new P,e=new P(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return e.copy(this.origin).addScaledVector(this.direction,t)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,yn)),this}closestPointToPoint(t,e){e.subVectors(t,this.origin);const n=e.dot(this.direction);return n<0?e.copy(this.origin):e.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=yn.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(yn.copy(this.origin).addScaledVector(this.direction,e),yn.distanceToSquared(t))}distanceSqToSegment(t,e,n,s){Xr.copy(t).add(e).multiplyScalar(.5),Bs.copy(e).sub(t).normalize(),Nn.copy(this.origin).sub(Xr);const r=t.distanceTo(e)*.5,a=-this.direction.dot(Bs),o=Nn.dot(this.direction),l=-Nn.dot(Bs),c=Nn.lengthSq(),h=Math.abs(1-a*a);let d,p,u,g;if(h>0)if(d=a*l-o,p=a*o-l,g=r*h,d>=0)if(p>=-g)if(p<=g){const _=1/h;d*=_,p*=_,u=d*(d+a*p+2*o)+p*(a*d+p+2*l)+c}else p=r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;else p=-r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;else p<=-g?(d=Math.max(0,-(-a*r+o)),p=d>0?-r:Math.min(Math.max(-r,-l),r),u=-d*d+p*(p+2*l)+c):p<=g?(d=0,p=Math.min(Math.max(-r,-l),r),u=p*(p+2*l)+c):(d=Math.max(0,-(a*r+o)),p=d>0?r:Math.min(Math.max(-r,-l),r),u=-d*d+p*(p+2*l)+c);else p=a>0?-r:r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,d),s&&s.copy(Xr).addScaledVector(Bs,p),u}intersectSphere(t,e){yn.subVectors(t.center,this.origin);const n=yn.dot(this.direction),s=yn.dot(yn)-n*n,r=t.radius*t.radius;if(s>r)return null;const a=Math.sqrt(r-s),o=n-a,l=n+a;return l<0?null:o<0?this.at(l,e):this.at(o,e)}intersectsSphere(t){return this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(e===0)return t.distanceToPoint(this.origin)===0?0:null;const n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null}intersectPlane(t,e){const n=this.distanceToPlane(t);return n===null?null:this.at(n,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);return e===0||t.normal.dot(this.direction)*e<0}intersectBox(t,e){let n,s,r,a,o,l;const c=1/this.direction.x,h=1/this.direction.y,d=1/this.direction.z,p=this.origin;return c>=0?(n=(t.min.x-p.x)*c,s=(t.max.x-p.x)*c):(n=(t.max.x-p.x)*c,s=(t.min.x-p.x)*c),h>=0?(r=(t.min.y-p.y)*h,a=(t.max.y-p.y)*h):(r=(t.max.y-p.y)*h,a=(t.min.y-p.y)*h),n>a||r>s||((r>n||isNaN(n))&&(n=r),(a=0?(o=(t.min.z-p.z)*d,l=(t.max.z-p.z)*d):(o=(t.max.z-p.z)*d,l=(t.min.z-p.z)*d),n>l||o>s)||((o>n||n!==n)&&(n=o),(l=0?n:s,e)}intersectsBox(t){return this.intersectBox(t,yn)!==null}intersectTriangle(t,e,n,s,r){Yr.subVectors(e,t),zs.subVectors(n,t),qr.crossVectors(Yr,zs);let a=this.direction.dot(qr),o;if(a>0){if(s)return null;o=1}else if(a<0)o=-1,a=-a;else return null;Nn.subVectors(this.origin,t);const l=o*this.direction.dot(zs.crossVectors(Nn,zs));if(l<0)return null;const c=o*this.direction.dot(Yr.cross(Nn));if(c<0||l+c>a)return null;const h=-o*Nn.dot(qr);return h<0?null:this.at(h/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}}class ne{constructor(t,e,n,s,r,a,o,l,c,h,d,p,u,g,_,m){ne.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],t!==void 0&&this.set(t,e,n,s,r,a,o,l,c,h,d,p,u,g,_,m)}set(t,e,n,s,r,a,o,l,c,h,d,p,u,g,_,m){const f=this.elements;return f[0]=t,f[4]=e,f[8]=n,f[12]=s,f[1]=r,f[5]=a,f[9]=o,f[13]=l,f[2]=c,f[6]=h,f[10]=d,f[14]=p,f[3]=u,f[7]=g,f[11]=_,f[15]=m,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return new ne().fromArray(this.elements)}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],e[9]=n[9],e[10]=n[10],e[11]=n[11],e[12]=n[12],e[13]=n[13],e[14]=n[14],e[15]=n[15],this}copyPosition(t){const e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,n){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,n=t.elements,s=1/yi.setFromMatrixColumn(t,0).length(),r=1/yi.setFromMatrixColumn(t,1).length(),a=1/yi.setFromMatrixColumn(t,2).length();return e[0]=n[0]*s,e[1]=n[1]*s,e[2]=n[2]*s,e[3]=0,e[4]=n[4]*r,e[5]=n[5]*r,e[6]=n[6]*r,e[7]=0,e[8]=n[8]*a,e[9]=n[9]*a,e[10]=n[10]*a,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){const e=this.elements,n=t.x,s=t.y,r=t.z,a=Math.cos(n),o=Math.sin(n),l=Math.cos(s),c=Math.sin(s),h=Math.cos(r),d=Math.sin(r);if(t.order==="XYZ"){const p=a*h,u=a*d,g=o*h,_=o*d;e[0]=l*h,e[4]=-l*d,e[8]=c,e[1]=u+g*c,e[5]=p-_*c,e[9]=-o*l,e[2]=_-p*c,e[6]=g+u*c,e[10]=a*l}else if(t.order==="YXZ"){const p=l*h,u=l*d,g=c*h,_=c*d;e[0]=p+_*o,e[4]=g*o-u,e[8]=a*c,e[1]=a*d,e[5]=a*h,e[9]=-o,e[2]=u*o-g,e[6]=_+p*o,e[10]=a*l}else if(t.order==="ZXY"){const p=l*h,u=l*d,g=c*h,_=c*d;e[0]=p-_*o,e[4]=-a*d,e[8]=g+u*o,e[1]=u+g*o,e[5]=a*h,e[9]=_-p*o,e[2]=-a*c,e[6]=o,e[10]=a*l}else if(t.order==="ZYX"){const p=a*h,u=a*d,g=o*h,_=o*d;e[0]=l*h,e[4]=g*c-u,e[8]=p*c+_,e[1]=l*d,e[5]=_*c+p,e[9]=u*c-g,e[2]=-c,e[6]=o*l,e[10]=a*l}else if(t.order==="YZX"){const p=a*l,u=a*c,g=o*l,_=o*c;e[0]=l*h,e[4]=_-p*d,e[8]=g*d+u,e[1]=d,e[5]=a*h,e[9]=-o*h,e[2]=-c*h,e[6]=u*d+g,e[10]=p-_*d}else if(t.order==="XZY"){const p=a*l,u=a*c,g=o*l,_=o*c;e[0]=l*h,e[4]=-d,e[8]=c*h,e[1]=p*d+_,e[5]=a*h,e[9]=u*d-g,e[2]=g*d-u,e[6]=o*h,e[10]=_*d+p}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(jh,t,Zh)}lookAt(t,e,n){const s=this.elements;return je.subVectors(t,e),je.lengthSq()===0&&(je.z=1),je.normalize(),Fn.crossVectors(n,je),Fn.lengthSq()===0&&(Math.abs(n.z)===1?je.x+=1e-4:je.z+=1e-4,je.normalize(),Fn.crossVectors(n,je)),Fn.normalize(),ks.crossVectors(je,Fn),s[0]=Fn.x,s[4]=ks.x,s[8]=je.x,s[1]=Fn.y,s[5]=ks.y,s[9]=je.y,s[2]=Fn.z,s[6]=ks.z,s[10]=je.z,this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,s=e.elements,r=this.elements,a=n[0],o=n[4],l=n[8],c=n[12],h=n[1],d=n[5],p=n[9],u=n[13],g=n[2],_=n[6],m=n[10],f=n[14],T=n[3],E=n[7],y=n[11],D=n[15],w=s[0],C=s[4],I=s[8],S=s[12],M=s[1],A=s[5],W=s[9],k=s[13],q=s[2],Q=s[6],X=s[10],tt=s[14],H=s[3],st=s[7],gt=s[11],Et=s[15];return r[0]=a*w+o*M+l*q+c*H,r[4]=a*C+o*A+l*Q+c*st,r[8]=a*I+o*W+l*X+c*gt,r[12]=a*S+o*k+l*tt+c*Et,r[1]=h*w+d*M+p*q+u*H,r[5]=h*C+d*A+p*Q+u*st,r[9]=h*I+d*W+p*X+u*gt,r[13]=h*S+d*k+p*tt+u*Et,r[2]=g*w+_*M+m*q+f*H,r[6]=g*C+_*A+m*Q+f*st,r[10]=g*I+_*W+m*X+f*gt,r[14]=g*S+_*k+m*tt+f*Et,r[3]=T*w+E*M+y*q+D*H,r[7]=T*C+E*A+y*Q+D*st,r[11]=T*I+E*W+y*X+D*gt,r[15]=T*S+E*k+y*tt+D*Et,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[4],s=t[8],r=t[12],a=t[1],o=t[5],l=t[9],c=t[13],h=t[2],d=t[6],p=t[10],u=t[14],g=t[3],_=t[7],m=t[11],f=t[15];return g*(+r*l*d-s*c*d-r*o*p+n*c*p+s*o*u-n*l*u)+_*(+e*l*u-e*c*p+r*a*p-s*a*u+s*c*h-r*l*h)+m*(+e*c*d-e*o*u-r*a*d+n*a*u+r*o*h-n*c*h)+f*(-s*o*h-e*l*d+e*o*p+s*a*d-n*a*p+n*l*h)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,n){const s=this.elements;return t.isVector3?(s[12]=t.x,s[13]=t.y,s[14]=t.z):(s[12]=t,s[13]=e,s[14]=n),this}invert(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8],d=t[9],p=t[10],u=t[11],g=t[12],_=t[13],m=t[14],f=t[15],T=d*m*c-_*p*c+_*l*u-o*m*u-d*l*f+o*p*f,E=g*p*c-h*m*c-g*l*u+a*m*u+h*l*f-a*p*f,y=h*_*c-g*d*c+g*o*u-a*_*u-h*o*f+a*d*f,D=g*d*l-h*_*l-g*o*p+a*_*p+h*o*m-a*d*m,w=e*T+n*E+s*y+r*D;if(w===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const C=1/w;return t[0]=T*C,t[1]=(_*p*r-d*m*r-_*s*u+n*m*u+d*s*f-n*p*f)*C,t[2]=(o*m*r-_*l*r+_*s*c-n*m*c-o*s*f+n*l*f)*C,t[3]=(d*l*r-o*p*r-d*s*c+n*p*c+o*s*u-n*l*u)*C,t[4]=E*C,t[5]=(h*m*r-g*p*r+g*s*u-e*m*u-h*s*f+e*p*f)*C,t[6]=(g*l*r-a*m*r-g*s*c+e*m*c+a*s*f-e*l*f)*C,t[7]=(a*p*r-h*l*r+h*s*c-e*p*c-a*s*u+e*l*u)*C,t[8]=y*C,t[9]=(g*d*r-h*_*r-g*n*u+e*_*u+h*n*f-e*d*f)*C,t[10]=(a*_*r-g*o*r+g*n*c-e*_*c-a*n*f+e*o*f)*C,t[11]=(h*o*r-a*d*r-h*n*c+e*d*c+a*n*u-e*o*u)*C,t[12]=D*C,t[13]=(h*_*s-g*d*s+g*n*p-e*_*p-h*n*m+e*d*m)*C,t[14]=(g*o*s-a*_*s-g*n*l+e*_*l+a*n*m-e*o*m)*C,t[15]=(a*d*s-h*o*s+h*n*l-e*d*l-a*n*p+e*o*p)*C,this}scale(t){const e=this.elements,n=t.x,s=t.y,r=t.z;return e[0]*=n,e[4]*=s,e[8]*=r,e[1]*=n,e[5]*=s,e[9]*=r,e[2]*=n,e[6]*=s,e[10]*=r,e[3]*=n,e[7]*=s,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],s=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,n,s))}makeTranslation(t,e,n){return t.isVector3?this.set(1,0,0,t.x,0,1,0,t.y,0,0,1,t.z,0,0,0,1):this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const n=Math.cos(e),s=Math.sin(e),r=1-n,a=t.x,o=t.y,l=t.z,c=r*a,h=r*o;return this.set(c*a+n,c*o-s*l,c*l+s*o,0,c*o+s*l,h*o+n,h*l-s*a,0,c*l-s*o,h*l+s*a,r*l*l+n,0,0,0,0,1),this}makeScale(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this}makeShear(t,e,n,s,r,a){return this.set(1,n,r,0,t,1,a,0,e,s,1,0,0,0,0,1),this}compose(t,e,n){const s=this.elements,r=e._x,a=e._y,o=e._z,l=e._w,c=r+r,h=a+a,d=o+o,p=r*c,u=r*h,g=r*d,_=a*h,m=a*d,f=o*d,T=l*c,E=l*h,y=l*d,D=n.x,w=n.y,C=n.z;return s[0]=(1-(_+f))*D,s[1]=(u+y)*D,s[2]=(g-E)*D,s[3]=0,s[4]=(u-y)*w,s[5]=(1-(p+f))*w,s[6]=(m+T)*w,s[7]=0,s[8]=(g+E)*C,s[9]=(m-T)*C,s[10]=(1-(p+_))*C,s[11]=0,s[12]=t.x,s[13]=t.y,s[14]=t.z,s[15]=1,this}decompose(t,e,n){const s=this.elements;let r=yi.set(s[0],s[1],s[2]).length();const a=yi.set(s[4],s[5],s[6]).length(),o=yi.set(s[8],s[9],s[10]).length();this.determinant()<0&&(r=-r),t.x=s[12],t.y=s[13],t.z=s[14],hn.copy(this);const c=1/r,h=1/a,d=1/o;return hn.elements[0]*=c,hn.elements[1]*=c,hn.elements[2]*=c,hn.elements[4]*=h,hn.elements[5]*=h,hn.elements[6]*=h,hn.elements[8]*=d,hn.elements[9]*=d,hn.elements[10]*=d,e.setFromRotationMatrix(hn),n.x=r,n.y=a,n.z=o,this}makePerspective(t,e,n,s,r,a,o=Rn){const l=this.elements,c=2*r/(e-t),h=2*r/(n-s),d=(e+t)/(e-t),p=(n+s)/(n-s);let u,g;if(o===Rn)u=-(a+r)/(a-r),g=-2*a*r/(a-r);else if(o===Er)u=-a/(a-r),g=-a*r/(a-r);else throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+o);return l[0]=c,l[4]=0,l[8]=d,l[12]=0,l[1]=0,l[5]=h,l[9]=p,l[13]=0,l[2]=0,l[6]=0,l[10]=u,l[14]=g,l[3]=0,l[7]=0,l[11]=-1,l[15]=0,this}makeOrthographic(t,e,n,s,r,a,o=Rn){const l=this.elements,c=1/(e-t),h=1/(n-s),d=1/(a-r),p=(e+t)*c,u=(n+s)*h;let g,_;if(o===Rn)g=(a+r)*d,_=-2*d;else if(o===Er)g=r*d,_=-1*d;else throw new Error("THREE.Matrix4.makeOrthographic(): Invalid coordinate system: "+o);return l[0]=2*c,l[4]=0,l[8]=0,l[12]=-p,l[1]=0,l[5]=2*h,l[9]=0,l[13]=-u,l[2]=0,l[6]=0,l[10]=_,l[14]=-g,l[3]=0,l[7]=0,l[11]=0,l[15]=1,this}equals(t){const e=this.elements,n=t.elements;for(let s=0;s<16;s++)if(e[s]!==n[s])return!1;return!0}fromArray(t,e=0){for(let n=0;n<16;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t}}const yi=new P,hn=new ne,jh=new P(0,0,0),Zh=new P(1,1,1),Fn=new P,ks=new P,je=new P,Ho=new ne,Vo=new ci;class Mn{constructor(t=0,e=0,n=0,s=Mn.DEFAULT_ORDER){this.isEuler=!0,this._x=t,this._y=e,this._z=n,this._order=s}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,n,s=this._order){return this._x=t,this._y=e,this._z=n,this._order=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e=this._order,n=!0){const s=t.elements,r=s[0],a=s[4],o=s[8],l=s[1],c=s[5],h=s[9],d=s[2],p=s[6],u=s[10];switch(e){case"XYZ":this._y=Math.asin(Yt(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-h,u),this._z=Math.atan2(-a,r)):(this._x=Math.atan2(p,c),this._z=0);break;case"YXZ":this._x=Math.asin(-Yt(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(o,u),this._z=Math.atan2(l,c)):(this._y=Math.atan2(-d,r),this._z=0);break;case"ZXY":this._x=Math.asin(Yt(p,-1,1)),Math.abs(p)<.9999999?(this._y=Math.atan2(-d,u),this._z=Math.atan2(-a,c)):(this._y=0,this._z=Math.atan2(l,r));break;case"ZYX":this._y=Math.asin(-Yt(d,-1,1)),Math.abs(d)<.9999999?(this._x=Math.atan2(p,u),this._z=Math.atan2(l,r)):(this._x=0,this._z=Math.atan2(-a,c));break;case"YZX":this._z=Math.asin(Yt(l,-1,1)),Math.abs(l)<.9999999?(this._x=Math.atan2(-h,c),this._y=Math.atan2(-d,r)):(this._x=0,this._y=Math.atan2(o,u));break;case"XZY":this._z=Math.asin(-Yt(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(p,c),this._y=Math.atan2(o,r)):(this._x=Math.atan2(-h,u),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,n===!0&&this._onChangeCallback(),this}setFromQuaternion(t,e,n){return Ho.makeRotationFromQuaternion(t),this.setFromRotationMatrix(Ho,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return Vo.setFromEuler(this),this.setFromQuaternion(Vo,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],t[3]!==void 0&&(this._order=t[3]),this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}}Mn.DEFAULT_ORDER="XYZ";class wo{constructor(){this.mask=1}set(t){this.mask=(1<>>0}enable(t){this.mask|=1<1){for(let e=0;e1){for(let n=0;n0&&(s.userData=this.userData),s.layers=this.layers.mask,s.matrix=this.matrix.toArray(),s.up=this.up.toArray(),this.matrixAutoUpdate===!1&&(s.matrixAutoUpdate=!1),this.isInstancedMesh&&(s.type="InstancedMesh",s.count=this.count,s.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(s.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(s.type="BatchedMesh",s.perObjectFrustumCulled=this.perObjectFrustumCulled,s.sortObjects=this.sortObjects,s.drawRanges=this._drawRanges,s.reservedRanges=this._reservedRanges,s.visibility=this._visibility,s.active=this._active,s.bounds=this._bounds.map(o=>({boxInitialized:o.boxInitialized,boxMin:o.box.min.toArray(),boxMax:o.box.max.toArray(),sphereInitialized:o.sphereInitialized,sphereRadius:o.sphere.radius,sphereCenter:o.sphere.center.toArray()})),s.maxInstanceCount=this._maxInstanceCount,s.maxVertexCount=this._maxVertexCount,s.maxIndexCount=this._maxIndexCount,s.geometryInitialized=this._geometryInitialized,s.geometryCount=this._geometryCount,s.matricesTexture=this._matricesTexture.toJSON(t),this._colorsTexture!==null&&(s.colorsTexture=this._colorsTexture.toJSON(t)),this.boundingSphere!==null&&(s.boundingSphere={center:s.boundingSphere.center.toArray(),radius:s.boundingSphere.radius}),this.boundingBox!==null&&(s.boundingBox={min:s.boundingBox.min.toArray(),max:s.boundingBox.max.toArray()}));function r(o,l){return o[l.uuid]===void 0&&(o[l.uuid]=l.toJSON(t)),l.uuid}if(this.isScene)this.background&&(this.background.isColor?s.background=this.background.toJSON():this.background.isTexture&&(s.background=this.background.toJSON(t).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(s.environment=this.environment.toJSON(t).uuid);else if(this.isMesh||this.isLine||this.isPoints){s.geometry=r(t.geometries,this.geometry);const o=this.geometry.parameters;if(o!==void 0&&o.shapes!==void 0){const l=o.shapes;if(Array.isArray(l))for(let c=0,h=l.length;c0){s.children=[];for(let o=0;o0){s.animations=[];for(let o=0;o0&&(n.geometries=o),l.length>0&&(n.materials=l),c.length>0&&(n.textures=c),h.length>0&&(n.images=h),d.length>0&&(n.shapes=d),p.length>0&&(n.skeletons=p),u.length>0&&(n.animations=u),g.length>0&&(n.nodes=g)}return n.object=s,n;function a(o){const l=[];for(const c in o){const h=o[c];delete h.metadata,l.push(h)}return l}}clone(t){return new this.constructor().copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldAutoUpdate=t.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.animations=t.animations.slice(),this.userData=JSON.parse(JSON.stringify(t.userData)),e===!0)for(let n=0;n0?s.multiplyScalar(1/Math.sqrt(r)):s.set(0,0,0)}static getBarycoord(t,e,n,s,r){un.subVectors(s,e),bn.subVectors(n,e),Zr.subVectors(t,e);const a=un.dot(un),o=un.dot(bn),l=un.dot(Zr),c=bn.dot(bn),h=bn.dot(Zr),d=a*c-o*o;if(d===0)return r.set(0,0,0),null;const p=1/d,u=(c*l-o*h)*p,g=(a*h-o*l)*p;return r.set(1-u-g,g,u)}static containsPoint(t,e,n,s){return this.getBarycoord(t,e,n,s,Tn)===null?!1:Tn.x>=0&&Tn.y>=0&&Tn.x+Tn.y<=1}static getInterpolation(t,e,n,s,r,a,o,l){return this.getBarycoord(t,e,n,s,Tn)===null?(l.x=0,l.y=0,"z"in l&&(l.z=0),"w"in l&&(l.w=0),null):(l.setScalar(0),l.addScaledVector(r,Tn.x),l.addScaledVector(a,Tn.y),l.addScaledVector(o,Tn.z),l)}static getInterpolatedAttribute(t,e,n,s,r,a){return Qr.setScalar(0),ta.setScalar(0),ea.setScalar(0),Qr.fromBufferAttribute(t,e),ta.fromBufferAttribute(t,n),ea.fromBufferAttribute(t,s),a.setScalar(0),a.addScaledVector(Qr,r.x),a.addScaledVector(ta,r.y),a.addScaledVector(ea,r.z),a}static isFrontFacing(t,e,n,s){return un.subVectors(n,e),bn.subVectors(t,e),un.cross(bn).dot(s)<0}set(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this}setFromPointsAndIndices(t,e,n,s){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[s]),this}setFromAttributeAndIndices(t,e,n,s){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,n),this.c.fromBufferAttribute(t,s),this}clone(){return new this.constructor().copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return un.subVectors(this.c,this.b),bn.subVectors(this.a,this.b),un.cross(bn).length()*.5}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return on.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return on.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,s,r){return on.getInterpolation(t,this.a,this.b,this.c,e,n,s,r)}containsPoint(t){return on.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return on.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const n=this.a,s=this.b,r=this.c;let a,o;Ti.subVectors(s,n),wi.subVectors(r,n),Kr.subVectors(t,n);const l=Ti.dot(Kr),c=wi.dot(Kr);if(l<=0&&c<=0)return e.copy(n);$r.subVectors(t,s);const h=Ti.dot($r),d=wi.dot($r);if(h>=0&&d<=h)return e.copy(s);const p=l*d-h*c;if(p<=0&&l>=0&&h<=0)return a=l/(l-h),e.copy(n).addScaledVector(Ti,a);Jr.subVectors(t,r);const u=Ti.dot(Jr),g=wi.dot(Jr);if(g>=0&&u<=g)return e.copy(r);const _=u*c-l*g;if(_<=0&&c>=0&&g<=0)return o=c/(c-g),e.copy(n).addScaledVector(wi,o);const m=h*g-u*d;if(m<=0&&d-h>=0&&u-g>=0)return jo.subVectors(r,s),o=(d-h)/(d-h+(u-g)),e.copy(s).addScaledVector(jo,o);const f=1/(m+_+p);return a=_*f,o=p*f,e.copy(n).addScaledVector(Ti,a).addScaledVector(wi,o)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}const _c={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},On={h:0,s:0,l:0},Vs={h:0,s:0,l:0};function na(i,t,e){return e<0&&(e+=1),e>1&&(e-=1),e<1/6?i+(t-i)*6*e:e<1/2?t:e<2/3?i+(t-i)*6*(2/3-e):i}class ot{constructor(t,e,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(t,e,n)}set(t,e,n){if(e===void 0&&n===void 0){const s=t;s&&s.isColor?this.copy(s):typeof s=="number"?this.setHex(s):typeof s=="string"&&this.setStyle(s)}else this.setRGB(t,e,n);return this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e=an){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(t&255)/255,Qt.toWorkingColorSpace(this,e),this}setRGB(t,e,n,s=Qt.workingColorSpace){return this.r=t,this.g=e,this.b=n,Qt.toWorkingColorSpace(this,s),this}setHSL(t,e,n,s=Qt.workingColorSpace){if(t=Nh(t,1),e=Yt(e,0,1),n=Yt(n,0,1),e===0)this.r=this.g=this.b=n;else{const r=n<=.5?n*(1+e):n+e-n*e,a=2*n-r;this.r=na(a,r,t+1/3),this.g=na(a,r,t),this.b=na(a,r,t-1/3)}return Qt.toWorkingColorSpace(this,s),this}setStyle(t,e=an){function n(r){r!==void 0&&parseFloat(r)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let s;if(s=/^(\w+)\(([^\)]*)\)/.exec(t)){let r;const a=s[1],o=s[2];switch(a){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,e);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,e);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,e);break;default:console.warn("THREE.Color: Unknown color model "+t)}}else if(s=/^\#([A-Fa-f\d]+)$/.exec(t)){const r=s[1],a=r.length;if(a===3)return this.setRGB(parseInt(r.charAt(0),16)/15,parseInt(r.charAt(1),16)/15,parseInt(r.charAt(2),16)/15,e);if(a===6)return this.setHex(parseInt(r,16),e);console.warn("THREE.Color: Invalid hex color "+t)}else if(t&&t.length>0)return this.setColorName(t,e);return this}setColorName(t,e=an){const n=_c[t.toLowerCase()];return n!==void 0?this.setHex(n,e):console.warn("THREE.Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copySRGBToLinear(t){return this.r=Dn(t.r),this.g=Dn(t.g),this.b=Dn(t.b),this}copyLinearToSRGB(t){return this.r=Wi(t.r),this.g=Wi(t.g),this.b=Wi(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=an){return Qt.fromWorkingColorSpace(Ne.copy(this),t),Math.round(Yt(Ne.r*255,0,255))*65536+Math.round(Yt(Ne.g*255,0,255))*256+Math.round(Yt(Ne.b*255,0,255))}getHexString(t=an){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Qt.workingColorSpace){Qt.fromWorkingColorSpace(Ne.copy(this),e);const n=Ne.r,s=Ne.g,r=Ne.b,a=Math.max(n,s,r),o=Math.min(n,s,r);let l,c;const h=(o+a)/2;if(o===a)l=0,c=0;else{const d=a-o;switch(c=h<=.5?d/(a+o):d/(2-a-o),a){case n:l=(s-r)/d+(s0!=t>0&&this.version++,this._alphaTest=t}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(t!==void 0)for(const e in t){const n=t[e];if(n===void 0){console.warn(`THREE.Material: parameter '${e}' has value of undefined.`);continue}const s=this[e];if(s===void 0){console.warn(`THREE.Material: '${e}' is not a property of THREE.${this.type}.`);continue}s&&s.isColor?s.set(n):s&&s.isVector3&&n&&n.isVector3?s.copy(n):this[e]=n}}toJSON(t){const e=t===void 0||typeof t=="string";e&&(t={textures:{},images:{}});const n={metadata:{version:4.6,type:"Material",generator:"Material.toJSON"}};n.uuid=this.uuid,n.type=this.type,this.name!==""&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity!==void 0&&this.emissiveIntensity!==1&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.dispersion!==void 0&&(n.dispersion=this.dispersion),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(t).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(t).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(t).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(t).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(t).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(t).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapRotation!==void 0&&(n.envMapRotation=this.envMapRotation.toArray()),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(t).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(t).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(t).uuid),this.attenuationDistance!==void 0&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.shadowSide!==null&&(n.shadowSide=this.shadowSide),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),this.blending!==Vi&&(n.blending=this.blending),this.side!==Yn&&(n.side=this.side),this.vertexColors===!0&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),this.transparent===!0&&(n.transparent=!0),this.blendSrc!==ya&&(n.blendSrc=this.blendSrc),this.blendDst!==Ea&&(n.blendDst=this.blendDst),this.blendEquation!==ii&&(n.blendEquation=this.blendEquation),this.blendSrcAlpha!==null&&(n.blendSrcAlpha=this.blendSrcAlpha),this.blendDstAlpha!==null&&(n.blendDstAlpha=this.blendDstAlpha),this.blendEquationAlpha!==null&&(n.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(n.blendColor=this.blendColor.getHex()),this.blendAlpha!==0&&(n.blendAlpha=this.blendAlpha),this.depthFunc!==qi&&(n.depthFunc=this.depthFunc),this.depthTest===!1&&(n.depthTest=this.depthTest),this.depthWrite===!1&&(n.depthWrite=this.depthWrite),this.colorWrite===!1&&(n.colorWrite=this.colorWrite),this.stencilWriteMask!==255&&(n.stencilWriteMask=this.stencilWriteMask),this.stencilFunc!==No&&(n.stencilFunc=this.stencilFunc),this.stencilRef!==0&&(n.stencilRef=this.stencilRef),this.stencilFuncMask!==255&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==_i&&(n.stencilFail=this.stencilFail),this.stencilZFail!==_i&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==_i&&(n.stencilZPass=this.stencilZPass),this.stencilWrite===!0&&(n.stencilWrite=this.stencilWrite),this.rotation!==void 0&&this.rotation!==0&&(n.rotation=this.rotation),this.polygonOffset===!0&&(n.polygonOffset=!0),this.polygonOffsetFactor!==0&&(n.polygonOffsetFactor=this.polygonOffsetFactor),this.polygonOffsetUnits!==0&&(n.polygonOffsetUnits=this.polygonOffsetUnits),this.linewidth!==void 0&&this.linewidth!==1&&(n.linewidth=this.linewidth),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.dithering===!0&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),this.alphaHash===!0&&(n.alphaHash=!0),this.alphaToCoverage===!0&&(n.alphaToCoverage=!0),this.premultipliedAlpha===!0&&(n.premultipliedAlpha=!0),this.forceSinglePass===!0&&(n.forceSinglePass=!0),this.wireframe===!0&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!=="round"&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!=="round"&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading===!0&&(n.flatShading=!0),this.visible===!1&&(n.visible=!1),this.toneMapped===!1&&(n.toneMapped=!1),this.fog===!1&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData);function s(r){const a=[];for(const o in r){const l=r[o];delete l.metadata,a.push(l)}return a}if(e){const r=s(t.textures),a=s(t.images);r.length>0&&(n.textures=r),a.length>0&&(n.images=a)}return n}clone(){return new this.constructor().copy(this)}copy(t){this.name=t.name,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.blendColor.copy(t.blendColor),this.blendAlpha=t.blendAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let n=null;if(e!==null){const s=e.length;n=new Array(s);for(let r=0;r!==s;++r)n[r]=e[r].clone()}return this.clippingPlanes=n,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaHash=t.alphaHash,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.forceSinglePass=t.forceSinglePass,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(t){t===!0&&this.version++}onBuild(){console.warn("Material: onBuild() has been removed.")}}class Ss extends qn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new ot(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new Mn,this.combine=tc,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}const Me=new P,Gs=new St;class he{constructor(t,e,n=!1){if(Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=t,this.itemSize=e,this.count=t!==void 0?t.length/e:0,this.normalized=n,this.usage=oo,this.updateRanges=[],this.gpuType=xn,this.version=0}onUploadCallback(){}set needsUpdate(t){t===!0&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.name=t.name,this.array=new t.array.constructor(t.array),this.itemSize=t.itemSize,this.count=t.count,this.normalized=t.normalized,this.usage=t.usage,this.gpuType=t.gpuType,this}copyAt(t,e,n){t*=this.itemSize,n*=e.itemSize;for(let s=0,r=this.itemSize;se.count&&console.warn("THREE.BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),e.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new ui);const t=this.attributes.position,e=this.morphAttributes.position;if(t&&t.isGLBufferAttribute){console.error("THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),this.boundingBox.set(new P(-1/0,-1/0,-1/0),new P(1/0,1/0,1/0));return}if(t!==void 0){if(this.boundingBox.setFromBufferAttribute(t),e)for(let n=0,s=e.length;n0&&(t.userData=this.userData),this.parameters!==void 0){const l=this.parameters;for(const c in l)l[c]!==void 0&&(t[c]=l[c]);return t}t.data={attributes:{}};const e=this.index;e!==null&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const n=this.attributes;for(const l in n){const c=n[l];t.data.attributes[l]=c.toJSON(t.data)}const s={};let r=!1;for(const l in this.morphAttributes){const c=this.morphAttributes[l],h=[];for(let d=0,p=c.length;d0&&(s[l]=h,r=!0)}r&&(t.data.morphAttributes=s,t.data.morphTargetsRelative=this.morphTargetsRelative);const a=this.groups;a.length>0&&(t.data.groups=JSON.parse(JSON.stringify(a)));const o=this.boundingSphere;return o!==null&&(t.data.boundingSphere={center:o.center.toArray(),radius:o.radius}),t}clone(){return new this.constructor().copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const n=t.index;n!==null&&this.setIndex(n.clone(e));const s=t.attributes;for(const c in s){const h=s[c];this.setAttribute(c,h.clone(e))}const r=t.morphAttributes;for(const c in r){const h=[],d=r[c];for(let p=0,u=d.length;p0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;r(t.far-t.near)**2))&&(Zo.copy(r).invert(),$n.copy(t.ray).applyMatrix4(Zo),!(n.boundingBox!==null&&$n.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(t,e,$n)))}_computeIntersections(t,e,n){let s;const r=this.geometry,a=this.material,o=r.index,l=r.attributes.position,c=r.attributes.uv,h=r.attributes.uv1,d=r.attributes.normal,p=r.groups,u=r.drawRange;if(o!==null)if(Array.isArray(a))for(let g=0,_=p.length;g<_;g++){const m=p[g],f=a[m.materialIndex],T=Math.max(m.start,u.start),E=Math.min(o.count,Math.min(m.start+m.count,u.start+u.count));for(let y=T,D=E;ye.far?null:{distance:c,point:Zs.clone(),object:i}}function Ks(i,t,e,n,s,r,a,o,l,c){i.getVertexPosition(o,Xs),i.getVertexPosition(l,Ys),i.getVertexPosition(c,qs);const h=nu(i,t,e,n,Xs,Ys,qs,$o);if(h){const d=new P;on.getBarycoord($o,Xs,Ys,qs,d),s&&(h.uv=on.getInterpolatedAttribute(s,o,l,c,d,new St)),r&&(h.uv1=on.getInterpolatedAttribute(r,o,l,c,d,new St)),a&&(h.normal=on.getInterpolatedAttribute(a,o,l,c,d,new P),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));const p={a:o,b:l,c,normal:new P,materialIndex:0};on.getNormal(Xs,Ys,qs,p.normal),h.face=p,h.barycoord=d}return h}class Ts extends ge{constructor(t=1,e=1,n=1,s=1,r=1,a=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:s,heightSegments:r,depthSegments:a};const o=this;s=Math.floor(s),r=Math.floor(r),a=Math.floor(a);const l=[],c=[],h=[],d=[];let p=0,u=0;g("z","y","x",-1,-1,n,e,t,a,r,0),g("z","y","x",1,-1,n,e,-t,a,r,1),g("x","z","y",1,1,t,n,e,s,a,2),g("x","z","y",1,-1,t,n,-e,s,a,3),g("x","y","z",1,-1,t,e,n,s,r,4),g("x","y","z",-1,-1,t,e,-n,s,r,5),this.setIndex(l),this.setAttribute("position",new Oe(c,3)),this.setAttribute("normal",new Oe(h,3)),this.setAttribute("uv",new Oe(d,2));function g(_,m,f,T,E,y,D,w,C,I,S){const M=y/C,A=D/I,W=y/2,k=D/2,q=w/2,Q=C+1,X=I+1;let tt=0,H=0;const st=new P;for(let gt=0;gt0?1:-1,h.push(st.x,st.y,st.z),d.push(Ft/C),d.push(1-gt/I),tt+=1}}for(let gt=0;gt0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader,e.lights=this.lights,e.clipping=this.clipping;const n={};for(const s in this.extensions)this.extensions[s]===!0&&(n[s]=!0);return Object.keys(n).length>0&&(e.extensions=n),e}}class Sc extends Ue{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new ne,this.projectionMatrix=new ne,this.projectionMatrixInverse=new ne,this.coordinateSystem=Rn}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this.coordinateSystem=t.coordinateSystem,this}getWorldDirection(t){return super.getWorldDirection(t).negate()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return new this.constructor().copy(this)}}const Bn=new P,Jo=new St,Qo=new St;class $e extends Sc{constructor(t=50,e=1,n=.1,s=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=n,this.far=s,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=t.view===null?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=lo*2*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(gr*.5*this.fov);return .5*this.getFilmHeight()/t}getEffectiveFOV(){return lo*2*Math.atan(Math.tan(gr*.5*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}getViewBounds(t,e,n){Bn.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(Bn.x,Bn.y).multiplyScalar(-t/Bn.z),Bn.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(Bn.x,Bn.y).multiplyScalar(-t/Bn.z)}getViewSize(t,e){return this.getViewBounds(t,Jo,Qo),e.subVectors(Qo,Jo)}setViewOffset(t,e,n,s,r,a){this.aspect=t/e,this.view===null&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=s,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){this.view!==null&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(gr*.5*this.fov)/this.zoom,n=2*e,s=this.aspect*n,r=-.5*s;const a=this.view;if(this.view!==null&&this.view.enabled){const l=a.fullWidth,c=a.fullHeight;r+=a.offsetX*s/l,e-=a.offsetY*n/c,s*=a.width/l,n*=a.height/c}const o=this.filmOffset;o!==0&&(r+=t*o/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+s,e,e-n,t,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,this.view!==null&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}const Ri=-90,Ci=1;class au extends Ue{constructor(t,e,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const s=new $e(Ri,Ci,t,e);s.layers=this.layers,this.add(s);const r=new $e(Ri,Ci,t,e);r.layers=this.layers,this.add(r);const a=new $e(Ri,Ci,t,e);a.layers=this.layers,this.add(a);const o=new $e(Ri,Ci,t,e);o.layers=this.layers,this.add(o);const l=new $e(Ri,Ci,t,e);l.layers=this.layers,this.add(l);const c=new $e(Ri,Ci,t,e);c.layers=this.layers,this.add(c)}updateCoordinateSystem(){const t=this.coordinateSystem,e=this.children.concat(),[n,s,r,a,o,l]=e;for(const c of e)this.remove(c);if(t===Rn)n.up.set(0,1,0),n.lookAt(1,0,0),s.up.set(0,1,0),s.lookAt(-1,0,0),r.up.set(0,0,-1),r.lookAt(0,1,0),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),l.up.set(0,1,0),l.lookAt(0,0,-1);else if(t===Er)n.up.set(0,-1,0),n.lookAt(-1,0,0),s.up.set(0,-1,0),s.lookAt(1,0,0),r.up.set(0,0,1),r.lookAt(0,1,0),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),l.up.set(0,-1,0),l.lookAt(0,0,-1);else throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+t);for(const c of e)this.add(c),c.updateMatrixWorld()}update(t,e){this.parent===null&&this.updateMatrixWorld();const{renderTarget:n,activeMipmapLevel:s}=this;this.coordinateSystem!==t.coordinateSystem&&(this.coordinateSystem=t.coordinateSystem,this.updateCoordinateSystem());const[r,a,o,l,c,h]=this.children,d=t.getRenderTarget(),p=t.getActiveCubeFace(),u=t.getActiveMipmapLevel(),g=t.xr.enabled;t.xr.enabled=!1;const _=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,t.setRenderTarget(n,0,s),t.render(e,r),t.setRenderTarget(n,1,s),t.render(e,a),t.setRenderTarget(n,2,s),t.render(e,o),t.setRenderTarget(n,3,s),t.render(e,l),t.setRenderTarget(n,4,s),t.render(e,c),n.texture.generateMipmaps=_,t.setRenderTarget(n,5,s),t.render(e,h),t.setRenderTarget(d,p,u),t.xr.enabled=g,n.texture.needsPMREMUpdate=!0}}class yc extends Pe{constructor(t,e,n,s,r,a,o,l,c,h){t=t!==void 0?t:[],e=e!==void 0?e:ji,super(t,e,n,s,r,a,o,l,c,h),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class ou extends fn{constructor(t=1,e={}){super(t,t,e),this.isWebGLCubeRenderTarget=!0;const n={width:t,height:t,depth:1},s=[n,n,n,n,n,n];this.texture=new yc(s,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=e.generateMipmaps!==void 0?e.generateMipmaps:!1,this.texture.minFilter=e.minFilter!==void 0?e.minFilter:vn}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.colorSpace=e.colorSpace,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:` + + varying vec3 vWorldDirection; + + vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); + + } + + void main() { + + vWorldDirection = transformDirection( position, modelMatrix ); + + #include + #include + + } + `,fragmentShader:` + + uniform sampler2D tEquirect; + + varying vec3 vWorldDirection; + + #include + + void main() { + + vec3 direction = normalize( vWorldDirection ); + + vec2 sampleUV = equirectUv( direction ); + + gl_FragColor = texture2D( tEquirect, sampleUV ); + + } + `},s=new Ts(5,5,5),r=new He({name:"CubemapFromEquirect",uniforms:Qi(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:Xe,blending:Cn});r.uniforms.tEquirect.value=e;const a=new be(s,r),o=e.minFilter;return e.minFilter===ai&&(e.minFilter=vn),new au(1,10,this).update(t,a),e.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(t,e,n,s){const r=t.getRenderTarget();for(let a=0;a<6;a++)t.setRenderTarget(this,a),t.clear(e,n,s);t.setRenderTarget(r)}}class Dr{constructor(t,e=25e-5){this.isFogExp2=!0,this.name="",this.color=new ot(t),this.density=e}clone(){return new Dr(this.color,this.density)}toJSON(){return{type:"FogExp2",name:this.name,color:this.color.getHex(),density:this.density}}}class lu extends Ue{constructor(){super(),this.isScene=!0,this.type="Scene",this.background=null,this.environment=null,this.fog=null,this.backgroundBlurriness=0,this.backgroundIntensity=1,this.backgroundRotation=new Mn,this.environmentIntensity=1,this.environmentRotation=new Mn,this.overrideMaterial=null,typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(t,e){return super.copy(t,e),t.background!==null&&(this.background=t.background.clone()),t.environment!==null&&(this.environment=t.environment.clone()),t.fog!==null&&(this.fog=t.fog.clone()),this.backgroundBlurriness=t.backgroundBlurriness,this.backgroundIntensity=t.backgroundIntensity,this.backgroundRotation.copy(t.backgroundRotation),this.environmentIntensity=t.environmentIntensity,this.environmentRotation.copy(t.environmentRotation),t.overrideMaterial!==null&&(this.overrideMaterial=t.overrideMaterial.clone()),this.matrixAutoUpdate=t.matrixAutoUpdate,this}toJSON(t){const e=super.toJSON(t);return this.fog!==null&&(e.object.fog=this.fog.toJSON()),this.backgroundBlurriness>0&&(e.object.backgroundBlurriness=this.backgroundBlurriness),this.backgroundIntensity!==1&&(e.object.backgroundIntensity=this.backgroundIntensity),e.object.backgroundRotation=this.backgroundRotation.toArray(),this.environmentIntensity!==1&&(e.object.environmentIntensity=this.environmentIntensity),e.object.environmentRotation=this.environmentRotation.toArray(),e}}class cu{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=t!==void 0?t.length/e:0,this.usage=oo,this.updateRanges=[],this.version=0,this.uuid=Xn()}onUploadCallback(){}set needsUpdate(t){t===!0&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.array=new t.array.constructor(t.array),this.count=t.count,this.stride=t.stride,this.usage=t.usage,this}copyAt(t,e,n){t*=this.stride,n*=e.stride;for(let s=0,r=this.stride;st.far||e.push({distance:l,point:ls.clone(),uv:on.getInterpolation(ls,$s,hs,Js,tl,ra,el,new St),face:null,object:this})}copy(t,e){return super.copy(t,e),t.center!==void 0&&this.center.copy(t.center),this.material=t.material,this}}function Qs(i,t,e,n,s,r){Ui.subVectors(i,e).addScalar(.5).multiply(n),s!==void 0?(cs.x=r*Ui.x-s*Ui.y,cs.y=s*Ui.x+r*Ui.y):cs.copy(Ui),i.copy(t),i.x+=cs.x,i.y+=cs.y,i.applyMatrix4(Ec)}class hu extends Pe{constructor(t=null,e=1,n=1,s,r,a,o,l,c=Je,h=Je,d,p){super(null,a,o,l,c,h,s,r,d,p),this.isDataTexture=!0,this.image={data:t,width:e,height:n},this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}}class nl extends he{constructor(t,e,n,s=1){super(t,e,n),this.isInstancedBufferAttribute=!0,this.meshPerAttribute=s}copy(t){return super.copy(t),this.meshPerAttribute=t.meshPerAttribute,this}toJSON(){const t=super.toJSON();return t.meshPerAttribute=this.meshPerAttribute,t.isInstancedBufferAttribute=!0,t}}const Ii=new ne,il=new ne,tr=[],sl=new ui,uu=new ne,us=new be,ds=new di;class du extends be{constructor(t,e,n){super(t,e),this.isInstancedMesh=!0,this.instanceMatrix=new nl(new Float32Array(n*16),16),this.instanceColor=null,this.morphTexture=null,this.count=n,this.boundingBox=null,this.boundingSphere=null;for(let s=0;s1?null:e.copy(t.start).addScaledVector(n,r)}intersectsLine(t){const e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return e<0&&n>0||n<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const n=e||pu.getNormalMatrix(t),s=this.coplanarPoint(aa).applyMatrix4(t),r=this.normal.applyMatrix3(n).normalize();return this.constant=-s.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return new this.constructor().copy(this)}}const Jn=new di,er=new P;class Ao{constructor(t=new Hn,e=new Hn,n=new Hn,s=new Hn,r=new Hn,a=new Hn){this.planes=[t,e,n,s,r,a]}set(t,e,n,s,r,a){const o=this.planes;return o[0].copy(t),o[1].copy(e),o[2].copy(n),o[3].copy(s),o[4].copy(r),o[5].copy(a),this}copy(t){const e=this.planes;for(let n=0;n<6;n++)e[n].copy(t.planes[n]);return this}setFromProjectionMatrix(t,e=Rn){const n=this.planes,s=t.elements,r=s[0],a=s[1],o=s[2],l=s[3],c=s[4],h=s[5],d=s[6],p=s[7],u=s[8],g=s[9],_=s[10],m=s[11],f=s[12],T=s[13],E=s[14],y=s[15];if(n[0].setComponents(l-r,p-c,m-u,y-f).normalize(),n[1].setComponents(l+r,p+c,m+u,y+f).normalize(),n[2].setComponents(l+a,p+h,m+g,y+T).normalize(),n[3].setComponents(l-a,p-h,m-g,y-T).normalize(),n[4].setComponents(l-o,p-d,m-_,y-E).normalize(),e===Rn)n[5].setComponents(l+o,p+d,m+_,y+E).normalize();else if(e===Er)n[5].setComponents(o,d,_,E).normalize();else throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+e);return this}intersectsObject(t){if(t.boundingSphere!==void 0)t.boundingSphere===null&&t.computeBoundingSphere(),Jn.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;e.boundingSphere===null&&e.computeBoundingSphere(),Jn.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere(Jn)}intersectsSprite(t){return Jn.center.set(0,0,0),Jn.radius=.7071067811865476,Jn.applyMatrix4(t.matrixWorld),this.intersectsSphere(Jn)}intersectsSphere(t){const e=this.planes,n=t.center,s=-t.radius;for(let r=0;r<6;r++)if(e[r].distanceToPoint(n)0?t.max.x:t.min.x,er.y=s.normal.y>0?t.max.y:t.min.y,er.z=s.normal.z>0?t.max.z:t.min.z,s.distanceToPoint(er)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let n=0;n<6;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}}class Ar extends qn{constructor(t){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new ot(16777215),this.map=null,this.linewidth=1,this.linecap="round",this.linejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.linewidth=t.linewidth,this.linecap=t.linecap,this.linejoin=t.linejoin,this.fog=t.fog,this}}const Rr=new P,Cr=new P,rl=new ne,fs=new bs,nr=new di,oa=new P,al=new P;class co extends Ue{constructor(t=new ge,e=new Ar){super(),this.isLine=!0,this.type="Line",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}computeLineDistances(){const t=this.geometry;if(t.index===null){const e=t.attributes.position,n=[0];for(let s=1,r=e.count;s0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;rn)return;oa.applyMatrix4(i.matrixWorld);const l=t.ray.origin.distanceTo(oa);if(!(lt.far))return{distance:l,point:al.clone().applyMatrix4(i.matrixWorld),index:s,face:null,faceIndex:null,barycoord:null,object:i}}class oi extends qn{constructor(t){super(),this.isPointsMaterial=!0,this.type="PointsMaterial",this.color=new ot(16777215),this.map=null,this.alphaMap=null,this.size=1,this.sizeAttenuation=!0,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.alphaMap=t.alphaMap,this.size=t.size,this.sizeAttenuation=t.sizeAttenuation,this.fog=t.fog,this}}const ol=new ne,ho=new bs,sr=new di,rr=new P;class Yi extends Ue{constructor(t=new ge,e=new oi){super(),this.isPoints=!0,this.type="Points",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}raycast(t,e){const n=this.geometry,s=this.matrixWorld,r=t.params.Points.threshold,a=n.drawRange;if(n.boundingSphere===null&&n.computeBoundingSphere(),sr.copy(n.boundingSphere),sr.applyMatrix4(s),sr.radius+=r,t.ray.intersectsSphere(sr)===!1)return;ol.copy(s).invert(),ho.copy(t.ray).applyMatrix4(ol);const o=r/((this.scale.x+this.scale.y+this.scale.z)/3),l=o*o,c=n.index,d=n.attributes.position;if(c!==null){const p=Math.max(0,a.start),u=Math.min(c.count,a.start+a.count);for(let g=p,_=u;g<_;g++){const m=c.getX(g);rr.fromBufferAttribute(d,m),ll(rr,m,l,s,t,e,this)}}else{const p=Math.max(0,a.start),u=Math.min(d.count,a.start+a.count);for(let g=p,_=u;g<_;g++)rr.fromBufferAttribute(d,g),ll(rr,g,l,s,t,e,this)}}updateMorphTargets(){const e=this.geometry.morphAttributes,n=Object.keys(e);if(n.length>0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;rs.far)return;r.push({distance:c,distanceToRay:Math.sqrt(o),point:l,index:t,face:null,faceIndex:null,barycoord:null,object:a})}}class zi extends Ue{constructor(){super(),this.isGroup=!0,this.type="Group"}}class bc extends Pe{constructor(t,e,n,s,r,a,o,l,c){super(t,e,n,s,r,a,o,l,c),this.isCanvasTexture=!0,this.needsUpdate=!0}}class Tc extends Pe{constructor(t,e,n,s,r,a,o,l,c,h=Gi){if(h!==Gi&&h!==$i)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");n===void 0&&h===Gi&&(n=li),n===void 0&&h===$i&&(n=Ki),super(null,s,r,a,o,l,h,n,c),this.isDepthTexture=!0,this.image={width:t,height:e},this.magFilter=o!==void 0?o:Je,this.minFilter=l!==void 0?l:Je,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(t){return super.copy(t),this.compareFunction=t.compareFunction,this}toJSON(t){const e=super.toJSON(t);return this.compareFunction!==null&&(e.compareFunction=this.compareFunction),e}}class ws extends ge{constructor(t=1,e=1,n=1,s=1){super(),this.type="PlaneGeometry",this.parameters={width:t,height:e,widthSegments:n,heightSegments:s};const r=t/2,a=e/2,o=Math.floor(n),l=Math.floor(s),c=o+1,h=l+1,d=t/o,p=e/l,u=[],g=[],_=[],m=[];for(let f=0;f0)&&u.push(E,y,w),(f!==n-1||lu.start-g.start);let p=0;for(let u=1;u 0 + vec4 plane; + #ifdef ALPHA_TO_COVERAGE + float distanceToPlane, distanceGradient; + float clipOpacity = 1.0; + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; + distanceGradient = fwidth( distanceToPlane ) / 2.0; + clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); + if ( clipOpacity == 0.0 ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + float unionClipOpacity = 1.0; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; + distanceGradient = fwidth( distanceToPlane ) / 2.0; + unionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); + } + #pragma unroll_loop_end + clipOpacity *= 1.0 - unionClipOpacity; + #endif + diffuseColor.a *= clipOpacity; + if ( diffuseColor.a == 0.0 ) discard; + #else + #pragma unroll_loop_start + for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard; + } + #pragma unroll_loop_end + #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES + bool clipped = true; + #pragma unroll_loop_start + for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { + plane = clippingPlanes[ i ]; + clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped; + } + #pragma unroll_loop_end + if ( clipped ) discard; + #endif + #endif +#endif`,Gu=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; + uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; +#endif`,Wu=`#if NUM_CLIPPING_PLANES > 0 + varying vec3 vClipPosition; +#endif`,Xu=`#if NUM_CLIPPING_PLANES > 0 + vClipPosition = - mvPosition.xyz; +#endif`,Yu=`#if defined( USE_COLOR_ALPHA ) + diffuseColor *= vColor; +#elif defined( USE_COLOR ) + diffuseColor.rgb *= vColor; +#endif`,qu=`#if defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#elif defined( USE_COLOR ) + varying vec3 vColor; +#endif`,ju=`#if defined( USE_COLOR_ALPHA ) + varying vec4 vColor; +#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) + varying vec3 vColor; +#endif`,Zu=`#if defined( USE_COLOR_ALPHA ) + vColor = vec4( 1.0 ); +#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) + vColor = vec3( 1.0 ); +#endif +#ifdef USE_COLOR + vColor *= color; +#endif +#ifdef USE_INSTANCING_COLOR + vColor.xyz *= instanceColor.xyz; +#endif +#ifdef USE_BATCHING_COLOR + vec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) ); + vColor.xyz *= batchingColor.xyz; +#endif`,Ku=`#define PI 3.141592653589793 +#define PI2 6.283185307179586 +#define PI_HALF 1.5707963267948966 +#define RECIPROCAL_PI 0.3183098861837907 +#define RECIPROCAL_PI2 0.15915494309189535 +#define EPSILON 1e-6 +#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +#define whiteComplement( a ) ( 1.0 - saturate( a ) ) +float pow2( const in float x ) { return x*x; } +vec3 pow2( const in vec3 x ) { return x*x; } +float pow3( const in float x ) { return x*x*x; } +float pow4( const in float x ) { float x2 = x*x; return x2*x2; } +float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); } +float average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); } +highp float rand( const in vec2 uv ) { + const highp float a = 12.9898, b = 78.233, c = 43758.5453; + highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); + return fract( sin( sn ) * c ); +} +#ifdef HIGH_PRECISION + float precisionSafeLength( vec3 v ) { return length( v ); } +#else + float precisionSafeLength( vec3 v ) { + float maxComponent = max3( abs( v ) ); + return length( v / maxComponent ) * maxComponent; + } +#endif +struct IncidentLight { + vec3 color; + vec3 direction; + bool visible; +}; +struct ReflectedLight { + vec3 directDiffuse; + vec3 directSpecular; + vec3 indirectDiffuse; + vec3 indirectSpecular; +}; +#ifdef USE_ALPHAHASH + varying vec3 vPosition; +#endif +vec3 transformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); +} +vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { + return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); +} +mat3 transposeMat3( const in mat3 m ) { + mat3 tmp; + tmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ); + tmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ); + tmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ); + return tmp; +} +bool isPerspectiveMatrix( mat4 m ) { + return m[ 2 ][ 3 ] == - 1.0; +} +vec2 equirectUv( in vec3 dir ) { + float u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5; + float v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; + return vec2( u, v ); +} +vec3 BRDF_Lambert( const in vec3 diffuseColor ) { + return RECIPROCAL_PI * diffuseColor; +} +vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} +float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { + float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); + return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); +} // validated`,$u=`#ifdef ENVMAP_TYPE_CUBE_UV + #define cubeUV_minMipLevel 4.0 + #define cubeUV_minTileSize 16.0 + float getFace( vec3 direction ) { + vec3 absDirection = abs( direction ); + float face = - 1.0; + if ( absDirection.x > absDirection.z ) { + if ( absDirection.x > absDirection.y ) + face = direction.x > 0.0 ? 0.0 : 3.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } else { + if ( absDirection.z > absDirection.y ) + face = direction.z > 0.0 ? 2.0 : 5.0; + else + face = direction.y > 0.0 ? 1.0 : 4.0; + } + return face; + } + vec2 getUV( vec3 direction, float face ) { + vec2 uv; + if ( face == 0.0 ) { + uv = vec2( direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 1.0 ) { + uv = vec2( - direction.x, - direction.z ) / abs( direction.y ); + } else if ( face == 2.0 ) { + uv = vec2( - direction.x, direction.y ) / abs( direction.z ); + } else if ( face == 3.0 ) { + uv = vec2( - direction.z, direction.y ) / abs( direction.x ); + } else if ( face == 4.0 ) { + uv = vec2( - direction.x, direction.z ) / abs( direction.y ); + } else { + uv = vec2( direction.x, direction.y ) / abs( direction.z ); + } + return 0.5 * ( uv + 1.0 ); + } + vec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) { + float face = getFace( direction ); + float filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 ); + mipInt = max( mipInt, cubeUV_minMipLevel ); + float faceSize = exp2( mipInt ); + highp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0; + if ( face > 2.0 ) { + uv.y += faceSize; + face -= 3.0; + } + uv.x += face * faceSize; + uv.x += filterInt * 3.0 * cubeUV_minTileSize; + uv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize ); + uv.x *= CUBEUV_TEXEL_WIDTH; + uv.y *= CUBEUV_TEXEL_HEIGHT; + #ifdef texture2DGradEXT + return texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb; + #else + return texture2D( envMap, uv ).rgb; + #endif + } + #define cubeUV_r0 1.0 + #define cubeUV_m0 - 2.0 + #define cubeUV_r1 0.8 + #define cubeUV_m1 - 1.0 + #define cubeUV_r4 0.4 + #define cubeUV_m4 2.0 + #define cubeUV_r5 0.305 + #define cubeUV_m5 3.0 + #define cubeUV_r6 0.21 + #define cubeUV_m6 4.0 + float roughnessToMip( float roughness ) { + float mip = 0.0; + if ( roughness >= cubeUV_r1 ) { + mip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0; + } else if ( roughness >= cubeUV_r4 ) { + mip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1; + } else if ( roughness >= cubeUV_r5 ) { + mip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4; + } else if ( roughness >= cubeUV_r6 ) { + mip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5; + } else { + mip = - 2.0 * log2( 1.16 * roughness ); } + return mip; + } + vec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) { + float mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP ); + float mipF = fract( mip ); + float mipInt = floor( mip ); + vec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt ); + if ( mipF == 0.0 ) { + return vec4( color0, 1.0 ); + } else { + vec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 ); + return vec4( mix( color0, color1, mipF ), 1.0 ); + } + } +#endif`,Ju=`vec3 transformedNormal = objectNormal; +#ifdef USE_TANGENT + vec3 transformedTangent = objectTangent; +#endif +#ifdef USE_BATCHING + mat3 bm = mat3( batchingMatrix ); + transformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) ); + transformedNormal = bm * transformedNormal; + #ifdef USE_TANGENT + transformedTangent = bm * transformedTangent; + #endif +#endif +#ifdef USE_INSTANCING + mat3 im = mat3( instanceMatrix ); + transformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) ); + transformedNormal = im * transformedNormal; + #ifdef USE_TANGENT + transformedTangent = im * transformedTangent; + #endif +#endif +transformedNormal = normalMatrix * transformedNormal; +#ifdef FLIP_SIDED + transformedNormal = - transformedNormal; +#endif +#ifdef USE_TANGENT + transformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz; + #ifdef FLIP_SIDED + transformedTangent = - transformedTangent; + #endif +#endif`,Qu=`#ifdef USE_DISPLACEMENTMAP + uniform sampler2D displacementMap; + uniform float displacementScale; + uniform float displacementBias; +#endif`,td=`#ifdef USE_DISPLACEMENTMAP + transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); +#endif`,ed=`#ifdef USE_EMISSIVEMAP + vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); + #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE + emissiveColor = sRGBTransferEOTF( emissiveColor ); + #endif + totalEmissiveRadiance *= emissiveColor.rgb; +#endif`,nd=`#ifdef USE_EMISSIVEMAP + uniform sampler2D emissiveMap; +#endif`,id="gl_FragColor = linearToOutputTexel( gl_FragColor );",sd=`vec4 LinearTransferOETF( in vec4 value ) { + return value; +} +vec4 sRGBTransferEOTF( in vec4 value ) { + return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a ); +} +vec4 sRGBTransferOETF( in vec4 value ) { + return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); +}`,rd=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vec3 cameraToFrag; + if ( isOrthographic ) { + cameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToFrag = normalize( vWorldPosition - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vec3 reflectVec = reflect( cameraToFrag, worldNormal ); + #else + vec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio ); + #endif + #else + vec3 reflectVec = vReflect; + #endif + #ifdef ENVMAP_TYPE_CUBE + vec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) ); + #else + vec4 envColor = vec4( 0.0 ); + #endif + #ifdef ENVMAP_BLENDING_MULTIPLY + outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_MIX ) + outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity ); + #elif defined( ENVMAP_BLENDING_ADD ) + outgoingLight += envColor.xyz * specularStrength * reflectivity; + #endif +#endif`,ad=`#ifdef USE_ENVMAP + uniform float envMapIntensity; + uniform float flipEnvMap; + uniform mat3 envMapRotation; + #ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; + #else + uniform sampler2D envMap; + #endif + +#endif`,od=`#ifdef USE_ENVMAP + uniform float reflectivity; + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + varying vec3 vWorldPosition; + uniform float refractionRatio; + #else + varying vec3 vReflect; + #endif +#endif`,ld=`#ifdef USE_ENVMAP + #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) + #define ENV_WORLDPOS + #endif + #ifdef ENV_WORLDPOS + + varying vec3 vWorldPosition; + #else + varying vec3 vReflect; + uniform float refractionRatio; + #endif +#endif`,cd=`#ifdef USE_ENVMAP + #ifdef ENV_WORLDPOS + vWorldPosition = worldPosition.xyz; + #else + vec3 cameraToVertex; + if ( isOrthographic ) { + cameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); + } else { + cameraToVertex = normalize( worldPosition.xyz - cameraPosition ); + } + vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + #ifdef ENVMAP_MODE_REFLECTION + vReflect = reflect( cameraToVertex, worldNormal ); + #else + vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); + #endif + #endif +#endif`,hd=`#ifdef USE_FOG + vFogDepth = - mvPosition.z; +#endif`,ud=`#ifdef USE_FOG + varying float vFogDepth; +#endif`,dd=`#ifdef USE_FOG + #ifdef FOG_EXP2 + float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth ); + #else + float fogFactor = smoothstep( fogNear, fogFar, vFogDepth ); + #endif + gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor ); +#endif`,fd=`#ifdef USE_FOG + uniform vec3 fogColor; + varying float vFogDepth; + #ifdef FOG_EXP2 + uniform float fogDensity; + #else + uniform float fogNear; + uniform float fogFar; + #endif +#endif`,pd=`#ifdef USE_GRADIENTMAP + uniform sampler2D gradientMap; +#endif +vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { + float dotNL = dot( normal, lightDirection ); + vec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 ); + #ifdef USE_GRADIENTMAP + return vec3( texture2D( gradientMap, coord ).r ); + #else + vec2 fw = fwidth( coord ) * 0.5; + return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) ); + #endif +}`,md=`#ifdef USE_LIGHTMAP + uniform sampler2D lightMap; + uniform float lightMapIntensity; +#endif`,gd=`LambertMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularStrength = specularStrength;`,_d=`varying vec3 vViewPosition; +struct LambertMaterial { + vec3 diffuseColor; + float specularStrength; +}; +void RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Lambert +#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,vd=`uniform bool receiveShadow; +uniform vec3 ambientLightColor; +#if defined( USE_LIGHT_PROBES ) + uniform vec3 lightProbe[ 9 ]; +#endif +vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) { + float x = normal.x, y = normal.y, z = normal.z; + vec3 result = shCoefficients[ 0 ] * 0.886227; + result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y; + result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z; + result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x; + result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y; + result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z; + result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 ); + result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z; + result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y ); + return result; +} +vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) { + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe ); + return irradiance; +} +vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) { + vec3 irradiance = ambientLightColor; + return irradiance; +} +float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) { + float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 ); + if ( cutoffDistance > 0.0 ) { + distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ); + } + return distanceFalloff; +} +float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) { + return smoothstep( coneCosine, penumbraCosine, angleCosine ); +} +#if NUM_DIR_LIGHTS > 0 + struct DirectionalLight { + vec3 direction; + vec3 color; + }; + uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; + void getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) { + light.color = directionalLight.color; + light.direction = directionalLight.direction; + light.visible = true; + } +#endif +#if NUM_POINT_LIGHTS > 0 + struct PointLight { + vec3 position; + vec3 color; + float distance; + float decay; + }; + uniform PointLight pointLights[ NUM_POINT_LIGHTS ]; + void getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = pointLight.position - geometryPosition; + light.direction = normalize( lVector ); + float lightDistance = length( lVector ); + light.color = pointLight.color; + light.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } +#endif +#if NUM_SPOT_LIGHTS > 0 + struct SpotLight { + vec3 position; + vec3 direction; + vec3 color; + float distance; + float decay; + float coneCos; + float penumbraCos; + }; + uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ]; + void getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) { + vec3 lVector = spotLight.position - geometryPosition; + light.direction = normalize( lVector ); + float angleCos = dot( light.direction, spotLight.direction ); + float spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos ); + if ( spotAttenuation > 0.0 ) { + float lightDistance = length( lVector ); + light.color = spotLight.color * spotAttenuation; + light.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay ); + light.visible = ( light.color != vec3( 0.0 ) ); + } else { + light.color = vec3( 0.0 ); + light.visible = false; + } + } +#endif +#if NUM_RECT_AREA_LIGHTS > 0 + struct RectAreaLight { + vec3 color; + vec3 position; + vec3 halfWidth; + vec3 halfHeight; + }; + uniform sampler2D ltc_1; uniform sampler2D ltc_2; + uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ]; +#endif +#if NUM_HEMI_LIGHTS > 0 + struct HemisphereLight { + vec3 direction; + vec3 skyColor; + vec3 groundColor; + }; + uniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ]; + vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) { + float dotNL = dot( normal, hemiLight.direction ); + float hemiDiffuseWeight = 0.5 * dotNL + 0.5; + vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); + return irradiance; + } +#endif`,xd=`#ifdef USE_ENVMAP + vec3 getIBLIrradiance( const in vec3 normal ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 ); + return PI * envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + vec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 reflectVec = reflect( - viewDir, normal ); + reflectVec = normalize( mix( reflectVec, normal, roughness * roughness) ); + reflectVec = inverseTransformDirection( reflectVec, viewMatrix ); + vec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness ); + return envMapColor.rgb * envMapIntensity; + #else + return vec3( 0.0 ); + #endif + } + #ifdef USE_ANISOTROPY + vec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) { + #ifdef ENVMAP_TYPE_CUBE_UV + vec3 bentNormal = cross( bitangent, viewDir ); + bentNormal = normalize( cross( bentNormal, bitangent ) ); + bentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) ); + return getIBLRadiance( viewDir, bentNormal, roughness ); + #else + return vec3( 0.0 ); + #endif + } + #endif +#endif`,Md=`ToonMaterial material; +material.diffuseColor = diffuseColor.rgb;`,Sd=`varying vec3 vViewPosition; +struct ToonMaterial { + vec3 diffuseColor; +}; +void RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + vec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_Toon +#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,yd=`BlinnPhongMaterial material; +material.diffuseColor = diffuseColor.rgb; +material.specularColor = specular; +material.specularShininess = shininess; +material.specularStrength = specularStrength;`,Ed=`varying vec3 vViewPosition; +struct BlinnPhongMaterial { + vec3 diffuseColor; + vec3 specularColor; + float specularShininess; + float specularStrength; +}; +void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); + reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength; +} +void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +#define RE_Direct RE_Direct_BlinnPhong +#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,bd=`PhysicalMaterial material; +material.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor ); +vec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) ); +float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z ); +material.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness; +material.roughness = min( material.roughness, 1.0 ); +#ifdef IOR + material.ior = ior; + #ifdef USE_SPECULAR + float specularIntensityFactor = specularIntensity; + vec3 specularColorFactor = specularColor; + #ifdef USE_SPECULAR_COLORMAP + specularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + specularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a; + #endif + material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor ); + #else + float specularIntensityFactor = 1.0; + vec3 specularColorFactor = vec3( 1.0 ); + material.specularF90 = 1.0; + #endif + material.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor ); +#else + material.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor ); + material.specularF90 = 1.0; +#endif +#ifdef USE_CLEARCOAT + material.clearcoat = clearcoat; + material.clearcoatRoughness = clearcoatRoughness; + material.clearcoatF0 = vec3( 0.04 ); + material.clearcoatF90 = 1.0; + #ifdef USE_CLEARCOATMAP + material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x; + #endif + #ifdef USE_CLEARCOAT_ROUGHNESSMAP + material.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y; + #endif + material.clearcoat = saturate( material.clearcoat ); material.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 ); + material.clearcoatRoughness += geometryRoughness; + material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 ); +#endif +#ifdef USE_DISPERSION + material.dispersion = dispersion; +#endif +#ifdef USE_IRIDESCENCE + material.iridescence = iridescence; + material.iridescenceIOR = iridescenceIOR; + #ifdef USE_IRIDESCENCEMAP + material.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r; + #endif + #ifdef USE_IRIDESCENCE_THICKNESSMAP + material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum; + #else + material.iridescenceThickness = iridescenceThicknessMaximum; + #endif +#endif +#ifdef USE_SHEEN + material.sheenColor = sheenColor; + #ifdef USE_SHEEN_COLORMAP + material.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb; + #endif + material.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 ); + #ifdef USE_SHEEN_ROUGHNESSMAP + material.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a; + #endif +#endif +#ifdef USE_ANISOTROPY + #ifdef USE_ANISOTROPYMAP + mat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x ); + vec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb; + vec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b; + #else + vec2 anisotropyV = anisotropyVector; + #endif + material.anisotropy = length( anisotropyV ); + if( material.anisotropy == 0.0 ) { + anisotropyV = vec2( 1.0, 0.0 ); + } else { + anisotropyV /= material.anisotropy; + material.anisotropy = saturate( material.anisotropy ); + } + material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) ); + material.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y; + material.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y; +#endif`,Td=`struct PhysicalMaterial { + vec3 diffuseColor; + float roughness; + vec3 specularColor; + float specularF90; + float dispersion; + #ifdef USE_CLEARCOAT + float clearcoat; + float clearcoatRoughness; + vec3 clearcoatF0; + float clearcoatF90; + #endif + #ifdef USE_IRIDESCENCE + float iridescence; + float iridescenceIOR; + float iridescenceThickness; + vec3 iridescenceFresnel; + vec3 iridescenceF0; + #endif + #ifdef USE_SHEEN + vec3 sheenColor; + float sheenRoughness; + #endif + #ifdef IOR + float ior; + #endif + #ifdef USE_TRANSMISSION + float transmission; + float transmissionAlpha; + float thickness; + float attenuationDistance; + vec3 attenuationColor; + #endif + #ifdef USE_ANISOTROPY + float anisotropy; + float alphaT; + vec3 anisotropyT; + vec3 anisotropyB; + #endif +}; +vec3 clearcoatSpecularDirect = vec3( 0.0 ); +vec3 clearcoatSpecularIndirect = vec3( 0.0 ); +vec3 sheenSpecularDirect = vec3( 0.0 ); +vec3 sheenSpecularIndirect = vec3(0.0 ); +vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { + float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); + float x2 = x * x; + float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); + return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); +} +float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { + float a2 = pow2( alpha ); + float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); + float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); + return 0.5 / max( gv + gl, EPSILON ); +} +float D_GGX( const in float alpha, const in float dotNH ) { + float a2 = pow2( alpha ); + float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; + return RECIPROCAL_PI * a2 / pow2( denom ); +} +#ifdef USE_ANISOTROPY + float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { + float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); + float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); + float v = 0.5 / ( gv + gl ); + return saturate(v); + } + float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { + float a2 = alphaT * alphaB; + highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); + highp float v2 = dot( v, v ); + float w2 = a2 / v2; + return RECIPROCAL_PI * a2 * pow2 ( w2 ); + } +#endif +#ifdef USE_CLEARCOAT + vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { + vec3 f0 = material.clearcoatF0; + float f90 = material.clearcoatF90; + float roughness = material.clearcoatRoughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + return F * ( V * D ); + } +#endif +vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { + vec3 f0 = material.specularColor; + float f90 = material.specularF90; + float roughness = material.roughness; + float alpha = pow2( roughness ); + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float dotVH = saturate( dot( viewDir, halfDir ) ); + vec3 F = F_Schlick( f0, f90, dotVH ); + #ifdef USE_IRIDESCENCE + F = mix( F, material.iridescenceFresnel, material.iridescence ); + #endif + #ifdef USE_ANISOTROPY + float dotTL = dot( material.anisotropyT, lightDir ); + float dotTV = dot( material.anisotropyT, viewDir ); + float dotTH = dot( material.anisotropyT, halfDir ); + float dotBL = dot( material.anisotropyB, lightDir ); + float dotBV = dot( material.anisotropyB, viewDir ); + float dotBH = dot( material.anisotropyB, halfDir ); + float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); + float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); + #else + float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); + float D = D_GGX( alpha, dotNH ); + #endif + return F * ( V * D ); +} +vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { + const float LUT_SIZE = 64.0; + const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; + const float LUT_BIAS = 0.5 / LUT_SIZE; + float dotNV = saturate( dot( N, V ) ); + vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); + uv = uv * LUT_SCALE + LUT_BIAS; + return uv; +} +float LTC_ClippedSphereFormFactor( const in vec3 f ) { + float l = length( f ); + return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); +} +vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { + float x = dot( v1, v2 ); + float y = abs( x ); + float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; + float b = 3.4175940 + ( 4.1616724 + y ) * y; + float v = a / b; + float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; + return cross( v1, v2 ) * theta_sintheta; +} +vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { + vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; + vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; + vec3 lightNormal = cross( v1, v2 ); + if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); + vec3 T1, T2; + T1 = normalize( V - N * dot( V, N ) ); + T2 = - cross( N, T1 ); + mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); + vec3 coords[ 4 ]; + coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); + coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); + coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); + coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); + coords[ 0 ] = normalize( coords[ 0 ] ); + coords[ 1 ] = normalize( coords[ 1 ] ); + coords[ 2 ] = normalize( coords[ 2 ] ); + coords[ 3 ] = normalize( coords[ 3 ] ); + vec3 vectorFormFactor = vec3( 0.0 ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); + vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); + float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); + return vec3( result ); +} +#if defined( USE_SHEEN ) +float D_Charlie( float roughness, float dotNH ) { + float alpha = pow2( roughness ); + float invAlpha = 1.0 / alpha; + float cos2h = dotNH * dotNH; + float sin2h = max( 1.0 - cos2h, 0.0078125 ); + return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); +} +float V_Neubelt( float dotNV, float dotNL ) { + return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); +} +vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { + vec3 halfDir = normalize( lightDir + viewDir ); + float dotNL = saturate( dot( normal, lightDir ) ); + float dotNV = saturate( dot( normal, viewDir ) ); + float dotNH = saturate( dot( normal, halfDir ) ); + float D = D_Charlie( sheenRoughness, dotNH ); + float V = V_Neubelt( dotNV, dotNL ); + return sheenColor * ( D * V ); +} +#endif +float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + float r2 = roughness * roughness; + float a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95; + float b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72; + float DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) ); + return saturate( DG * RECIPROCAL_PI ); +} +vec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { + float dotNV = saturate( dot( normal, viewDir ) ); + const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 ); + const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 ); + vec4 r = roughness * c0 + c1; + float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y; + vec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw; + return fab; +} +vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { + vec2 fab = DFGApprox( normal, viewDir, roughness ); + return specularColor * fab.x + specularF90 * fab.y; +} +#ifdef USE_IRIDESCENCE +void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#else +void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { +#endif + vec2 fab = DFGApprox( normal, viewDir, roughness ); + #ifdef USE_IRIDESCENCE + vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); + #else + vec3 Fr = specularColor; + #endif + vec3 FssEss = Fr * fab.x + specularF90 * fab.y; + float Ess = fab.x + fab.y; + float Ems = 1.0 - Ess; + vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); + singleScatter += FssEss; + multiScatter += Fms * Ems; +} +#if NUM_RECT_AREA_LIGHTS > 0 + void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + vec3 normal = geometryNormal; + vec3 viewDir = geometryViewDir; + vec3 position = geometryPosition; + vec3 lightPos = rectAreaLight.position; + vec3 halfWidth = rectAreaLight.halfWidth; + vec3 halfHeight = rectAreaLight.halfHeight; + vec3 lightColor = rectAreaLight.color; + float roughness = material.roughness; + vec3 rectCoords[ 4 ]; + rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; + rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; + rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; + vec2 uv = LTC_Uv( normal, viewDir, roughness ); + vec4 t1 = texture2D( ltc_1, uv ); + vec4 t2 = texture2D( ltc_2, uv ); + mat3 mInv = mat3( + vec3( t1.x, 0, t1.y ), + vec3( 0, 1, 0 ), + vec3( t1.z, 0, t1.w ) + ); + vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y ); + reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); + reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); + } +#endif +void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); + vec3 irradiance = dotNL * directLight.color; + #ifdef USE_CLEARCOAT + float dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) ); + vec3 ccIrradiance = dotNLcc * directLight.color; + clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material ); + #endif + #ifdef USE_SHEEN + sheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); + #endif + reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material ); + reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { + reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); +} +void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { + #ifdef USE_CLEARCOAT + clearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); + #endif + #ifdef USE_SHEEN + sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); + #endif + vec3 singleScattering = vec3( 0.0 ); + vec3 multiScattering = vec3( 0.0 ); + vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; + #ifdef USE_IRIDESCENCE + computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering ); + #else + computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering ); + #endif + vec3 totalScattering = singleScattering + multiScattering; + vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); + reflectedLight.indirectSpecular += radiance * singleScattering; + reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; + reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; +} +#define RE_Direct RE_Direct_Physical +#define RE_Direct_RectArea RE_Direct_RectArea_Physical +#define RE_IndirectDiffuse RE_IndirectDiffuse_Physical +#define RE_IndirectSpecular RE_IndirectSpecular_Physical +float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { + return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); +}`,wd=` +vec3 geometryPosition = - vViewPosition; +vec3 geometryNormal = normal; +vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); +vec3 geometryClearcoatNormal = vec3( 0.0 ); +#ifdef USE_CLEARCOAT + geometryClearcoatNormal = clearcoatNormal; +#endif +#ifdef USE_IRIDESCENCE + float dotNVi = saturate( dot( normal, geometryViewDir ) ); + if ( material.iridescenceThickness == 0.0 ) { + material.iridescence = 0.0; + } else { + material.iridescence = saturate( material.iridescence ); + } + if ( material.iridescence > 0.0 ) { + material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor ); + material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi ); + } +#endif +IncidentLight directLight; +#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) + PointLight pointLight; + #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { + pointLight = pointLights[ i ]; + getPointLightInfo( pointLight, geometryPosition, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) + pointLightShadow = pointLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) + SpotLight spotLight; + vec4 spotColor; + vec3 spotLightCoord; + bool inSpotLightMap; + #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { + spotLight = spotLights[ i ]; + getSpotLightInfo( spotLight, geometryPosition, directLight ); + #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX + #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS + #else + #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) + #endif + #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS ) + spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w; + inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) ); + spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy ); + directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color; + #endif + #undef SPOT_LIGHT_MAP_INDEX + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + spotLightShadow = spotLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) + DirectionalLight directionalLight; + #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLightShadow; + #endif + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { + directionalLight = directionalLights[ i ]; + getDirectionalLightInfo( directionalLight, directLight ); + #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) + directionalLightShadow = directionalLightShadows[ i ]; + directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + #endif + RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) + RectAreaLight rectAreaLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { + rectAreaLight = rectAreaLights[ i ]; + RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); + } + #pragma unroll_loop_end +#endif +#if defined( RE_IndirectDiffuse ) + vec3 iblIrradiance = vec3( 0.0 ); + vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); + #if defined( USE_LIGHT_PROBES ) + irradiance += getLightProbeIrradiance( lightProbe, geometryNormal ); + #endif + #if ( NUM_HEMI_LIGHTS > 0 ) + #pragma unroll_loop_start + for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { + irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal ); + } + #pragma unroll_loop_end + #endif +#endif +#if defined( RE_IndirectSpecular ) + vec3 radiance = vec3( 0.0 ); + vec3 clearcoatRadiance = vec3( 0.0 ); +#endif`,Ad=`#if defined( RE_IndirectDiffuse ) + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; + irradiance += lightMapIrradiance; + #endif + #if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV ) + iblIrradiance += getIBLIrradiance( geometryNormal ); + #endif +#endif +#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) + #ifdef USE_ANISOTROPY + radiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy ); + #else + radiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness ); + #endif + #ifdef USE_CLEARCOAT + clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); + #endif +#endif`,Rd=`#if defined( RE_IndirectDiffuse ) + RE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif +#if defined( RE_IndirectSpecular ) + RE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); +#endif`,Cd=`#if defined( USE_LOGDEPTHBUF ) + gl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; +#endif`,Pd=`#if defined( USE_LOGDEPTHBUF ) + uniform float logDepthBufFC; + varying float vFragDepth; + varying float vIsPerspective; +#endif`,Dd=`#ifdef USE_LOGDEPTHBUF + varying float vFragDepth; + varying float vIsPerspective; +#endif`,Ld=`#ifdef USE_LOGDEPTHBUF + vFragDepth = 1.0 + gl_Position.w; + vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); +#endif`,Ud=`#ifdef USE_MAP + vec4 sampledDiffuseColor = texture2D( map, vMapUv ); + #ifdef DECODE_VIDEO_TEXTURE + sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); + #endif + diffuseColor *= sampledDiffuseColor; +#endif`,Id=`#ifdef USE_MAP + uniform sampler2D map; +#endif`,Nd=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + #if defined( USE_POINTS_UV ) + vec2 uv = vUv; + #else + vec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy; + #endif +#endif +#ifdef USE_MAP + diffuseColor *= texture2D( map, uv ); +#endif +#ifdef USE_ALPHAMAP + diffuseColor.a *= texture2D( alphaMap, uv ).g; +#endif`,Fd=`#if defined( USE_POINTS_UV ) + varying vec2 vUv; +#else + #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) + uniform mat3 uvTransform; + #endif +#endif +#ifdef USE_MAP + uniform sampler2D map; +#endif +#ifdef USE_ALPHAMAP + uniform sampler2D alphaMap; +#endif`,Od=`float metalnessFactor = metalness; +#ifdef USE_METALNESSMAP + vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); + metalnessFactor *= texelMetalness.b; +#endif`,Bd=`#ifdef USE_METALNESSMAP + uniform sampler2D metalnessMap; +#endif`,zd=`#ifdef USE_INSTANCING_MORPH + float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + float morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + morphTargetInfluences[i] = texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r; + } +#endif`,kd=`#if defined( USE_MORPHCOLORS ) + vColor *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + #if defined( USE_COLOR_ALPHA ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ]; + #elif defined( USE_COLOR ) + if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; + #endif + } +#endif`,Hd=`#ifdef USE_MORPHNORMALS + objectNormal *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ]; + } +#endif`,Vd=`#ifdef USE_MORPHTARGETS + #ifndef USE_INSTANCING_MORPH + uniform float morphTargetBaseInfluence; + uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; + #endif + uniform sampler2DArray morphTargetsTexture; + uniform ivec2 morphTargetsTextureSize; + vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) { + int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset; + int y = texelIndex / morphTargetsTextureSize.x; + int x = texelIndex - y * morphTargetsTextureSize.x; + ivec3 morphUV = ivec3( x, y, morphTargetIndex ); + return texelFetch( morphTargetsTexture, morphUV, 0 ); + } +#endif`,Gd=`#ifdef USE_MORPHTARGETS + transformed *= morphTargetBaseInfluence; + for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { + if ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ]; + } +#endif`,Wd=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; +#ifdef FLAT_SHADED + vec3 fdx = dFdx( vViewPosition ); + vec3 fdy = dFdy( vViewPosition ); + vec3 normal = normalize( cross( fdx, fdy ) ); +#else + vec3 normal = normalize( vNormal ); + #ifdef DOUBLE_SIDED + normal *= faceDirection; + #endif +#endif +#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) + #ifdef USE_TANGENT + mat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn = getTangentFrame( - vViewPosition, normal, + #if defined( USE_NORMALMAP ) + vNormalMapUv + #elif defined( USE_CLEARCOAT_NORMALMAP ) + vClearcoatNormalMapUv + #else + vUv + #endif + ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn[0] *= faceDirection; + tbn[1] *= faceDirection; + #endif +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + #ifdef USE_TANGENT + mat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); + #else + mat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv ); + #endif + #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) + tbn2[0] *= faceDirection; + tbn2[1] *= faceDirection; + #endif +#endif +vec3 nonPerturbedNormal = normal;`,Xd=`#ifdef USE_NORMALMAP_OBJECTSPACE + normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + #ifdef FLIP_SIDED + normal = - normal; + #endif + #ifdef DOUBLE_SIDED + normal = normal * faceDirection; + #endif + normal = normalize( normalMatrix * normal ); +#elif defined( USE_NORMALMAP_TANGENTSPACE ) + vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; + mapN.xy *= normalScale; + normal = normalize( tbn * mapN ); +#elif defined( USE_BUMPMAP ) + normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); +#endif`,Yd=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,qd=`#ifndef FLAT_SHADED + varying vec3 vNormal; + #ifdef USE_TANGENT + varying vec3 vTangent; + varying vec3 vBitangent; + #endif +#endif`,jd=`#ifndef FLAT_SHADED + vNormal = normalize( transformedNormal ); + #ifdef USE_TANGENT + vTangent = normalize( transformedTangent ); + vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); + #endif +#endif`,Zd=`#ifdef USE_NORMALMAP + uniform sampler2D normalMap; + uniform vec2 normalScale; +#endif +#ifdef USE_NORMALMAP_OBJECTSPACE + uniform mat3 normalMatrix; +#endif +#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) ) + mat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) { + vec3 q0 = dFdx( eye_pos.xyz ); + vec3 q1 = dFdy( eye_pos.xyz ); + vec2 st0 = dFdx( uv.st ); + vec2 st1 = dFdy( uv.st ); + vec3 N = surf_norm; + vec3 q1perp = cross( q1, N ); + vec3 q0perp = cross( N, q0 ); + vec3 T = q1perp * st0.x + q0perp * st1.x; + vec3 B = q1perp * st0.y + q0perp * st1.y; + float det = max( dot( T, T ), dot( B, B ) ); + float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); + return mat3( T * scale, B * scale, N ); + } +#endif`,Kd=`#ifdef USE_CLEARCOAT + vec3 clearcoatNormal = nonPerturbedNormal; +#endif`,$d=`#ifdef USE_CLEARCOAT_NORMALMAP + vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; + clearcoatMapN.xy *= clearcoatNormalScale; + clearcoatNormal = normalize( tbn2 * clearcoatMapN ); +#endif`,Jd=`#ifdef USE_CLEARCOATMAP + uniform sampler2D clearcoatMap; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform sampler2D clearcoatNormalMap; + uniform vec2 clearcoatNormalScale; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform sampler2D clearcoatRoughnessMap; +#endif`,Qd=`#ifdef USE_IRIDESCENCEMAP + uniform sampler2D iridescenceMap; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform sampler2D iridescenceThicknessMap; +#endif`,tf=`#ifdef OPAQUE +diffuseColor.a = 1.0; +#endif +#ifdef USE_TRANSMISSION +diffuseColor.a *= material.transmissionAlpha; +#endif +gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,ef=`vec3 packNormalToRGB( const in vec3 normal ) { + return normalize( normal ) * 0.5 + 0.5; +} +vec3 unpackRGBToNormal( const in vec3 rgb ) { + return 2.0 * rgb.xyz - 1.0; +} +const float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.; +const float Inv255 = 1. / 255.; +const vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 ); +const vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g ); +const vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b ); +const vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a ); +vec4 packDepthToRGBA( const in float v ) { + if( v <= 0.0 ) + return vec4( 0., 0., 0., 0. ); + if( v >= 1.0 ) + return vec4( 1., 1., 1., 1. ); + float vuf; + float af = modf( v * PackFactors.a, vuf ); + float bf = modf( vuf * ShiftRight8, vuf ); + float gf = modf( vuf * ShiftRight8, vuf ); + return vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af ); +} +vec3 packDepthToRGB( const in float v ) { + if( v <= 0.0 ) + return vec3( 0., 0., 0. ); + if( v >= 1.0 ) + return vec3( 1., 1., 1. ); + float vuf; + float bf = modf( v * PackFactors.b, vuf ); + float gf = modf( vuf * ShiftRight8, vuf ); + return vec3( vuf * Inv255, gf * PackUpscale, bf ); +} +vec2 packDepthToRG( const in float v ) { + if( v <= 0.0 ) + return vec2( 0., 0. ); + if( v >= 1.0 ) + return vec2( 1., 1. ); + float vuf; + float gf = modf( v * 256., vuf ); + return vec2( vuf * Inv255, gf ); +} +float unpackRGBAToDepth( const in vec4 v ) { + return dot( v, UnpackFactors4 ); +} +float unpackRGBToDepth( const in vec3 v ) { + return dot( v, UnpackFactors3 ); +} +float unpackRGToDepth( const in vec2 v ) { + return v.r * UnpackFactors2.r + v.g * UnpackFactors2.g; +} +vec4 pack2HalfToRGBA( const in vec2 v ) { + vec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) ); + return vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w ); +} +vec2 unpackRGBATo2Half( const in vec4 v ) { + return vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) ); +} +float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { + return ( viewZ + near ) / ( near - far ); +} +float orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) { + return depth * ( near - far ) - near; +} +float viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) { + return ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ ); +} +float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { + return ( near * far ) / ( ( far - near ) * depth - far ); +}`,nf=`#ifdef PREMULTIPLIED_ALPHA + gl_FragColor.rgb *= gl_FragColor.a; +#endif`,sf=`vec4 mvPosition = vec4( transformed, 1.0 ); +#ifdef USE_BATCHING + mvPosition = batchingMatrix * mvPosition; +#endif +#ifdef USE_INSTANCING + mvPosition = instanceMatrix * mvPosition; +#endif +mvPosition = modelViewMatrix * mvPosition; +gl_Position = projectionMatrix * mvPosition;`,rf=`#ifdef DITHERING + gl_FragColor.rgb = dithering( gl_FragColor.rgb ); +#endif`,af=`#ifdef DITHERING + vec3 dithering( vec3 color ) { + float grid_position = rand( gl_FragCoord.xy ); + vec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 ); + dither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position ); + return color + dither_shift_RGB; + } +#endif`,of=`float roughnessFactor = roughness; +#ifdef USE_ROUGHNESSMAP + vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); + roughnessFactor *= texelRoughness.g; +#endif`,lf=`#ifdef USE_ROUGHNESSMAP + uniform sampler2D roughnessMap; +#endif`,cf=`#if NUM_SPOT_LIGHT_COORDS > 0 + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#if NUM_SPOT_LIGHT_MAPS > 0 + uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + uniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; + struct SpotLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif + float texture2DCompare( sampler2D depths, vec2 uv, float compare ) { + return step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) ); + } + vec2 texture2DDistribution( sampler2D shadow, vec2 uv ) { + return unpackRGBATo2Half( texture2D( shadow, uv ) ); + } + float VSMShadow (sampler2D shadow, vec2 uv, float compare ){ + float occlusion = 1.0; + vec2 distribution = texture2DDistribution( shadow, uv ); + float hard_shadow = step( compare , distribution.x ); + if (hard_shadow != 1.0 ) { + float distance = compare - distribution.x ; + float variance = max( 0.00000, distribution.y * distribution.y ); + float softness_probability = variance / (variance + distance * distance ); softness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 ); occlusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 ); + } + return occlusion; + } + float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { + float shadow = 1.0; + shadowCoord.xyz /= shadowCoord.w; + shadowCoord.z += shadowBias; + bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; + bool frustumTest = inFrustum && shadowCoord.z <= 1.0; + if ( frustumTest ) { + #if defined( SHADOWMAP_TYPE_PCF ) + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float dx0 = - texelSize.x * shadowRadius; + float dy0 = - texelSize.y * shadowRadius; + float dx1 = + texelSize.x * shadowRadius; + float dy1 = + texelSize.y * shadowRadius; + float dx2 = dx0 / 2.0; + float dy2 = dy0 / 2.0; + float dx3 = dx1 / 2.0; + float dy3 = dy1 / 2.0; + shadow = ( + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) + + texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z ) + ) * ( 1.0 / 17.0 ); + #elif defined( SHADOWMAP_TYPE_PCF_SOFT ) + vec2 texelSize = vec2( 1.0 ) / shadowMapSize; + float dx = texelSize.x; + float dy = texelSize.y; + vec2 uv = shadowCoord.xy; + vec2 f = fract( uv * shadowMapSize + 0.5 ); + uv -= f * texelSize; + shadow = ( + texture2DCompare( shadowMap, uv, shadowCoord.z ) + + texture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) + + texture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) + + texture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) + + mix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ), + f.x ) + + mix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ), + f.x ) + + mix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ), + f.y ) + + mix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ), + f.y ) + + mix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ), + f.x ), + mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), + texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ), + f.x ), + f.y ) + ) * ( 1.0 / 9.0 ); + #elif defined( SHADOWMAP_TYPE_VSM ) + shadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z ); + #else + shadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ); + #endif + } + return mix( 1.0, shadow, shadowIntensity ); + } + vec2 cubeToUV( vec3 v, float texelSizeY ) { + vec3 absV = abs( v ); + float scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) ); + absV *= scaleToCube; + v *= scaleToCube * ( 1.0 - 2.0 * texelSizeY ); + vec2 planar = v.xy; + float almostATexel = 1.5 * texelSizeY; + float almostOne = 1.0 - almostATexel; + if ( absV.z >= almostOne ) { + if ( v.z > 0.0 ) + planar.x = 4.0 - v.x; + } else if ( absV.x >= almostOne ) { + float signX = sign( v.x ); + planar.x = v.z * signX + 2.0 * signX; + } else if ( absV.y >= almostOne ) { + float signY = sign( v.y ); + planar.x = v.x + 2.0 * signY + 2.0; + planar.y = v.z * signY - 2.0; + } + return vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 ); + } + float getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { + float shadow = 1.0; + vec3 lightToPosition = shadowCoord.xyz; + + float lightToPositionLength = length( lightToPosition ); + if ( lightToPositionLength - shadowCameraFar <= 0.0 && lightToPositionLength - shadowCameraNear >= 0.0 ) { + float dp = ( lightToPositionLength - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear ); dp += shadowBias; + vec3 bd3D = normalize( lightToPosition ); + vec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) ); + #if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM ) + vec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y; + shadow = ( + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) + + texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp ) + ) * ( 1.0 / 9.0 ); + #else + shadow = texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ); + #endif + } + return mix( 1.0, shadow, shadowIntensity ); + } +#endif`,hf=`#if NUM_SPOT_LIGHT_COORDS > 0 + uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; + varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; +#endif +#ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ]; + varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; + struct DirectionalLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + struct SpotLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + }; + uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ]; + varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; + struct PointLightShadow { + float shadowIntensity; + float shadowBias; + float shadowNormalBias; + float shadowRadius; + vec2 shadowMapSize; + float shadowCameraNear; + float shadowCameraFar; + }; + uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; + #endif +#endif`,uf=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) + vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); + vec4 shadowWorldPosition; +#endif +#if defined( USE_SHADOWMAP ) + #if NUM_DIR_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 ); + vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 ); + vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end + #endif +#endif +#if NUM_SPOT_LIGHT_COORDS > 0 + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) { + shadowWorldPosition = worldPosition; + #if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) + shadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias; + #endif + vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; + } + #pragma unroll_loop_end +#endif`,df=`float getShadowMask() { + float shadow = 1.0; + #ifdef USE_SHADOWMAP + #if NUM_DIR_LIGHT_SHADOWS > 0 + DirectionalLightShadow directionalLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { + directionalLight = directionalLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_SPOT_LIGHT_SHADOWS > 0 + SpotLightShadow spotLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) { + spotLight = spotLightShadows[ i ]; + shadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; + } + #pragma unroll_loop_end + #endif + #if NUM_POINT_LIGHT_SHADOWS > 0 + PointLightShadow pointLight; + #pragma unroll_loop_start + for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { + pointLight = pointLightShadows[ i ]; + shadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; + } + #pragma unroll_loop_end + #endif + #endif + return shadow; +}`,ff=`#ifdef USE_SKINNING + mat4 boneMatX = getBoneMatrix( skinIndex.x ); + mat4 boneMatY = getBoneMatrix( skinIndex.y ); + mat4 boneMatZ = getBoneMatrix( skinIndex.z ); + mat4 boneMatW = getBoneMatrix( skinIndex.w ); +#endif`,pf=`#ifdef USE_SKINNING + uniform mat4 bindMatrix; + uniform mat4 bindMatrixInverse; + uniform highp sampler2D boneTexture; + mat4 getBoneMatrix( const in float i ) { + int size = textureSize( boneTexture, 0 ).x; + int j = int( i ) * 4; + int x = j % size; + int y = j / size; + vec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 ); + vec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 ); + vec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 ); + vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 ); + return mat4( v1, v2, v3, v4 ); + } +#endif`,mf=`#ifdef USE_SKINNING + vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); + vec4 skinned = vec4( 0.0 ); + skinned += boneMatX * skinVertex * skinWeight.x; + skinned += boneMatY * skinVertex * skinWeight.y; + skinned += boneMatZ * skinVertex * skinWeight.z; + skinned += boneMatW * skinVertex * skinWeight.w; + transformed = ( bindMatrixInverse * skinned ).xyz; +#endif`,gf=`#ifdef USE_SKINNING + mat4 skinMatrix = mat4( 0.0 ); + skinMatrix += skinWeight.x * boneMatX; + skinMatrix += skinWeight.y * boneMatY; + skinMatrix += skinWeight.z * boneMatZ; + skinMatrix += skinWeight.w * boneMatW; + skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; + objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; + #ifdef USE_TANGENT + objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; + #endif +#endif`,_f=`float specularStrength; +#ifdef USE_SPECULARMAP + vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); + specularStrength = texelSpecular.r; +#else + specularStrength = 1.0; +#endif`,vf=`#ifdef USE_SPECULARMAP + uniform sampler2D specularMap; +#endif`,xf=`#if defined( TONE_MAPPING ) + gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); +#endif`,Mf=`#ifndef saturate +#define saturate( a ) clamp( a, 0.0, 1.0 ) +#endif +uniform float toneMappingExposure; +vec3 LinearToneMapping( vec3 color ) { + return saturate( toneMappingExposure * color ); +} +vec3 ReinhardToneMapping( vec3 color ) { + color *= toneMappingExposure; + return saturate( color / ( vec3( 1.0 ) + color ) ); +} +vec3 CineonToneMapping( vec3 color ) { + color *= toneMappingExposure; + color = max( vec3( 0.0 ), color - 0.004 ); + return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) ); +} +vec3 RRTAndODTFit( vec3 v ) { + vec3 a = v * ( v + 0.0245786 ) - 0.000090537; + vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; + return a / b; +} +vec3 ACESFilmicToneMapping( vec3 color ) { + const mat3 ACESInputMat = mat3( + vec3( 0.59719, 0.07600, 0.02840 ), vec3( 0.35458, 0.90834, 0.13383 ), + vec3( 0.04823, 0.01566, 0.83777 ) + ); + const mat3 ACESOutputMat = mat3( + vec3( 1.60475, -0.10208, -0.00327 ), vec3( -0.53108, 1.10813, -0.07276 ), + vec3( -0.07367, -0.00605, 1.07602 ) + ); + color *= toneMappingExposure / 0.6; + color = ACESInputMat * color; + color = RRTAndODTFit( color ); + color = ACESOutputMat * color; + return saturate( color ); +} +const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( + vec3( 1.6605, - 0.1246, - 0.0182 ), + vec3( - 0.5876, 1.1329, - 0.1006 ), + vec3( - 0.0728, - 0.0083, 1.1187 ) +); +const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( + vec3( 0.6274, 0.0691, 0.0164 ), + vec3( 0.3293, 0.9195, 0.0880 ), + vec3( 0.0433, 0.0113, 0.8956 ) +); +vec3 agxDefaultContrastApprox( vec3 x ) { + vec3 x2 = x * x; + vec3 x4 = x2 * x2; + return + 15.5 * x4 * x2 + - 40.14 * x4 * x + + 31.96 * x4 + - 6.868 * x2 * x + + 0.4298 * x2 + + 0.1191 * x + - 0.00232; +} +vec3 AgXToneMapping( vec3 color ) { + const mat3 AgXInsetMatrix = mat3( + vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), + vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), + vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) + ); + const mat3 AgXOutsetMatrix = mat3( + vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), + vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), + vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) + ); + const float AgxMinEv = - 12.47393; const float AgxMaxEv = 4.026069; + color *= toneMappingExposure; + color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; + color = AgXInsetMatrix * color; + color = max( color, 1e-10 ); color = log2( color ); + color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv ); + color = clamp( color, 0.0, 1.0 ); + color = agxDefaultContrastApprox( color ); + color = AgXOutsetMatrix * color; + color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) ); + color = LINEAR_REC2020_TO_LINEAR_SRGB * color; + color = clamp( color, 0.0, 1.0 ); + return color; +} +vec3 NeutralToneMapping( vec3 color ) { + const float StartCompression = 0.8 - 0.04; + const float Desaturation = 0.15; + color *= toneMappingExposure; + float x = min( color.r, min( color.g, color.b ) ); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color -= offset; + float peak = max( color.r, max( color.g, color.b ) ); + if ( peak < StartCompression ) return color; + float d = 1. - StartCompression; + float newPeak = 1. - d * d / ( peak + d - StartCompression ); + color *= newPeak / peak; + float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. ); + return mix( color, vec3( newPeak ), g ); +} +vec3 CustomToneMapping( vec3 color ) { return color; }`,Sf=`#ifdef USE_TRANSMISSION + material.transmission = transmission; + material.transmissionAlpha = 1.0; + material.thickness = thickness; + material.attenuationDistance = attenuationDistance; + material.attenuationColor = attenuationColor; + #ifdef USE_TRANSMISSIONMAP + material.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r; + #endif + #ifdef USE_THICKNESSMAP + material.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g; + #endif + vec3 pos = vWorldPosition; + vec3 v = normalize( cameraPosition - pos ); + vec3 n = inverseTransformDirection( normal, viewMatrix ); + vec4 transmitted = getIBLVolumeRefraction( + n, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, + pos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness, + material.attenuationColor, material.attenuationDistance ); + material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); + totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); +#endif`,yf=`#ifdef USE_TRANSMISSION + uniform float transmission; + uniform float thickness; + uniform float attenuationDistance; + uniform vec3 attenuationColor; + #ifdef USE_TRANSMISSIONMAP + uniform sampler2D transmissionMap; + #endif + #ifdef USE_THICKNESSMAP + uniform sampler2D thicknessMap; + #endif + uniform vec2 transmissionSamplerSize; + uniform sampler2D transmissionSamplerMap; + uniform mat4 modelMatrix; + uniform mat4 projectionMatrix; + varying vec3 vWorldPosition; + float w0( float a ) { + return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); + } + float w1( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); + } + float w2( float a ){ + return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); + } + float w3( float a ) { + return ( 1.0 / 6.0 ) * ( a * a * a ); + } + float g0( float a ) { + return w0( a ) + w1( a ); + } + float g1( float a ) { + return w2( a ) + w3( a ); + } + float h0( float a ) { + return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); + } + float h1( float a ) { + return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); + } + vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { + uv = uv * texelSize.zw + 0.5; + vec2 iuv = floor( uv ); + vec2 fuv = fract( uv ); + float g0x = g0( fuv.x ); + float g1x = g1( fuv.x ); + float h0x = h0( fuv.x ); + float h1x = h1( fuv.x ); + float h0y = h0( fuv.y ); + float h1y = h1( fuv.y ); + vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; + vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; + return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + + g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); + } + vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { + vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); + vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); + vec2 fLodSizeInv = 1.0 / fLodSize; + vec2 cLodSizeInv = 1.0 / cLodSize; + vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); + vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); + return mix( fSample, cSample, fract( lod ) ); + } + vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { + vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); + vec3 modelScale; + modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); + modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); + modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); + return normalize( refractionVector ) * thickness * modelScale; + } + float applyIorToRoughness( const in float roughness, const in float ior ) { + return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); + } + vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { + float lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); + return textureBicubic( transmissionSamplerMap, fragCoord.xy, lod ); + } + vec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { + if ( isinf( attenuationDistance ) ) { + return vec3( 1.0 ); + } else { + vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; + vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); return transmittance; + } + } + vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, + const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, + const in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness, + const in vec3 attenuationColor, const in float attenuationDistance ) { + vec4 transmittedLight; + vec3 transmittance; + #ifdef USE_DISPERSION + float halfSpread = ( ior - 1.0 ) * 0.025 * dispersion; + vec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread ); + for ( int i = 0; i < 3; i ++ ) { + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + vec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] ); + transmittedLight[ i ] = transmissionSample[ i ]; + transmittedLight.a += transmissionSample.a; + transmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ]; + } + transmittedLight.a /= 3.0; + #else + vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); + vec3 refractedRayExit = position + transmissionRay; + vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); + vec2 refractionCoords = ndcPos.xy / ndcPos.w; + refractionCoords += 1.0; + refractionCoords /= 2.0; + transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); + transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ); + #endif + vec3 attenuatedColor = transmittance * transmittedLight.rgb; + vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); + float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; + return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); + } +#endif`,Ef=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + varying vec2 vNormalMapUv; +#endif +#ifdef USE_EMISSIVEMAP + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_SPECULARMAP + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,bf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + varying vec2 vUv; +#endif +#ifdef USE_MAP + uniform mat3 mapTransform; + varying vec2 vMapUv; +#endif +#ifdef USE_ALPHAMAP + uniform mat3 alphaMapTransform; + varying vec2 vAlphaMapUv; +#endif +#ifdef USE_LIGHTMAP + uniform mat3 lightMapTransform; + varying vec2 vLightMapUv; +#endif +#ifdef USE_AOMAP + uniform mat3 aoMapTransform; + varying vec2 vAoMapUv; +#endif +#ifdef USE_BUMPMAP + uniform mat3 bumpMapTransform; + varying vec2 vBumpMapUv; +#endif +#ifdef USE_NORMALMAP + uniform mat3 normalMapTransform; + varying vec2 vNormalMapUv; +#endif +#ifdef USE_DISPLACEMENTMAP + uniform mat3 displacementMapTransform; + varying vec2 vDisplacementMapUv; +#endif +#ifdef USE_EMISSIVEMAP + uniform mat3 emissiveMapTransform; + varying vec2 vEmissiveMapUv; +#endif +#ifdef USE_METALNESSMAP + uniform mat3 metalnessMapTransform; + varying vec2 vMetalnessMapUv; +#endif +#ifdef USE_ROUGHNESSMAP + uniform mat3 roughnessMapTransform; + varying vec2 vRoughnessMapUv; +#endif +#ifdef USE_ANISOTROPYMAP + uniform mat3 anisotropyMapTransform; + varying vec2 vAnisotropyMapUv; +#endif +#ifdef USE_CLEARCOATMAP + uniform mat3 clearcoatMapTransform; + varying vec2 vClearcoatMapUv; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + uniform mat3 clearcoatNormalMapTransform; + varying vec2 vClearcoatNormalMapUv; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + uniform mat3 clearcoatRoughnessMapTransform; + varying vec2 vClearcoatRoughnessMapUv; +#endif +#ifdef USE_SHEEN_COLORMAP + uniform mat3 sheenColorMapTransform; + varying vec2 vSheenColorMapUv; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + uniform mat3 sheenRoughnessMapTransform; + varying vec2 vSheenRoughnessMapUv; +#endif +#ifdef USE_IRIDESCENCEMAP + uniform mat3 iridescenceMapTransform; + varying vec2 vIridescenceMapUv; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + uniform mat3 iridescenceThicknessMapTransform; + varying vec2 vIridescenceThicknessMapUv; +#endif +#ifdef USE_SPECULARMAP + uniform mat3 specularMapTransform; + varying vec2 vSpecularMapUv; +#endif +#ifdef USE_SPECULAR_COLORMAP + uniform mat3 specularColorMapTransform; + varying vec2 vSpecularColorMapUv; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + uniform mat3 specularIntensityMapTransform; + varying vec2 vSpecularIntensityMapUv; +#endif +#ifdef USE_TRANSMISSIONMAP + uniform mat3 transmissionMapTransform; + varying vec2 vTransmissionMapUv; +#endif +#ifdef USE_THICKNESSMAP + uniform mat3 thicknessMapTransform; + varying vec2 vThicknessMapUv; +#endif`,Tf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) + vUv = vec3( uv, 1 ).xy; +#endif +#ifdef USE_MAP + vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ALPHAMAP + vAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_LIGHTMAP + vLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_AOMAP + vAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_BUMPMAP + vBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_NORMALMAP + vNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_DISPLACEMENTMAP + vDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_EMISSIVEMAP + vEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_METALNESSMAP + vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ROUGHNESSMAP + vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_ANISOTROPYMAP + vAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOATMAP + vClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_NORMALMAP + vClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_CLEARCOAT_ROUGHNESSMAP + vClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCEMAP + vIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_IRIDESCENCE_THICKNESSMAP + vIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_COLORMAP + vSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SHEEN_ROUGHNESSMAP + vSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULARMAP + vSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_COLORMAP + vSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_SPECULAR_INTENSITYMAP + vSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_TRANSMISSIONMAP + vTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy; +#endif +#ifdef USE_THICKNESSMAP + vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; +#endif`,wf=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 + vec4 worldPosition = vec4( transformed, 1.0 ); + #ifdef USE_BATCHING + worldPosition = batchingMatrix * worldPosition; + #endif + #ifdef USE_INSTANCING + worldPosition = instanceMatrix * worldPosition; + #endif + worldPosition = modelMatrix * worldPosition; +#endif`;const Af=`varying vec2 vUv; +uniform mat3 uvTransform; +void main() { + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + gl_Position = vec4( position.xy, 1.0, 1.0 ); +}`,Rf=`uniform sampler2D t2D; +uniform float backgroundIntensity; +varying vec2 vUv; +void main() { + vec4 texColor = texture2D( t2D, vUv ); + #ifdef DECODE_VIDEO_TEXTURE + texColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,Cf=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,Pf=`#ifdef ENVMAP_TYPE_CUBE + uniform samplerCube envMap; +#elif defined( ENVMAP_TYPE_CUBE_UV ) + uniform sampler2D envMap; +#endif +uniform float flipEnvMap; +uniform float backgroundBlurriness; +uniform float backgroundIntensity; +uniform mat3 backgroundRotation; +varying vec3 vWorldDirection; +#include +void main() { + #ifdef ENVMAP_TYPE_CUBE + vec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) ); + #elif defined( ENVMAP_TYPE_CUBE_UV ) + vec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness ); + #else + vec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + #endif + texColor.rgb *= backgroundIntensity; + gl_FragColor = texColor; + #include + #include +}`,Df=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include + gl_Position.z = gl_Position.w; +}`,Lf=`uniform samplerCube tCube; +uniform float tFlip; +uniform float opacity; +varying vec3 vWorldDirection; +void main() { + vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) ); + gl_FragColor = texColor; + gl_FragColor.a *= opacity; + #include + #include +}`,Uf=`#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + #include + #include + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vHighPrecisionZW = gl_Position.zw; +}`,If=`#if DEPTH_PACKING == 3200 + uniform float opacity; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +varying vec2 vHighPrecisionZW; +void main() { + vec4 diffuseColor = vec4( 1.0 ); + #include + #if DEPTH_PACKING == 3200 + diffuseColor.a = opacity; + #endif + #include + #include + #include + #include + #include + float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5; + #if DEPTH_PACKING == 3200 + gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity ); + #elif DEPTH_PACKING == 3201 + gl_FragColor = packDepthToRGBA( fragCoordZ ); + #elif DEPTH_PACKING == 3202 + gl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 ); + #elif DEPTH_PACKING == 3203 + gl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 ); + #endif +}`,Nf=`#define DISTANCE +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #ifdef USE_DISPLACEMENTMAP + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + vWorldPosition = worldPosition.xyz; +}`,Ff=`#define DISTANCE +uniform vec3 referencePosition; +uniform float nearDistance; +uniform float farDistance; +varying vec3 vWorldPosition; +#include +#include +#include +#include +#include +#include +#include +#include +void main () { + vec4 diffuseColor = vec4( 1.0 ); + #include + #include + #include + #include + #include + float dist = length( vWorldPosition - referencePosition ); + dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); + dist = saturate( dist ); + gl_FragColor = packDepthToRGBA( dist ); +}`,Of=`varying vec3 vWorldDirection; +#include +void main() { + vWorldDirection = transformDirection( position, modelMatrix ); + #include + #include +}`,Bf=`uniform sampler2D tEquirect; +varying vec3 vWorldDirection; +#include +void main() { + vec3 direction = normalize( vWorldDirection ); + vec2 sampleUV = equirectUv( direction ); + gl_FragColor = texture2D( tEquirect, sampleUV ); + #include + #include +}`,zf=`uniform float scale; +attribute float lineDistance; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vLineDistance = scale * lineDistance; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,kf=`uniform vec3 diffuse; +uniform float opacity; +uniform float dashSize; +uniform float totalSize; +varying float vLineDistance; +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + if ( mod( vLineDistance, totalSize ) > dashSize ) { + discard; + } + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,Hf=`#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #if defined ( USE_ENVMAP ) || defined ( USE_SKINNING ) + #include + #include + #include + #include + #include + #endif + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,Vf=`uniform vec3 diffuse; +uniform float opacity; +#ifndef FLAT_SHADED + varying vec3 vNormal; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + #ifdef USE_LIGHTMAP + vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); + reflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI; + #else + reflectedLight.indirectDiffuse += vec3( 1.0 ); + #endif + #include + reflectedLight.indirectDiffuse *= diffuseColor.rgb; + vec3 outgoingLight = reflectedLight.indirectDiffuse; + #include + #include + #include + #include + #include + #include + #include +}`,Gf=`#define LAMBERT +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Wf=`#define LAMBERT +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,Xf=`#define MATCAP +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; +}`,Yf=`#define MATCAP +uniform vec3 diffuse; +uniform float opacity; +uniform sampler2D matcap; +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 viewDir = normalize( vViewPosition ); + vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) ); + vec3 y = cross( viewDir, x ); + vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; + #ifdef USE_MATCAP + vec4 matcapColor = texture2D( matcap, uv ); + #else + vec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 ); + #endif + vec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb; + #include + #include + #include + #include + #include + #include +}`,qf=`#define NORMAL +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + vViewPosition = - mvPosition.xyz; +#endif +}`,jf=`#define NORMAL +uniform float opacity; +#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) + varying vec3 vViewPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity ); + #include + #include + #include + #include + gl_FragColor = vec4( packNormalToRGB( normal ), diffuseColor.a ); + #ifdef OPAQUE + gl_FragColor.a = 1.0; + #endif +}`,Zf=`#define PHONG +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include + #include +}`,Kf=`#define PHONG +uniform vec3 diffuse; +uniform vec3 emissive; +uniform vec3 specular; +uniform float shininess; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include + #include +}`,$f=`#define STANDARD +varying vec3 vViewPosition; +#ifdef USE_TRANSMISSION + varying vec3 vWorldPosition; +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +#ifdef USE_TRANSMISSION + vWorldPosition = worldPosition.xyz; +#endif +}`,Jf=`#define STANDARD +#ifdef PHYSICAL + #define IOR + #define USE_SPECULAR +#endif +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float roughness; +uniform float metalness; +uniform float opacity; +#ifdef IOR + uniform float ior; +#endif +#ifdef USE_SPECULAR + uniform float specularIntensity; + uniform vec3 specularColor; + #ifdef USE_SPECULAR_COLORMAP + uniform sampler2D specularColorMap; + #endif + #ifdef USE_SPECULAR_INTENSITYMAP + uniform sampler2D specularIntensityMap; + #endif +#endif +#ifdef USE_CLEARCOAT + uniform float clearcoat; + uniform float clearcoatRoughness; +#endif +#ifdef USE_DISPERSION + uniform float dispersion; +#endif +#ifdef USE_IRIDESCENCE + uniform float iridescence; + uniform float iridescenceIOR; + uniform float iridescenceThicknessMinimum; + uniform float iridescenceThicknessMaximum; +#endif +#ifdef USE_SHEEN + uniform vec3 sheenColor; + uniform float sheenRoughness; + #ifdef USE_SHEEN_COLORMAP + uniform sampler2D sheenColorMap; + #endif + #ifdef USE_SHEEN_ROUGHNESSMAP + uniform sampler2D sheenRoughnessMap; + #endif +#endif +#ifdef USE_ANISOTROPY + uniform vec2 anisotropyVector; + #ifdef USE_ANISOTROPYMAP + uniform sampler2D anisotropyMap; + #endif +#endif +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; + vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; + #include + vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; + #ifdef USE_SHEEN + float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); + outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect; + #endif + #ifdef USE_CLEARCOAT + float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) ); + vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); + outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat; + #endif + #include + #include + #include + #include + #include + #include +}`,Qf=`#define TOON +varying vec3 vViewPosition; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vViewPosition = - mvPosition.xyz; + #include + #include + #include +}`,tp=`#define TOON +uniform vec3 diffuse; +uniform vec3 emissive; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); + vec3 totalEmissiveRadiance = emissive; + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; + #include + #include + #include + #include + #include + #include +}`,ep=`uniform float size; +uniform float scale; +#include +#include +#include +#include +#include +#include +#ifdef USE_POINTS_UV + varying vec2 vUv; + uniform mat3 uvTransform; +#endif +void main() { + #ifdef USE_POINTS_UV + vUv = ( uvTransform * vec3( uv, 1 ) ).xy; + #endif + #include + #include + #include + #include + #include + #include + gl_PointSize = size; + #ifdef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z ); + #endif + #include + #include + #include + #include +}`,np=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include + #include +}`,ip=`#include +#include +#include +#include +#include +#include +#include +void main() { + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include + #include +}`,sp=`uniform vec3 color; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + #include + gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) ); + #include + #include + #include +}`,rp=`uniform float rotation; +uniform vec2 center; +#include +#include +#include +#include +#include +void main() { + #include + vec4 mvPosition = modelViewMatrix[ 3 ]; + vec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) ); + #ifndef USE_SIZEATTENUATION + bool isPerspective = isPerspectiveMatrix( projectionMatrix ); + if ( isPerspective ) scale *= - mvPosition.z; + #endif + vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale; + vec2 rotatedPosition; + rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; + rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; + mvPosition.xy += rotatedPosition; + gl_Position = projectionMatrix * mvPosition; + #include + #include + #include +}`,ap=`uniform vec3 diffuse; +uniform float opacity; +#include +#include +#include +#include +#include +#include +#include +#include +#include +void main() { + vec4 diffuseColor = vec4( diffuse, opacity ); + #include + vec3 outgoingLight = vec3( 0.0 ); + #include + #include + #include + #include + #include + outgoingLight = diffuseColor.rgb; + #include + #include + #include + #include +}`,Gt={alphahash_fragment:Au,alphahash_pars_fragment:Ru,alphamap_fragment:Cu,alphamap_pars_fragment:Pu,alphatest_fragment:Du,alphatest_pars_fragment:Lu,aomap_fragment:Uu,aomap_pars_fragment:Iu,batching_pars_vertex:Nu,batching_vertex:Fu,begin_vertex:Ou,beginnormal_vertex:Bu,bsdfs:zu,iridescence_fragment:ku,bumpmap_pars_fragment:Hu,clipping_planes_fragment:Vu,clipping_planes_pars_fragment:Gu,clipping_planes_pars_vertex:Wu,clipping_planes_vertex:Xu,color_fragment:Yu,color_pars_fragment:qu,color_pars_vertex:ju,color_vertex:Zu,common:Ku,cube_uv_reflection_fragment:$u,defaultnormal_vertex:Ju,displacementmap_pars_vertex:Qu,displacementmap_vertex:td,emissivemap_fragment:ed,emissivemap_pars_fragment:nd,colorspace_fragment:id,colorspace_pars_fragment:sd,envmap_fragment:rd,envmap_common_pars_fragment:ad,envmap_pars_fragment:od,envmap_pars_vertex:ld,envmap_physical_pars_fragment:xd,envmap_vertex:cd,fog_vertex:hd,fog_pars_vertex:ud,fog_fragment:dd,fog_pars_fragment:fd,gradientmap_pars_fragment:pd,lightmap_pars_fragment:md,lights_lambert_fragment:gd,lights_lambert_pars_fragment:_d,lights_pars_begin:vd,lights_toon_fragment:Md,lights_toon_pars_fragment:Sd,lights_phong_fragment:yd,lights_phong_pars_fragment:Ed,lights_physical_fragment:bd,lights_physical_pars_fragment:Td,lights_fragment_begin:wd,lights_fragment_maps:Ad,lights_fragment_end:Rd,logdepthbuf_fragment:Cd,logdepthbuf_pars_fragment:Pd,logdepthbuf_pars_vertex:Dd,logdepthbuf_vertex:Ld,map_fragment:Ud,map_pars_fragment:Id,map_particle_fragment:Nd,map_particle_pars_fragment:Fd,metalnessmap_fragment:Od,metalnessmap_pars_fragment:Bd,morphinstance_vertex:zd,morphcolor_vertex:kd,morphnormal_vertex:Hd,morphtarget_pars_vertex:Vd,morphtarget_vertex:Gd,normal_fragment_begin:Wd,normal_fragment_maps:Xd,normal_pars_fragment:Yd,normal_pars_vertex:qd,normal_vertex:jd,normalmap_pars_fragment:Zd,clearcoat_normal_fragment_begin:Kd,clearcoat_normal_fragment_maps:$d,clearcoat_pars_fragment:Jd,iridescence_pars_fragment:Qd,opaque_fragment:tf,packing:ef,premultiplied_alpha_fragment:nf,project_vertex:sf,dithering_fragment:rf,dithering_pars_fragment:af,roughnessmap_fragment:of,roughnessmap_pars_fragment:lf,shadowmap_pars_fragment:cf,shadowmap_pars_vertex:hf,shadowmap_vertex:uf,shadowmask_pars_fragment:df,skinbase_vertex:ff,skinning_pars_vertex:pf,skinning_vertex:mf,skinnormal_vertex:gf,specularmap_fragment:_f,specularmap_pars_fragment:vf,tonemapping_fragment:xf,tonemapping_pars_fragment:Mf,transmission_fragment:Sf,transmission_pars_fragment:yf,uv_pars_fragment:Ef,uv_pars_vertex:bf,uv_vertex:Tf,worldpos_vertex:wf,background_vert:Af,background_frag:Rf,backgroundCube_vert:Cf,backgroundCube_frag:Pf,cube_vert:Df,cube_frag:Lf,depth_vert:Uf,depth_frag:If,distanceRGBA_vert:Nf,distanceRGBA_frag:Ff,equirect_vert:Of,equirect_frag:Bf,linedashed_vert:zf,linedashed_frag:kf,meshbasic_vert:Hf,meshbasic_frag:Vf,meshlambert_vert:Gf,meshlambert_frag:Wf,meshmatcap_vert:Xf,meshmatcap_frag:Yf,meshnormal_vert:qf,meshnormal_frag:jf,meshphong_vert:Zf,meshphong_frag:Kf,meshphysical_vert:$f,meshphysical_frag:Jf,meshtoon_vert:Qf,meshtoon_frag:tp,points_vert:ep,points_frag:np,shadow_vert:ip,shadow_frag:sp,sprite_vert:rp,sprite_frag:ap},ct={common:{diffuse:{value:new ot(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Ht},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Ht}},envmap:{envMap:{value:null},envMapRotation:{value:new Ht},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Ht}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Ht}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Ht},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Ht},normalScale:{value:new St(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Ht},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Ht}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Ht}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Ht}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new ot(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new ot(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0},uvTransform:{value:new Ht}},sprite:{diffuse:{value:new ot(16777215)},opacity:{value:1},center:{value:new St(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Ht},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0}}},mn={basic:{uniforms:ke([ct.common,ct.specularmap,ct.envmap,ct.aomap,ct.lightmap,ct.fog]),vertexShader:Gt.meshbasic_vert,fragmentShader:Gt.meshbasic_frag},lambert:{uniforms:ke([ct.common,ct.specularmap,ct.envmap,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.fog,ct.lights,{emissive:{value:new ot(0)}}]),vertexShader:Gt.meshlambert_vert,fragmentShader:Gt.meshlambert_frag},phong:{uniforms:ke([ct.common,ct.specularmap,ct.envmap,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.fog,ct.lights,{emissive:{value:new ot(0)},specular:{value:new ot(1118481)},shininess:{value:30}}]),vertexShader:Gt.meshphong_vert,fragmentShader:Gt.meshphong_frag},standard:{uniforms:ke([ct.common,ct.envmap,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.roughnessmap,ct.metalnessmap,ct.fog,ct.lights,{emissive:{value:new ot(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Gt.meshphysical_vert,fragmentShader:Gt.meshphysical_frag},toon:{uniforms:ke([ct.common,ct.aomap,ct.lightmap,ct.emissivemap,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.gradientmap,ct.fog,ct.lights,{emissive:{value:new ot(0)}}]),vertexShader:Gt.meshtoon_vert,fragmentShader:Gt.meshtoon_frag},matcap:{uniforms:ke([ct.common,ct.bumpmap,ct.normalmap,ct.displacementmap,ct.fog,{matcap:{value:null}}]),vertexShader:Gt.meshmatcap_vert,fragmentShader:Gt.meshmatcap_frag},points:{uniforms:ke([ct.points,ct.fog]),vertexShader:Gt.points_vert,fragmentShader:Gt.points_frag},dashed:{uniforms:ke([ct.common,ct.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Gt.linedashed_vert,fragmentShader:Gt.linedashed_frag},depth:{uniforms:ke([ct.common,ct.displacementmap]),vertexShader:Gt.depth_vert,fragmentShader:Gt.depth_frag},normal:{uniforms:ke([ct.common,ct.bumpmap,ct.normalmap,ct.displacementmap,{opacity:{value:1}}]),vertexShader:Gt.meshnormal_vert,fragmentShader:Gt.meshnormal_frag},sprite:{uniforms:ke([ct.sprite,ct.fog]),vertexShader:Gt.sprite_vert,fragmentShader:Gt.sprite_frag},background:{uniforms:{uvTransform:{value:new Ht},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Gt.background_vert,fragmentShader:Gt.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Ht}},vertexShader:Gt.backgroundCube_vert,fragmentShader:Gt.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Gt.cube_vert,fragmentShader:Gt.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Gt.equirect_vert,fragmentShader:Gt.equirect_frag},distanceRGBA:{uniforms:ke([ct.common,ct.displacementmap,{referencePosition:{value:new P},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Gt.distanceRGBA_vert,fragmentShader:Gt.distanceRGBA_frag},shadow:{uniforms:ke([ct.lights,ct.fog,{color:{value:new ot(0)},opacity:{value:1}}]),vertexShader:Gt.shadow_vert,fragmentShader:Gt.shadow_frag}};mn.physical={uniforms:ke([mn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Ht},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Ht},clearcoatNormalScale:{value:new St(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Ht},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Ht},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Ht},sheen:{value:0},sheenColor:{value:new ot(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Ht},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Ht},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Ht},transmissionSamplerSize:{value:new St},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Ht},attenuationDistance:{value:0},attenuationColor:{value:new ot(0)},specularColor:{value:new ot(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Ht},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Ht},anisotropyVector:{value:new St},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Ht}}]),vertexShader:Gt.meshphysical_vert,fragmentShader:Gt.meshphysical_frag};const ar={r:0,b:0,g:0},Qn=new Mn,op=new ne;function lp(i,t,e,n,s,r,a){const o=new ot(0);let l=r===!0?0:1,c,h,d=null,p=0,u=null;function g(E){let y=E.isScene===!0?E.background:null;return y&&y.isTexture&&(y=(E.backgroundBlurriness>0?e:t).get(y)),y}function _(E){let y=!1;const D=g(E);D===null?f(o,l):D&&D.isColor&&(f(D,1),y=!0);const w=i.xr.getEnvironmentBlendMode();w==="additive"?n.buffers.color.setClear(0,0,0,1,a):w==="alpha-blend"&&n.buffers.color.setClear(0,0,0,0,a),(i.autoClear||y)&&(n.buffers.depth.setTest(!0),n.buffers.depth.setMask(!0),n.buffers.color.setMask(!0),i.clear(i.autoClearColor,i.autoClearDepth,i.autoClearStencil))}function m(E,y){const D=g(y);D&&(D.isCubeTexture||D.mapping===Pr)?(h===void 0&&(h=new be(new Ts(1,1,1),new He({name:"BackgroundCubeMaterial",uniforms:Qi(mn.backgroundCube.uniforms),vertexShader:mn.backgroundCube.vertexShader,fragmentShader:mn.backgroundCube.fragmentShader,side:Xe,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(w,C,I){this.matrixWorld.copyPosition(I.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),s.update(h)),Qn.copy(y.backgroundRotation),Qn.x*=-1,Qn.y*=-1,Qn.z*=-1,D.isCubeTexture&&D.isRenderTargetTexture===!1&&(Qn.y*=-1,Qn.z*=-1),h.material.uniforms.envMap.value=D,h.material.uniforms.flipEnvMap.value=D.isCubeTexture&&D.isRenderTargetTexture===!1?-1:1,h.material.uniforms.backgroundBlurriness.value=y.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=y.backgroundIntensity,h.material.uniforms.backgroundRotation.value.setFromMatrix4(op.makeRotationFromEuler(Qn)),h.material.toneMapped=Qt.getTransfer(D.colorSpace)!==se,(d!==D||p!==D.version||u!==i.toneMapping)&&(h.material.needsUpdate=!0,d=D,p=D.version,u=i.toneMapping),h.layers.enableAll(),E.unshift(h,h.geometry,h.material,0,0,null)):D&&D.isTexture&&(c===void 0&&(c=new be(new ws(2,2),new He({name:"BackgroundMaterial",uniforms:Qi(mn.background.uniforms),vertexShader:mn.background.vertexShader,fragmentShader:mn.background.fragmentShader,side:Yn,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),s.update(c)),c.material.uniforms.t2D.value=D,c.material.uniforms.backgroundIntensity.value=y.backgroundIntensity,c.material.toneMapped=Qt.getTransfer(D.colorSpace)!==se,D.matrixAutoUpdate===!0&&D.updateMatrix(),c.material.uniforms.uvTransform.value.copy(D.matrix),(d!==D||p!==D.version||u!==i.toneMapping)&&(c.material.needsUpdate=!0,d=D,p=D.version,u=i.toneMapping),c.layers.enableAll(),E.unshift(c,c.geometry,c.material,0,0,null))}function f(E,y){E.getRGB(ar,Mc(i)),n.buffers.color.setClear(ar.r,ar.g,ar.b,y,a)}function T(){h!==void 0&&(h.geometry.dispose(),h.material.dispose()),c!==void 0&&(c.geometry.dispose(),c.material.dispose())}return{getClearColor:function(){return o},setClearColor:function(E,y=1){o.set(E),l=y,f(o,l)},getClearAlpha:function(){return l},setClearAlpha:function(E){l=E,f(o,l)},render:_,addToRenderList:m,dispose:T}}function cp(i,t){const e=i.getParameter(i.MAX_VERTEX_ATTRIBS),n={},s=p(null);let r=s,a=!1;function o(M,A,W,k,q){let Q=!1;const X=d(k,W,A);r!==X&&(r=X,c(r.object)),Q=u(M,k,W,q),Q&&g(M,k,W,q),q!==null&&t.update(q,i.ELEMENT_ARRAY_BUFFER),(Q||a)&&(a=!1,y(M,A,W,k),q!==null&&i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,t.get(q).buffer))}function l(){return i.createVertexArray()}function c(M){return i.bindVertexArray(M)}function h(M){return i.deleteVertexArray(M)}function d(M,A,W){const k=W.wireframe===!0;let q=n[M.id];q===void 0&&(q={},n[M.id]=q);let Q=q[A.id];Q===void 0&&(Q={},q[A.id]=Q);let X=Q[k];return X===void 0&&(X=p(l()),Q[k]=X),X}function p(M){const A=[],W=[],k=[];for(let q=0;q=0){const gt=q[H];let Et=Q[H];if(Et===void 0&&(H==="instanceMatrix"&&M.instanceMatrix&&(Et=M.instanceMatrix),H==="instanceColor"&&M.instanceColor&&(Et=M.instanceColor)),gt===void 0||gt.attribute!==Et||Et&>.data!==Et.data)return!0;X++}return r.attributesNum!==X||r.index!==k}function g(M,A,W,k){const q={},Q=A.attributes;let X=0;const tt=W.getAttributes();for(const H in tt)if(tt[H].location>=0){let gt=Q[H];gt===void 0&&(H==="instanceMatrix"&&M.instanceMatrix&&(gt=M.instanceMatrix),H==="instanceColor"&&M.instanceColor&&(gt=M.instanceColor));const Et={};Et.attribute=gt,gt&>.data&&(Et.data=gt.data),q[H]=Et,X++}r.attributes=q,r.attributesNum=X,r.index=k}function _(){const M=r.newAttributes;for(let A=0,W=M.length;A=0){let st=q[tt];if(st===void 0&&(tt==="instanceMatrix"&&M.instanceMatrix&&(st=M.instanceMatrix),tt==="instanceColor"&&M.instanceColor&&(st=M.instanceColor)),st!==void 0){const gt=st.normalized,Et=st.itemSize,Ft=t.get(st);if(Ft===void 0)continue;const Xt=Ft.buffer,Y=Ft.type,nt=Ft.bytesPerElement,_t=Y===i.INT||Y===i.UNSIGNED_INT||st.gpuType===xo;if(st.isInterleavedBufferAttribute){const lt=st.data,Ct=lt.stride,Lt=st.offset;if(lt.isInstancedInterleavedBuffer){for(let Vt=0;Vt0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.HIGH_FLOAT).precision>0)return"highp";C="mediump"}return C==="mediump"&&i.getShaderPrecisionFormat(i.VERTEX_SHADER,i.MEDIUM_FLOAT).precision>0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let c=e.precision!==void 0?e.precision:"highp";const h=l(c);h!==c&&(console.warn("THREE.WebGLRenderer:",c,"not supported, using",h,"instead."),c=h);const d=e.logarithmicDepthBuffer===!0,p=e.reverseDepthBuffer===!0&&t.has("EXT_clip_control"),u=i.getParameter(i.MAX_TEXTURE_IMAGE_UNITS),g=i.getParameter(i.MAX_VERTEX_TEXTURE_IMAGE_UNITS),_=i.getParameter(i.MAX_TEXTURE_SIZE),m=i.getParameter(i.MAX_CUBE_MAP_TEXTURE_SIZE),f=i.getParameter(i.MAX_VERTEX_ATTRIBS),T=i.getParameter(i.MAX_VERTEX_UNIFORM_VECTORS),E=i.getParameter(i.MAX_VARYING_VECTORS),y=i.getParameter(i.MAX_FRAGMENT_UNIFORM_VECTORS),D=g>0,w=i.getParameter(i.MAX_SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:r,getMaxPrecision:l,textureFormatReadable:a,textureTypeReadable:o,precision:c,logarithmicDepthBuffer:d,reverseDepthBuffer:p,maxTextures:u,maxVertexTextures:g,maxTextureSize:_,maxCubemapSize:m,maxAttributes:f,maxVertexUniforms:T,maxVaryings:E,maxFragmentUniforms:y,vertexTextures:D,maxSamples:w}}function dp(i){const t=this;let e=null,n=0,s=!1,r=!1;const a=new Hn,o=new Ht,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(d,p){const u=d.length!==0||p||n!==0||s;return s=p,n=d.length,u},this.beginShadows=function(){r=!0,h(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(d,p){e=h(d,p,0)},this.setState=function(d,p,u){const g=d.clippingPlanes,_=d.clipIntersection,m=d.clipShadows,f=i.get(d);if(!s||g===null||g.length===0||r&&!m)r?h(null):c();else{const T=r?0:n,E=T*4;let y=f.clippingState||null;l.value=y,y=h(g,p,E,u);for(let D=0;D!==E;++D)y[D]=e[D];f.clippingState=y,this.numIntersection=_?this.numPlanes:0,this.numPlanes+=T}};function c(){l.value!==e&&(l.value=e,l.needsUpdate=n>0),t.numPlanes=n,t.numIntersection=0}function h(d,p,u,g){const _=d!==null?d.length:0;let m=null;if(_!==0){if(m=l.value,g!==!0||m===null){const f=u+_*4,T=p.matrixWorldInverse;o.getNormalMatrix(T),(m===null||m.length0){const c=new ou(l.height);return c.fromEquirectangularTexture(i,a),t.set(a,c),a.addEventListener("dispose",s),e(c.texture,a.mapping)}else return null}}return a}function s(a){const o=a.target;o.removeEventListener("dispose",s);const l=t.get(o);l!==void 0&&(t.delete(o),l.dispose())}function r(){t=new WeakMap}return{get:n,dispose:r}}const ki=4,vl=[.125,.215,.35,.446,.526,.582],si=20,ha=new Ac,xl=new ot;let ua=null,da=0,fa=0,pa=!1;const ei=(1+Math.sqrt(5))/2,Ni=1/ei,Ml=[new P(-ei,Ni,0),new P(ei,Ni,0),new P(-Ni,0,ei),new P(Ni,0,ei),new P(0,ei,-Ni),new P(0,ei,Ni),new P(-1,1,-1),new P(1,1,-1),new P(-1,1,1),new P(1,1,1)];class Sl{constructor(t){this._renderer=t,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,n=.1,s=100){ua=this._renderer.getRenderTarget(),da=this._renderer.getActiveCubeFace(),fa=this._renderer.getActiveMipmapLevel(),pa=this._renderer.xr.enabled,this._renderer.xr.enabled=!1,this._setSize(256);const r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(t,n,s,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t,e=null){return this._fromTexture(t,e)}fromCubemap(t,e=null){return this._fromTexture(t,e)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=bl(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=El(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose()}_setSize(t){this._lodMax=Math.floor(Math.log2(t)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let t=0;t2?E:0,E,E),h.setRenderTarget(s),_&&h.render(g,o),h.render(t,o)}g.geometry.dispose(),g.material.dispose(),h.toneMapping=p,h.autoClear=d,t.background=m}_textureToCubeUV(t,e){const n=this._renderer,s=t.mapping===ji||t.mapping===Zi;s?(this._cubemapMaterial===null&&(this._cubemapMaterial=bl()),this._cubemapMaterial.uniforms.flipEnvMap.value=t.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=El());const r=s?this._cubemapMaterial:this._equirectMaterial,a=new be(this._lodPlanes[0],r),o=r.uniforms;o.envMap.value=t;const l=this._cubeSize;or(e,0,0,3*l,2*l),n.setRenderTarget(e),n.render(a,ha)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;const s=this._lodPlanes.length;for(let r=1;rsi&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to ${si}`);const f=[];let T=0;for(let C=0;CE-ki?s-E+ki:0),w=4*(this._cubeSize-y);or(e,D,w,3*y,2*y),l.setRenderTarget(e),l.render(d,ha)}}function pp(i){const t=[],e=[],n=[];let s=i;const r=i-ki+1+vl.length;for(let a=0;ai-ki?l=vl[a-i+ki-1]:a===0&&(l=0),n.push(l);const c=1/(o-2),h=-c,d=1+c,p=[h,h,d,h,d,d,h,h,d,d,h,d],u=6,g=6,_=3,m=2,f=1,T=new Float32Array(_*g*u),E=new Float32Array(m*g*u),y=new Float32Array(f*g*u);for(let w=0;w2?0:-1,S=[C,I,0,C+2/3,I,0,C+2/3,I+1,0,C,I,0,C+2/3,I+1,0,C,I+1,0];T.set(S,_*g*w),E.set(p,m*g*w);const M=[w,w,w,w,w,w];y.set(M,f*g*w)}const D=new ge;D.setAttribute("position",new he(T,_)),D.setAttribute("uv",new he(E,m)),D.setAttribute("faceIndex",new he(y,f)),t.push(D),s>ki&&s--}return{lodPlanes:t,sizeLods:e,sigmas:n}}function yl(i,t,e){const n=new fn(i,t,e);return n.texture.mapping=Pr,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function or(i,t,e,n,s){i.viewport.set(t,e,n,s),i.scissor.set(t,e,n,s)}function mp(i,t,e){const n=new Float32Array(si),s=new P(0,1,0);return new He({name:"SphericalGaussianBlur",defines:{n:si,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/e,CUBEUV_MAX_MIP:`${i}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:s}},vertexShader:Co(),fragmentShader:` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + uniform int samples; + uniform float weights[ n ]; + uniform bool latitudinal; + uniform float dTheta; + uniform float mipInt; + uniform vec3 poleAxis; + + #define ENVMAP_TYPE_CUBE_UV + #include + + vec3 getSample( float theta, vec3 axis ) { + + float cosTheta = cos( theta ); + // Rodrigues' axis-angle rotation + vec3 sampleDirection = vOutputDirection * cosTheta + + cross( axis, vOutputDirection ) * sin( theta ) + + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); + + return bilinearCubeUV( envMap, sampleDirection, mipInt ); + + } + + void main() { + + vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); + + if ( all( equal( axis, vec3( 0.0 ) ) ) ) { + + axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); + + } + + axis = normalize( axis ); + + gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); + gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); + + for ( int i = 1; i < n; i++ ) { + + if ( i >= samples ) { + + break; + + } + + float theta = dTheta * float( i ); + gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); + gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); + + } + + } + `,blending:Cn,depthTest:!1,depthWrite:!1})}function El(){return new He({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:Co(),fragmentShader:` + + precision mediump float; + precision mediump int; + + varying vec3 vOutputDirection; + + uniform sampler2D envMap; + + #include + + void main() { + + vec3 outputDirection = normalize( vOutputDirection ); + vec2 uv = equirectUv( outputDirection ); + + gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); + + } + `,blending:Cn,depthTest:!1,depthWrite:!1})}function bl(){return new He({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:Co(),fragmentShader:` + + precision mediump float; + precision mediump int; + + uniform float flipEnvMap; + + varying vec3 vOutputDirection; + + uniform samplerCube envMap; + + void main() { + + gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); + + } + `,blending:Cn,depthTest:!1,depthWrite:!1})}function Co(){return` + + precision mediump float; + precision mediump int; + + attribute float faceIndex; + + varying vec3 vOutputDirection; + + // RH coordinate system; PMREM face-indexing convention + vec3 getDirection( vec2 uv, float face ) { + + uv = 2.0 * uv - 1.0; + + vec3 direction = vec3( uv, 1.0 ); + + if ( face == 0.0 ) { + + direction = direction.zyx; // ( 1, v, u ) pos x + + } else if ( face == 1.0 ) { + + direction = direction.xzy; + direction.xz *= -1.0; // ( -u, 1, -v ) pos y + + } else if ( face == 2.0 ) { + + direction.x *= -1.0; // ( -u, v, 1 ) pos z + + } else if ( face == 3.0 ) { + + direction = direction.zyx; + direction.xz *= -1.0; // ( -1, v, -u ) neg x + + } else if ( face == 4.0 ) { + + direction = direction.xzy; + direction.xy *= -1.0; // ( -u, -1, v ) neg y + + } else if ( face == 5.0 ) { + + direction.z *= -1.0; // ( u, v, -1 ) neg z + + } + + return direction; + + } + + void main() { + + vOutputDirection = getDirection( uv, faceIndex ); + gl_Position = vec4( position, 1.0 ); + + } + `}function gp(i){let t=new WeakMap,e=null;function n(o){if(o&&o.isTexture){const l=o.mapping,c=l===Da||l===La,h=l===ji||l===Zi;if(c||h){let d=t.get(o);const p=d!==void 0?d.texture.pmremVersion:0;if(o.isRenderTargetTexture&&o.pmremVersion!==p)return e===null&&(e=new Sl(i)),d=c?e.fromEquirectangular(o,d):e.fromCubemap(o,d),d.texture.pmremVersion=o.pmremVersion,t.set(o,d),d.texture;if(d!==void 0)return d.texture;{const u=o.image;return c&&u&&u.height>0||h&&u&&s(u)?(e===null&&(e=new Sl(i)),d=c?e.fromEquirectangular(o):e.fromCubemap(o),d.texture.pmremVersion=o.pmremVersion,t.set(o,d),o.addEventListener("dispose",r),d.texture):null}}}return o}function s(o){let l=0;const c=6;for(let h=0;ht.maxTextureSize&&(D=Math.ceil(y/t.maxTextureSize),y=t.maxTextureSize);const w=new Float32Array(y*D*4*d),C=new gc(w,y,D,d);C.type=xn,C.needsUpdate=!0;const I=E*4;for(let M=0;M0)return i;const s=t*e;let r=wl[s];if(r===void 0&&(r=new Float32Array(s),wl[s]=r),t!==0){n.toArray(r,0);for(let a=1,o=0;a!==t;++a)o+=e,i[a].toArray(r,o)}return r}function Te(i,t){if(i.length!==t.length)return!1;for(let e=0,n=i.length;e":" "} ${o}: ${e[a]}`)}return n.join(` +`)}const Ul=new Ht;function gm(i){Qt._getMatrix(Ul,Qt.workingColorSpace,i);const t=`mat3( ${Ul.elements.map(e=>e.toFixed(4))} )`;switch(Qt.getTransfer(i)){case yr:return[t,"LinearTransferOETF"];case se:return[t,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",i),[t,"LinearTransferOETF"]}}function Il(i,t,e){const n=i.getShaderParameter(t,i.COMPILE_STATUS),s=i.getShaderInfoLog(t).trim();if(n&&s==="")return"";const r=/ERROR: 0:(\d+)/.exec(s);if(r){const a=parseInt(r[1]);return e.toUpperCase()+` + +`+s+` + +`+mm(i.getShaderSource(t),a)}else return s}function _m(i,t){const e=gm(t);return[`vec4 ${i}( vec4 value ) {`,` return ${e[1]}( vec4( value.rgb * ${e[0]}, value.a ) );`,"}"].join(` +`)}function vm(i,t){let e;switch(t){case vh:e="Linear";break;case xh:e="Reinhard";break;case Mh:e="Cineon";break;case ec:e="ACESFilmic";break;case yh:e="AgX";break;case Eh:e="Neutral";break;case Sh:e="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",t),e="Linear"}return"vec3 "+i+"( vec3 color ) { return "+e+"ToneMapping( color ); }"}const lr=new P;function xm(){Qt.getLuminanceCoefficients(lr);const i=lr.x.toFixed(4),t=lr.y.toFixed(4),e=lr.z.toFixed(4);return["float luminance( const in vec3 rgb ) {",` const vec3 weights = vec3( ${i}, ${t}, ${e} );`," return dot( weights, rgb );","}"].join(` +`)}function Mm(i){return[i.extensionClipCullDistance?"#extension GL_ANGLE_clip_cull_distance : require":"",i.extensionMultiDraw?"#extension GL_ANGLE_multi_draw : require":""].filter(gs).join(` +`)}function Sm(i){const t=[];for(const e in i){const n=i[e];n!==!1&&t.push("#define "+e+" "+n)}return t.join(` +`)}function ym(i,t){const e={},n=i.getProgramParameter(t,i.ACTIVE_ATTRIBUTES);for(let s=0;s/gm;function fo(i){return i.replace(Em,Tm)}const bm=new Map;function Tm(i,t){let e=Gt[t];if(e===void 0){const n=bm.get(t);if(n!==void 0)e=Gt[n],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',t,n);else throw new Error("Can not resolve #include <"+t+">")}return fo(e)}const wm=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function Ol(i){return i.replace(wm,Am)}function Am(i,t,e,n){let s="";for(let r=parseInt(t);r0&&(m+=` +`),f=["#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g].filter(gs).join(` +`),f.length>0&&(f+=` +`)):(m=[Bl(e),"#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g,e.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",e.batching?"#define USE_BATCHING":"",e.batchingColor?"#define USE_BATCHING_COLOR":"",e.instancing?"#define USE_INSTANCING":"",e.instancingColor?"#define USE_INSTANCING_COLOR":"",e.instancingMorph?"#define USE_INSTANCING_MORPH":"",e.useFog&&e.fog?"#define USE_FOG":"",e.useFog&&e.fogExp2?"#define FOG_EXP2":"",e.map?"#define USE_MAP":"",e.envMap?"#define USE_ENVMAP":"",e.envMap?"#define "+h:"",e.lightMap?"#define USE_LIGHTMAP":"",e.aoMap?"#define USE_AOMAP":"",e.bumpMap?"#define USE_BUMPMAP":"",e.normalMap?"#define USE_NORMALMAP":"",e.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",e.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",e.displacementMap?"#define USE_DISPLACEMENTMAP":"",e.emissiveMap?"#define USE_EMISSIVEMAP":"",e.anisotropy?"#define USE_ANISOTROPY":"",e.anisotropyMap?"#define USE_ANISOTROPYMAP":"",e.clearcoatMap?"#define USE_CLEARCOATMAP":"",e.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",e.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",e.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",e.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",e.specularMap?"#define USE_SPECULARMAP":"",e.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",e.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",e.roughnessMap?"#define USE_ROUGHNESSMAP":"",e.metalnessMap?"#define USE_METALNESSMAP":"",e.alphaMap?"#define USE_ALPHAMAP":"",e.alphaHash?"#define USE_ALPHAHASH":"",e.transmission?"#define USE_TRANSMISSION":"",e.transmissionMap?"#define USE_TRANSMISSIONMAP":"",e.thicknessMap?"#define USE_THICKNESSMAP":"",e.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",e.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",e.mapUv?"#define MAP_UV "+e.mapUv:"",e.alphaMapUv?"#define ALPHAMAP_UV "+e.alphaMapUv:"",e.lightMapUv?"#define LIGHTMAP_UV "+e.lightMapUv:"",e.aoMapUv?"#define AOMAP_UV "+e.aoMapUv:"",e.emissiveMapUv?"#define EMISSIVEMAP_UV "+e.emissiveMapUv:"",e.bumpMapUv?"#define BUMPMAP_UV "+e.bumpMapUv:"",e.normalMapUv?"#define NORMALMAP_UV "+e.normalMapUv:"",e.displacementMapUv?"#define DISPLACEMENTMAP_UV "+e.displacementMapUv:"",e.metalnessMapUv?"#define METALNESSMAP_UV "+e.metalnessMapUv:"",e.roughnessMapUv?"#define ROUGHNESSMAP_UV "+e.roughnessMapUv:"",e.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+e.anisotropyMapUv:"",e.clearcoatMapUv?"#define CLEARCOATMAP_UV "+e.clearcoatMapUv:"",e.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+e.clearcoatNormalMapUv:"",e.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+e.clearcoatRoughnessMapUv:"",e.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+e.iridescenceMapUv:"",e.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+e.iridescenceThicknessMapUv:"",e.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+e.sheenColorMapUv:"",e.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+e.sheenRoughnessMapUv:"",e.specularMapUv?"#define SPECULARMAP_UV "+e.specularMapUv:"",e.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+e.specularColorMapUv:"",e.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+e.specularIntensityMapUv:"",e.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+e.transmissionMapUv:"",e.thicknessMapUv?"#define THICKNESSMAP_UV "+e.thicknessMapUv:"",e.vertexTangents&&e.flatShading===!1?"#define USE_TANGENT":"",e.vertexColors?"#define USE_COLOR":"",e.vertexAlphas?"#define USE_COLOR_ALPHA":"",e.vertexUv1s?"#define USE_UV1":"",e.vertexUv2s?"#define USE_UV2":"",e.vertexUv3s?"#define USE_UV3":"",e.pointsUvs?"#define USE_POINTS_UV":"",e.flatShading?"#define FLAT_SHADED":"",e.skinning?"#define USE_SKINNING":"",e.morphTargets?"#define USE_MORPHTARGETS":"",e.morphNormals&&e.flatShading===!1?"#define USE_MORPHNORMALS":"",e.morphColors?"#define USE_MORPHCOLORS":"",e.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+e.morphTextureStride:"",e.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+e.morphTargetsCount:"",e.doubleSided?"#define DOUBLE_SIDED":"",e.flipSided?"#define FLIP_SIDED":"",e.shadowMapEnabled?"#define USE_SHADOWMAP":"",e.shadowMapEnabled?"#define "+l:"",e.sizeAttenuation?"#define USE_SIZEATTENUATION":"",e.numLightProbes>0?"#define USE_LIGHT_PROBES":"",e.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",e.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING"," attribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR"," attribute vec3 instanceColor;","#endif","#ifdef USE_INSTANCING_MORPH"," uniform sampler2D morphTexture;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1"," attribute vec2 uv1;","#endif","#ifdef USE_UV2"," attribute vec2 uv2;","#endif","#ifdef USE_UV3"," attribute vec2 uv3;","#endif","#ifdef USE_TANGENT"," attribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )"," attribute vec4 color;","#elif defined( USE_COLOR )"," attribute vec3 color;","#endif","#ifdef USE_SKINNING"," attribute vec4 skinIndex;"," attribute vec4 skinWeight;","#endif",` +`].filter(gs).join(` +`),f=[Bl(e),"#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g,e.useFog&&e.fog?"#define USE_FOG":"",e.useFog&&e.fogExp2?"#define FOG_EXP2":"",e.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",e.map?"#define USE_MAP":"",e.matcap?"#define USE_MATCAP":"",e.envMap?"#define USE_ENVMAP":"",e.envMap?"#define "+c:"",e.envMap?"#define "+h:"",e.envMap?"#define "+d:"",p?"#define CUBEUV_TEXEL_WIDTH "+p.texelWidth:"",p?"#define CUBEUV_TEXEL_HEIGHT "+p.texelHeight:"",p?"#define CUBEUV_MAX_MIP "+p.maxMip+".0":"",e.lightMap?"#define USE_LIGHTMAP":"",e.aoMap?"#define USE_AOMAP":"",e.bumpMap?"#define USE_BUMPMAP":"",e.normalMap?"#define USE_NORMALMAP":"",e.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",e.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",e.emissiveMap?"#define USE_EMISSIVEMAP":"",e.anisotropy?"#define USE_ANISOTROPY":"",e.anisotropyMap?"#define USE_ANISOTROPYMAP":"",e.clearcoat?"#define USE_CLEARCOAT":"",e.clearcoatMap?"#define USE_CLEARCOATMAP":"",e.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",e.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",e.dispersion?"#define USE_DISPERSION":"",e.iridescence?"#define USE_IRIDESCENCE":"",e.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",e.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",e.specularMap?"#define USE_SPECULARMAP":"",e.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",e.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",e.roughnessMap?"#define USE_ROUGHNESSMAP":"",e.metalnessMap?"#define USE_METALNESSMAP":"",e.alphaMap?"#define USE_ALPHAMAP":"",e.alphaTest?"#define USE_ALPHATEST":"",e.alphaHash?"#define USE_ALPHAHASH":"",e.sheen?"#define USE_SHEEN":"",e.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",e.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",e.transmission?"#define USE_TRANSMISSION":"",e.transmissionMap?"#define USE_TRANSMISSIONMAP":"",e.thicknessMap?"#define USE_THICKNESSMAP":"",e.vertexTangents&&e.flatShading===!1?"#define USE_TANGENT":"",e.vertexColors||e.instancingColor||e.batchingColor?"#define USE_COLOR":"",e.vertexAlphas?"#define USE_COLOR_ALPHA":"",e.vertexUv1s?"#define USE_UV1":"",e.vertexUv2s?"#define USE_UV2":"",e.vertexUv3s?"#define USE_UV3":"",e.pointsUvs?"#define USE_POINTS_UV":"",e.gradientMap?"#define USE_GRADIENTMAP":"",e.flatShading?"#define FLAT_SHADED":"",e.doubleSided?"#define DOUBLE_SIDED":"",e.flipSided?"#define FLIP_SIDED":"",e.shadowMapEnabled?"#define USE_SHADOWMAP":"",e.shadowMapEnabled?"#define "+l:"",e.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",e.numLightProbes>0?"#define USE_LIGHT_PROBES":"",e.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",e.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",e.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",e.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",e.toneMapping!==Wn?"#define TONE_MAPPING":"",e.toneMapping!==Wn?Gt.tonemapping_pars_fragment:"",e.toneMapping!==Wn?vm("toneMapping",e.toneMapping):"",e.dithering?"#define DITHERING":"",e.opaque?"#define OPAQUE":"",Gt.colorspace_pars_fragment,_m("linearToOutputTexel",e.outputColorSpace),xm(),e.useDepthPacking?"#define DEPTH_PACKING "+e.depthPacking:"",` +`].filter(gs).join(` +`)),a=fo(a),a=Nl(a,e),a=Fl(a,e),o=fo(o),o=Nl(o,e),o=Fl(o,e),a=Ol(a),o=Ol(o),e.isRawShaderMaterial!==!0&&(T=`#version 300 es +`,m=[u,"#define attribute in","#define varying out","#define texture2D texture"].join(` +`)+` +`+m,f=["#define varying in",e.glslVersion===Fo?"":"layout(location = 0) out highp vec4 pc_fragColor;",e.glslVersion===Fo?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join(` +`)+` +`+f);const E=T+m+a,y=T+f+o,D=Ll(s,s.VERTEX_SHADER,E),w=Ll(s,s.FRAGMENT_SHADER,y);s.attachShader(_,D),s.attachShader(_,w),e.index0AttributeName!==void 0?s.bindAttribLocation(_,0,e.index0AttributeName):e.morphTargets===!0&&s.bindAttribLocation(_,0,"position"),s.linkProgram(_);function C(A){if(i.debug.checkShaderErrors){const W=s.getProgramInfoLog(_).trim(),k=s.getShaderInfoLog(D).trim(),q=s.getShaderInfoLog(w).trim();let Q=!0,X=!0;if(s.getProgramParameter(_,s.LINK_STATUS)===!1)if(Q=!1,typeof i.debug.onShaderError=="function")i.debug.onShaderError(s,_,D,w);else{const tt=Il(s,D,"vertex"),H=Il(s,w,"fragment");console.error("THREE.WebGLProgram: Shader Error "+s.getError()+" - VALIDATE_STATUS "+s.getProgramParameter(_,s.VALIDATE_STATUS)+` + +Material Name: `+A.name+` +Material Type: `+A.type+` + +Program Info Log: `+W+` +`+tt+` +`+H)}else W!==""?console.warn("THREE.WebGLProgram: Program Info Log:",W):(k===""||q==="")&&(X=!1);X&&(A.diagnostics={runnable:Q,programLog:W,vertexShader:{log:k,prefix:m},fragmentShader:{log:q,prefix:f}})}s.deleteShader(D),s.deleteShader(w),I=new _r(s,_),S=ym(s,_)}let I;this.getUniforms=function(){return I===void 0&&C(this),I};let S;this.getAttributes=function(){return S===void 0&&C(this),S};let M=e.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return M===!1&&(M=s.getProgramParameter(_,fm)),M},this.destroy=function(){n.releaseStatesOfProgram(this),s.deleteProgram(_),this.program=void 0},this.type=e.shaderType,this.name=e.shaderName,this.id=pm++,this.cacheKey=t,this.usedTimes=1,this.program=_,this.vertexShader=D,this.fragmentShader=w,this}let Im=0;class Nm{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(t){const e=t.vertexShader,n=t.fragmentShader,s=this._getShaderStage(e),r=this._getShaderStage(n),a=this._getShaderCacheForMaterial(t);return a.has(s)===!1&&(a.add(s),s.usedTimes++),a.has(r)===!1&&(a.add(r),r.usedTimes++),this}remove(t){const e=this.materialCache.get(t);for(const n of e)n.usedTimes--,n.usedTimes===0&&this.shaderCache.delete(n.code);return this.materialCache.delete(t),this}getVertexShaderID(t){return this._getShaderStage(t.vertexShader).id}getFragmentShaderID(t){return this._getShaderStage(t.fragmentShader).id}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(t){const e=this.materialCache;let n=e.get(t);return n===void 0&&(n=new Set,e.set(t,n)),n}_getShaderStage(t){const e=this.shaderCache;let n=e.get(t);return n===void 0&&(n=new Fm(t),e.set(t,n)),n}}class Fm{constructor(t){this.id=Im++,this.code=t,this.usedTimes=0}}function Om(i,t,e,n,s,r,a){const o=new wo,l=new Nm,c=new Set,h=[],d=s.logarithmicDepthBuffer,p=s.vertexTextures;let u=s.precision;const g={MeshDepthMaterial:"depth",MeshDistanceMaterial:"distanceRGBA",MeshNormalMaterial:"normal",MeshBasicMaterial:"basic",MeshLambertMaterial:"lambert",MeshPhongMaterial:"phong",MeshToonMaterial:"toon",MeshStandardMaterial:"physical",MeshPhysicalMaterial:"physical",MeshMatcapMaterial:"matcap",LineBasicMaterial:"basic",LineDashedMaterial:"dashed",PointsMaterial:"points",ShadowMaterial:"shadow",SpriteMaterial:"sprite"};function _(S){return c.add(S),S===0?"uv":`uv${S}`}function m(S,M,A,W,k){const q=W.fog,Q=k.geometry,X=S.isMeshStandardMaterial?W.environment:null,tt=(S.isMeshStandardMaterial?e:t).get(S.envMap||X),H=tt&&tt.mapping===Pr?tt.image.height:null,st=g[S.type];S.precision!==null&&(u=s.getMaxPrecision(S.precision),u!==S.precision&&console.warn("THREE.WebGLProgram.getParameters:",S.precision,"not supported, using",u,"instead."));const gt=Q.morphAttributes.position||Q.morphAttributes.normal||Q.morphAttributes.color,Et=gt!==void 0?gt.length:0;let Ft=0;Q.morphAttributes.position!==void 0&&(Ft=1),Q.morphAttributes.normal!==void 0&&(Ft=2),Q.morphAttributes.color!==void 0&&(Ft=3);let Xt,Y,nt,_t;if(st){const $t=mn[st];Xt=$t.vertexShader,Y=$t.fragmentShader}else Xt=S.vertexShader,Y=S.fragmentShader,l.update(S),nt=l.getVertexShaderID(S),_t=l.getFragmentShaderID(S);const lt=i.getRenderTarget(),Ct=i.state.buffers.depth.getReversed(),Lt=k.isInstancedMesh===!0,Vt=k.isBatchedMesh===!0,ie=!!S.map,Wt=!!S.matcap,ue=!!tt,R=!!S.aoMap,ye=!!S.lightMap,qt=!!S.bumpMap,jt=!!S.normalMap,At=!!S.displacementMap,le=!!S.emissiveMap,Rt=!!S.metalnessMap,b=!!S.roughnessMap,v=S.anisotropy>0,F=S.clearcoat>0,K=S.dispersion>0,J=S.iridescence>0,j=S.sheen>0,Tt=S.transmission>0,dt=v&&!!S.anisotropyMap,V=F&&!!S.clearcoatMap,at=F&&!!S.clearcoatNormalMap,Z=F&&!!S.clearcoatRoughnessMap,it=J&&!!S.iridescenceMap,pt=J&&!!S.iridescenceThicknessMap,wt=j&&!!S.sheenColorMap,ht=j&&!!S.sheenRoughnessMap,Bt=!!S.specularMap,Ut=!!S.specularColorMap,Zt=!!S.specularIntensityMap,L=Tt&&!!S.transmissionMap,rt=Tt&&!!S.thicknessMap,G=!!S.gradientMap,$=!!S.alphaMap,ft=S.alphaTest>0,ut=!!S.alphaHash,Ot=!!S.extensions;let ce=Wn;S.toneMapped&&(lt===null||lt.isXRRenderTarget===!0)&&(ce=i.toneMapping);const Ae={shaderID:st,shaderType:S.type,shaderName:S.name,vertexShader:Xt,fragmentShader:Y,defines:S.defines,customVertexShaderID:nt,customFragmentShaderID:_t,isRawShaderMaterial:S.isRawShaderMaterial===!0,glslVersion:S.glslVersion,precision:u,batching:Vt,batchingColor:Vt&&k._colorsTexture!==null,instancing:Lt,instancingColor:Lt&&k.instanceColor!==null,instancingMorph:Lt&&k.morphTexture!==null,supportsVertexTextures:p,outputColorSpace:lt===null?i.outputColorSpace:lt.isXRRenderTarget===!0?lt.texture.colorSpace:Ji,alphaToCoverage:!!S.alphaToCoverage,map:ie,matcap:Wt,envMap:ue,envMapMode:ue&&tt.mapping,envMapCubeUVHeight:H,aoMap:R,lightMap:ye,bumpMap:qt,normalMap:jt,displacementMap:p&&At,emissiveMap:le,normalMapObjectSpace:jt&&S.normalMapType===Ah,normalMapTangentSpace:jt&&S.normalMapType===dc,metalnessMap:Rt,roughnessMap:b,anisotropy:v,anisotropyMap:dt,clearcoat:F,clearcoatMap:V,clearcoatNormalMap:at,clearcoatRoughnessMap:Z,dispersion:K,iridescence:J,iridescenceMap:it,iridescenceThicknessMap:pt,sheen:j,sheenColorMap:wt,sheenRoughnessMap:ht,specularMap:Bt,specularColorMap:Ut,specularIntensityMap:Zt,transmission:Tt,transmissionMap:L,thicknessMap:rt,gradientMap:G,opaque:S.transparent===!1&&S.blending===Vi&&S.alphaToCoverage===!1,alphaMap:$,alphaTest:ft,alphaHash:ut,combine:S.combine,mapUv:ie&&_(S.map.channel),aoMapUv:R&&_(S.aoMap.channel),lightMapUv:ye&&_(S.lightMap.channel),bumpMapUv:qt&&_(S.bumpMap.channel),normalMapUv:jt&&_(S.normalMap.channel),displacementMapUv:At&&_(S.displacementMap.channel),emissiveMapUv:le&&_(S.emissiveMap.channel),metalnessMapUv:Rt&&_(S.metalnessMap.channel),roughnessMapUv:b&&_(S.roughnessMap.channel),anisotropyMapUv:dt&&_(S.anisotropyMap.channel),clearcoatMapUv:V&&_(S.clearcoatMap.channel),clearcoatNormalMapUv:at&&_(S.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:Z&&_(S.clearcoatRoughnessMap.channel),iridescenceMapUv:it&&_(S.iridescenceMap.channel),iridescenceThicknessMapUv:pt&&_(S.iridescenceThicknessMap.channel),sheenColorMapUv:wt&&_(S.sheenColorMap.channel),sheenRoughnessMapUv:ht&&_(S.sheenRoughnessMap.channel),specularMapUv:Bt&&_(S.specularMap.channel),specularColorMapUv:Ut&&_(S.specularColorMap.channel),specularIntensityMapUv:Zt&&_(S.specularIntensityMap.channel),transmissionMapUv:L&&_(S.transmissionMap.channel),thicknessMapUv:rt&&_(S.thicknessMap.channel),alphaMapUv:$&&_(S.alphaMap.channel),vertexTangents:!!Q.attributes.tangent&&(jt||v),vertexColors:S.vertexColors,vertexAlphas:S.vertexColors===!0&&!!Q.attributes.color&&Q.attributes.color.itemSize===4,pointsUvs:k.isPoints===!0&&!!Q.attributes.uv&&(ie||$),fog:!!q,useFog:S.fog===!0,fogExp2:!!q&&q.isFogExp2,flatShading:S.flatShading===!0,sizeAttenuation:S.sizeAttenuation===!0,logarithmicDepthBuffer:d,reverseDepthBuffer:Ct,skinning:k.isSkinnedMesh===!0,morphTargets:Q.morphAttributes.position!==void 0,morphNormals:Q.morphAttributes.normal!==void 0,morphColors:Q.morphAttributes.color!==void 0,morphTargetsCount:Et,morphTextureStride:Ft,numDirLights:M.directional.length,numPointLights:M.point.length,numSpotLights:M.spot.length,numSpotLightMaps:M.spotLightMap.length,numRectAreaLights:M.rectArea.length,numHemiLights:M.hemi.length,numDirLightShadows:M.directionalShadowMap.length,numPointLightShadows:M.pointShadowMap.length,numSpotLightShadows:M.spotShadowMap.length,numSpotLightShadowsWithMaps:M.numSpotLightShadowsWithMaps,numLightProbes:M.numLightProbes,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:S.dithering,shadowMapEnabled:i.shadowMap.enabled&&A.length>0,shadowMapType:i.shadowMap.type,toneMapping:ce,decodeVideoTexture:ie&&S.map.isVideoTexture===!0&&Qt.getTransfer(S.map.colorSpace)===se,decodeVideoTextureEmissive:le&&S.emissiveMap.isVideoTexture===!0&&Qt.getTransfer(S.emissiveMap.colorSpace)===se,premultipliedAlpha:S.premultipliedAlpha,doubleSided:S.side===gn,flipSided:S.side===Xe,useDepthPacking:S.depthPacking>=0,depthPacking:S.depthPacking||0,index0AttributeName:S.index0AttributeName,extensionClipCullDistance:Ot&&S.extensions.clipCullDistance===!0&&n.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(Ot&&S.extensions.multiDraw===!0||Vt)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:S.customProgramCacheKey()};return Ae.vertexUv1s=c.has(1),Ae.vertexUv2s=c.has(2),Ae.vertexUv3s=c.has(3),c.clear(),Ae}function f(S){const M=[];if(S.shaderID?M.push(S.shaderID):(M.push(S.customVertexShaderID),M.push(S.customFragmentShaderID)),S.defines!==void 0)for(const A in S.defines)M.push(A),M.push(S.defines[A]);return S.isRawShaderMaterial===!1&&(T(M,S),E(M,S),M.push(i.outputColorSpace)),M.push(S.customProgramCacheKey),M.join()}function T(S,M){S.push(M.precision),S.push(M.outputColorSpace),S.push(M.envMapMode),S.push(M.envMapCubeUVHeight),S.push(M.mapUv),S.push(M.alphaMapUv),S.push(M.lightMapUv),S.push(M.aoMapUv),S.push(M.bumpMapUv),S.push(M.normalMapUv),S.push(M.displacementMapUv),S.push(M.emissiveMapUv),S.push(M.metalnessMapUv),S.push(M.roughnessMapUv),S.push(M.anisotropyMapUv),S.push(M.clearcoatMapUv),S.push(M.clearcoatNormalMapUv),S.push(M.clearcoatRoughnessMapUv),S.push(M.iridescenceMapUv),S.push(M.iridescenceThicknessMapUv),S.push(M.sheenColorMapUv),S.push(M.sheenRoughnessMapUv),S.push(M.specularMapUv),S.push(M.specularColorMapUv),S.push(M.specularIntensityMapUv),S.push(M.transmissionMapUv),S.push(M.thicknessMapUv),S.push(M.combine),S.push(M.fogExp2),S.push(M.sizeAttenuation),S.push(M.morphTargetsCount),S.push(M.morphAttributeCount),S.push(M.numDirLights),S.push(M.numPointLights),S.push(M.numSpotLights),S.push(M.numSpotLightMaps),S.push(M.numHemiLights),S.push(M.numRectAreaLights),S.push(M.numDirLightShadows),S.push(M.numPointLightShadows),S.push(M.numSpotLightShadows),S.push(M.numSpotLightShadowsWithMaps),S.push(M.numLightProbes),S.push(M.shadowMapType),S.push(M.toneMapping),S.push(M.numClippingPlanes),S.push(M.numClipIntersection),S.push(M.depthPacking)}function E(S,M){o.disableAll(),M.supportsVertexTextures&&o.enable(0),M.instancing&&o.enable(1),M.instancingColor&&o.enable(2),M.instancingMorph&&o.enable(3),M.matcap&&o.enable(4),M.envMap&&o.enable(5),M.normalMapObjectSpace&&o.enable(6),M.normalMapTangentSpace&&o.enable(7),M.clearcoat&&o.enable(8),M.iridescence&&o.enable(9),M.alphaTest&&o.enable(10),M.vertexColors&&o.enable(11),M.vertexAlphas&&o.enable(12),M.vertexUv1s&&o.enable(13),M.vertexUv2s&&o.enable(14),M.vertexUv3s&&o.enable(15),M.vertexTangents&&o.enable(16),M.anisotropy&&o.enable(17),M.alphaHash&&o.enable(18),M.batching&&o.enable(19),M.dispersion&&o.enable(20),M.batchingColor&&o.enable(21),S.push(o.mask),o.disableAll(),M.fog&&o.enable(0),M.useFog&&o.enable(1),M.flatShading&&o.enable(2),M.logarithmicDepthBuffer&&o.enable(3),M.reverseDepthBuffer&&o.enable(4),M.skinning&&o.enable(5),M.morphTargets&&o.enable(6),M.morphNormals&&o.enable(7),M.morphColors&&o.enable(8),M.premultipliedAlpha&&o.enable(9),M.shadowMapEnabled&&o.enable(10),M.doubleSided&&o.enable(11),M.flipSided&&o.enable(12),M.useDepthPacking&&o.enable(13),M.dithering&&o.enable(14),M.transmission&&o.enable(15),M.sheen&&o.enable(16),M.opaque&&o.enable(17),M.pointsUvs&&o.enable(18),M.decodeVideoTexture&&o.enable(19),M.decodeVideoTextureEmissive&&o.enable(20),M.alphaToCoverage&&o.enable(21),S.push(o.mask)}function y(S){const M=g[S.type];let A;if(M){const W=mn[M];A=Tr.clone(W.uniforms)}else A=S.uniforms;return A}function D(S,M){let A;for(let W=0,k=h.length;W0?n.push(f):u.transparent===!0?s.push(f):e.push(f)}function l(d,p,u,g,_,m){const f=a(d,p,u,g,_,m);u.transmission>0?n.unshift(f):u.transparent===!0?s.unshift(f):e.unshift(f)}function c(d,p){e.length>1&&e.sort(d||zm),n.length>1&&n.sort(p||zl),s.length>1&&s.sort(p||zl)}function h(){for(let d=t,p=i.length;d=r.length?(a=new kl,r.push(a)):a=r[s],a}function e(){i=new WeakMap}return{get:t,dispose:e}}function Hm(){const i={};return{get:function(t){if(i[t.id]!==void 0)return i[t.id];let e;switch(t.type){case"DirectionalLight":e={direction:new P,color:new ot};break;case"SpotLight":e={position:new P,direction:new P,color:new ot,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":e={position:new P,color:new ot,distance:0,decay:0};break;case"HemisphereLight":e={direction:new P,skyColor:new ot,groundColor:new ot};break;case"RectAreaLight":e={color:new ot,position:new P,halfWidth:new P,halfHeight:new P};break}return i[t.id]=e,e}}}function Vm(){const i={};return{get:function(t){if(i[t.id]!==void 0)return i[t.id];let e;switch(t.type){case"DirectionalLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new St};break;case"SpotLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new St};break;case"PointLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new St,shadowCameraNear:1,shadowCameraFar:1e3};break}return i[t.id]=e,e}}}let Gm=0;function Wm(i,t){return(t.castShadow?2:0)-(i.castShadow?2:0)+(t.map?1:0)-(i.map?1:0)}function Xm(i){const t=new Hm,e=Vm(),n={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let c=0;c<9;c++)n.probe.push(new P);const s=new P,r=new ne,a=new ne;function o(c){let h=0,d=0,p=0;for(let S=0;S<9;S++)n.probe[S].set(0,0,0);let u=0,g=0,_=0,m=0,f=0,T=0,E=0,y=0,D=0,w=0,C=0;c.sort(Wm);for(let S=0,M=c.length;S0&&(i.has("OES_texture_float_linear")===!0?(n.rectAreaLTC1=ct.LTC_FLOAT_1,n.rectAreaLTC2=ct.LTC_FLOAT_2):(n.rectAreaLTC1=ct.LTC_HALF_1,n.rectAreaLTC2=ct.LTC_HALF_2)),n.ambient[0]=h,n.ambient[1]=d,n.ambient[2]=p;const I=n.hash;(I.directionalLength!==u||I.pointLength!==g||I.spotLength!==_||I.rectAreaLength!==m||I.hemiLength!==f||I.numDirectionalShadows!==T||I.numPointShadows!==E||I.numSpotShadows!==y||I.numSpotMaps!==D||I.numLightProbes!==C)&&(n.directional.length=u,n.spot.length=_,n.rectArea.length=m,n.point.length=g,n.hemi.length=f,n.directionalShadow.length=T,n.directionalShadowMap.length=T,n.pointShadow.length=E,n.pointShadowMap.length=E,n.spotShadow.length=y,n.spotShadowMap.length=y,n.directionalShadowMatrix.length=T,n.pointShadowMatrix.length=E,n.spotLightMatrix.length=y+D-w,n.spotLightMap.length=D,n.numSpotLightShadowsWithMaps=w,n.numLightProbes=C,I.directionalLength=u,I.pointLength=g,I.spotLength=_,I.rectAreaLength=m,I.hemiLength=f,I.numDirectionalShadows=T,I.numPointShadows=E,I.numSpotShadows=y,I.numSpotMaps=D,I.numLightProbes=C,n.version=Gm++)}function l(c,h){let d=0,p=0,u=0,g=0,_=0;const m=h.matrixWorldInverse;for(let f=0,T=c.length;f=a.length?(o=new Hl(i),a.push(o)):o=a[r],o}function n(){t=new WeakMap}return{get:e,dispose:n}}const qm=`void main() { + gl_Position = vec4( position, 1.0 ); +}`,jm=`uniform sampler2D shadow_pass; +uniform vec2 resolution; +uniform float radius; +#include +void main() { + const float samples = float( VSM_SAMPLES ); + float mean = 0.0; + float squared_mean = 0.0; + float uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 ); + float uvStart = samples <= 1.0 ? 0.0 : - 1.0; + for ( float i = 0.0; i < samples; i ++ ) { + float uvOffset = uvStart + i * uvStride; + #ifdef HORIZONTAL_PASS + vec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) ); + mean += distribution.x; + squared_mean += distribution.y * distribution.y + distribution.x * distribution.x; + #else + float depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) ); + mean += depth; + squared_mean += depth * depth; + #endif + } + mean = mean / samples; + squared_mean = squared_mean / samples; + float std_dev = sqrt( squared_mean - mean * mean ); + gl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) ); +}`;function Zm(i,t,e){let n=new Ao;const s=new St,r=new St,a=new oe,o=new gu({depthPacking:wh}),l=new _u,c={},h=e.maxTextureSize,d={[Yn]:Xe,[Xe]:Yn,[gn]:gn},p=new He({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new St},radius:{value:4}},vertexShader:qm,fragmentShader:jm}),u=p.clone();u.defines.HORIZONTAL_PASS=1;const g=new ge;g.setAttribute("position",new he(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const _=new be(g,p),m=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=Ql;let f=this.type;this.render=function(w,C,I){if(m.enabled===!1||m.autoUpdate===!1&&m.needsUpdate===!1||w.length===0)return;const S=i.getRenderTarget(),M=i.getActiveCubeFace(),A=i.getActiveMipmapLevel(),W=i.state;W.setBlending(Cn),W.buffers.color.setClear(1,1,1,1),W.buffers.depth.setTest(!0),W.setScissorTest(!1);const k=f!==An&&this.type===An,q=f===An&&this.type!==An;for(let Q=0,X=w.length;Qh||s.y>h)&&(s.x>h&&(r.x=Math.floor(h/st.x),s.x=r.x*st.x,H.mapSize.x=r.x),s.y>h&&(r.y=Math.floor(h/st.y),s.y=r.y*st.y,H.mapSize.y=r.y)),H.map===null||k===!0||q===!0){const Et=this.type!==An?{minFilter:Je,magFilter:Je}:{};H.map!==null&&H.map.dispose(),H.map=new fn(s.x,s.y,Et),H.map.texture.name=tt.name+".shadowMap",H.camera.updateProjectionMatrix()}i.setRenderTarget(H.map),i.clear();const gt=H.getViewportCount();for(let Et=0;Et0||C.map&&C.alphaTest>0){const W=M.uuid,k=C.uuid;let q=c[W];q===void 0&&(q={},c[W]=q);let Q=q[k];Q===void 0&&(Q=M.clone(),q[k]=Q,C.addEventListener("dispose",D)),M=Q}if(M.visible=C.visible,M.wireframe=C.wireframe,S===An?M.side=C.shadowSide!==null?C.shadowSide:C.side:M.side=C.shadowSide!==null?C.shadowSide:d[C.side],M.alphaMap=C.alphaMap,M.alphaTest=C.alphaTest,M.map=C.map,M.clipShadows=C.clipShadows,M.clippingPlanes=C.clippingPlanes,M.clipIntersection=C.clipIntersection,M.displacementMap=C.displacementMap,M.displacementScale=C.displacementScale,M.displacementBias=C.displacementBias,M.wireframeLinewidth=C.wireframeLinewidth,M.linewidth=C.linewidth,I.isPointLight===!0&&M.isMeshDistanceMaterial===!0){const W=i.properties.get(M);W.light=I}return M}function y(w,C,I,S,M){if(w.visible===!1)return;if(w.layers.test(C.layers)&&(w.isMesh||w.isLine||w.isPoints)&&(w.castShadow||w.receiveShadow&&M===An)&&(!w.frustumCulled||n.intersectsObject(w))){w.modelViewMatrix.multiplyMatrices(I.matrixWorldInverse,w.matrixWorld);const k=t.update(w),q=w.material;if(Array.isArray(q)){const Q=k.groups;for(let X=0,tt=Q.length;X=1):H.indexOf("OpenGL ES")!==-1&&(tt=parseFloat(/^OpenGL ES (\d)/.exec(H)[1]),X=tt>=2);let st=null,gt={};const Et=i.getParameter(i.SCISSOR_BOX),Ft=i.getParameter(i.VIEWPORT),Xt=new oe().fromArray(Et),Y=new oe().fromArray(Ft);function nt(L,rt,G,$){const ft=new Uint8Array(4),ut=i.createTexture();i.bindTexture(L,ut),i.texParameteri(L,i.TEXTURE_MIN_FILTER,i.NEAREST),i.texParameteri(L,i.TEXTURE_MAG_FILTER,i.NEAREST);for(let Ot=0;Ot"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new St,h=new WeakMap;let d;const p=new WeakMap;let u=!1;try{u=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function g(b,v){return u?new OffscreenCanvas(b,v):br("canvas")}function _(b,v,F){let K=1;const J=Rt(b);if((J.width>F||J.height>F)&&(K=F/Math.max(J.width,J.height)),K<1)if(typeof HTMLImageElement<"u"&&b instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&b instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&b instanceof ImageBitmap||typeof VideoFrame<"u"&&b instanceof VideoFrame){const j=Math.floor(K*J.width),Tt=Math.floor(K*J.height);d===void 0&&(d=g(j,Tt));const dt=v?g(j,Tt):d;return dt.width=j,dt.height=Tt,dt.getContext("2d").drawImage(b,0,0,j,Tt),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+J.width+"x"+J.height+") to ("+j+"x"+Tt+")."),dt}else return"data"in b&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+J.width+"x"+J.height+")."),b;return b}function m(b){return b.generateMipmaps}function f(b){i.generateMipmap(b)}function T(b){return b.isWebGLCubeRenderTarget?i.TEXTURE_CUBE_MAP:b.isWebGL3DRenderTarget?i.TEXTURE_3D:b.isWebGLArrayRenderTarget||b.isCompressedArrayTexture?i.TEXTURE_2D_ARRAY:i.TEXTURE_2D}function E(b,v,F,K,J=!1){if(b!==null){if(i[b]!==void 0)return i[b];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+b+"'")}let j=v;if(v===i.RED&&(F===i.FLOAT&&(j=i.R32F),F===i.HALF_FLOAT&&(j=i.R16F),F===i.UNSIGNED_BYTE&&(j=i.R8)),v===i.RED_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.R8UI),F===i.UNSIGNED_SHORT&&(j=i.R16UI),F===i.UNSIGNED_INT&&(j=i.R32UI),F===i.BYTE&&(j=i.R8I),F===i.SHORT&&(j=i.R16I),F===i.INT&&(j=i.R32I)),v===i.RG&&(F===i.FLOAT&&(j=i.RG32F),F===i.HALF_FLOAT&&(j=i.RG16F),F===i.UNSIGNED_BYTE&&(j=i.RG8)),v===i.RG_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RG8UI),F===i.UNSIGNED_SHORT&&(j=i.RG16UI),F===i.UNSIGNED_INT&&(j=i.RG32UI),F===i.BYTE&&(j=i.RG8I),F===i.SHORT&&(j=i.RG16I),F===i.INT&&(j=i.RG32I)),v===i.RGB_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RGB8UI),F===i.UNSIGNED_SHORT&&(j=i.RGB16UI),F===i.UNSIGNED_INT&&(j=i.RGB32UI),F===i.BYTE&&(j=i.RGB8I),F===i.SHORT&&(j=i.RGB16I),F===i.INT&&(j=i.RGB32I)),v===i.RGBA_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RGBA8UI),F===i.UNSIGNED_SHORT&&(j=i.RGBA16UI),F===i.UNSIGNED_INT&&(j=i.RGBA32UI),F===i.BYTE&&(j=i.RGBA8I),F===i.SHORT&&(j=i.RGBA16I),F===i.INT&&(j=i.RGBA32I)),v===i.RGB&&F===i.UNSIGNED_INT_5_9_9_9_REV&&(j=i.RGB9_E5),v===i.RGBA){const Tt=J?yr:Qt.getTransfer(K);F===i.FLOAT&&(j=i.RGBA32F),F===i.HALF_FLOAT&&(j=i.RGBA16F),F===i.UNSIGNED_BYTE&&(j=Tt===se?i.SRGB8_ALPHA8:i.RGBA8),F===i.UNSIGNED_SHORT_4_4_4_4&&(j=i.RGBA4),F===i.UNSIGNED_SHORT_5_5_5_1&&(j=i.RGB5_A1)}return(j===i.R16F||j===i.R32F||j===i.RG16F||j===i.RG32F||j===i.RGBA16F||j===i.RGBA32F)&&t.get("EXT_color_buffer_float"),j}function y(b,v){let F;return b?v===null||v===li||v===Ki?F=i.DEPTH24_STENCIL8:v===xn?F=i.DEPTH32F_STENCIL8:v===Ms&&(F=i.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):v===null||v===li||v===Ki?F=i.DEPTH_COMPONENT24:v===xn?F=i.DEPTH_COMPONENT32F:v===Ms&&(F=i.DEPTH_COMPONENT16),F}function D(b,v){return m(b)===!0||b.isFramebufferTexture&&b.minFilter!==Je&&b.minFilter!==vn?Math.log2(Math.max(v.width,v.height))+1:b.mipmaps!==void 0&&b.mipmaps.length>0?b.mipmaps.length:b.isCompressedTexture&&Array.isArray(b.image)?v.mipmaps.length:1}function w(b){const v=b.target;v.removeEventListener("dispose",w),I(v),v.isVideoTexture&&h.delete(v)}function C(b){const v=b.target;v.removeEventListener("dispose",C),M(v)}function I(b){const v=n.get(b);if(v.__webglInit===void 0)return;const F=b.source,K=p.get(F);if(K){const J=K[v.__cacheKey];J.usedTimes--,J.usedTimes===0&&S(b),Object.keys(K).length===0&&p.delete(F)}n.remove(b)}function S(b){const v=n.get(b);i.deleteTexture(v.__webglTexture);const F=b.source,K=p.get(F);delete K[v.__cacheKey],a.memory.textures--}function M(b){const v=n.get(b);if(b.depthTexture&&(b.depthTexture.dispose(),n.remove(b.depthTexture)),b.isWebGLCubeRenderTarget)for(let K=0;K<6;K++){if(Array.isArray(v.__webglFramebuffer[K]))for(let J=0;J=s.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+b+" texture units while this GPU supports only "+s.maxTextures),A+=1,b}function q(b){const v=[];return v.push(b.wrapS),v.push(b.wrapT),v.push(b.wrapR||0),v.push(b.magFilter),v.push(b.minFilter),v.push(b.anisotropy),v.push(b.internalFormat),v.push(b.format),v.push(b.type),v.push(b.generateMipmaps),v.push(b.premultiplyAlpha),v.push(b.flipY),v.push(b.unpackAlignment),v.push(b.colorSpace),v.join()}function Q(b,v){const F=n.get(b);if(b.isVideoTexture&&At(b),b.isRenderTargetTexture===!1&&b.version>0&&F.__version!==b.version){const K=b.image;if(K===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(K.complete===!1)console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete");else{Y(F,b,v);return}}e.bindTexture(i.TEXTURE_2D,F.__webglTexture,i.TEXTURE0+v)}function X(b,v){const F=n.get(b);if(b.version>0&&F.__version!==b.version){Y(F,b,v);return}e.bindTexture(i.TEXTURE_2D_ARRAY,F.__webglTexture,i.TEXTURE0+v)}function tt(b,v){const F=n.get(b);if(b.version>0&&F.__version!==b.version){Y(F,b,v);return}e.bindTexture(i.TEXTURE_3D,F.__webglTexture,i.TEXTURE0+v)}function H(b,v){const F=n.get(b);if(b.version>0&&F.__version!==b.version){nt(F,b,v);return}e.bindTexture(i.TEXTURE_CUBE_MAP,F.__webglTexture,i.TEXTURE0+v)}const st={[Ua]:i.REPEAT,[ri]:i.CLAMP_TO_EDGE,[Ia]:i.MIRRORED_REPEAT},gt={[Je]:i.NEAREST,[bh]:i.NEAREST_MIPMAP_NEAREST,[Is]:i.NEAREST_MIPMAP_LINEAR,[vn]:i.LINEAR,[Br]:i.LINEAR_MIPMAP_NEAREST,[ai]:i.LINEAR_MIPMAP_LINEAR},Et={[Rh]:i.NEVER,[Ih]:i.ALWAYS,[Ch]:i.LESS,[fc]:i.LEQUAL,[Ph]:i.EQUAL,[Uh]:i.GEQUAL,[Dh]:i.GREATER,[Lh]:i.NOTEQUAL};function Ft(b,v){if(v.type===xn&&t.has("OES_texture_float_linear")===!1&&(v.magFilter===vn||v.magFilter===Br||v.magFilter===Is||v.magFilter===ai||v.minFilter===vn||v.minFilter===Br||v.minFilter===Is||v.minFilter===ai)&&console.warn("THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),i.texParameteri(b,i.TEXTURE_WRAP_S,st[v.wrapS]),i.texParameteri(b,i.TEXTURE_WRAP_T,st[v.wrapT]),(b===i.TEXTURE_3D||b===i.TEXTURE_2D_ARRAY)&&i.texParameteri(b,i.TEXTURE_WRAP_R,st[v.wrapR]),i.texParameteri(b,i.TEXTURE_MAG_FILTER,gt[v.magFilter]),i.texParameteri(b,i.TEXTURE_MIN_FILTER,gt[v.minFilter]),v.compareFunction&&(i.texParameteri(b,i.TEXTURE_COMPARE_MODE,i.COMPARE_REF_TO_TEXTURE),i.texParameteri(b,i.TEXTURE_COMPARE_FUNC,Et[v.compareFunction])),t.has("EXT_texture_filter_anisotropic")===!0){if(v.magFilter===Je||v.minFilter!==Is&&v.minFilter!==ai||v.type===xn&&t.has("OES_texture_float_linear")===!1)return;if(v.anisotropy>1||n.get(v).__currentAnisotropy){const F=t.get("EXT_texture_filter_anisotropic");i.texParameterf(b,F.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(v.anisotropy,s.getMaxAnisotropy())),n.get(v).__currentAnisotropy=v.anisotropy}}}function Xt(b,v){let F=!1;b.__webglInit===void 0&&(b.__webglInit=!0,v.addEventListener("dispose",w));const K=v.source;let J=p.get(K);J===void 0&&(J={},p.set(K,J));const j=q(v);if(j!==b.__cacheKey){J[j]===void 0&&(J[j]={texture:i.createTexture(),usedTimes:0},a.memory.textures++,F=!0),J[j].usedTimes++;const Tt=J[b.__cacheKey];Tt!==void 0&&(J[b.__cacheKey].usedTimes--,Tt.usedTimes===0&&S(v)),b.__cacheKey=j,b.__webglTexture=J[j].texture}return F}function Y(b,v,F){let K=i.TEXTURE_2D;(v.isDataArrayTexture||v.isCompressedArrayTexture)&&(K=i.TEXTURE_2D_ARRAY),v.isData3DTexture&&(K=i.TEXTURE_3D);const J=Xt(b,v),j=v.source;e.bindTexture(K,b.__webglTexture,i.TEXTURE0+F);const Tt=n.get(j);if(j.version!==Tt.__version||J===!0){e.activeTexture(i.TEXTURE0+F);const dt=Qt.getPrimaries(Qt.workingColorSpace),V=v.colorSpace===Vn?null:Qt.getPrimaries(v.colorSpace),at=v.colorSpace===Vn||dt===V?i.NONE:i.BROWSER_DEFAULT_WEBGL;i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,v.flipY),i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,v.premultiplyAlpha),i.pixelStorei(i.UNPACK_ALIGNMENT,v.unpackAlignment),i.pixelStorei(i.UNPACK_COLORSPACE_CONVERSION_WEBGL,at);let Z=_(v.image,!1,s.maxTextureSize);Z=le(v,Z);const it=r.convert(v.format,v.colorSpace),pt=r.convert(v.type);let wt=E(v.internalFormat,it,pt,v.colorSpace,v.isVideoTexture);Ft(K,v);let ht;const Bt=v.mipmaps,Ut=v.isVideoTexture!==!0,Zt=Tt.__version===void 0||J===!0,L=j.dataReady,rt=D(v,Z);if(v.isDepthTexture)wt=y(v.format===$i,v.type),Zt&&(Ut?e.texStorage2D(i.TEXTURE_2D,1,wt,Z.width,Z.height):e.texImage2D(i.TEXTURE_2D,0,wt,Z.width,Z.height,0,it,pt,null));else if(v.isDataTexture)if(Bt.length>0){Ut&&Zt&&e.texStorage2D(i.TEXTURE_2D,rt,wt,Bt[0].width,Bt[0].height);for(let G=0,$=Bt.length;G<$;G++)ht=Bt[G],Ut?L&&e.texSubImage2D(i.TEXTURE_2D,G,0,0,ht.width,ht.height,it,pt,ht.data):e.texImage2D(i.TEXTURE_2D,G,wt,ht.width,ht.height,0,it,pt,ht.data);v.generateMipmaps=!1}else Ut?(Zt&&e.texStorage2D(i.TEXTURE_2D,rt,wt,Z.width,Z.height),L&&e.texSubImage2D(i.TEXTURE_2D,0,0,0,Z.width,Z.height,it,pt,Z.data)):e.texImage2D(i.TEXTURE_2D,0,wt,Z.width,Z.height,0,it,pt,Z.data);else if(v.isCompressedTexture)if(v.isCompressedArrayTexture){Ut&&Zt&&e.texStorage3D(i.TEXTURE_2D_ARRAY,rt,wt,Bt[0].width,Bt[0].height,Z.depth);for(let G=0,$=Bt.length;G<$;G++)if(ht=Bt[G],v.format!==dn)if(it!==null)if(Ut){if(L)if(v.layerUpdates.size>0){const ft=_l(ht.width,ht.height,v.format,v.type);for(const ut of v.layerUpdates){const Ot=ht.data.subarray(ut*ft/ht.data.BYTES_PER_ELEMENT,(ut+1)*ft/ht.data.BYTES_PER_ELEMENT);e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,G,0,0,ut,ht.width,ht.height,1,it,Ot)}v.clearLayerUpdates()}else e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,G,0,0,0,ht.width,ht.height,Z.depth,it,ht.data)}else e.compressedTexImage3D(i.TEXTURE_2D_ARRAY,G,wt,ht.width,ht.height,Z.depth,0,ht.data,0,0);else console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else Ut?L&&e.texSubImage3D(i.TEXTURE_2D_ARRAY,G,0,0,0,ht.width,ht.height,Z.depth,it,pt,ht.data):e.texImage3D(i.TEXTURE_2D_ARRAY,G,wt,ht.width,ht.height,Z.depth,0,it,pt,ht.data)}else{Ut&&Zt&&e.texStorage2D(i.TEXTURE_2D,rt,wt,Bt[0].width,Bt[0].height);for(let G=0,$=Bt.length;G<$;G++)ht=Bt[G],v.format!==dn?it!==null?Ut?L&&e.compressedTexSubImage2D(i.TEXTURE_2D,G,0,0,ht.width,ht.height,it,ht.data):e.compressedTexImage2D(i.TEXTURE_2D,G,wt,ht.width,ht.height,0,ht.data):console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()"):Ut?L&&e.texSubImage2D(i.TEXTURE_2D,G,0,0,ht.width,ht.height,it,pt,ht.data):e.texImage2D(i.TEXTURE_2D,G,wt,ht.width,ht.height,0,it,pt,ht.data)}else if(v.isDataArrayTexture)if(Ut){if(Zt&&e.texStorage3D(i.TEXTURE_2D_ARRAY,rt,wt,Z.width,Z.height,Z.depth),L)if(v.layerUpdates.size>0){const G=_l(Z.width,Z.height,v.format,v.type);for(const $ of v.layerUpdates){const ft=Z.data.subarray($*G/Z.data.BYTES_PER_ELEMENT,($+1)*G/Z.data.BYTES_PER_ELEMENT);e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,$,Z.width,Z.height,1,it,pt,ft)}v.clearLayerUpdates()}else e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,0,Z.width,Z.height,Z.depth,it,pt,Z.data)}else e.texImage3D(i.TEXTURE_2D_ARRAY,0,wt,Z.width,Z.height,Z.depth,0,it,pt,Z.data);else if(v.isData3DTexture)Ut?(Zt&&e.texStorage3D(i.TEXTURE_3D,rt,wt,Z.width,Z.height,Z.depth),L&&e.texSubImage3D(i.TEXTURE_3D,0,0,0,0,Z.width,Z.height,Z.depth,it,pt,Z.data)):e.texImage3D(i.TEXTURE_3D,0,wt,Z.width,Z.height,Z.depth,0,it,pt,Z.data);else if(v.isFramebufferTexture){if(Zt)if(Ut)e.texStorage2D(i.TEXTURE_2D,rt,wt,Z.width,Z.height);else{let G=Z.width,$=Z.height;for(let ft=0;ft>=1,$>>=1}}else if(Bt.length>0){if(Ut&&Zt){const G=Rt(Bt[0]);e.texStorage2D(i.TEXTURE_2D,rt,wt,G.width,G.height)}for(let G=0,$=Bt.length;G<$;G++)ht=Bt[G],Ut?L&&e.texSubImage2D(i.TEXTURE_2D,G,0,0,it,pt,ht):e.texImage2D(i.TEXTURE_2D,G,wt,it,pt,ht);v.generateMipmaps=!1}else if(Ut){if(Zt){const G=Rt(Z);e.texStorage2D(i.TEXTURE_2D,rt,wt,G.width,G.height)}L&&e.texSubImage2D(i.TEXTURE_2D,0,0,0,it,pt,Z)}else e.texImage2D(i.TEXTURE_2D,0,wt,it,pt,Z);m(v)&&f(K),Tt.__version=j.version,v.onUpdate&&v.onUpdate(v)}b.__version=v.version}function nt(b,v,F){if(v.image.length!==6)return;const K=Xt(b,v),J=v.source;e.bindTexture(i.TEXTURE_CUBE_MAP,b.__webglTexture,i.TEXTURE0+F);const j=n.get(J);if(J.version!==j.__version||K===!0){e.activeTexture(i.TEXTURE0+F);const Tt=Qt.getPrimaries(Qt.workingColorSpace),dt=v.colorSpace===Vn?null:Qt.getPrimaries(v.colorSpace),V=v.colorSpace===Vn||Tt===dt?i.NONE:i.BROWSER_DEFAULT_WEBGL;i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,v.flipY),i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,v.premultiplyAlpha),i.pixelStorei(i.UNPACK_ALIGNMENT,v.unpackAlignment),i.pixelStorei(i.UNPACK_COLORSPACE_CONVERSION_WEBGL,V);const at=v.isCompressedTexture||v.image[0].isCompressedTexture,Z=v.image[0]&&v.image[0].isDataTexture,it=[];for(let $=0;$<6;$++)!at&&!Z?it[$]=_(v.image[$],!0,s.maxCubemapSize):it[$]=Z?v.image[$].image:v.image[$],it[$]=le(v,it[$]);const pt=it[0],wt=r.convert(v.format,v.colorSpace),ht=r.convert(v.type),Bt=E(v.internalFormat,wt,ht,v.colorSpace),Ut=v.isVideoTexture!==!0,Zt=j.__version===void 0||K===!0,L=J.dataReady;let rt=D(v,pt);Ft(i.TEXTURE_CUBE_MAP,v);let G;if(at){Ut&&Zt&&e.texStorage2D(i.TEXTURE_CUBE_MAP,rt,Bt,pt.width,pt.height);for(let $=0;$<6;$++){G=it[$].mipmaps;for(let ft=0;ft0&&rt++;const $=Rt(it[0]);e.texStorage2D(i.TEXTURE_CUBE_MAP,rt,Bt,$.width,$.height)}for(let $=0;$<6;$++)if(Z){Ut?L&&e.texSubImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+$,0,0,0,it[$].width,it[$].height,wt,ht,it[$].data):e.texImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+$,0,Bt,it[$].width,it[$].height,0,wt,ht,it[$].data);for(let ft=0;ft>j),pt=Math.max(1,v.height>>j);J===i.TEXTURE_3D||J===i.TEXTURE_2D_ARRAY?e.texImage3D(J,j,V,it,pt,v.depth,0,Tt,dt,null):e.texImage2D(J,j,V,it,pt,0,Tt,dt,null)}e.bindFramebuffer(i.FRAMEBUFFER,b),jt(v)?o.framebufferTexture2DMultisampleEXT(i.FRAMEBUFFER,K,J,Z.__webglTexture,0,qt(v)):(J===i.TEXTURE_2D||J>=i.TEXTURE_CUBE_MAP_POSITIVE_X&&J<=i.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&i.framebufferTexture2D(i.FRAMEBUFFER,K,J,Z.__webglTexture,j),e.bindFramebuffer(i.FRAMEBUFFER,null)}function lt(b,v,F){if(i.bindRenderbuffer(i.RENDERBUFFER,b),v.depthBuffer){const K=v.depthTexture,J=K&&K.isDepthTexture?K.type:null,j=y(v.stencilBuffer,J),Tt=v.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,dt=qt(v);jt(v)?o.renderbufferStorageMultisampleEXT(i.RENDERBUFFER,dt,j,v.width,v.height):F?i.renderbufferStorageMultisample(i.RENDERBUFFER,dt,j,v.width,v.height):i.renderbufferStorage(i.RENDERBUFFER,j,v.width,v.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,Tt,i.RENDERBUFFER,b)}else{const K=v.textures;for(let J=0;J{delete v.__boundDepthTexture,delete v.__depthDisposeCallback,K.removeEventListener("dispose",J)};K.addEventListener("dispose",J),v.__depthDisposeCallback=J}v.__boundDepthTexture=K}if(b.depthTexture&&!v.__autoAllocateDepthBuffer){if(F)throw new Error("target.depthTexture not supported in Cube render targets");Ct(v.__webglFramebuffer,b)}else if(F){v.__webglDepthbuffer=[];for(let K=0;K<6;K++)if(e.bindFramebuffer(i.FRAMEBUFFER,v.__webglFramebuffer[K]),v.__webglDepthbuffer[K]===void 0)v.__webglDepthbuffer[K]=i.createRenderbuffer(),lt(v.__webglDepthbuffer[K],b,!1);else{const J=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,j=v.__webglDepthbuffer[K];i.bindRenderbuffer(i.RENDERBUFFER,j),i.framebufferRenderbuffer(i.FRAMEBUFFER,J,i.RENDERBUFFER,j)}}else if(e.bindFramebuffer(i.FRAMEBUFFER,v.__webglFramebuffer),v.__webglDepthbuffer===void 0)v.__webglDepthbuffer=i.createRenderbuffer(),lt(v.__webglDepthbuffer,b,!1);else{const K=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,J=v.__webglDepthbuffer;i.bindRenderbuffer(i.RENDERBUFFER,J),i.framebufferRenderbuffer(i.FRAMEBUFFER,K,i.RENDERBUFFER,J)}e.bindFramebuffer(i.FRAMEBUFFER,null)}function Vt(b,v,F){const K=n.get(b);v!==void 0&&_t(K.__webglFramebuffer,b,b.texture,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,0),F!==void 0&&Lt(b)}function ie(b){const v=b.texture,F=n.get(b),K=n.get(v);b.addEventListener("dispose",C);const J=b.textures,j=b.isWebGLCubeRenderTarget===!0,Tt=J.length>1;if(Tt||(K.__webglTexture===void 0&&(K.__webglTexture=i.createTexture()),K.__version=v.version,a.memory.textures++),j){F.__webglFramebuffer=[];for(let dt=0;dt<6;dt++)if(v.mipmaps&&v.mipmaps.length>0){F.__webglFramebuffer[dt]=[];for(let V=0;V0){F.__webglFramebuffer=[];for(let dt=0;dt0&&jt(b)===!1){F.__webglMultisampledFramebuffer=i.createFramebuffer(),F.__webglColorRenderbuffer=[],e.bindFramebuffer(i.FRAMEBUFFER,F.__webglMultisampledFramebuffer);for(let dt=0;dt0)for(let V=0;V0)for(let V=0;V0){if(jt(b)===!1){const v=b.textures,F=b.width,K=b.height;let J=i.COLOR_BUFFER_BIT;const j=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,Tt=n.get(b),dt=v.length>1;if(dt)for(let V=0;V0&&t.has("WEBGL_multisampled_render_to_texture")===!0&&v.__useRenderToTexture!==!1}function At(b){const v=a.render.frame;h.get(b)!==v&&(h.set(b,v),b.update())}function le(b,v){const F=b.colorSpace,K=b.format,J=b.type;return b.isCompressedTexture===!0||b.isVideoTexture===!0||F!==Ji&&F!==Vn&&(Qt.getTransfer(F)===se?(K!==dn||J!==Ln)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",F)),v}function Rt(b){return typeof HTMLImageElement<"u"&&b instanceof HTMLImageElement?(c.width=b.naturalWidth||b.width,c.height=b.naturalHeight||b.height):typeof VideoFrame<"u"&&b instanceof VideoFrame?(c.width=b.displayWidth,c.height=b.displayHeight):(c.width=b.width,c.height=b.height),c}this.allocateTextureUnit=k,this.resetTextureUnits=W,this.setTexture2D=Q,this.setTexture2DArray=X,this.setTexture3D=tt,this.setTextureCube=H,this.rebindTextures=Vt,this.setupRenderTarget=ie,this.updateRenderTargetMipmap=Wt,this.updateMultisampleRenderTarget=ye,this.setupDepthRenderbuffer=Lt,this.setupFrameBufferTexture=_t,this.useMultisampledRTT=jt}function Qm(i,t){function e(n,s=Vn){let r;const a=Qt.getTransfer(s);if(n===Ln)return i.UNSIGNED_BYTE;if(n===Mo)return i.UNSIGNED_SHORT_4_4_4_4;if(n===So)return i.UNSIGNED_SHORT_5_5_5_1;if(n===rc)return i.UNSIGNED_INT_5_9_9_9_REV;if(n===ic)return i.BYTE;if(n===sc)return i.SHORT;if(n===Ms)return i.UNSIGNED_SHORT;if(n===xo)return i.INT;if(n===li)return i.UNSIGNED_INT;if(n===xn)return i.FLOAT;if(n===Pn)return i.HALF_FLOAT;if(n===ac)return i.ALPHA;if(n===oc)return i.RGB;if(n===dn)return i.RGBA;if(n===lc)return i.LUMINANCE;if(n===cc)return i.LUMINANCE_ALPHA;if(n===Gi)return i.DEPTH_COMPONENT;if(n===$i)return i.DEPTH_STENCIL;if(n===yo)return i.RED;if(n===Eo)return i.RED_INTEGER;if(n===hc)return i.RG;if(n===bo)return i.RG_INTEGER;if(n===To)return i.RGBA_INTEGER;if(n===ur||n===dr||n===fr||n===pr)if(a===se)if(r=t.get("WEBGL_compressed_texture_s3tc_srgb"),r!==null){if(n===ur)return r.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===dr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===fr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===pr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(r=t.get("WEBGL_compressed_texture_s3tc"),r!==null){if(n===ur)return r.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===dr)return r.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===fr)return r.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===pr)return r.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===Na||n===Fa||n===Oa||n===Ba)if(r=t.get("WEBGL_compressed_texture_pvrtc"),r!==null){if(n===Na)return r.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===Fa)return r.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===Oa)return r.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===Ba)return r.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===za||n===ka||n===Ha)if(r=t.get("WEBGL_compressed_texture_etc"),r!==null){if(n===za||n===ka)return a===se?r.COMPRESSED_SRGB8_ETC2:r.COMPRESSED_RGB8_ETC2;if(n===Ha)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:r.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(n===Va||n===Ga||n===Wa||n===Xa||n===Ya||n===qa||n===ja||n===Za||n===Ka||n===$a||n===Ja||n===Qa||n===to||n===eo)if(r=t.get("WEBGL_compressed_texture_astc"),r!==null){if(n===Va)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:r.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===Ga)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:r.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Wa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:r.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===Xa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:r.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===Ya)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:r.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===qa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:r.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===ja)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:r.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===Za)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:r.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===Ka)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:r.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===$a)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:r.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===Ja)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:r.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===Qa)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:r.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===to)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:r.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===eo)return a===se?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:r.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===mr||n===no||n===io)if(r=t.get("EXT_texture_compression_bptc"),r!==null){if(n===mr)return a===se?r.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:r.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===no)return r.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===io)return r.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===uc||n===so||n===ro||n===ao)if(r=t.get("EXT_texture_compression_rgtc"),r!==null){if(n===mr)return r.COMPRESSED_RED_RGTC1_EXT;if(n===so)return r.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===ro)return r.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===ao)return r.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===Ki?i.UNSIGNED_INT_24_8:i[n]!==void 0?i[n]:null}return{convert:e}}const tg={type:"move"};class ga{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return this._hand===null&&(this._hand=new zi,this._hand.matrixAutoUpdate=!1,this._hand.visible=!1,this._hand.joints={},this._hand.inputState={pinching:!1}),this._hand}getTargetRaySpace(){return this._targetRay===null&&(this._targetRay=new zi,this._targetRay.matrixAutoUpdate=!1,this._targetRay.visible=!1,this._targetRay.hasLinearVelocity=!1,this._targetRay.linearVelocity=new P,this._targetRay.hasAngularVelocity=!1,this._targetRay.angularVelocity=new P),this._targetRay}getGripSpace(){return this._grip===null&&(this._grip=new zi,this._grip.matrixAutoUpdate=!1,this._grip.visible=!1,this._grip.hasLinearVelocity=!1,this._grip.linearVelocity=new P,this._grip.hasAngularVelocity=!1,this._grip.angularVelocity=new P),this._grip}dispatchEvent(t){return this._targetRay!==null&&this._targetRay.dispatchEvent(t),this._grip!==null&&this._grip.dispatchEvent(t),this._hand!==null&&this._hand.dispatchEvent(t),this}connect(t){if(t&&t.hand){const e=this._hand;if(e)for(const n of t.hand.values())this._getHandJoint(e,n)}return this.dispatchEvent({type:"connected",data:t}),this}disconnect(t){return this.dispatchEvent({type:"disconnected",data:t}),this._targetRay!==null&&(this._targetRay.visible=!1),this._grip!==null&&(this._grip.visible=!1),this._hand!==null&&(this._hand.visible=!1),this}update(t,e,n){let s=null,r=null,a=null;const o=this._targetRay,l=this._grip,c=this._hand;if(t&&e.session.visibilityState!=="visible-blurred"){if(c&&t.hand){a=!0;for(const _ of t.hand.values()){const m=e.getJointPose(_,n),f=this._getHandJoint(c,_);m!==null&&(f.matrix.fromArray(m.transform.matrix),f.matrix.decompose(f.position,f.rotation,f.scale),f.matrixWorldNeedsUpdate=!0,f.jointRadius=m.radius),f.visible=m!==null}const h=c.joints["index-finger-tip"],d=c.joints["thumb-tip"],p=h.position.distanceTo(d.position),u=.02,g=.005;c.inputState.pinching&&p>u+g?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!c.inputState.pinching&&p<=u-g&&(c.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else l!==null&&t.gripSpace&&(r=e.getPose(t.gripSpace,n),r!==null&&(l.matrix.fromArray(r.transform.matrix),l.matrix.decompose(l.position,l.rotation,l.scale),l.matrixWorldNeedsUpdate=!0,r.linearVelocity?(l.hasLinearVelocity=!0,l.linearVelocity.copy(r.linearVelocity)):l.hasLinearVelocity=!1,r.angularVelocity?(l.hasAngularVelocity=!0,l.angularVelocity.copy(r.angularVelocity)):l.hasAngularVelocity=!1));o!==null&&(s=e.getPose(t.targetRaySpace,n),s===null&&r!==null&&(s=r),s!==null&&(o.matrix.fromArray(s.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,s.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(s.linearVelocity)):o.hasLinearVelocity=!1,s.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(s.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(tg)))}return o!==null&&(o.visible=s!==null),l!==null&&(l.visible=r!==null),c!==null&&(c.visible=a!==null),this}_getHandJoint(t,e){if(t.joints[e.jointName]===void 0){const n=new zi;n.matrixAutoUpdate=!1,n.visible=!1,t.joints[e.jointName]=n,t.add(n)}return t.joints[e.jointName]}}const eg=` +void main() { + + gl_Position = vec4( position, 1.0 ); + +}`,ng=` +uniform sampler2DArray depthColor; +uniform float depthWidth; +uniform float depthHeight; + +void main() { + + vec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight ); + + if ( coord.x >= 1.0 ) { + + gl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r; + + } else { + + gl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r; + + } + +}`;class ig{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(t,e,n){if(this.texture===null){const s=new Pe,r=t.properties.get(s);r.__webglTexture=e.texture,(e.depthNear!==n.depthNear||e.depthFar!==n.depthFar)&&(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=s}}getMesh(t){if(this.texture!==null&&this.mesh===null){const e=t.cameras[0].viewport,n=new He({vertexShader:eg,fragmentShader:ng,uniforms:{depthColor:{value:this.texture},depthWidth:{value:e.z},depthHeight:{value:e.w}}});this.mesh=new be(new ws(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class sg extends hi{constructor(t,e){super();const n=this;let s=null,r=1,a=null,o="local-floor",l=1,c=null,h=null,d=null,p=null,u=null,g=null;const _=new ig,m=e.getContextAttributes();let f=null,T=null;const E=[],y=[],D=new St;let w=null;const C=new $e;C.viewport=new oe;const I=new $e;I.viewport=new oe;const S=[C,I],M=new Su;let A=null,W=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(Y){let nt=E[Y];return nt===void 0&&(nt=new ga,E[Y]=nt),nt.getTargetRaySpace()},this.getControllerGrip=function(Y){let nt=E[Y];return nt===void 0&&(nt=new ga,E[Y]=nt),nt.getGripSpace()},this.getHand=function(Y){let nt=E[Y];return nt===void 0&&(nt=new ga,E[Y]=nt),nt.getHandSpace()};function k(Y){const nt=y.indexOf(Y.inputSource);if(nt===-1)return;const _t=E[nt];_t!==void 0&&(_t.update(Y.inputSource,Y.frame,c||a),_t.dispatchEvent({type:Y.type,data:Y.inputSource}))}function q(){s.removeEventListener("select",k),s.removeEventListener("selectstart",k),s.removeEventListener("selectend",k),s.removeEventListener("squeeze",k),s.removeEventListener("squeezestart",k),s.removeEventListener("squeezeend",k),s.removeEventListener("end",q),s.removeEventListener("inputsourceschange",Q);for(let Y=0;Y=0&&(y[lt]=null,E[lt].disconnect(_t))}for(let nt=0;nt=y.length){y.push(_t),lt=Lt;break}else if(y[Lt]===null){y[Lt]=_t,lt=Lt;break}if(lt===-1)break}const Ct=E[lt];Ct&&Ct.connect(_t)}}const X=new P,tt=new P;function H(Y,nt,_t){X.setFromMatrixPosition(nt.matrixWorld),tt.setFromMatrixPosition(_t.matrixWorld);const lt=X.distanceTo(tt),Ct=nt.projectionMatrix.elements,Lt=_t.projectionMatrix.elements,Vt=Ct[14]/(Ct[10]-1),ie=Ct[14]/(Ct[10]+1),Wt=(Ct[9]+1)/Ct[5],ue=(Ct[9]-1)/Ct[5],R=(Ct[8]-1)/Ct[0],ye=(Lt[8]+1)/Lt[0],qt=Vt*R,jt=Vt*ye,At=lt/(-R+ye),le=At*-R;if(nt.matrixWorld.decompose(Y.position,Y.quaternion,Y.scale),Y.translateX(le),Y.translateZ(At),Y.matrixWorld.compose(Y.position,Y.quaternion,Y.scale),Y.matrixWorldInverse.copy(Y.matrixWorld).invert(),Ct[10]===-1)Y.projectionMatrix.copy(nt.projectionMatrix),Y.projectionMatrixInverse.copy(nt.projectionMatrixInverse);else{const Rt=Vt+At,b=ie+At,v=qt-le,F=jt+(lt-le),K=Wt*ie/b*Rt,J=ue*ie/b*Rt;Y.projectionMatrix.makePerspective(v,F,K,J,Rt,b),Y.projectionMatrixInverse.copy(Y.projectionMatrix).invert()}}function st(Y,nt){nt===null?Y.matrixWorld.copy(Y.matrix):Y.matrixWorld.multiplyMatrices(nt.matrixWorld,Y.matrix),Y.matrixWorldInverse.copy(Y.matrixWorld).invert()}this.updateCamera=function(Y){if(s===null)return;let nt=Y.near,_t=Y.far;_.texture!==null&&(_.depthNear>0&&(nt=_.depthNear),_.depthFar>0&&(_t=_.depthFar)),M.near=I.near=C.near=nt,M.far=I.far=C.far=_t,(A!==M.near||W!==M.far)&&(s.updateRenderState({depthNear:M.near,depthFar:M.far}),A=M.near,W=M.far),C.layers.mask=Y.layers.mask|2,I.layers.mask=Y.layers.mask|4,M.layers.mask=C.layers.mask|I.layers.mask;const lt=Y.parent,Ct=M.cameras;st(M,lt);for(let Lt=0;Lt0&&(m.alphaTest.value=f.alphaTest);const T=t.get(f),E=T.envMap,y=T.envMapRotation;E&&(m.envMap.value=E,ti.copy(y),ti.x*=-1,ti.y*=-1,ti.z*=-1,E.isCubeTexture&&E.isRenderTargetTexture===!1&&(ti.y*=-1,ti.z*=-1),m.envMapRotation.value.setFromMatrix4(rg.makeRotationFromEuler(ti)),m.flipEnvMap.value=E.isCubeTexture&&E.isRenderTargetTexture===!1?-1:1,m.reflectivity.value=f.reflectivity,m.ior.value=f.ior,m.refractionRatio.value=f.refractionRatio),f.lightMap&&(m.lightMap.value=f.lightMap,m.lightMapIntensity.value=f.lightMapIntensity,e(f.lightMap,m.lightMapTransform)),f.aoMap&&(m.aoMap.value=f.aoMap,m.aoMapIntensity.value=f.aoMapIntensity,e(f.aoMap,m.aoMapTransform))}function a(m,f){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,f.map&&(m.map.value=f.map,e(f.map,m.mapTransform))}function o(m,f){m.dashSize.value=f.dashSize,m.totalSize.value=f.dashSize+f.gapSize,m.scale.value=f.scale}function l(m,f,T,E){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,m.size.value=f.size*T,m.scale.value=E*.5,f.map&&(m.map.value=f.map,e(f.map,m.uvTransform)),f.alphaMap&&(m.alphaMap.value=f.alphaMap,e(f.alphaMap,m.alphaMapTransform)),f.alphaTest>0&&(m.alphaTest.value=f.alphaTest)}function c(m,f){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,m.rotation.value=f.rotation,f.map&&(m.map.value=f.map,e(f.map,m.mapTransform)),f.alphaMap&&(m.alphaMap.value=f.alphaMap,e(f.alphaMap,m.alphaMapTransform)),f.alphaTest>0&&(m.alphaTest.value=f.alphaTest)}function h(m,f){m.specular.value.copy(f.specular),m.shininess.value=Math.max(f.shininess,1e-4)}function d(m,f){f.gradientMap&&(m.gradientMap.value=f.gradientMap)}function p(m,f){m.metalness.value=f.metalness,f.metalnessMap&&(m.metalnessMap.value=f.metalnessMap,e(f.metalnessMap,m.metalnessMapTransform)),m.roughness.value=f.roughness,f.roughnessMap&&(m.roughnessMap.value=f.roughnessMap,e(f.roughnessMap,m.roughnessMapTransform)),f.envMap&&(m.envMapIntensity.value=f.envMapIntensity)}function u(m,f,T){m.ior.value=f.ior,f.sheen>0&&(m.sheenColor.value.copy(f.sheenColor).multiplyScalar(f.sheen),m.sheenRoughness.value=f.sheenRoughness,f.sheenColorMap&&(m.sheenColorMap.value=f.sheenColorMap,e(f.sheenColorMap,m.sheenColorMapTransform)),f.sheenRoughnessMap&&(m.sheenRoughnessMap.value=f.sheenRoughnessMap,e(f.sheenRoughnessMap,m.sheenRoughnessMapTransform))),f.clearcoat>0&&(m.clearcoat.value=f.clearcoat,m.clearcoatRoughness.value=f.clearcoatRoughness,f.clearcoatMap&&(m.clearcoatMap.value=f.clearcoatMap,e(f.clearcoatMap,m.clearcoatMapTransform)),f.clearcoatRoughnessMap&&(m.clearcoatRoughnessMap.value=f.clearcoatRoughnessMap,e(f.clearcoatRoughnessMap,m.clearcoatRoughnessMapTransform)),f.clearcoatNormalMap&&(m.clearcoatNormalMap.value=f.clearcoatNormalMap,e(f.clearcoatNormalMap,m.clearcoatNormalMapTransform),m.clearcoatNormalScale.value.copy(f.clearcoatNormalScale),f.side===Xe&&m.clearcoatNormalScale.value.negate())),f.dispersion>0&&(m.dispersion.value=f.dispersion),f.iridescence>0&&(m.iridescence.value=f.iridescence,m.iridescenceIOR.value=f.iridescenceIOR,m.iridescenceThicknessMinimum.value=f.iridescenceThicknessRange[0],m.iridescenceThicknessMaximum.value=f.iridescenceThicknessRange[1],f.iridescenceMap&&(m.iridescenceMap.value=f.iridescenceMap,e(f.iridescenceMap,m.iridescenceMapTransform)),f.iridescenceThicknessMap&&(m.iridescenceThicknessMap.value=f.iridescenceThicknessMap,e(f.iridescenceThicknessMap,m.iridescenceThicknessMapTransform))),f.transmission>0&&(m.transmission.value=f.transmission,m.transmissionSamplerMap.value=T.texture,m.transmissionSamplerSize.value.set(T.width,T.height),f.transmissionMap&&(m.transmissionMap.value=f.transmissionMap,e(f.transmissionMap,m.transmissionMapTransform)),m.thickness.value=f.thickness,f.thicknessMap&&(m.thicknessMap.value=f.thicknessMap,e(f.thicknessMap,m.thicknessMapTransform)),m.attenuationDistance.value=f.attenuationDistance,m.attenuationColor.value.copy(f.attenuationColor)),f.anisotropy>0&&(m.anisotropyVector.value.set(f.anisotropy*Math.cos(f.anisotropyRotation),f.anisotropy*Math.sin(f.anisotropyRotation)),f.anisotropyMap&&(m.anisotropyMap.value=f.anisotropyMap,e(f.anisotropyMap,m.anisotropyMapTransform))),m.specularIntensity.value=f.specularIntensity,m.specularColor.value.copy(f.specularColor),f.specularColorMap&&(m.specularColorMap.value=f.specularColorMap,e(f.specularColorMap,m.specularColorMapTransform)),f.specularIntensityMap&&(m.specularIntensityMap.value=f.specularIntensityMap,e(f.specularIntensityMap,m.specularIntensityMapTransform))}function g(m,f){f.matcap&&(m.matcap.value=f.matcap)}function _(m,f){const T=t.get(f).light;m.referencePosition.value.setFromMatrixPosition(T.matrixWorld),m.nearDistance.value=T.shadow.camera.near,m.farDistance.value=T.shadow.camera.far}return{refreshFogUniforms:n,refreshMaterialUniforms:s}}function og(i,t,e,n){let s={},r={},a=[];const o=i.getParameter(i.MAX_UNIFORM_BUFFER_BINDINGS);function l(T,E){const y=E.program;n.uniformBlockBinding(T,y)}function c(T,E){let y=s[T.id];y===void 0&&(g(T),y=h(T),s[T.id]=y,T.addEventListener("dispose",m));const D=E.program;n.updateUBOMapping(T,D);const w=t.render.frame;r[T.id]!==w&&(p(T),r[T.id]=w)}function h(T){const E=d();T.__bindingPointIndex=E;const y=i.createBuffer(),D=T.__size,w=T.usage;return i.bindBuffer(i.UNIFORM_BUFFER,y),i.bufferData(i.UNIFORM_BUFFER,D,w),i.bindBuffer(i.UNIFORM_BUFFER,null),i.bindBufferBase(i.UNIFORM_BUFFER,E,y),y}function d(){for(let T=0;T0&&(y+=D-w),T.__size=y,T.__cache={},this}function _(T){const E={boundary:0,storage:0};return typeof T=="number"||typeof T=="boolean"?(E.boundary=4,E.storage=4):T.isVector2?(E.boundary=8,E.storage=8):T.isVector3||T.isColor?(E.boundary=16,E.storage=12):T.isVector4?(E.boundary=16,E.storage=16):T.isMatrix3?(E.boundary=48,E.storage=48):T.isMatrix4?(E.boundary=64,E.storage=64):T.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",T),E}function m(T){const E=T.target;E.removeEventListener("dispose",m);const y=a.indexOf(E.__bindingPointIndex);a.splice(y,1),i.deleteBuffer(s[E.id]),delete s[E.id],delete r[E.id]}function f(){for(const T in s)i.deleteBuffer(s[T]);a=[],s={},r={}}return{bind:l,update:c,dispose:f}}class lg{constructor(t={}){const{canvas:e=Oh(),context:n=null,depth:s=!0,stencil:r=!1,alpha:a=!1,antialias:o=!1,premultipliedAlpha:l=!0,preserveDrawingBuffer:c=!1,powerPreference:h="default",failIfMajorPerformanceCaveat:d=!1,reverseDepthBuffer:p=!1}=t;this.isWebGLRenderer=!0;let u;if(n!==null){if(typeof WebGLRenderingContext<"u"&&n instanceof WebGLRenderingContext)throw new Error("THREE.WebGLRenderer: WebGL 1 is not supported since r163.");u=n.getContextAttributes().alpha}else u=a;const g=new Uint32Array(4),_=new Int32Array(4);let m=null,f=null;const T=[],E=[];this.domElement=e,this.debug={checkShaderErrors:!0,onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this._outputColorSpace=an,this.toneMapping=Wn,this.toneMappingExposure=1;const y=this;let D=!1,w=0,C=0,I=null,S=-1,M=null;const A=new oe,W=new oe;let k=null;const q=new ot(0);let Q=0,X=e.width,tt=e.height,H=1,st=null,gt=null;const Et=new oe(0,0,X,tt),Ft=new oe(0,0,X,tt);let Xt=!1;const Y=new Ao;let nt=!1,_t=!1;this.transmissionResolutionScale=1;const lt=new ne,Ct=new ne,Lt=new P,Vt=new oe,ie={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};let Wt=!1;function ue(){return I===null?H:1}let R=n;function ye(x,U){return e.getContext(x,U)}try{const x={alpha:!0,depth:s,stencil:r,antialias:o,premultipliedAlpha:l,preserveDrawingBuffer:c,powerPreference:h,failIfMajorPerformanceCaveat:d};if("setAttribute"in e&&e.setAttribute("data-engine",`three.js r${vo}`),e.addEventListener("webglcontextlost",$,!1),e.addEventListener("webglcontextrestored",ft,!1),e.addEventListener("webglcontextcreationerror",ut,!1),R===null){const U="webgl2";if(R=ye(U,x),R===null)throw ye(U)?new Error("Error creating WebGL context with your selected attributes."):new Error("Error creating WebGL context.")}}catch(x){throw console.error("THREE.WebGLRenderer: "+x.message),x}let qt,jt,At,le,Rt,b,v,F,K,J,j,Tt,dt,V,at,Z,it,pt,wt,ht,Bt,Ut,Zt,L;function rt(){qt=new _p(R),qt.init(),Ut=new Qm(R,qt),jt=new up(R,qt,t,Ut),At=new $m(R,qt),jt.reverseDepthBuffer&&p&&At.buffers.depth.setReversed(!0),le=new Mp(R),Rt=new Bm,b=new Jm(R,qt,At,Rt,jt,Ut,le),v=new fp(y),F=new gp(y),K=new wu(R),Zt=new cp(R,K),J=new vp(R,K,le,Zt),j=new yp(R,J,K,le),wt=new Sp(R,jt,b),Z=new dp(Rt),Tt=new Om(y,v,F,qt,jt,Zt,Z),dt=new ag(y,Rt),V=new km,at=new Ym(qt),pt=new lp(y,v,F,At,j,u,l),it=new Zm(y,j,jt),L=new og(R,le,jt,At),ht=new hp(R,qt,le),Bt=new xp(R,qt,le),le.programs=Tt.programs,y.capabilities=jt,y.extensions=qt,y.properties=Rt,y.renderLists=V,y.shadowMap=it,y.state=At,y.info=le}rt();const G=new sg(y,R);this.xr=G,this.getContext=function(){return R},this.getContextAttributes=function(){return R.getContextAttributes()},this.forceContextLoss=function(){const x=qt.get("WEBGL_lose_context");x&&x.loseContext()},this.forceContextRestore=function(){const x=qt.get("WEBGL_lose_context");x&&x.restoreContext()},this.getPixelRatio=function(){return H},this.setPixelRatio=function(x){x!==void 0&&(H=x,this.setSize(X,tt,!1))},this.getSize=function(x){return x.set(X,tt)},this.setSize=function(x,U,O=!0){if(G.isPresenting){console.warn("THREE.WebGLRenderer: Can't change size while VR device is presenting.");return}X=x,tt=U,e.width=Math.floor(x*H),e.height=Math.floor(U*H),O===!0&&(e.style.width=x+"px",e.style.height=U+"px"),this.setViewport(0,0,x,U)},this.getDrawingBufferSize=function(x){return x.set(X*H,tt*H).floor()},this.setDrawingBufferSize=function(x,U,O){X=x,tt=U,H=O,e.width=Math.floor(x*O),e.height=Math.floor(U*O),this.setViewport(0,0,x,U)},this.getCurrentViewport=function(x){return x.copy(A)},this.getViewport=function(x){return x.copy(Et)},this.setViewport=function(x,U,O,z){x.isVector4?Et.set(x.x,x.y,x.z,x.w):Et.set(x,U,O,z),At.viewport(A.copy(Et).multiplyScalar(H).round())},this.getScissor=function(x){return x.copy(Ft)},this.setScissor=function(x,U,O,z){x.isVector4?Ft.set(x.x,x.y,x.z,x.w):Ft.set(x,U,O,z),At.scissor(W.copy(Ft).multiplyScalar(H).round())},this.getScissorTest=function(){return Xt},this.setScissorTest=function(x){At.setScissorTest(Xt=x)},this.setOpaqueSort=function(x){st=x},this.setTransparentSort=function(x){gt=x},this.getClearColor=function(x){return x.copy(pt.getClearColor())},this.setClearColor=function(){pt.setClearColor.apply(pt,arguments)},this.getClearAlpha=function(){return pt.getClearAlpha()},this.setClearAlpha=function(){pt.setClearAlpha.apply(pt,arguments)},this.clear=function(x=!0,U=!0,O=!0){let z=0;if(x){let N=!1;if(I!==null){const et=I.texture.format;N=et===To||et===bo||et===Eo}if(N){const et=I.texture.type,mt=et===Ln||et===li||et===Ms||et===Ki||et===Mo||et===So,vt=pt.getClearColor(),Mt=pt.getClearAlpha(),It=vt.r,Nt=vt.g,Pt=vt.b;mt?(g[0]=It,g[1]=Nt,g[2]=Pt,g[3]=Mt,R.clearBufferuiv(R.COLOR,0,g)):(_[0]=It,_[1]=Nt,_[2]=Pt,_[3]=Mt,R.clearBufferiv(R.COLOR,0,_))}else z|=R.COLOR_BUFFER_BIT}U&&(z|=R.DEPTH_BUFFER_BIT),O&&(z|=R.STENCIL_BUFFER_BIT,this.state.buffers.stencil.setMask(4294967295)),R.clear(z)},this.clearColor=function(){this.clear(!0,!1,!1)},this.clearDepth=function(){this.clear(!1,!0,!1)},this.clearStencil=function(){this.clear(!1,!1,!0)},this.dispose=function(){e.removeEventListener("webglcontextlost",$,!1),e.removeEventListener("webglcontextrestored",ft,!1),e.removeEventListener("webglcontextcreationerror",ut,!1),pt.dispose(),V.dispose(),at.dispose(),Rt.dispose(),v.dispose(),F.dispose(),j.dispose(),Zt.dispose(),L.dispose(),Tt.dispose(),G.dispose(),G.removeEventListener("sessionstart",Rs),G.removeEventListener("sessionend",fi),pn.stop()};function $(x){x.preventDefault(),console.log("THREE.WebGLRenderer: Context Lost."),D=!0}function ft(){console.log("THREE.WebGLRenderer: Context Restored."),D=!1;const x=le.autoReset,U=it.enabled,O=it.autoUpdate,z=it.needsUpdate,N=it.type;rt(),le.autoReset=x,it.enabled=U,it.autoUpdate=O,it.needsUpdate=z,it.type=N}function ut(x){console.error("THREE.WebGLRenderer: A WebGL context could not be created. Reason: ",x.statusMessage)}function Ot(x){const U=x.target;U.removeEventListener("dispose",Ot),ce(U)}function ce(x){Ae(x),Rt.remove(x)}function Ae(x){const U=Rt.get(x).programs;U!==void 0&&(U.forEach(function(O){Tt.releaseProgram(O)}),x.isShaderMaterial&&Tt.releaseShaderCache(x))}this.renderBufferDirect=function(x,U,O,z,N,et){U===null&&(U=ie);const mt=N.isMesh&&N.matrixWorld.determinant()<0,vt=Ds(x,U,O,z,N);At.setMaterial(z,mt);let Mt=O.index,It=1;if(z.wireframe===!0){if(Mt=J.getWireframeAttribute(O),Mt===void 0)return;It=2}const Nt=O.drawRange,Pt=O.attributes.position;let Kt=Nt.start*It,te=(Nt.start+Nt.count)*It;et!==null&&(Kt=Math.max(Kt,et.start*It),te=Math.min(te,(et.start+et.count)*It)),Mt!==null?(Kt=Math.max(Kt,0),te=Math.min(te,Mt.count)):Pt!=null&&(Kt=Math.max(Kt,0),te=Math.min(te,Pt.count));const xe=te-Kt;if(xe<0||xe===1/0)return;Zt.setup(N,z,vt,O,Mt);let pe,Jt=ht;if(Mt!==null&&(pe=K.get(Mt),Jt=Bt,Jt.setIndex(pe)),N.isMesh)z.wireframe===!0?(At.setLineWidth(z.wireframeLinewidth*ue()),Jt.setMode(R.LINES)):Jt.setMode(R.TRIANGLES);else if(N.isLine){let Dt=z.linewidth;Dt===void 0&&(Dt=1),At.setLineWidth(Dt*ue()),N.isLineSegments?Jt.setMode(R.LINES):N.isLineLoop?Jt.setMode(R.LINE_LOOP):Jt.setMode(R.LINE_STRIP)}else N.isPoints?Jt.setMode(R.POINTS):N.isSprite&&Jt.setMode(R.TRIANGLES);if(N.isBatchedMesh)if(N._multiDrawInstances!==null)Jt.renderMultiDrawInstances(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount,N._multiDrawInstances);else if(qt.get("WEBGL_multi_draw"))Jt.renderMultiDraw(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount);else{const Dt=N._multiDrawStarts,De=N._multiDrawCounts,ee=N._multiDrawCount,ln=Mt?K.get(Mt).bytesPerElement:1,mi=Rt.get(z).currentProgram.getUniforms();for(let qe=0;qe{function et(){if(z.forEach(function(mt){Rt.get(mt).currentProgram.isReady()&&z.delete(mt)}),z.size===0){N(x);return}setTimeout(et,10)}qt.get("KHR_parallel_shader_compile")!==null?et():setTimeout(et,10)})};let Ye=null;function Qe(x){Ye&&Ye(x)}function Rs(){pn.stop()}function fi(){pn.start()}const pn=new Rc;pn.setAnimationLoop(Qe),typeof self<"u"&&pn.setContext(self),this.setAnimationLoop=function(x){Ye=x,G.setAnimationLoop(x),x===null?pn.stop():pn.start()},G.addEventListener("sessionstart",Rs),G.addEventListener("sessionend",fi),this.render=function(x,U){if(U!==void 0&&U.isCamera!==!0){console.error("THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.");return}if(D===!0)return;if(x.matrixWorldAutoUpdate===!0&&x.updateMatrixWorld(),U.parent===null&&U.matrixWorldAutoUpdate===!0&&U.updateMatrixWorld(),G.enabled===!0&&G.isPresenting===!0&&(G.cameraAutoUpdate===!0&&G.updateCamera(U),U=G.getCamera()),x.isScene===!0&&x.onBeforeRender(y,x,U,I),f=at.get(x,E.length),f.init(U),E.push(f),Ct.multiplyMatrices(U.projectionMatrix,U.matrixWorldInverse),Y.setFromProjectionMatrix(Ct),_t=this.localClippingEnabled,nt=Z.init(this.clippingPlanes,_t),m=V.get(x,T.length),m.init(),T.push(m),G.enabled===!0&&G.isPresenting===!0){const et=y.xr.getDepthSensingMesh();et!==null&&ns(et,U,-1/0,y.sortObjects)}ns(x,U,0,y.sortObjects),m.finish(),y.sortObjects===!0&&m.sort(st,gt),Wt=G.enabled===!1||G.isPresenting===!1||G.hasDepthSensing()===!1,Wt&&pt.addToRenderList(m,x),this.info.render.frame++,nt===!0&&Z.beginShadows();const O=f.state.shadowsArray;it.render(O,x,U),nt===!0&&Z.endShadows(),this.info.autoReset===!0&&this.info.reset();const z=m.opaque,N=m.transmissive;if(f.setupLights(),U.isArrayCamera){const et=U.cameras;if(N.length>0)for(let mt=0,vt=et.length;mt0&&Ve(z,N,x,U),Wt&&pt.render(x),Cs(m,x,U);I!==null&&C===0&&(b.updateMultisampleRenderTarget(I),b.updateRenderTargetMipmap(I)),x.isScene===!0&&x.onAfterRender(y,x,U),Zt.resetDefaultState(),S=-1,M=null,E.pop(),E.length>0?(f=E[E.length-1],nt===!0&&Z.setGlobalState(y.clippingPlanes,f.state.camera)):f=null,T.pop(),T.length>0?m=T[T.length-1]:m=null};function ns(x,U,O,z){if(x.visible===!1)return;if(x.layers.test(U.layers)){if(x.isGroup)O=x.renderOrder;else if(x.isLOD)x.autoUpdate===!0&&x.update(U);else if(x.isLight)f.pushLight(x),x.castShadow&&f.pushShadow(x);else if(x.isSprite){if(!x.frustumCulled||Y.intersectsSprite(x)){z&&Vt.setFromMatrixPosition(x.matrixWorld).applyMatrix4(Ct);const mt=j.update(x),vt=x.material;vt.visible&&m.push(x,mt,vt,O,Vt.z,null)}}else if((x.isMesh||x.isLine||x.isPoints)&&(!x.frustumCulled||Y.intersectsObject(x))){const mt=j.update(x),vt=x.material;if(z&&(x.boundingSphere!==void 0?(x.boundingSphere===null&&x.computeBoundingSphere(),Vt.copy(x.boundingSphere.center)):(mt.boundingSphere===null&&mt.computeBoundingSphere(),Vt.copy(mt.boundingSphere.center)),Vt.applyMatrix4(x.matrixWorld).applyMatrix4(Ct)),Array.isArray(vt)){const Mt=mt.groups;for(let It=0,Nt=Mt.length;It0&&Re(N,U,O),et.length>0&&Re(et,U,O),mt.length>0&&Re(mt,U,O),At.buffers.depth.setTest(!0),At.buffers.depth.setMask(!0),At.buffers.color.setMask(!0),At.setPolygonOffset(!1)}function Ve(x,U,O,z){if((O.isScene===!0?O.overrideMaterial:null)!==null)return;f.state.transmissionRenderTarget[z.id]===void 0&&(f.state.transmissionRenderTarget[z.id]=new fn(1,1,{generateMipmaps:!0,type:qt.has("EXT_color_buffer_half_float")||qt.has("EXT_color_buffer_float")?Pn:Ln,minFilter:ai,samples:4,stencilBuffer:r,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:Qt.workingColorSpace}));const et=f.state.transmissionRenderTarget[z.id],mt=z.viewport||A;et.setSize(mt.z*y.transmissionResolutionScale,mt.w*y.transmissionResolutionScale);const vt=y.getRenderTarget();y.setRenderTarget(et),y.getClearColor(q),Q=y.getClearAlpha(),Q<1&&y.setClearColor(16777215,.5),y.clear(),Wt&&pt.render(O);const Mt=y.toneMapping;y.toneMapping=Wn;const It=z.viewport;if(z.viewport!==void 0&&(z.viewport=void 0),f.setupLightsView(z),nt===!0&&Z.setGlobalState(y.clippingPlanes,z),Re(x,O,z),b.updateMultisampleRenderTarget(et),b.updateRenderTargetMipmap(et),qt.has("WEBGL_multisampled_render_to_texture")===!1){let Nt=!1;for(let Pt=0,Kt=U.length;Pt0),Pt=!!O.morphAttributes.position,Kt=!!O.morphAttributes.normal,te=!!O.morphAttributes.color;let xe=Wn;z.toneMapped&&(I===null||I.isXRRenderTarget===!0)&&(xe=y.toneMapping);const pe=O.morphAttributes.position||O.morphAttributes.normal||O.morphAttributes.color,Jt=pe!==void 0?pe.length:0,Dt=Rt.get(z),De=f.state.lights;if(nt===!0&&(_t===!0||x!==M)){const Be=x===M&&z.id===S;Z.setState(z,x,Be)}let ee=!1;z.version===Dt.__version?(Dt.needsLights&&Dt.lightsStateVersion!==De.state.version||Dt.outputColorSpace!==vt||N.isBatchedMesh&&Dt.batching===!1||!N.isBatchedMesh&&Dt.batching===!0||N.isBatchedMesh&&Dt.batchingColor===!0&&N.colorTexture===null||N.isBatchedMesh&&Dt.batchingColor===!1&&N.colorTexture!==null||N.isInstancedMesh&&Dt.instancing===!1||!N.isInstancedMesh&&Dt.instancing===!0||N.isSkinnedMesh&&Dt.skinning===!1||!N.isSkinnedMesh&&Dt.skinning===!0||N.isInstancedMesh&&Dt.instancingColor===!0&&N.instanceColor===null||N.isInstancedMesh&&Dt.instancingColor===!1&&N.instanceColor!==null||N.isInstancedMesh&&Dt.instancingMorph===!0&&N.morphTexture===null||N.isInstancedMesh&&Dt.instancingMorph===!1&&N.morphTexture!==null||Dt.envMap!==Mt||z.fog===!0&&Dt.fog!==et||Dt.numClippingPlanes!==void 0&&(Dt.numClippingPlanes!==Z.numPlanes||Dt.numIntersection!==Z.numIntersection)||Dt.vertexAlphas!==It||Dt.vertexTangents!==Nt||Dt.morphTargets!==Pt||Dt.morphNormals!==Kt||Dt.morphColors!==te||Dt.toneMapping!==xe||Dt.morphTargetsCount!==Jt)&&(ee=!0):(ee=!0,Dt.__version=z.version);let ln=Dt.currentProgram;ee===!0&&(ln=en(z,U,N));let mi=!1,qe=!1,is=!1;const de=ln.getUniforms(),nn=Dt.uniforms;if(At.useProgram(ln.program)&&(mi=!0,qe=!0,is=!0),z.id!==S&&(S=z.id,qe=!0),mi||M!==x){At.buffers.depth.getReversed()?(lt.copy(x.projectionMatrix),zh(lt),kh(lt),de.setValue(R,"projectionMatrix",lt)):de.setValue(R,"projectionMatrix",x.projectionMatrix),de.setValue(R,"viewMatrix",x.matrixWorldInverse);const Ge=de.map.cameraPosition;Ge!==void 0&&Ge.setValue(R,Lt.setFromMatrixPosition(x.matrixWorld)),jt.logarithmicDepthBuffer&&de.setValue(R,"logDepthBufFC",2/(Math.log(x.far+1)/Math.LN2)),(z.isMeshPhongMaterial||z.isMeshToonMaterial||z.isMeshLambertMaterial||z.isMeshBasicMaterial||z.isMeshStandardMaterial||z.isShaderMaterial)&&de.setValue(R,"isOrthographic",x.isOrthographicCamera===!0),M!==x&&(M=x,qe=!0,is=!0)}if(N.isSkinnedMesh){de.setOptional(R,N,"bindMatrix"),de.setOptional(R,N,"bindMatrixInverse");const Be=N.skeleton;Be&&(Be.boneTexture===null&&Be.computeBoneTexture(),de.setValue(R,"boneTexture",Be.boneTexture,b))}N.isBatchedMesh&&(de.setOptional(R,N,"batchingTexture"),de.setValue(R,"batchingTexture",N._matricesTexture,b),de.setOptional(R,N,"batchingIdTexture"),de.setValue(R,"batchingIdTexture",N._indirectTexture,b),de.setOptional(R,N,"batchingColorTexture"),N._colorsTexture!==null&&de.setValue(R,"batchingColorTexture",N._colorsTexture,b));const sn=O.morphAttributes;if((sn.position!==void 0||sn.normal!==void 0||sn.color!==void 0)&&wt.update(N,O,ln),(qe||Dt.receiveShadow!==N.receiveShadow)&&(Dt.receiveShadow=N.receiveShadow,de.setValue(R,"receiveShadow",N.receiveShadow)),z.isMeshGouraudMaterial&&z.envMap!==null&&(nn.envMap.value=Mt,nn.flipEnvMap.value=Mt.isCubeTexture&&Mt.isRenderTargetTexture===!1?-1:1),z.isMeshStandardMaterial&&z.envMap===null&&U.environment!==null&&(nn.envMapIntensity.value=U.environmentIntensity),qe&&(de.setValue(R,"toneMappingExposure",y.toneMappingExposure),Dt.needsLights&&Ir(nn,is),et&&z.fog===!0&&dt.refreshFogUniforms(nn,et),dt.refreshMaterialUniforms(nn,z,H,tt,f.state.transmissionRenderTarget[x.id]),_r.upload(R,pi(Dt),nn,b)),z.isShaderMaterial&&z.uniformsNeedUpdate===!0&&(_r.upload(R,pi(Dt),nn,b),z.uniformsNeedUpdate=!1),z.isSpriteMaterial&&de.setValue(R,"center",N.center),de.setValue(R,"modelViewMatrix",N.modelViewMatrix),de.setValue(R,"normalMatrix",N.normalMatrix),de.setValue(R,"modelMatrix",N.matrixWorld),z.isShaderMaterial||z.isRawShaderMaterial){const Be=z.uniformsGroups;for(let Ge=0,Or=Be.length;Ge0&&b.useMultisampledRTT(x)===!1?N=Rt.get(x).__webglMultisampledFramebuffer:Array.isArray(Nt)?N=Nt[O]:N=Nt,A.copy(x.viewport),W.copy(x.scissor),k=x.scissorTest}else A.copy(Et).multiplyScalar(H).floor(),W.copy(Ft).multiplyScalar(H).floor(),k=Xt;if(O!==0&&(N=Nr),At.bindFramebuffer(R.FRAMEBUFFER,N)&&z&&At.drawBuffers(x,N),At.viewport(A),At.scissor(W),At.setScissorTest(k),et){const Mt=Rt.get(x.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_CUBE_MAP_POSITIVE_X+U,Mt.__webglTexture,O)}else if(mt){const Mt=Rt.get(x.texture),It=U;R.framebufferTextureLayer(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,Mt.__webglTexture,O,It)}else if(x!==null&&O!==0){const Mt=Rt.get(x.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_2D,Mt.__webglTexture,O)}S=-1},this.readRenderTargetPixels=function(x,U,O,z,N,et,mt){if(!(x&&x.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let vt=Rt.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&mt!==void 0&&(vt=vt[mt]),vt){At.bindFramebuffer(R.FRAMEBUFFER,vt);try{const Mt=x.texture,It=Mt.format,Nt=Mt.type;if(!jt.textureFormatReadable(It)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!jt.textureTypeReadable(Nt)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}U>=0&&U<=x.width-z&&O>=0&&O<=x.height-N&&R.readPixels(U,O,z,N,Ut.convert(It),Ut.convert(Nt),et)}finally{const Mt=I!==null?Rt.get(I).__webglFramebuffer:null;At.bindFramebuffer(R.FRAMEBUFFER,Mt)}}},this.readRenderTargetPixelsAsync=async function(x,U,O,z,N,et,mt){if(!(x&&x.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let vt=Rt.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&mt!==void 0&&(vt=vt[mt]),vt){const Mt=x.texture,It=Mt.format,Nt=Mt.type;if(!jt.textureFormatReadable(It))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!jt.textureTypeReadable(Nt))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");if(U>=0&&U<=x.width-z&&O>=0&&O<=x.height-N){At.bindFramebuffer(R.FRAMEBUFFER,vt);const Pt=R.createBuffer();R.bindBuffer(R.PIXEL_PACK_BUFFER,Pt),R.bufferData(R.PIXEL_PACK_BUFFER,et.byteLength,R.STREAM_READ),R.readPixels(U,O,z,N,Ut.convert(It),Ut.convert(Nt),0);const Kt=I!==null?Rt.get(I).__webglFramebuffer:null;At.bindFramebuffer(R.FRAMEBUFFER,Kt);const te=R.fenceSync(R.SYNC_GPU_COMMANDS_COMPLETE,0);return R.flush(),await Bh(R,te,4),R.bindBuffer(R.PIXEL_PACK_BUFFER,Pt),R.getBufferSubData(R.PIXEL_PACK_BUFFER,0,et),R.deleteBuffer(Pt),R.deleteSync(te),et}else throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(x,U=null,O=0){x.isTexture!==!0&&(Fi("WebGLRenderer: copyFramebufferToTexture function signature has changed."),U=arguments[0]||null,x=arguments[1]);const z=Math.pow(2,-O),N=Math.floor(x.image.width*z),et=Math.floor(x.image.height*z),mt=U!==null?U.x:0,vt=U!==null?U.y:0;b.setTexture2D(x,0),R.copyTexSubImage2D(R.TEXTURE_2D,O,0,0,mt,vt,N,et),At.unbindTexture()};const Fr=R.createFramebuffer(),Oc=R.createFramebuffer();this.copyTextureToTexture=function(x,U,O=null,z=null,N=0,et=null){x.isTexture!==!0&&(Fi("WebGLRenderer: copyTextureToTexture function signature has changed."),z=arguments[0]||null,x=arguments[1],U=arguments[2],et=arguments[3]||0,O=null),et===null&&(N!==0?(Fi("WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels."),et=N,N=0):et=0);let mt,vt,Mt,It,Nt,Pt,Kt,te,xe;const pe=x.isCompressedTexture?x.mipmaps[et]:x.image;if(O!==null)mt=O.max.x-O.min.x,vt=O.max.y-O.min.y,Mt=O.isBox3?O.max.z-O.min.z:1,It=O.min.x,Nt=O.min.y,Pt=O.isBox3?O.min.z:0;else{const sn=Math.pow(2,-N);mt=Math.floor(pe.width*sn),vt=Math.floor(pe.height*sn),x.isDataArrayTexture?Mt=pe.depth:x.isData3DTexture?Mt=Math.floor(pe.depth*sn):Mt=1,It=0,Nt=0,Pt=0}z!==null?(Kt=z.x,te=z.y,xe=z.z):(Kt=0,te=0,xe=0);const Jt=Ut.convert(U.format),Dt=Ut.convert(U.type);let De;U.isData3DTexture?(b.setTexture3D(U,0),De=R.TEXTURE_3D):U.isDataArrayTexture||U.isCompressedArrayTexture?(b.setTexture2DArray(U,0),De=R.TEXTURE_2D_ARRAY):(b.setTexture2D(U,0),De=R.TEXTURE_2D),R.pixelStorei(R.UNPACK_FLIP_Y_WEBGL,U.flipY),R.pixelStorei(R.UNPACK_PREMULTIPLY_ALPHA_WEBGL,U.premultiplyAlpha),R.pixelStorei(R.UNPACK_ALIGNMENT,U.unpackAlignment);const ee=R.getParameter(R.UNPACK_ROW_LENGTH),ln=R.getParameter(R.UNPACK_IMAGE_HEIGHT),mi=R.getParameter(R.UNPACK_SKIP_PIXELS),qe=R.getParameter(R.UNPACK_SKIP_ROWS),is=R.getParameter(R.UNPACK_SKIP_IMAGES);R.pixelStorei(R.UNPACK_ROW_LENGTH,pe.width),R.pixelStorei(R.UNPACK_IMAGE_HEIGHT,pe.height),R.pixelStorei(R.UNPACK_SKIP_PIXELS,It),R.pixelStorei(R.UNPACK_SKIP_ROWS,Nt),R.pixelStorei(R.UNPACK_SKIP_IMAGES,Pt);const de=x.isDataArrayTexture||x.isData3DTexture,nn=U.isDataArrayTexture||U.isData3DTexture;if(x.isDepthTexture){const sn=Rt.get(x),Be=Rt.get(U),Ge=Rt.get(sn.__renderTarget),Or=Rt.get(Be.__renderTarget);At.bindFramebuffer(R.READ_FRAMEBUFFER,Ge.__webglFramebuffer),At.bindFramebuffer(R.DRAW_FRAMEBUFFER,Or.__webglFramebuffer);for(let jn=0;jnMath.PI&&(n-=We),s<-Math.PI?s+=We:s>Math.PI&&(s-=We),n<=s?this._spherical.theta=Math.max(n,Math.min(s,this._spherical.theta)):this._spherical.theta=this._spherical.theta>(n+s)/2?Math.max(n,this._spherical.theta):Math.min(s,this._spherical.theta)),this._spherical.phi=Math.max(this.minPolarAngle,Math.min(this.maxPolarAngle,this._spherical.phi)),this._spherical.makeSafe(),this.enableDamping===!0?this.target.addScaledVector(this._panOffset,this.dampingFactor):this.target.add(this._panOffset),this.target.sub(this.cursor),this.target.clampLength(this.minTargetRadius,this.maxTargetRadius),this.target.add(this.cursor);let r=!1;if(this.zoomToCursor&&this._performCursorZoom||this.object.isOrthographicCamera)this._spherical.radius=this._clampDistance(this._spherical.radius);else{const a=this._spherical.radius;this._spherical.radius=this._clampDistance(this._spherical.radius*this._scale),r=a!=this._spherical.radius}if(Ee.setFromSpherical(this._spherical),Ee.applyQuaternion(this._quatInverse),e.copy(this.target).add(Ee),this.object.lookAt(this.target),this.enableDamping===!0?(this._sphericalDelta.theta*=1-this.dampingFactor,this._sphericalDelta.phi*=1-this.dampingFactor,this._panOffset.multiplyScalar(1-this.dampingFactor)):(this._sphericalDelta.set(0,0,0),this._panOffset.set(0,0,0)),this.zoomToCursor&&this._performCursorZoom){let a=null;if(this.object.isPerspectiveCamera){const o=Ee.length();a=this._clampDistance(o*this._scale);const l=o-a;this.object.position.addScaledVector(this._dollyDirection,l),this.object.updateMatrixWorld(),r=!!l}else if(this.object.isOrthographicCamera){const o=new P(this._mouse.x,this._mouse.y,0);o.unproject(this.object);const l=this.object.zoom;this.object.zoom=Math.max(this.minZoom,Math.min(this.maxZoom,this.object.zoom/this._scale)),this.object.updateProjectionMatrix(),r=l!==this.object.zoom;const c=new P(this._mouse.x,this._mouse.y,0);c.unproject(this.object),this.object.position.sub(c).add(o),this.object.updateMatrixWorld(),a=Ee.length()}else console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."),this.zoomToCursor=!1;a!==null&&(this.screenSpacePanning?this.target.set(0,0,-1).transformDirection(this.object.matrix).multiplyScalar(a).add(this.object.position):(cr.origin.copy(this.object.position),cr.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(cr.direction))_a||8*(1-this._lastQuaternion.dot(this.object.quaternion))>_a||this._lastTargetPosition.distanceToSquared(this.target)>_a?(this.dispatchEvent(Vl),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(t){return t!==null?We/60*this.autoRotateSpeed*t:We/60/60*this.autoRotateSpeed}_getZoomScale(t){const e=Math.abs(t*.01);return Math.pow(.95,this.zoomSpeed*e)}_rotateLeft(t){this._sphericalDelta.theta-=t}_rotateUp(t){this._sphericalDelta.phi-=t}_panLeft(t,e){Ee.setFromMatrixColumn(e,0),Ee.multiplyScalar(-t),this._panOffset.add(Ee)}_panUp(t,e){this.screenSpacePanning===!0?Ee.setFromMatrixColumn(e,1):(Ee.setFromMatrixColumn(e,0),Ee.crossVectors(this.object.up,Ee)),Ee.multiplyScalar(t),this._panOffset.add(Ee)}_pan(t,e){const n=this.domElement;if(this.object.isPerspectiveCamera){const s=this.object.position;Ee.copy(s).sub(this.target);let r=Ee.length();r*=Math.tan(this.object.fov/2*Math.PI/180),this._panLeft(2*t*r/n.clientHeight,this.object.matrix),this._panUp(2*e*r/n.clientHeight,this.object.matrix)}else this.object.isOrthographicCamera?(this._panLeft(t*(this.object.right-this.object.left)/this.object.zoom/n.clientWidth,this.object.matrix),this._panUp(e*(this.object.top-this.object.bottom)/this.object.zoom/n.clientHeight,this.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),this.enablePan=!1)}_dollyOut(t){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale/=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyIn(t){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale*=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_updateZoomParameters(t,e){if(!this.zoomToCursor)return;this._performCursorZoom=!0;const n=this.domElement.getBoundingClientRect(),s=t-n.left,r=e-n.top,a=n.width,o=n.height;this._mouse.x=s/a*2-1,this._mouse.y=-(r/o)*2+1,this._dollyDirection.set(this._mouse.x,this._mouse.y,1).unproject(this.object).sub(this.object.position).normalize()}_clampDistance(t){return Math.max(this.minDistance,Math.min(this.maxDistance,t))}_handleMouseDownRotate(t){this._rotateStart.set(t.clientX,t.clientY)}_handleMouseDownDolly(t){this._updateZoomParameters(t.clientX,t.clientX),this._dollyStart.set(t.clientX,t.clientY)}_handleMouseDownPan(t){this._panStart.set(t.clientX,t.clientY)}_handleMouseMoveRotate(t){this._rotateEnd.set(t.clientX,t.clientY),this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const e=this.domElement;this._rotateLeft(We*this._rotateDelta.x/e.clientHeight),this._rotateUp(We*this._rotateDelta.y/e.clientHeight),this._rotateStart.copy(this._rotateEnd),this.update()}_handleMouseMoveDolly(t){this._dollyEnd.set(t.clientX,t.clientY),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyOut(this._getZoomScale(this._dollyDelta.y)):this._dollyDelta.y<0&&this._dollyIn(this._getZoomScale(this._dollyDelta.y)),this._dollyStart.copy(this._dollyEnd),this.update()}_handleMouseMovePan(t){this._panEnd.set(t.clientX,t.clientY),this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleMouseWheel(t){this._updateZoomParameters(t.clientX,t.clientY),t.deltaY<0?this._dollyIn(this._getZoomScale(t.deltaY)):t.deltaY>0&&this._dollyOut(this._getZoomScale(t.deltaY)),this.update()}_handleKeyDown(t){let e=!1;switch(t.code){case this.keys.UP:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateUp(We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,this.keyPanSpeed),e=!0;break;case this.keys.BOTTOM:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateUp(-We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,-this.keyPanSpeed),e=!0;break;case this.keys.LEFT:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateLeft(We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(this.keyPanSpeed,0),e=!0;break;case this.keys.RIGHT:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateLeft(-We*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(-this.keyPanSpeed,0),e=!0;break}e&&(t.preventDefault(),this.update())}_handleTouchStartRotate(t){if(this._pointers.length===1)this._rotateStart.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._rotateStart.set(n,s)}}_handleTouchStartPan(t){if(this._pointers.length===1)this._panStart.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._panStart.set(n,s)}}_handleTouchStartDolly(t){const e=this._getSecondPointerPosition(t),n=t.pageX-e.x,s=t.pageY-e.y,r=Math.sqrt(n*n+s*s);this._dollyStart.set(0,r)}_handleTouchStartDollyPan(t){this.enableZoom&&this._handleTouchStartDolly(t),this.enablePan&&this._handleTouchStartPan(t)}_handleTouchStartDollyRotate(t){this.enableZoom&&this._handleTouchStartDolly(t),this.enableRotate&&this._handleTouchStartRotate(t)}_handleTouchMoveRotate(t){if(this._pointers.length==1)this._rotateEnd.set(t.pageX,t.pageY);else{const n=this._getSecondPointerPosition(t),s=.5*(t.pageX+n.x),r=.5*(t.pageY+n.y);this._rotateEnd.set(s,r)}this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const e=this.domElement;this._rotateLeft(We*this._rotateDelta.x/e.clientHeight),this._rotateUp(We*this._rotateDelta.y/e.clientHeight),this._rotateStart.copy(this._rotateEnd)}_handleTouchMovePan(t){if(this._pointers.length===1)this._panEnd.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._panEnd.set(n,s)}this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd)}_handleTouchMoveDolly(t){const e=this._getSecondPointerPosition(t),n=t.pageX-e.x,s=t.pageY-e.y,r=Math.sqrt(n*n+s*s);this._dollyEnd.set(0,r),this._dollyDelta.set(0,Math.pow(this._dollyEnd.y/this._dollyStart.y,this.zoomSpeed)),this._dollyOut(this._dollyDelta.y),this._dollyStart.copy(this._dollyEnd);const a=(t.pageX+e.x)*.5,o=(t.pageY+e.y)*.5;this._updateZoomParameters(a,o)}_handleTouchMoveDollyPan(t){this.enableZoom&&this._handleTouchMoveDolly(t),this.enablePan&&this._handleTouchMovePan(t)}_handleTouchMoveDollyRotate(t){this.enableZoom&&this._handleTouchMoveDolly(t),this.enableRotate&&this._handleTouchMoveRotate(t)}_addPointer(t){this._pointers.push(t.pointerId)}_removePointer(t){delete this._pointerPositions[t.pointerId];for(let e=0;e + varying vec2 vUv; + uniform sampler2D colorTexture; + uniform vec2 invSize; + uniform vec2 direction; + uniform float gaussianCoefficients[KERNEL_RADIUS]; + + void main() { + float weightSum = gaussianCoefficients[0]; + vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; + for( int i = 1; i < KERNEL_RADIUS; i ++ ) { + float x = float(i); + float w = gaussianCoefficients[i]; + vec2 uvOffset = direction * invSize * x; + vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb; + vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb; + diffuseSum += (sample1 + sample2) * w; + weightSum += 2.0 * w; + } + gl_FragColor = vec4(diffuseSum/weightSum, 1.0); + }`})}getCompositeMaterial(t){return new He({defines:{NUM_MIPS:t},uniforms:{blurTexture1:{value:null},blurTexture2:{value:null},blurTexture3:{value:null},blurTexture4:{value:null},blurTexture5:{value:null},bloomStrength:{value:1},bloomFactors:{value:null},bloomTintColors:{value:null},bloomRadius:{value:0}},vertexShader:`varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); + }`,fragmentShader:`varying vec2 vUv; + uniform sampler2D blurTexture1; + uniform sampler2D blurTexture2; + uniform sampler2D blurTexture3; + uniform sampler2D blurTexture4; + uniform sampler2D blurTexture5; + uniform float bloomStrength; + uniform float bloomRadius; + uniform float bloomFactors[NUM_MIPS]; + uniform vec3 bloomTintColors[NUM_MIPS]; + + float lerpBloomFactor(const in float factor) { + float mirrorFactor = 1.2 - factor; + return mix(factor, mirrorFactor, bloomRadius); + } + + void main() { + gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + + lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + + lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + + lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + + lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); + }`})}}ts.BlurDirectionX=new St(1,0);ts.BlurDirectionY=new St(0,1);function Pg(){const t=new Float32Array(6e3),e=new Float32Array(2e3*3);for(let r=0;r<2e3;r++){const a=Math.random()*Math.PI*2,o=Math.acos(2*Math.random()-1),l=600+Math.random()*400;t[r*3]=l*Math.sin(o)*Math.cos(a),t[r*3+1]=l*Math.sin(o)*Math.sin(a),t[r*3+2]=l*Math.cos(o);const c=Math.random();e[r*3]=.55+c*.25,e[r*3+1]=.55+c*.15,e[r*3+2]=.75+c*.25}const n=new ge;n.setAttribute("position",new he(t,3)),n.setAttribute("color",new he(e,3));const s=new oi({size:1.6,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.6,depthWrite:!1,blending:Le});return new Yi(n,s)}function Dg(i){const t=new lu;t.background=new ot(328975),t.fog=new Dr(657946,.0035);const e=new $e(60,i.clientWidth/i.clientHeight,.1,2e3);e.position.set(0,30,80);const n=new lg({antialias:!0,alpha:!0,powerPreference:"high-performance"});n.setSize(i.clientWidth,i.clientHeight),n.setPixelRatio(Math.min(window.devicePixelRatio,2)),n.toneMapping=ec,n.toneMappingExposure=1.25,i.appendChild(n.domElement);const s=new hg(e,n.domElement);s.enableDamping=!0,s.dampingFactor=.05,s.rotateSpeed=.5,s.zoomSpeed=.8,s.minDistance=12,s.maxDistance=180,s.autoRotate=!0,s.autoRotateSpeed=.3;const r=new Ag(n);r.addPass(new Rg(t,e));const a=new ts(new St(i.clientWidth,i.clientHeight),.55,.6,.2);r.addPass(a);const o=new Mu(2763354,.7);t.add(o);const l=new dl(6514417,1.8,240);l.position.set(50,50,50),t.add(l);const c=new dl(11032055,1.2,240);c.position.set(-50,-30,-50),t.add(c);const h=Pg();t.add(h);const d=new Eu;d.params.Points={threshold:2};const p=new St;return{scene:t,camera:e,renderer:n,controls:s,composer:r,bloomPass:a,raycaster:d,mouse:p,lights:{ambient:o,point1:l,point2:c},starfield:h}}function Lg(i,t){const e=t.clientWidth,n=t.clientHeight;i.camera.aspect=e/n,i.camera.updateProjectionMatrix(),i.renderer.setSize(e,n),i.composer.setSize(e,n)}function Ug(i){i.scene.traverse(t=>{var e;(t instanceof be||t instanceof du)&&((e=t.geometry)==null||e.dispose(),Array.isArray(t.material)?t.material.forEach(n=>n.dispose()):t.material&&t.material.dispose())}),i.renderer.dispose(),i.composer.dispose()}class Ig{constructor(t){kt(this,"positions");kt(this,"velocities");kt(this,"running",!0);kt(this,"step",0);kt(this,"repulsionStrength",500);kt(this,"attractionStrength",.01);kt(this,"dampening",.9);kt(this,"baseMaxSteps",300);kt(this,"maxSteps",300);kt(this,"cooldownExtension",0);this.positions=t,this.velocities=new Map;for(const e of t.keys())this.velocities.set(e,new P)}addNode(t,e){this.positions.set(t,e.clone()),this.velocities.set(t,new P),this.cooldownExtension=100,this.maxSteps=Math.max(this.maxSteps,this.step+this.cooldownExtension),this.running=!0}removeNode(t){this.positions.delete(t),this.velocities.delete(t)}tick(t){if(!this.running)return;if(this.step>this.maxSteps){this.cooldownExtension>0&&(this.cooldownExtension=0,this.maxSteps=this.baseMaxSteps);return}this.step++;const e=Math.max(.001,1-this.step/this.maxSteps),n=Array.from(this.positions.keys());for(let s=0;s=.7?"active":i>=.4?"dormant":i>=.1?"silent":"unavailable"}const po={active:"#10b981",dormant:"#f59e0b",silent:"#8b5cf6",unavailable:"#6b7280"},Fg={active:"Easily retrievable (retention ≥ 70%)",dormant:"Retrievable with effort (40–70%)",silent:"Difficult, needs cues (10–40%)",unavailable:"Needs reinforcement (< 10%)"},xr={aha:"#FFD700",confusion:"#EF4444",failure:"#9CA3AF"},Og={aha:"Aha moments and breakthroughs",confusion:"Confusions and weak spots",failure:"Failures and guardrails"};function Xl(i,t){return t==="state"?po[Ng(i.retention)]:t==="ahagraph"?Bg(i)??Sa[i.type]??"#8B95A5":Sa[i.type]||"#8B95A5"}function Bg(i){const t=new Set((i.tags??[]).map(e=>e.toLowerCase()));return t.has("aha")?xr.aha:t.has("confusion")||t.has("weak-spot")?xr.confusion:t.has("failure")||t.has("guardrail")?xr.failure:null}let ms=null;function mo(){if(ms)return ms;const i=128,t=document.createElement("canvas");t.width=i,t.height=i;const e=t.getContext("2d");if(!e)return ms=new Pe,ms;const n=e.createRadialGradient(i/2,i/2,0,i/2,i/2,i/2);n.addColorStop(0,"rgba(255, 255, 255, 1.0)"),n.addColorStop(.25,"rgba(255, 255, 255, 0.7)"),n.addColorStop(.55,"rgba(255, 255, 255, 0.2)"),n.addColorStop(1,"rgba(255, 255, 255, 0.0)"),e.fillStyle=n,e.fillRect(0,0,i,i);const s=new bc(t);return s.needsUpdate=!0,ms=s,s}function Yl(i){if(i===0||i===1)return i;const t=.3;return Math.pow(2,-10*i)*Math.sin((i-t/4)*(2*Math.PI)/t)+1}function zg(i){return i*i*((1.70158+1)*i-1.70158)}class kg{constructor(){kt(this,"group");kt(this,"meshMap",new Map);kt(this,"glowMap",new Map);kt(this,"positions",new Map);kt(this,"labelSprites",new Map);kt(this,"hoveredNode",null);kt(this,"selectedNode",null);kt(this,"colorMode","type");kt(this,"materializingNodes",[]);kt(this,"dissolvingNodes",[]);kt(this,"growingNodes",[]);this.group=new zi}setColorMode(t){if(this.colorMode!==t){this.colorMode=t;for(const[e,n]of this.meshMap){const s=n.userData.retention??0,r=n.userData.type??"fact",a=Array.isArray(n.userData.tags)?n.userData.tags:[],l=Xl({type:r,retention:s,tags:a},t),c=new ot(l),h=n.material;h.color.copy(c),h.emissive.copy(c);const d=this.glowMap.get(e);d&&d.material.color.copy(c)}}}createNodes(t){const e=(1+Math.sqrt(5))/2,n=t.length;for(let s=0;s0,o=new Lr(s,16,16),l=new mu({color:new ot(r),emissive:new ot(r),emissiveIntensity:a?0:.3+t.retention*.5,roughness:.3,metalness:.1,transparent:!0,opacity:a?.2:.3+t.retention*.7}),c=new be(o,l);c.position.copy(e),c.scale.setScalar(n),c.userData={nodeId:t.id,type:t.type,retention:t.retention,tags:t.tags},this.meshMap.set(t.id,c),this.group.add(c);const h=new Xi({map:mo(),color:new ot(r),transparent:!0,opacity:n>0?a?.1:.3+t.retention*.35:0,blending:Le,depthWrite:!1}),d=new Bi(h);d.scale.set(s*6*n,s*6*n,1),d.position.copy(e),d.userData={isGlow:!0,nodeId:t.id},this.glowMap.set(t.id,d),this.group.add(d);const p=t.label||t.type,u=this.createTextSprite(p,"#94a3b8");return u.position.copy(e),u.position.y+=s*2+1.5,u.userData={isLabel:!0,nodeId:t.id,offset:s*2+1.5},this.group.add(u),this.labelSprites.set(t.id,u),{mesh:c,glow:d,label:u,size:s}}addNode(t,e,n={}){const s=(e==null?void 0:e.clone())??new P((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40);this.positions.set(t.id,s);const{mesh:r,glow:a,label:o}=this.createNodeMeshes(t,s,0);return r.scale.setScalar(.001),a.scale.set(.001,.001,1),a.material.opacity=0,o.material.opacity=0,n.isBirthRitual?(r.visible=!1,a.visible=!1,o.visible=!1,r.userData.birthRitualPending={totalFrames:30,targetScale:.5+t.retention*2}):this.materializingNodes.push({id:t.id,frame:0,totalFrames:30,mesh:r,glow:a,label:o,targetScale:.5+t.retention*2}),s}igniteNode(t){const e=this.meshMap.get(t),n=this.glowMap.get(t),s=this.labelSprites.get(t);if(!e||!n||!s)return;const r=e.userData.birthRitualPending;r&&(e.visible=!0,n.visible=!0,s.visible=!0,delete e.userData.birthRitualPending,this.materializingNodes.push({id:t,frame:0,totalFrames:r.totalFrames,mesh:e,glow:n,label:s,targetScale:r.targetScale}))}removeNode(t){const e=this.meshMap.get(t),n=this.glowMap.get(t),s=this.labelSprites.get(t);!e||!n||!s||(this.materializingNodes=this.materializingNodes.filter(r=>r.id!==t),this.dissolvingNodes.push({id:t,frame:0,totalFrames:60,mesh:e,glow:n,label:s,originalScale:e.scale.x}))}growNode(t,e){const n=this.meshMap.get(t);if(!n)return;const s=n.scale.x,r=.5+e*2;n.userData.retention=e,this.growingNodes.push({id:t,frame:0,totalFrames:30,startScale:s,targetScale:r})}createTextSprite(t,e){const n=document.createElement("canvas"),s=n.getContext("2d");if(!s){const f=new Pe;return new Bi(new Xi({map:f,transparent:!0,opacity:0}))}n.width=512,n.height=64;const r=t.length>40?t.slice(0,37)+"...":t;s.clearRect(0,0,n.width,n.height),s.font='600 22px -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif';const o=s.measureText(r).width,c=Math.min(o+14*2,n.width-4),h=40,d=(n.width-c)/2,p=(n.height-h)/2,u=h/2;s.fillStyle="rgba(10, 16, 28, 0.82)",s.beginPath(),s.moveTo(d+u,p),s.lineTo(d+c-u,p),s.quadraticCurveTo(d+c,p,d+c,p+u),s.lineTo(d+c,p+h-u),s.quadraticCurveTo(d+c,p+h,d+c-u,p+h),s.lineTo(d+u,p+h),s.quadraticCurveTo(d,p+h,d,p+h-u),s.lineTo(d,p+u),s.quadraticCurveTo(d,p,d+u,p),s.closePath(),s.fill(),s.strokeStyle="rgba(148, 163, 184, 0.18)",s.lineWidth=1,s.stroke(),s.textAlign="center",s.textBaseline="middle",s.fillStyle=e,s.fillText(r,n.width/2,n.height/2+1);const g=new bc(n);g.needsUpdate=!0;const _=new Xi({map:g,transparent:!0,opacity:0,depthTest:!1,sizeAttenuation:!0}),m=new Bi(_);return m.scale.set(9,1.2,1),m}updatePositions(){this.group.children.forEach(t=>{if(t.userData.nodeId){const e=this.positions.get(t.userData.nodeId);if(!e)return;t.userData.isGlow?t.position.copy(e):t.userData.isLabel?(t.position.copy(e),t.position.y+=t.userData.offset):t instanceof be&&t.position.copy(e)}})}animate(t,e,n,s=1){var a,o;for(let l=this.materializingNodes.length-1;l>=0;l--){const c=this.materializingNodes[l];c.frame++;const h=Math.min(c.frame/c.totalFrames,1),d=Yl(h);if(c.mesh.scale.setScalar(Math.max(.001,d)),c.frame>=5){const p=Math.min((c.frame-5)/5,1),u=c.glow.material;u.opacity=p*.4;const g=c.targetScale*6*d;c.glow.scale.set(g,g,1)}if(c.frame>=40){const p=Math.min((c.frame-40)/20,1);c.label.material.opacity=p*.9}c.frame>=60&&this.materializingNodes.splice(l,1)}for(let l=this.dissolvingNodes.length-1;l>=0;l--){const c=this.dissolvingNodes[l];c.frame++;const h=Math.min(c.frame/c.totalFrames,1),d=1-zg(h),p=Math.max(.001,c.originalScale*d);c.mesh.scale.setScalar(p);const u=p*6;c.glow.scale.set(u,u,1);const g=c.mesh.material;g.opacity*=.97,c.glow.material.opacity*=.95,c.label.material.opacity*=.93,c.frame>=c.totalFrames&&(this.group.remove(c.mesh),this.group.remove(c.glow),this.group.remove(c.label),c.mesh.geometry.dispose(),c.mesh.material.dispose(),(a=c.glow.material.map)==null||a.dispose(),c.glow.material.dispose(),(o=c.label.material.map)==null||o.dispose(),c.label.material.dispose(),this.meshMap.delete(c.id),this.glowMap.delete(c.id),this.labelSprites.delete(c.id),this.positions.delete(c.id),this.dissolvingNodes.splice(l,1))}for(let l=this.growingNodes.length-1;l>=0;l--){const c=this.growingNodes[l];c.frame++;const h=Math.min(c.frame/c.totalFrames,1),d=c.startScale+(c.targetScale-c.startScale)*Yl(h),p=this.meshMap.get(c.id);p&&p.scale.setScalar(d);const u=this.glowMap.get(c.id);if(u){const g=d*6;u.scale.set(g,g,1)}c.frame>=c.totalFrames&&this.growingNodes.splice(l,1)}const r=new Set([...this.materializingNodes.map(l=>l.id),...this.dissolvingNodes.map(l=>l.id),...this.growingNodes.map(l=>l.id)]);this.meshMap.forEach((l,c)=>{if(r.has(c))return;const h=e.find(T=>T.id===c);if(!h)return;const d=1+Math.sin(t*1.5+e.indexOf(h)*.5)*.15*h.retention;l.scale.setScalar(d);const p=this.positions.get(c),u=p?n.position.distanceTo(p):0,g=1+Math.min(1.4,Math.max(0,(u-60)/100)),_=l.material;if(c===this.hoveredNode)_.emissiveIntensity=1*s;else if(c===this.selectedNode)_.emissiveIntensity=.8*s;else{const E=.3+h.retention*.5+Math.sin(t*(.8+h.retention*.7))*.1*h.retention;_.emissiveIntensity=E*s*g}const m=.3+h.retention*.7;_.opacity=Math.min(1,m*s*g);const f=this.glowMap.get(c);if(f){const T=f.material,E=.3+h.retention*.35;T.opacity=Math.min(.95,E*s*g)}}),this.labelSprites.forEach((l,c)=>{if(r.has(c))return;const h=this.positions.get(c);if(!h)return;const d=n.position.distanceTo(h),p=l.material,u=c===this.hoveredNode||c===this.selectedNode?1:d<40?.9:d<80?.9*(1-(d-40)/40):0;p.opacity+=(u-p.opacity)*.1})}getMeshes(){return Array.from(this.meshMap.values())}dispose(){this.group.traverse(t=>{var e,n,s,r,a;t instanceof be?((e=t.geometry)==null||e.dispose(),(n=t.material)==null||n.dispose()):t instanceof Bi&&((r=(s=t.material)==null?void 0:s.map)==null||r.dispose(),(a=t.material)==null||a.dispose())}),this.materializingNodes=[],this.dissolvingNodes=[],this.growingNodes=[]}}function Hg(i){return 1-Math.pow(1-i,3)}class Vg{constructor(){kt(this,"group");kt(this,"growingEdges",[]);kt(this,"dissolvingEdges",[]);this.group=new zi}createEdges(t,e){for(const n of t){const s=e.get(n.source),r=e.get(n.target);if(!s||!r)continue;const a=[s,r],o=new ge().setFromPoints(a),l=new Ar({color:9133302,transparent:!0,opacity:Math.min(.25+n.weight*.5,.8),blending:Le,depthWrite:!1}),c=new co(o,l);c.userData={source:n.source,target:n.target},this.group.add(c)}}addEdge(t,e){const n=e.get(t.source),s=e.get(t.target);if(!n||!s)return;const r=[n.clone(),n.clone()],a=new ge().setFromPoints(r),o=new Ar({color:9133302,transparent:!0,opacity:0,blending:Le,depthWrite:!1}),l=new co(a,o);l.userData={source:t.source,target:t.target},this.group.add(l),this.growingEdges.push({line:l,source:t.source,target:t.target,frame:0,totalFrames:45})}removeEdgesForNode(t){const e=[];this.group.children.forEach(n=>{const s=n;(s.userData.source===t||s.userData.target===t)&&e.push(s)});for(const n of e)this.growingEdges=this.growingEdges.filter(s=>s.line!==n),this.dissolvingEdges.push({line:n,frame:0,totalFrames:40})}animateEdges(t){for(let e=this.growingEdges.length-1;e>=0;e--){const n=this.growingEdges[e];n.frame++;const s=Hg(Math.min(n.frame/n.totalFrames,1)),r=t.get(n.source),a=t.get(n.target);if(!r||!a)continue;const o=r.clone().lerp(a,s),l=n.line.geometry.attributes.position;l.setXYZ(0,r.x,r.y,r.z),l.setXYZ(1,o.x,o.y,o.z),l.needsUpdate=!0;const c=n.line.material;c.opacity=s*.65,n.frame>=n.totalFrames&&(c.opacity=.65,this.growingEdges.splice(e,1))}for(let e=this.dissolvingEdges.length-1;e>=0;e--){const n=this.dissolvingEdges[e];n.frame++;const s=n.frame/n.totalFrames,r=n.line.material;r.opacity=Math.max(0,.65*(1-s)),n.frame>=n.totalFrames&&(this.group.remove(n.line),n.line.geometry.dispose(),n.line.material.dispose(),this.dissolvingEdges.splice(e,1))}}updatePositions(t){this.group.children.forEach(e=>{const n=e;if(this.growingEdges.some(a=>a.line===n)||this.dissolvingEdges.some(a=>a.line===n))return;const s=t.get(n.userData.source),r=t.get(n.userData.target);if(s&&r){const a=n.geometry.attributes.position;a.setXYZ(0,s.x,s.y,s.z),a.setXYZ(1,r.x,r.y,r.z),a.needsUpdate=!0}})}dispose(){this.group.children.forEach(t=>{var n,s;const e=t;(n=e.geometry)==null||n.dispose(),(s=e.material)==null||s.dispose()}),this.growingEdges=[],this.dissolvingEdges=[]}}class Gg{constructor(t){kt(this,"starField");kt(this,"neuralParticles");this.starField=this.createStarField(),this.neuralParticles=this.createNeuralParticles(),t.add(this.starField),t.add(this.neuralParticles)}createStarField(){const e=new ge,n=new Float32Array(3e3*3),s=new Float32Array(3e3);for(let a=0;a<3e3;a++)n[a*3]=(Math.random()-.5)*1e3,n[a*3+1]=(Math.random()-.5)*1e3,n[a*3+2]=(Math.random()-.5)*1e3,s[a]=Math.random()*1.5;e.setAttribute("position",new he(n,3)),e.setAttribute("size",new he(s,1));const r=new oi({color:6514417,size:.5,transparent:!0,opacity:.4,sizeAttenuation:!0,blending:Le});return new Yi(e,r)}createNeuralParticles(){const e=new ge,n=new Float32Array(500*3),s=new Float32Array(500*3);for(let a=0;a<500;a++)n[a*3]=(Math.random()-.5)*100,n[a*3+1]=(Math.random()-.5)*100,n[a*3+2]=(Math.random()-.5)*100,s[a*3]=.4+Math.random()*.3,s[a*3+1]=.3+Math.random()*.2,s[a*3+2]=.8+Math.random()*.2;e.setAttribute("position",new he(n,3)),e.setAttribute("color",new he(s,3));const r=new oi({size:.3,vertexColors:!0,transparent:!0,opacity:.4,blending:Le,sizeAttenuation:!0});return new Yi(e,r)}animate(t){this.starField.rotation.y+=1e-4,this.starField.rotation.x+=5e-5;const e=this.neuralParticles.geometry.attributes.position;for(let n=0;n=0;s--){const r=this.pulseEffects[s];if(r.intensity-=r.decay,r.intensity<=0){this.pulseEffects.splice(s,1);continue}const a=t.get(r.nodeId);if(a){const o=a.material;o.emissive.lerp(r.color,r.intensity*.3),o.emissiveIntensity=Math.max(o.emissiveIntensity,r.intensity)}}for(let s=this.spawnBursts.length-1;s>=0;s--){const r=this.spawnBursts[s];if(r.age++,r.age>120){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),this.spawnBursts.splice(s,1);continue}const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity;for(let c=0;c=0;s--){const r=this.rainbowBursts[s];if(r.age++,r.age>r.maxAge){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),this.rainbowBursts.splice(s,1);continue}const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity;for(let p=0;p=0;s--){const r=this.rippleWaves[s];if(r.age++,r.radius+=r.speed,r.age>r.maxAge){this.rippleWaves.splice(s,1);continue}const a=r.radius,o=3;n.forEach((l,c)=>{if(r.pulsedNodes.has(c))return;const h=l.distanceTo(r.origin);h>=a-o&&h<=a+o&&(r.pulsedNodes.add(c),this.addPulse(c,.8,new ot(65489),.03))})}for(let s=this.implosions.length-1;s>=0;s--){const r=this.implosions[s];if(r.age++,r.age>r.maxAge+20){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),r.flash&&(this.scene.remove(r.flash),r.flash.geometry.dispose(),r.flash.material.dispose()),this.implosions.splice(s,1);continue}if(r.age<=r.maxAge){const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity,l=1+r.age*.02;for(let h=0;hr.maxAge){const a=(r.age-r.maxAge)/20;r.flash.material.opacity=Math.max(0,1-a),r.flash.scale.setScalar(1+a*3)}}for(let s=this.shockwaves.length-1;s>=0;s--){const r=this.shockwaves[s];if(r.age++,r.age>r.maxAge){this.scene.remove(r.mesh),r.mesh.geometry.dispose(),r.mesh.material.dispose(),this.shockwaves.splice(s,1);continue}const a=r.age/r.maxAge;r.mesh.scale.setScalar(1+a*20),r.mesh.material.opacity=.8*(1-a),r.mesh.lookAt(e.position)}for(let s=this.connectionFlashes.length-1;s>=0;s--){const r=this.connectionFlashes[s];if(r.intensity-=.015,r.intensity<=0){this.scene.remove(r.line),r.line.geometry.dispose(),r.line.material.dispose(),this.connectionFlashes.splice(s,1);continue}r.line.material.opacity=r.intensity}for(let s=this.birthOrbs.length-1;s>=0;s--){const r=this.birthOrbs[s];r.age++;const a=r.gestationFrames+r.flightFrames,o=r.sprite.material,l=r.core.material,c=r.getTargetPos();if(c)r.lastTargetPos.copy(c);else if(r.age>r.gestationFrames&&!r.aborted){r.aborted=!0;const h=r.sprite.position;o.color.setRGB(1,.15,.2),l.color.setRGB(1,.6,.6),this.createImplosion(h,new ot(16721203)),r.arriveFired=!0,r.age=a+1}if(r.age<=r.gestationFrames){const h=r.age/r.gestationFrames,d=1-Math.pow(1-h,3),p=.85+Math.sin(r.age*.35)*.15,u=.5+d*4.5*p,g=.2+d*1.8*p;r.sprite.scale.set(u,u,1),r.core.scale.set(g,g,1),o.opacity=d*.95,l.opacity=d,o.color.copy(r.color).multiplyScalar(.7+d*.3),r.sprite.position.copy(r.startPos),r.core.position.copy(r.startPos)}else if(r.age<=a){const h=(r.age-r.gestationFrames)/r.flightFrames,d=h<.5?2*h*h:1-Math.pow(-2*h+2,2)/2,p=r.startPos,u=r.lastTargetPos,g=u.x-p.x,_=u.y-p.y,m=u.z-p.z,f=Math.sqrt(g*g+_*_+m*m),T=(p.x+u.x)*.5,E=(p.y+u.y)*.5+30+f*.15,y=(p.z+u.z)*.5,D=1-d,w=D*D,C=2*D*d,I=d*d,S=w*p.x+C*T+I*u.x,M=w*p.y+C*E+I*u.y,A=w*p.z+C*y+I*u.z;r.sprite.position.set(S,M,A),r.core.position.set(S,M,A);const W=1-d*.35;r.sprite.scale.setScalar(5*W),r.core.scale.setScalar(2*W),o.opacity=.95,l.opacity=1,o.color.copy(r.color)}else if(r.arriveFired){const h=r.age-a,d=Math.max(0,1-h/8);o.opacity=.95*d,l.opacity=1*d,r.sprite.scale.setScalar(5*(1+(1-d)*2)),d<=0&&(this.scene.remove(r.sprite),this.scene.remove(r.core),o.dispose(),l.dispose(),this.birthOrbs.splice(s,1))}else{r.arriveFired=!0;try{r.onArrive()}catch(h){console.warn("[birth-orb] onArrive threw",h)}}}}dispose(){for(const t of this.spawnBursts)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose();for(const t of this.rainbowBursts)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose();for(const t of this.implosions)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose(),t.flash&&(this.scene.remove(t.flash),t.flash.geometry.dispose(),t.flash.material.dispose());for(const t of this.shockwaves)this.scene.remove(t.mesh),t.mesh.geometry.dispose(),t.mesh.material.dispose();for(const t of this.connectionFlashes)this.scene.remove(t.line),t.line.geometry.dispose(),t.line.material.dispose();for(const t of this.birthOrbs)this.scene.remove(t.sprite),this.scene.remove(t.core),t.sprite.material.dispose(),t.core.material.dispose();this.pulseEffects=[],this.spawnBursts=[],this.rainbowBursts=[],this.rippleWaves=[],this.implosions=[],this.shockwaves=[],this.connectionFlashes=[],this.birthOrbs=[]}}const wn={bloomStrength:.8,rotateSpeed:.3,fogColor:328976,fogDensity:.008,nebulaIntensity:0,chromaticIntensity:.002,vignetteRadius:.9,breatheAmplitude:1},zn={bloomStrength:1.8,rotateSpeed:.08,fogColor:656672,fogDensity:.006,nebulaIntensity:1,chromaticIntensity:.005,vignetteRadius:.7,breatheAmplitude:2};class Xg{constructor(){kt(this,"active",!1);kt(this,"transition",0);kt(this,"transitionSpeed",.008);kt(this,"current");kt(this,"auroraHue",0);this.current={...wn}}setActive(t){this.active=t}update(t,e,n,s,r){const a=this.active?1:0;this.transition+=(a-this.transition)*this.transitionSpeed*60*(1/60),this.transition=Math.max(0,Math.min(1,this.transition));const o=this.transition;this.current.bloomStrength=this.lerp(wn.bloomStrength,zn.bloomStrength,o),this.current.rotateSpeed=this.lerp(wn.rotateSpeed,zn.rotateSpeed,o),this.current.fogDensity=this.lerp(wn.fogDensity,zn.fogDensity,o),this.current.nebulaIntensity=this.lerp(wn.nebulaIntensity,zn.nebulaIntensity,o),this.current.chromaticIntensity=this.lerp(wn.chromaticIntensity,zn.chromaticIntensity,o),this.current.vignetteRadius=this.lerp(wn.vignetteRadius,zn.vignetteRadius,o),this.current.breatheAmplitude=this.lerp(wn.breatheAmplitude,zn.breatheAmplitude,o),e.strength=this.current.bloomStrength,n.autoRotateSpeed=this.current.rotateSpeed;const l=new ot(wn.fogColor),c=new ot(zn.fogColor),h=l.clone().lerp(c,o);if(t.fog=new Dr(h,this.current.fogDensity),o>.01){this.auroraHue=r*.1%1;const d=new ot().setHSL(.75+this.auroraHue*.15,.8,.5),p=new ot().setHSL(.55+this.auroraHue*.2,.7,.4);s.point1.color.lerp(d,o*.3),s.point2.color.lerp(p,o*.3)}else s.point1.color.set(6514417),s.point2.color.set(11032055)}lerp(t,e,n){return t+(e-t)*n}}const Yg=50,xs=[];function qg(i,t,e){const n=i.tags??[],s=i.type??"";let r=null,a=0;for(const o of t){let l=0;o.type===s&&(l+=2);for(const c of o.tags)n.includes(c)&&(l+=1);l>a&&(a=l,r=o.id)}if(r&&a>0){const o=e.get(r);if(o)return new P(o.x+(Math.random()-.5)*10,o.y+(Math.random()-.5)*10,o.z+(Math.random()-.5)*10)}return new P((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40)}function jg(i,t){if(xs.length<=Yg)return;const e=xs.shift();i.edgeManager.removeEdgesForNode(e),i.nodeManager.removeNode(e),i.forceSim.removeNode(e),i.onMutation({type:"edgesRemoved",nodeId:e}),i.onMutation({type:"nodeRemoved",nodeId:e});const n=t.findIndex(s=>s.id===e);n!==-1&&t.splice(n,1)}function Zg(i,t,e){var d,p;const{effects:n,nodeManager:s,edgeManager:r,forceSim:a,camera:o,onMutation:l}=t,c=s.positions,h=s.meshMap;switch(i.type){case"MemoryCreated":{const u=i.data;if(!u.id)break;const g={id:u.id,label:(u.content??"").slice(0,60),type:u.node_type??"fact",retention:Math.max(0,Math.min(1,u.retention??.9)),tags:u.tags??[],createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),isCenter:!1},_=qg(g,e,c),m=s.addNode(g,_,{isBirthRitual:!0});a.addNode(u.id,m),xs.push(u.id),jg(t,e);const f=new ot(Sa[g.type]||"#00ffd1"),T=f.clone();T.offsetHSL(.15,0,0),n.createBirthOrb(o,f,()=>s.positions.get(g.id),()=>{s.igniteNode(g.id);const E=s.positions.get(g.id)??_,y=s.meshMap.get(g.id);y&&y.scale.multiplyScalar(1.8),n.createRainbowBurst(E,f),n.createShockwave(E,f,o),n.createShockwave(E,T,o),n.createRippleWave(E)}),l({type:"nodeAdded",node:g});break}case"ConnectionDiscovered":{const u=i.data;if(!u.source_id||!u.target_id)break;const g=c.get(u.source_id),_=c.get(u.target_id),m={source:u.source_id,target:u.target_id,weight:u.weight??.5,type:u.connection_type??"semantic"};r.addEdge(m,c),g&&_&&n.createConnectionFlash(g,_,new ot(54527)),u.source_id&&h.has(u.source_id)&&n.addPulse(u.source_id,1,new ot(54527),.02),u.target_id&&h.has(u.target_id)&&n.addPulse(u.target_id,1,new ot(54527),.02),l({type:"edgeAdded",edge:m});break}case"MemoryDeleted":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){const m=new ot(16729943);n.createImplosion(g,m)}r.removeEdgesForNode(u.id),s.removeNode(u.id),a.removeNode(u.id);const _=xs.indexOf(u.id);_!==-1&&xs.splice(_,1),l({type:"edgesRemoved",nodeId:u.id}),l({type:"nodeRemoved",nodeId:u.id});break}case"MemoryPromoted":{const u=i.data,g=u==null?void 0:u.id;if(!g)break;const _=u.new_retention??.95;if(h.has(g)){s.growNode(g,_),n.addPulse(g,1.2,new ot(65416),.01);const m=c.get(g);m&&(n.createShockwave(m,new ot(65416),o),n.createSpawnBurst(m,new ot(65416))),l({type:"nodeUpdated",nodeId:g,retention:_})}break}case"MemoryDemoted":{const u=i.data,g=u==null?void 0:u.id;if(!g)break;const _=u.new_retention??.3;h.has(g)&&(s.growNode(g,_),n.addPulse(g,.8,new ot(16729943),.03),l({type:"nodeUpdated",nodeId:g,retention:_}));break}case"MemoryUpdated":{const u=i.data,g=u==null?void 0:u.id;if(!g||!h.has(g))break;n.addPulse(g,.6,new ot(8490232),.02),u.retention!==void 0&&(s.growNode(g,u.retention),l({type:"nodeUpdated",nodeId:g,retention:u.retention}));break}case"SearchPerformed":{h.forEach((u,g)=>{n.addPulse(g,.6+Math.random()*.4,new ot(8490232),.02)});break}case"DreamStarted":{h.forEach((u,g)=>{n.addPulse(g,1,new ot(11032055),.005)});break}case"DreamProgress":{const u=(d=i.data)==null?void 0:d.memory_id;u&&h.has(u)&&n.addPulse(u,1.5,new ot(12616956),.01);break}case"DreamCompleted":{n.createSpawnBurst(new P(0,0,0),new ot(11032055)),n.createShockwave(new P(0,0,0),new ot(11032055),o);break}case"RetentionDecayed":{const u=(p=i.data)==null?void 0:p.id;u&&h.has(u)&&n.addPulse(u,.8,new ot(16729943),.03);break}case"ConsolidationCompleted":{h.forEach((u,g)=>{n.addPulse(g,.4+Math.random()*.3,new ot(16758784),.015)});break}case"ActivationSpread":{const u=i.data;if(u.source_id&&u.target_ids){const g=c.get(u.source_id);if(g)for(const _ of u.target_ids){const m=c.get(_);m&&n.createConnectionFlash(g,m,new ot(1370310))}}break}case"MemorySuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){n.createImplosion(g,new ot(11032055));const _=Math.max(1,u.suppression_count??1),m=Math.min(.4+_*.15,1);n.addPulse(u.id,m,new ot(11032055),.04)}break}case"MemoryUnsuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);g&&h.has(u.id)&&(n.createRainbowBurst(g,new ot(65416)),n.addPulse(u.id,1,new ot(65416),.02));break}case"Rac1CascadeSwept":{const g=i.data.neighbors_affected??0;if(g===0)break;const _=Array.from(h.keys()),m=Math.min(g,_.length,12);for(let f=0;f"u")return Mr;const i=localStorage.getItem(Fc);if(i===null)return Mr;const t=Number(i);return Number.isFinite(t)?Math.min(_o,Math.max(go,t)):Mr}const ni=a_();function a_(){let i=me(!1),t=me(va(new Date)),e=me(!1),n=me(1),s=me(!1),r=me(va(r_()));return{get temporalEnabled(){return B(i)},set temporalEnabled(a){zt(i,a,!0)},get temporalDate(){return B(t)},set temporalDate(a){zt(t,a,!0)},get temporalPlaying(){return B(e)},set temporalPlaying(a){zt(e,a,!0)},get temporalSpeed(){return B(n)},set temporalSpeed(a){zt(n,a,!0)},get dreamMode(){return B(s)},set dreamMode(a){zt(s,a,!0)},get brightness(){return B(r)},set brightness(a){const o=Math.min(_o,Math.max(go,a));if(zt(r,o,!0),typeof localStorage<"u")try{localStorage.setItem(Fc,String(o))}catch{}},brightnessMin:go,brightnessMax:_o,brightnessDefault:Mr}}var o_=Se('
      ');function l_(i,t){ys(t,!0);let e=vs(t,"events",19,()=>[]),n=vs(t,"isDreaming",3,!1),s=vs(t,"colorMode",3,"type");kc(()=>{l==null||l.setColorMode(s())});let r,a,o,l,c,h,d,p,u,g,_,m=null,f=[];jl(()=>{a=Dg(r),g=Jg(a.scene).material,_=i_(a.composer),h=new Gg(a.scene),l=new kg,l.colorMode=s(),c=new Vg,d=new Wg(a.scene),u=new Xg;const M=l.createNodes(t.nodes);c.createEdges(t.edges,M),p=new Ig(M),f=[...t.nodes],a.scene.add(c.group),a.scene.add(l.group),E(),window.addEventListener("resize",D),r.addEventListener("pointermove",w),r.addEventListener("click",C)}),Zl(()=>{cancelAnimationFrame(o),window.removeEventListener("resize",D),r==null||r.removeEventListener("pointermove",w),r==null||r.removeEventListener("click",C),d==null||d.dispose(),h==null||h.dispose(),l==null||l.dispose(),c==null||c.dispose(),a&&Ug(a)});let T=0;function E(){o=requestAnimationFrame(E);const S=performance.now();T===0&&(T=S);const M=S-T;if(M<16)return;T=S-M%16;const A=S*.001;p.tick(t.edges),l.updatePositions(),c.updatePositions(l.positions),c.animateEdges(l.positions),h.animate(A),l.animate(A,f,a.camera,ni.brightness),u.setActive(n()),u.update(a.scene,a.bloomPass,a.controls,a.lights,A),Qg(g,A,u.current.nebulaIntensity,r.clientWidth,r.clientHeight),s_(_,A,u.current.nebulaIntensity),y(),d.update(l.meshMap,a.camera,l.positions),a.controls.update(),a.composer.render()}function y(){if(!e()||e().length===0)return;const S=[];for(const A of e()){if(A===m)break;S.push(A)}if(S.length===0)return;if(S.length===e().length&&e().length>=200){console.warn("[vestige] Event horizon overflow: dropping visuals for",S.length,"events"),m=e()[0];return}m=e()[0];const M={effects:d,nodeManager:l,edgeManager:c,forceSim:p,camera:a.camera,onMutation:A=>{var W;A.type==="nodeAdded"?f=[...f,A.node]:A.type==="nodeRemoved"&&(f=f.filter(k=>k.id!==A.nodeId)),(W=t.onGraphMutation)==null||W.call(t,A)}};for(let A=S.length-1;A>=0;A--)Zg(S[A],M,f)}function D(){!r||!a||Lg(a,r)}function w(S){const M=r.getBoundingClientRect();a.mouse.x=(S.clientX-M.left)/M.width*2-1,a.mouse.y=-((S.clientY-M.top)/M.height)*2+1,a.raycaster.setFromCamera(a.mouse,a.camera);const A=a.raycaster.intersectObjects(l.getMeshes());A.length>0?(l.hoveredNode=A[0].object.userData.nodeId,r.style.cursor="pointer"):(l.hoveredNode=null,r.style.cursor="grab")}function C(){var S;if(l.hoveredNode){l.selectedNode=l.hoveredNode,(S=t.onSelect)==null||S.call(t,l.hoveredNode);const M=l.positions.get(l.hoveredNode);M&&a.controls.target.lerp(M.clone(),.5)}}var I=o_();Yc(I,S=>r=S,()=>r),_e(i,I),Es()}var c_=Se('
      '),h_=Se('
      ');function u_(i,t){ys(t,!0);let e=vs(t,"width",3,240),n=vs(t,"height",3,80);function s(m){return t.stability<=0?0:Math.exp(-m/t.stability)}let r=Gn(()=>{const m=[],f=Math.max(t.stability*3,30),T=4,E=e()-T*2,y=n()-T*2;for(let D=0;D<=50;D++){const w=D/50*f,C=s(w),I=T+D/50*E,S=T+(1-C)*y;m.push(`${D===0?"M":"L"}${I.toFixed(1)},${S.toFixed(1)}`)}return m.join(" ")}),a=Gn(()=>[{label:"Now",days:0,value:t.retention},{label:"1d",days:1,value:s(1)},{label:"7d",days:7,value:s(7)},{label:"30d",days:30,value:s(30)}]);function o(m){return m>.7?"#10b981":m>.4?"#f59e0b":"#ef4444"}var l=h_(),c=yt(l),h=yt(c),d=bt(h),p=bt(d),u=bt(p),g=bt(u);Hc(),xt(c);var _=bt(c,2);_s(_,21,()=>B(a),hr,(m,f)=>{var T=c_(),E=yt(T),y=yt(E);xt(E);var D=bt(E,2),w=yt(D);xt(D),xt(T),Ke((C,I)=>{fe(y,`${B(f).label??""}:`),Sr(D,`color: ${C??""}`),fe(w,`${I??""}%`)},[()=>o(B(f).value),()=>(B(f).value*100).toFixed(0)]),_e(m,T)}),xt(_),xt(l),Ke(m=>{ve(c,"width",e()),ve(c,"height",n()),ve(c,"viewBox",`0 0 ${e()??""} ${n()??""}`),ve(h,"y1",4+(n()-8)*.5),ve(h,"x2",e()-4),ve(h,"y2",4+(n()-8)*.5),ve(d,"y1",4+(n()-8)*.8),ve(d,"x2",e()-4),ve(d,"y2",4+(n()-8)*.8),ve(p,"d",B(r)),ve(u,"d",`${B(r)??""} L${e()-4},${n()-4} L4,${n()-4} Z`),ve(g,"cy",4+(1-t.retention)*(n()-8)),ve(g,"fill",m)},[()=>o(t.retention)]),_e(i,l),Es()}function ql(i,t,e){const n=e.getTime(),s=new Set,r=new Map,a=i.filter(l=>{const c=new Date(l.createdAt).getTime();if(c<=n){s.add(l.id);const h=n-c,d=1440*60*1e3,p=hs.has(l.source)&&s.has(l.target));return{visibleNodes:a,visibleEdges:o,nodeOpacities:r}}function d_(i){if(i.length===0){const n=new Date;return{oldest:n,newest:n}}let t=1/0,e=-1/0;for(const n of i){const s=new Date(n.createdAt).getTime();se&&(e=s)}return{oldest:new Date(t),newest:new Date(e)}}var f_=Se(`
      `),p_=Se('');function m_(i,t){ys(t,!0);let e=me(!1),n=me(!1),s=me(1),r=me(100),a,o=0,l=Gn(()=>d_(t.nodes)),c=Gn(()=>{const E=B(l).oldest.getTime(),D=B(l).newest.getTime()-E||1;return new Date(E+B(r)/100*D)});function h(E){return E.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function d(){zt(e,!B(e)),t.onToggle(B(e)),B(e)&&(zt(r,100),t.onDateChange(B(c)))}function p(){zt(n,!B(n)),B(n)?(zt(r,0),o=performance.now(),u()):cancelAnimationFrame(a)}function u(){B(n)&&(a=requestAnimationFrame(E=>{const y=(E-o)/1e3;o=E;const D=B(l).oldest.getTime(),C=(B(l).newest.getTime()-D)/(1440*60*1e3)||1,I=B(s)/C*100;if(zt(r,Math.min(100,B(r)+I*y),!0),t.onDateChange(B(c)),B(r)>=100){zt(n,!1);return}u()}))}function g(){t.onDateChange(B(c))}Zl(()=>{zt(n,!1),cancelAnimationFrame(a)});var _=Gc(),m=Kl(_);{var f=E=>{var y=f_(),D=yt(y),w=yt(D),C=yt(w),I=yt(C),S=yt(I,!0);xt(I);var M=bt(I,2),A=yt(M);A.value=A.__value=1;var W=bt(A);W.value=W.__value=7;var k=bt(W);k.value=k.__value=30,xt(M),xt(C);var q=bt(C,2),Q=yt(q,!0);xt(q);var X=bt(q,2);xt(w);var tt=bt(w,2);xa(tt);var H=bt(tt,2),st=yt(H),gt=yt(st,!0);xt(st);var Et=bt(st,2),Ft=yt(Et,!0);xt(Et),xt(H),xt(D),xt(y),Ke((Xt,Y,nt)=>{fe(S,B(n)?"⏸":"▶"),fe(Q,Xt),fe(gt,Y),fe(Ft,nt)},[()=>h(B(c)),()=>h(B(l).oldest),()=>h(B(l).newest)]),Fe("click",I,p),Jl(M,()=>B(s),Xt=>zt(s,Xt)),Fe("click",X,d),Fe("input",tt,g),Ma(tt,()=>B(r),Xt=>zt(r,Xt)),_e(E,y)},T=E=>{var y=p_();Fe("click",y,d),_e(E,y)};kn(m,E=>{B(e)?E(f):E(T,!1)})}_e(i,_),Es()}$l(["click","input"]);var g_=Se('
      '),__=Se('
      FSRS accessibility
      ');function v_(i,t){ys(t,!1);const e=["active","dormant","silent","unavailable"];qc();var n=__(),s=bt(yt(n),2);_s(s,1,()=>e,r=>r,(r,a)=>{var o=g_(),l=yt(o),c=bt(l,2),h=yt(c,!0);xt(c);var d=bt(c,2),p=yt(d,!0);xt(d),xt(o),Ke(u=>{Sr(l,`background: ${po[B(a)]??""}; box-shadow: 0 0 6px ${po[B(a)]??""}55;`),fe(h,B(a)),fe(p,u)},[()=>{var u;return((u=Fg[B(a)].match(/\(([^)]+)\)/))==null?void 0:u[1])??""}]),_e(r,o)}),xt(n),_e(i,n),Es()}var x_=Se('

      Loading memory graph...

      '),M_=Se(`

      MCP Backend Offline

      The Vestige MCP server isn't reachable on :3927. + The dashboard is running but has nothing to query.

      Start the backend:
      nohup bash -c 'tail -f /dev/null | VESTIGE_DASHBOARD_ENABLED=true ~/.local/bin/vestige-mcp' > /tmp/vestige.log 2>&1 & +disown
      `),S_=Se('

      Your Mind Awaits

      No memories yet. Start using Vestige to populate your graph.

      '),y_=Se('

      Your Mind Awaits

      '),E_=Se(' · · ',1),b_=Se('
      '),T_=Se('
      '),w_=Se('
      AhaGraph
      '),A_=Se(' '),R_=Se('
      '),C_=Se("
      "),P_=Se(`

      Memory Detail

      Retention Forecast
      ◬ Explore Connections
      `),D_=Se(`
      `);function Q_(i,t){ys(t,!0);const e=()=>Xc(jc,"$eventFeed",n),[n,s]=Wc();let r=me(null),a=me(null),o=me(!0),l=me(""),c=me(!1),h=me(""),d=me(150),p=me(!1),u=me(va(new Date)),g=me("type");const _=Object.entries(xr);let m=me(0),f=me(0),T=Gn(()=>B(r)?B(p)?ql(B(r).nodes,B(r).edges,B(u)).visibleNodes:B(r).nodes:[]),E=Gn(()=>B(r)?B(p)?ql(B(r).nodes,B(r).edges,B(u)).visibleEdges:B(r).edges:[]);function y(V){if(B(r))switch(V.type){case"nodeAdded":B(r).nodes=[...B(r).nodes,V.node],B(r).nodeCount=B(r).nodes.length,zt(m,B(r).nodeCount,!0);break;case"nodeRemoved":B(r).nodes=B(r).nodes.filter(at=>at.id!==V.nodeId),B(r).nodeCount=B(r).nodes.length,zt(m,B(r).nodeCount,!0);break;case"edgeAdded":B(r).edges=[...B(r).edges,V.edge],B(r).edgeCount=B(r).edges.length,zt(f,B(r).edgeCount,!0);break;case"edgesRemoved":B(r).edges=B(r).edges.filter(at=>at.source!==V.nodeId&&at.target!==V.nodeId),B(r).edgeCount=B(r).edges.length,zt(f,B(r).edgeCount,!0);break;case"nodeUpdated":{const at=B(r).nodes.find(Z=>Z.id===V.nodeId);at&&(at.retention=V.retention);break}}}jl(()=>{const V=new URLSearchParams(window.location.search).get("colorMode");D(V)&&zt(g,V,!0),w()});function D(V){return V==="type"||V==="state"||V==="ahagraph"}async function w(V,at){var Z;zt(o,!0),zt(l,"");try{const it=!V&&!at;if(zt(r,await gi.graph({max_nodes:B(d),depth:3,query:V||void 0,center_id:at||void 0,sort:it?"recent":void 0}),!0),it&&B(r)&&B(r).nodeCount<=1&&B(r).edgeCount===0){const pt=await gi.graph({max_nodes:B(d),depth:3,sort:"connected"});pt&&pt.nodeCount>B(r).nodeCount&&zt(r,pt,!0)}B(r)&&(zt(m,B(r).nodeCount,!0),zt(f,B(r).edgeCount,!0))}catch(it){const pt=it instanceof Error?it.message:String(it),wt=pt.replace(/\/[\w./-]+\.(sqlite|rs|db|toml|lock)\b/g,"[path]").slice(0,200),ht=it instanceof TypeError||/failed to fetch|NetworkError|load failed/i.test(pt)||/^API 500:?\s*(Internal Server Error)?\s*$/i.test(pt.trim()),Bt=(((Z=B(r))==null?void 0:Z.nodeCount)??0)===0&&/not found|404|empty|no memor/i.test(pt);ht?zt(l,"OFFLINE"):Bt?zt(l,"EMPTY"):zt(l,`Failed to load graph: ${wt}`)}finally{zt(o,!1)}}async function C(){zt(c,!0);try{await gi.dream(),await w()}catch{}finally{zt(c,!1)}}async function I(V){try{zt(a,await gi.memories.get(V),!0)}catch{zt(a,null)}}function S(){B(h).trim()&&w(B(h))}var M=D_(),A=yt(M);{var W=V=>{var at=x_();_e(V,at)},k=V=>{var at=M_(),Z=yt(at),it=bt(yt(Z),8),pt=yt(it),wt=bt(pt,2);xt(it),xt(Z),xt(at),Ke(()=>ve(wt,"href",`${Do??""}/settings`)),Fe("click",pt,()=>w()),_e(V,at)},q=V=>{var at=S_();_e(V,at)},Q=V=>{var at=y_(),Z=yt(at),it=bt(yt(Z),4),pt=yt(it,!0);xt(it),xt(Z),xt(at),Ke(()=>fe(pt,B(l))),_e(V,at)},X=V=>{l_(V,{get nodes(){return B(T)},get edges(){return B(E)},get centerId(){return B(r).center_id},get events(){return e()},get isDreaming(){return B(c)},get colorMode(){return B(g)},onSelect:I,onGraphMutation:y})};kn(A,V=>{B(o)?V(W):B(l)==="OFFLINE"?V(k,1):B(l)==="EMPTY"?V(q,2):B(l)?V(Q,3):B(r)&&V(X,4)})}var tt=bt(A,2),H=yt(tt),st=yt(H);xa(st);var gt=bt(st,2);xt(H);var Et=bt(H,2),Ft=yt(Et),Xt=yt(Ft),Y=bt(Xt,2),nt=bt(Y,2);xt(Ft);var _t=bt(Ft,2),lt=yt(_t);lt.value=lt.__value=50;var Ct=bt(lt);Ct.value=Ct.__value=100;var Lt=bt(Ct);Lt.value=Lt.__value=150;var Vt=bt(Lt);Vt.value=Vt.__value=200,xt(_t);var ie=bt(_t,2),Wt=bt(yt(ie),2);xa(Wt);var ue=bt(Wt,2),R=yt(ue);xt(ue),xt(ie);var ye=bt(ie,2),qt=yt(ye,!0);xt(ye);var jt=bt(ye,2);xt(Et),xt(tt);var At=bt(tt,2),le=yt(At);{var Rt=V=>{var at=E_(),Z=Kl(at),it=yt(Z);xt(Z);var pt=bt(Z,4),wt=yt(pt);xt(pt);var ht=bt(pt,4),Bt=yt(ht);xt(ht),Ke(()=>{fe(it,`${B(m)??""} nodes`),fe(wt,`${B(f)??""} edges`),fe(Bt,`depth ${B(r).depth??""}`)}),_e(V,at)};kn(le,V=>{B(r)&&V(Rt)})}xt(At);var b=bt(At,2);{var v=V=>{var at=b_(),Z=yt(at);v_(Z,{}),xt(at),_e(V,at)};kn(b,V=>{B(g)==="state"&&V(v)})}var F=bt(b,2);{var K=V=>{var at=w_(),Z=bt(yt(at),2);_s(Z,21,()=>_,hr,(it,pt)=>{var wt=Gn(()=>Vc(B(pt),2));let ht=()=>B(wt)[0],Bt=()=>B(wt)[1];var Ut=T_(),Zt=yt(Ut),L=bt(Zt,2),rt=yt(L,!0);xt(L),xt(Ut),Ke(()=>{Sr(Zt,`background: ${Bt()??""}`),fe(rt,Og[ht()])}),_e(it,Ut)}),xt(Z),xt(at),_e(V,at)};kn(F,V=>{B(g)==="ahagraph"&&V(K)})}var J=bt(F,2);{var j=V=>{m_(V,{get nodes(){return B(r).nodes},onDateChange:at=>{zt(u,at,!0)},onToggle:at=>{zt(p,at,!0)}})};kn(J,V=>{B(r)&&V(j)})}var Tt=bt(J,2);{var dt=V=>{var at=P_(),Z=yt(at),it=bt(yt(Z),2);xt(Z);var pt=bt(Z,2),wt=yt(pt),ht=yt(wt),Bt=yt(ht,!0);xt(ht);var Ut=bt(ht,2);_s(Ut,17,()=>B(a).tags,hr,(Ve,Re)=>{var tn=A_(),en=yt(tn,!0);xt(tn),Ke(()=>fe(en,B(Re))),_e(Ve,tn)}),xt(wt);var Zt=bt(wt,2),L=yt(Zt,!0);xt(Zt);var rt=bt(Zt,2);_s(rt,21,()=>[{label:"Retention",value:B(a).retentionStrength},{label:"Storage",value:B(a).storageStrength},{label:"Retrieval",value:B(a).retrievalStrength}],hr,(Ve,Re)=>{var tn=R_(),en=yt(tn),pi=yt(en),Ps=yt(pi,!0);xt(pi);var Ds=bt(pi,2),Ir=yt(Ds);xt(Ds),xt(en);var Ls=bt(en,2),Nr=yt(Ls);xt(Ls),xt(tn),Ke(Fr=>{fe(Ps,B(Re).label),fe(Ir,`${Fr??""}%`),Sr(Nr,`width: ${B(Re).value*100}%; background: ${B(Re).value>.7?"#10b981":B(Re).value>.4?"#f59e0b":"#ef4444"}`)},[()=>(B(Re).value*100).toFixed(1)]),_e(Ve,tn)}),xt(rt);var G=bt(rt,2),$=bt(yt(G),2);{let Ve=Gn(()=>B(a).storageStrength*30);u_($,{get retention(){return B(a).retentionStrength},get stability(){return B(Ve)}})}xt(G);var ft=bt(G,2),ut=yt(ft),Ot=yt(ut);xt(ut);var ce=bt(ut,2),Ae=yt(ce);xt(ce);var $t=bt(ce,2);{var Ye=Ve=>{var Re=C_(),tn=yt(Re);xt(Re),Ke(en=>fe(tn,`Accessed: ${en??""}`),[()=>new Date(B(a).lastAccessedAt).toLocaleString()]),_e(Ve,Re)};kn($t,Ve=>{B(a).lastAccessedAt&&Ve(Ye)})}var Qe=bt($t,2),Rs=yt(Qe);xt(Qe),xt(ft);var fi=bt(ft,2),pn=yt(fi),ns=bt(pn,2);xt(fi);var Cs=bt(fi,2);xt(pt),xt(at),Ke((Ve,Re)=>{fe(Bt,B(a).nodeType),fe(L,B(a).content),fe(Ot,`Created: ${Ve??""}`),fe(Ae,`Updated: ${Re??""}`),fe(Rs,`Reviews: ${B(a).reviewCount??0??""}`),ve(Cs,"href",`${Do??""}/explore`)},[()=>new Date(B(a).createdAt).toLocaleString(),()=>new Date(B(a).updatedAt).toLocaleString()]),Fe("click",it,()=>zt(a,null)),Fe("click",pn,()=>{B(a)&&gi.memories.promote(B(a).id)}),Fe("click",ns,()=>{B(a)&&gi.memories.demote(B(a).id)}),_e(V,at)};kn(Tt,V=>{B(a)&&V(dt)})}xt(M),Ke((V,at)=>{ve(Xt,"aria-checked",B(g)==="type"),Us(Xt,1,`px-3 py-1.5 rounded-lg transition ${B(g)==="type"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),ve(Y,"aria-checked",B(g)==="state"),Us(Y,1,`px-3 py-1.5 rounded-lg transition ${B(g)==="state"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),ve(nt,"aria-checked",B(g)==="ahagraph"),Us(nt,1,`px-3 py-1.5 rounded-lg transition ${B(g)==="ahagraph"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),ve(ie,"title",`Adjust graph brightness (${V??""}x). Combines with auto distance compensation.`),ve(Wt,"min",ni.brightnessMin),ve(Wt,"max",ni.brightnessMax),fe(R,`${at??""}x`),ye.disabled=B(c),Us(ye,1,`px-4 py-2 rounded-xl bg-dream/20 border border-dream/40 text-dream-glow text-sm + hover:bg-dream/30 transition-all backdrop-blur-sm disabled:opacity-50 + ${B(c)?"glow-dream animate-pulse-glow":""}`),fe(qt,B(c)?"◈ Dreaming...":"◈ Dream")},[()=>ni.brightness.toFixed(1),()=>ni.brightness.toFixed(1)]),Fe("keydown",st,V=>V.key==="Enter"&&S()),Ma(st,()=>B(h),V=>zt(h,V)),Fe("click",gt,S),Fe("click",Xt,()=>zt(g,"type")),Fe("click",Y,()=>zt(g,"state")),Fe("click",nt,()=>zt(g,"ahagraph")),Fe("change",_t,()=>w()),Jl(_t,()=>B(d),V=>zt(d,V)),Ma(Wt,()=>ni.brightness,V=>ni.brightness=V),Fe("click",ye,C),Fe("click",jt,()=>w()),_e(i,M),Es(),s()}$l(["click","keydown","change"]);export{Q_ as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js.br b/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js.br new file mode 100644 index 0000000..624f719 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js.gz b/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js.gz new file mode 100644 index 0000000..58803e2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/10.CjP_ylq3.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js.br b/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js.br deleted file mode 100644 index e32ae12..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js.gz b/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js.gz deleted file mode 100644 index 96c4125..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js b/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js new file mode 100644 index 0000000..3bdf9c3 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js @@ -0,0 +1,7 @@ +import"../chunks/Bzak7iHL.js";import{o as Pt}from"../chunks/GG5zm9kr.js";import{N as Kt,ab as qt,aP as Ht,b as Wt,p as Rt,h as F,d as i,t as S,g as t,e as o,r as n,a as Tt,u as y,s as q,f as U,c as Ft,C as Xt,i as Zt,n as Gt}from"../chunks/CpWkWWOo.js";import{s as _,d as Ut,a as kt}from"../chunks/BlVfL1ME.js";import{i as R}from"../chunks/B4yTwGkE.js";import{B as Vt}from"../chunks/DdEqwvdI.js";import{e as V,i as rt}from"../chunks/CGEBXrjl.js";import{a as x,c as Nt,b as yt,f as h}from"../chunks/CHOnp4oo.js";import{s as Yt}from"../chunks/Cx-f-Pzo.js";import{b as Jt}from"../chunks/sZcqyNBA.js";import{g as Qt}from"../chunks/D-gDZzN6.js";import{b as te}from"../chunks/DGcYlAAw.js";import{a as Ct}from"../chunks/554JRhq6.js";import{N as ee}from"../chunks/CcUbQ_Wl.js";import{s as p}from"../chunks/A7po6GxK.js";import{p as ae}from"../chunks/V6gjw5Ec.js";const re=Symbol("NaN");function se(a,C,m){Kt&&qt();var v=new Vt(a),T=!Ht();Wt(()=>{var w=C();w!==w&&(w=re),T&&w!==null&&typeof w=="object"&&(w={}),v.ensure(w,m)})}function Mt(a){return a==null||!Number.isFinite(a)||a<0?0:a>1?1:a}function ne(a){return{novelty:Mt(a==null?void 0:a.novelty),arousal:Mt(a==null?void 0:a.arousal),reward:Mt(a==null?void 0:a.reward),attention:Mt(a==null?void 0:a.attention)}}const It={sm:80,md:180,lg:320};function $t(a){return a&&(a==="sm"||a==="md"||a==="lg")?It[a]:It.md}const gt=[{key:"novelty",angle:-Math.PI/2},{key:"arousal",angle:0},{key:"reward",angle:Math.PI/2},{key:"attention",angle:Math.PI}];function oe(a){const C=$t(a);let m;switch(a){case"lg":m=44;break;case"sm":m=4;break;default:m=28}return Math.max(0,C/2-m)}var ie=yt(''),le=yt(''),de=yt(''),ce=yt(' ',1),ve=yt('');function Et(a,C){Rt(C,!0);let m=ae(C,"size",3,"md"),v=y(()=>$t(m())),T=y(()=>m()!=="sm"),w=y(()=>oe(m())),Y=y(()=>t(v)/2),J=y(()=>t(v)/2);const St={novelty:"Novelty",arousal:"Arousal",reward:"Reward",attention:"Attention"};let N=y(()=>ne({novelty:C.novelty,arousal:C.arousal,reward:C.reward,attention:C.attention}));function H(d,l){const r=d*t(w);return[t(Y)+Math.cos(l)*r,t(J)+Math.sin(l)*r]}const st=[.25,.5,.75,1];function nt(d){return gt.map(({angle:r})=>H(d,r)).map((r,c)=>`${c===0?"M":"L"}${r[0].toFixed(2)},${r[1].toFixed(2)}`).join(" ")+" Z"}let $=q(0);Pt(()=>{const l=performance.now();let r=0;const c=b=>{const g=Math.min(1,(b-l)/600);F($,1-Math.pow(1-g,3)),g<1&&(r=requestAnimationFrame(c))};return r=requestAnimationFrame(c),()=>cancelAnimationFrame(r)});let ot=y(()=>{const d=t($);return gt.map(({key:r,angle:c})=>H(t(N)[r]*d,c)).map((r,c)=>`${c===0?"M":"L"}${r[0].toFixed(2)},${r[1].toFixed(2)}`).join(" ")+" Z"});function _t(d){const l=t(w)+(m()==="lg"?18:12),r=t(Y)+Math.cos(d)*l,c=t(J)+Math.sin(d)*l;let b="middle";return Math.abs(Math.cos(d))>.5&&(b=Math.cos(d)>0?"start":"end"),{x:r,y:c,anchor:b}}var P=ve(),it=i(P);V(it,17,()=>st,rt,(d,l)=>{var r=ie();S(c=>{p(r,"d",c),p(r,"stroke-opacity",t(l)===1?.45:.18),p(r,"stroke-width",t(l)===1?1:.75)},[()=>nt(t(l))]),x(d,r)});var ht=o(it);V(ht,17,()=>gt,rt,(d,l)=>{const r=y(()=>{const[b,g]=H(1,t(l).angle);return{x:b,y:g}});var c=le();S(()=>{p(c,"x1",t(Y)),p(c,"y1",t(J)),p(c,"x2",t(r).x),p(c,"y2",t(r).y)}),x(d,c)});var W=o(ht),Q=o(W);{var wt=d=>{var l=Nt(),r=U(l);V(r,17,()=>gt,rt,(c,b)=>{const g=y(()=>{const[I,dt]=H(t(N)[t(b).key]*t($),t(b).angle);return{px:I,py:dt}});var j=de();S(()=>{p(j,"cx",t(g).px),p(j,"cy",t(g).py),p(j,"r",m()==="lg"?3:2.25)}),x(c,j)}),x(d,l)};R(Q,d=>{m()!=="sm"&&d(wt)})}var lt=o(Q);{var tt=d=>{var l=Nt(),r=U(l);V(r,17,()=>gt,rt,(c,b)=>{const g=y(()=>_t(t(b).angle));var j=ce(),I=U(j),dt=i(I);n(I);var L=o(I),ct=i(L,!0);n(L),S(At=>{p(I,"x",t(g).x),p(I,"y",t(g).y),p(I,"text-anchor",t(g).anchor),p(I,"font-size",m()==="lg"?12:10),_(dt,`${At??""}%`),p(L,"x",t(g).x),p(L,"y",t(g).y+(m()==="lg"?14:11)),p(L,"text-anchor",t(g).anchor),p(L,"font-size",m()==="lg"?10:8.5),_(ct,St[t(b).key])},[()=>(t(N)[t(b).key]*100).toFixed(0)]),x(c,j)}),x(d,l)};R(lt,d=>{t(T)&&d(tt)})}n(P),S((d,l,r,c)=>{p(P,"width",t(v)),p(P,"height",t(v)),p(P,"viewBox",`0 0 ${t(v)??""} ${t(v)??""}`),p(P,"aria-label",`Importance radar: novelty ${d??""}%, arousal ${l??""}%, reward ${r??""}%, attention ${c??""}%`),p(W,"d",t(ot)),p(W,"stroke-width",m()==="sm"?1:1.5)},[()=>(t(N).novelty*100).toFixed(0),()=>(t(N).arousal*100).toFixed(0),()=>(t(N).reward*100).toFixed(0),()=>(t(N).attention*100).toFixed(0)]),x(a,P),Tt()}var pe=h(' '),xe=h('Driven by ',1),me=h('
      ✓ Save

      '),ue=h('Weakest channel: ',1),fe=h('
      ⨯ Skip

      '),ge=h('
      Composite
      %
      ',1),ye=h(`

      Type some content above to score its importance.

      Composite = 0.25·novelty + 0.30·arousal + 0.25·reward + 0.20·attention. + Threshold for save: 60%.

      `),_e=h('
      '),he=h('
      '),we=h('

      No memories yet.

      '),be=h('· ',1),ke=h(' '),Me=h('
      '),Se=h(``),Ae=h('
      '),Ce=h(`

      Importance Radar

      4-channel importance model: Novelty · Arousal · Reward · Attention

      Test Importance

      Paste any content below. Vestige scores it across 4 channels and + decides whether it is worth saving.

      ⌘/Ctrl + Enter

      Top Important Memories This Week

      Ranked by retention × reviews ÷ age. Click any card to open it.

      `);function He(a,C){Rt(C,!0);let m=q(""),v=q(null),T=q(!1),w=q(null),Y=q(0);async function J(){const e=t(m).trim();if(!(!e||t(T))){F(T,!0),F(w,null);try{F(v,await Ct.importance(e),!0),Xt(Y)}catch(s){F(w,s instanceof Error?s.message:String(s),!0),F(v,null)}finally{F(T,!1)}}}function St(e){(e.metaKey||e.ctrlKey)&&e.key==="Enter"&&(e.preventDefault(),J())}const N={novelty:.25,arousal:.3,reward:.25,attention:.2},H={novelty:{high:"new information not already in your graph",low:"overlaps heavily with what you already know"},arousal:{high:"emotionally salient — decisions, bugs, or discoveries stick",low:"neutral tone, no strong affect signal"},reward:{high:"high reward value — preferences, wins, or solutions you will revisit",low:"low reward value — transient or incidental detail"},attention:{high:"strong attentional markers (imperatives, questions, urgency)",low:"passive phrasing, no clear attentional hook"}};let st=y(()=>t(v)?Object.keys(N).map(s=>({key:s,contribution:t(v).channels[s]*N[s]})).sort((s,u)=>u.contribution-s.contribution)[0]:null),nt=y(()=>t(v)?Object.keys(N).slice().sort((e,s)=>t(v).channels[e]-t(v).channels[s])[0]:null),$=q(Ft([])),ot=q(!0),_t=Ft({});function P(e){const s=Math.max(1,(Date.now()-new Date(e.createdAt).getTime())/864e5),u=e.reviewCount??0,f=1/Math.pow(s,.5);return e.retentionStrength*Math.log1p(u+1)*f}async function it(){F(ot,!0);try{const s=(await Ct.memories.list({limit:"20"})).memories.slice().sort((u,f)=>P(f)-P(u)).slice(0,20);F($,s,!0),t($).forEach(async u=>{try{const f=await Ct.importance(u.content);_t[u.id]=f.channels}catch{}})}catch{F($,[],!0)}finally{F(ot,!1)}}Pt(it);function ht(e){Qt(`${te}/memories`)}var W=Ce(),Q=o(i(W),2),wt=o(i(Q),2),lt=i(wt),tt=i(lt);Zt(tt);var d=o(tt,2),l=i(d),r=i(l,!0);n(l);var c=o(l,4);{var b=e=>{var s=pe(),u=i(s,!0);n(s),S(()=>_(u,t(w))),x(e,s)};R(c,e=>{t(w)&&e(b)})}n(d),n(lt);var g=o(lt,2),j=i(g);{var I=e=>{var s=ge(),u=U(s),f=o(i(u),2),z=i(f,!0);Gt(),n(f),n(u);var X=o(u,2);se(X,()=>t(Y),A=>{Et(A,{get novelty(){return t(v).channels.novelty},get arousal(){return t(v).channels.arousal},get reward(){return t(v).channels.reward},get attention(){return t(v).channels.attention},size:"lg"})});var vt=o(X,2);{var pt=A=>{var B=me(),D=o(i(B),2),Z=i(D),xt=o(Z);{var mt=k=>{var G=xe(),O=o(U(G)),ut=i(O,!0);n(O);var et=o(O);S(()=>{_(ut,t(st).key),_(et,` — ${H[t(st).key].high??""}.`)}),x(k,G)};R(xt,k=>{t(st)&&k(mt)})}n(D),n(B),S(k=>_(Z,`Composite ${k??""}% > 60% threshold. `),[()=>(t(v).composite*100).toFixed(0)]),x(A,B)},bt=A=>{var B=fe(),D=o(i(B),2),Z=i(D),xt=o(Z);{var mt=k=>{var G=ue(),O=o(U(G)),ut=i(O,!0);n(O);var et=o(O);S(()=>{_(ut,t(nt)),_(et,` — ${H[t(nt)].low??""}.`)}),x(k,G)};R(xt,k=>{t(nt)&&k(mt)})}n(D),n(B),S(k=>_(Z,`Composite ${k??""}% < 60% threshold. `),[()=>(t(v).composite*100).toFixed(0)]),x(A,B)};R(vt,A=>{t(v).composite>.6?A(pt):A(bt,!1)})}S(A=>_(z,A),[()=>(t(v).composite*100).toFixed(0)]),x(e,s)},dt=e=>{var s=ye();x(e,s)};R(j,e=>{t(v)?e(I):e(dt,!1)})}n(g),n(wt),n(Q);var L=o(Q,2),ct=i(L),At=o(i(ct),2);n(ct);var jt=o(ct,2);{var Lt=e=>{var s=he();V(s,20,()=>Array(6),rt,(u,f)=>{var z=_e();x(u,z)}),n(s),x(e,s)},Bt=e=>{var s=we();x(e,s)},Dt=e=>{var s=Ae();V(s,21,()=>t($),u=>u.id,(u,f)=>{const z=y(()=>_t[t(f).id]);var X=Se(),vt=i(X),pt=i(vt),bt=i(pt),A=o(bt,2),B=i(A,!0);n(A);var D=o(A,4),Z=i(D);n(D);var xt=o(D,2);{var mt=E=>{var K=be(),at=o(U(K),2),ft=i(at);n(at),S(()=>_(ft,`${t(f).reviewCount??""} reviews`)),x(E,K)};R(xt,E=>{t(f).reviewCount&&E(mt)})}n(pt);var k=o(pt,2),G=i(k,!0);n(k);var O=o(k,2);{var ut=E=>{var K=Me();V(K,21,()=>t(f).tags.slice(0,4),rt,(at,ft)=>{var M=ke(),zt=i(M,!0);n(M),S(()=>_(zt,t(ft))),x(at,M)}),n(K),x(E,K)};R(O,E=>{t(f).tags.length>0&&E(ut)})}n(vt);var et=o(vt,2),Ot=i(et);{let E=y(()=>{var M;return((M=t(z))==null?void 0:M.novelty)??0}),K=y(()=>{var M;return((M=t(z))==null?void 0:M.arousal)??0}),at=y(()=>{var M;return((M=t(z))==null?void 0:M.reward)??0}),ft=y(()=>{var M;return((M=t(z))==null?void 0:M.attention)??0});Et(Ot,{get novelty(){return t(E)},get arousal(){return t(K)},get reward(){return t(at)},get attention(){return t(ft)},size:"sm"})}n(et),n(X),S(E=>{Yt(bt,`background: ${(ee[t(f).nodeType]||"#8B95A5")??""}`),_(B,t(f).nodeType),_(Z,`${E??""}% retention`),_(G,t(f).content)},[()=>(t(f).retentionStrength*100).toFixed(0)]),kt("click",X,()=>ht(t(f).id)),x(u,X)}),n(s),x(e,s)};R(jt,e=>{t(ot)?e(Lt):t($).length===0?e(Bt,1):e(Dt,!1)})}n(L),n(W),S(e=>{l.disabled=e,_(r,t(T)?"Scoring…":"Score Importance")},[()=>t(T)||!t(m).trim()]),kt("keydown",tt,St),Jt(tt,()=>t(m),e=>F(m,e)),kt("click",l,J),kt("click",At,it),x(a,W),Tt()}Ut(["keydown","click"]);export{He as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js.br b/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js.br new file mode 100644 index 0000000..b847d73 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js.gz b/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js.gz new file mode 100644 index 0000000..e6ed95b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/11.k15P8M2H.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js b/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js similarity index 77% rename from apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js rename to apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js index 8325f37..e437171 100644 --- a/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js +++ b/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js @@ -1 +1 @@ -import"../chunks/Bzak7iHL.js";import{o as gt}from"../chunks/DWVWfZUn.js";import{p as yt,s as D,c as Q,t as u,a as bt,d as o,e as n,h as N,g as r,r as a,O as ht}from"../chunks/VE8Jor13.js";import{d as wt,s as d,a as Rt}from"../chunks/DHnEMX8z.js";import{i as R}from"../chunks/JkhlGLjU.js";import{e as L,i as U}from"../chunks/ByItJEsC.js";import{a as l,f as v}from"../chunks/7UNxJI5L.js";import{s as W}from"../chunks/BR2EHpd7.js";import{a as Z}from"../chunks/DcQGRi49.js";var St=v(""),$t=v('
      '),Ot=v('
      '),Pt=v('

      Use "Remind me..." in conversation to create intentions.

      '),Nt=v(' '),It=v(' '),Tt=v('

      '),kt=v('
      '),zt=v('

      No predictions yet. Use Vestige more to train the predictive model.

      '),Dt=v(" "),Lt=v(' '),Ut=v('

      '),Ct=v('
      '),At=v('

      Intentions & Predictions

      Prospective Memory

      "Remember to do X when Y happens"

      Predicted Needs

      What you might need next
      ');function Wt(tt,et){yt(et,!0);let I=D(Q([])),C=D(Q([])),A=D(!0),S=D("active");const at={active:"text-synapse-glow bg-synapse/10 border-synapse/30",fulfilled:"text-recall bg-recall/10 border-recall/30",cancelled:"text-dim bg-white/[0.03] border-subtle/20",snoozed:"text-dream-glow bg-dream/10 border-dream/30"},st={4:"critical",3:"high",2:"normal",1:"low"},rt={4:"text-decay",3:"text-amber-400",2:"text-dim",1:"text-muted"},it={time:"⏰",context:"◎",event:"⚡",manual:"◇"};function nt(s){let t;try{const e=JSON.parse(s.trigger_data||"{}");if(typeof e.condition=="string"&&e.condition)t=e.condition;else if(typeof e.topic=="string"&&e.topic)t=e.topic;else if(typeof e.at=="string"&&e.at)try{t=new Date(e.at).toLocaleDateString("en-US",{month:"short",day:"numeric"})}catch{t=e.at}else if(typeof e.in_minutes=="number")t=`in ${e.in_minutes} min`;else if(typeof e.inMinutes=="number")t=`in ${e.inMinutes} min`;else if(typeof e.codebase=="string"&&e.codebase){const i=typeof e.filePattern=="string"&&e.filePattern?`/${e.filePattern}`:"";t=`${e.codebase}${i}`}else t=s.trigger_type}catch{t=s.trigger_type}return t.length>40?t.slice(0,37)+"...":t}gt(async()=>{await X()});async function X(){N(A,!0);try{const[s,t]=await Promise.all([Z.intentions(r(S)),Z.predict()]);N(I,s.intentions||[],!0),N(C,t.predictions||[],!0)}catch{}finally{N(A,!1)}}async function ot(s){N(S,s,!0),await X()}function M(s){if(!s)return"";try{return new Date(s).toLocaleDateString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return s}}var j=At(),F=n(j),q=o(n(F),2),dt=n(q);a(q),a(F);var Y=o(F,2),E=o(n(Y),2);L(E,20,()=>["active","fulfilled","snoozed","cancelled","all"],U,(s,t)=>{var e=St(),i=n(e,!0);a(e),u(p=>{W(e,1,`px-3 py-1.5 rounded-xl text-xs transition ${r(S)===t?"bg-synapse/20 text-synapse-glow border border-synapse/40":"glass-subtle text-dim hover:bg-white/[0.03]"}`),d(i,p)},[()=>t.charAt(0).toUpperCase()+t.slice(1)]),Rt("click",e,()=>ot(t)),l(s,e)}),a(E);var lt=o(E,2);{var vt=s=>{var t=Ot();L(t,20,()=>Array(4),U,(e,i)=>{var p=$t();l(e,p)}),a(t),l(s,t)},ct=s=>{var t=Pt(),e=o(n(t),2),i=n(e);a(e),ht(2),a(t),u(()=>d(i,`No ${r(S)==="all"?"":r(S)+" "}intentions.`)),l(s,t)},pt=s=>{var t=kt();L(t,21,()=>r(I),U,(e,i)=>{var p=Tt(),g=n(p),y=n(g),T=n(y,!0);a(y);var f=o(y,2),$=n(f),k=n($,!0);a($);var b=o($,2),h=n(b),z=n(h,!0);a(h);var w=o(h,2),G=n(w);a(w);var O=o(w,2),x=n(O);a(O);var c=o(O,2);{var P=m=>{var _=Nt(),J=n(_);a(_),u(V=>d(J,`deadline: ${V??""}`),[()=>M(r(i).deadline)]),l(m,_)};R(c,m=>{r(i).deadline&&m(P)})}var B=o(c,2);{var ut=m=>{var _=It(),J=n(_);a(_),u(V=>d(J,`snoozed until ${V??""}`),[()=>M(r(i).snoozed_until)]),l(m,_)};R(B,m=>{r(i).snoozed_until&&m(ut)})}a(b),a(f);var K=o(f,2),ft=n(K,!0);a(K),a(g),a(p),u((m,_)=>{d(T,it[r(i).trigger_type]||"◇"),d(k,r(i).content),W(h,1,`px-2 py-0.5 text-[10px] rounded-lg border ${(at[r(i).status]||"text-dim bg-white/[0.03] border-subtle/20")??""}`),d(z,r(i).status),W(w,1,`text-[10px] ${(rt[r(i).priority]||"text-muted")??""}`),d(G,`${(st[r(i).priority]||"normal")??""} priority`),d(x,`${r(i).trigger_type??""}: ${m??""}`),d(ft,_)},[()=>nt(r(i)),()=>M(r(i).created_at)]),l(e,p)}),a(t),l(s,t)};R(lt,s=>{r(A)?s(vt):r(I).length===0?s(ct,1):s(pt,!1)})}a(Y);var H=o(Y,2),xt=o(n(H),2);{var mt=s=>{var t=zt();l(s,t)},_t=s=>{var t=Ct();L(t,21,()=>r(C),U,(e,i,p)=>{var g=Ut(),y=n(g);y.textContent=p+1;var T=o(y,2),f=n(T),$=n(f,!0);a(f);var k=o(f,2),b=n(k),h=n(b,!0);a(b);var z=o(b,2);{var w=x=>{var c=Dt(),P=n(c);a(c),u(B=>d(P,`${B??""}% retention`),[()=>(Number(r(i).retention)*100).toFixed(0)]),l(x,c)};R(z,x=>{r(i).retention&&x(w)})}var G=o(z,2);{var O=x=>{var c=Lt(),P=n(c);a(c),u(()=>d(P,`${r(i).predictedNeed??""} need`)),l(x,c)};R(G,x=>{r(i).predictedNeed&&x(O)})}a(k),a(T),a(g),u(()=>{d($,r(i).content),d(h,r(i).nodeType)}),l(e,g)}),a(t),l(s,t)};R(xt,s=>{r(C).length===0?s(mt):s(_t,!1)})}a(H),a(j),u(()=>d(dt,`${r(I).length??""} intentions`)),l(tt,j),bt()}wt(["click"]);export{Wt as component}; +import"../chunks/Bzak7iHL.js";import{o as gt}from"../chunks/GG5zm9kr.js";import{p as yt,s as D,c as Q,t as u,a as bt,e as o,d as n,h as O,g as r,r as a,n as ht}from"../chunks/CpWkWWOo.js";import{d as wt,s as d,a as Rt}from"../chunks/BlVfL1ME.js";import{i as R}from"../chunks/B4yTwGkE.js";import{e as L,i as U}from"../chunks/CGEBXrjl.js";import{a as l,f as v}from"../chunks/CHOnp4oo.js";import{s as W}from"../chunks/aVbAZ-t7.js";import{a as Z}from"../chunks/554JRhq6.js";var St=v(""),$t=v('
      '),Pt=v('
      '),Nt=v('

      Use "Remind me..." in conversation to create intentions.

      '),Ot=v(' '),It=v(' '),Tt=v('

      '),kt=v('
      '),zt=v('

      No predictions yet. Use Vestige more to train the predictive model.

      '),Dt=v(" "),Lt=v(' '),Ut=v('

      '),Ct=v('
      '),At=v('

      Intentions & Predictions

      Prospective Memory

      "Remember to do X when Y happens"

      Predicted Needs

      What you might need next
      ');function Wt(tt,et){yt(et,!0);let I=D(Q([])),C=D(Q([])),A=D(!0),S=D("active");const at={active:"text-synapse-glow bg-synapse/10 border-synapse/30",fulfilled:"text-recall bg-recall/10 border-recall/30",cancelled:"text-dim bg-white/[0.03] border-subtle/20",snoozed:"text-dream-glow bg-dream/10 border-dream/30"},st={4:"critical",3:"high",2:"normal",1:"low"},rt={4:"text-decay",3:"text-amber-400",2:"text-dim",1:"text-muted"},it={time:"⏰",context:"◎",event:"⚡",manual:"◇"};function nt(s){let t;try{const e=JSON.parse(s.trigger_data||"{}");if(typeof e.condition=="string"&&e.condition)t=e.condition;else if(typeof e.topic=="string"&&e.topic)t=e.topic;else if(typeof e.at=="string"&&e.at)try{t=new Date(e.at).toLocaleDateString("en-US",{month:"short",day:"numeric"})}catch{t=e.at}else if(typeof e.in_minutes=="number")t=`in ${e.in_minutes} min`;else if(typeof e.inMinutes=="number")t=`in ${e.inMinutes} min`;else if(typeof e.codebase=="string"&&e.codebase){const i=typeof e.filePattern=="string"&&e.filePattern?`/${e.filePattern}`:"";t=`${e.codebase}${i}`}else t=s.trigger_type}catch{t=s.trigger_type}return t.length>40?t.slice(0,37)+"...":t}gt(async()=>{await X()});async function X(){O(A,!0);try{const[s,t]=await Promise.all([Z.intentions(r(S)),Z.predict()]);O(I,s.intentions||[],!0),O(C,t.predictions||[],!0)}catch{}finally{O(A,!1)}}async function ot(s){O(S,s,!0),await X()}function M(s){if(!s)return"";try{return new Date(s).toLocaleDateString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return s}}var j=At(),F=n(j),q=o(n(F),2),dt=n(q);a(q),a(F);var Y=o(F,2),E=o(n(Y),2);L(E,20,()=>["active","fulfilled","snoozed","cancelled","all"],U,(s,t)=>{var e=St(),i=n(e,!0);a(e),u(p=>{W(e,1,`px-3 py-1.5 rounded-xl text-xs transition ${r(S)===t?"bg-synapse/20 text-synapse-glow border border-synapse/40":"glass-subtle text-dim hover:bg-white/[0.03]"}`),d(i,p)},[()=>t.charAt(0).toUpperCase()+t.slice(1)]),Rt("click",e,()=>ot(t)),l(s,e)}),a(E);var lt=o(E,2);{var vt=s=>{var t=Pt();L(t,20,()=>Array(4),U,(e,i)=>{var p=$t();l(e,p)}),a(t),l(s,t)},ct=s=>{var t=Nt(),e=o(n(t),2),i=n(e);a(e),ht(2),a(t),u(()=>d(i,`No ${r(S)==="all"?"":r(S)+" "}intentions.`)),l(s,t)},pt=s=>{var t=kt();L(t,21,()=>r(I),U,(e,i)=>{var p=Tt(),g=n(p),y=n(g),T=n(y,!0);a(y);var f=o(y,2),$=n(f),k=n($,!0);a($);var b=o($,2),h=n(b),z=n(h,!0);a(h);var w=o(h,2),G=n(w);a(w);var P=o(w,2),x=n(P);a(P);var c=o(P,2);{var N=m=>{var _=Ot(),J=n(_);a(_),u(V=>d(J,`deadline: ${V??""}`),[()=>M(r(i).deadline)]),l(m,_)};R(c,m=>{r(i).deadline&&m(N)})}var B=o(c,2);{var ut=m=>{var _=It(),J=n(_);a(_),u(V=>d(J,`snoozed until ${V??""}`),[()=>M(r(i).snoozed_until)]),l(m,_)};R(B,m=>{r(i).snoozed_until&&m(ut)})}a(b),a(f);var K=o(f,2),ft=n(K,!0);a(K),a(g),a(p),u((m,_)=>{d(T,it[r(i).trigger_type]||"◇"),d(k,r(i).content),W(h,1,`px-2 py-0.5 text-[10px] rounded-lg border ${(at[r(i).status]||"text-dim bg-white/[0.03] border-subtle/20")??""}`),d(z,r(i).status),W(w,1,`text-[10px] ${(rt[r(i).priority]||"text-muted")??""}`),d(G,`${(st[r(i).priority]||"normal")??""} priority`),d(x,`${r(i).trigger_type??""}: ${m??""}`),d(ft,_)},[()=>nt(r(i)),()=>M(r(i).created_at)]),l(e,p)}),a(t),l(s,t)};R(lt,s=>{r(A)?s(vt):r(I).length===0?s(ct,1):s(pt,!1)})}a(Y);var H=o(Y,2),xt=o(n(H),2);{var mt=s=>{var t=zt();l(s,t)},_t=s=>{var t=Ct();L(t,21,()=>r(C),U,(e,i,p)=>{var g=Ut(),y=n(g);y.textContent=p+1;var T=o(y,2),f=n(T),$=n(f,!0);a(f);var k=o(f,2),b=n(k),h=n(b,!0);a(b);var z=o(b,2);{var w=x=>{var c=Dt(),N=n(c);a(c),u(B=>d(N,`${B??""}% retention`),[()=>(Number(r(i).retention)*100).toFixed(0)]),l(x,c)};R(z,x=>{r(i).retention&&x(w)})}var G=o(z,2);{var P=x=>{var c=Lt(),N=n(c);a(c),u(()=>d(N,`${r(i).predictedNeed??""} need`)),l(x,c)};R(G,x=>{r(i).predictedNeed&&x(P)})}a(k),a(T),a(g),u(()=>{d($,r(i).content),d(h,r(i).nodeType)}),l(e,g)}),a(t),l(s,t)};R(xt,s=>{r(C).length===0?s(mt):s(_t,!1)})}a(H),a(j),u(()=>d(dt,`${r(I).length??""} intentions`)),l(tt,j),bt()}wt(["click"]);export{Wt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js.br b/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js.br new file mode 100644 index 0000000..7aabd93 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js.gz b/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js.gz new file mode 100644 index 0000000..1777f53 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/12.BthmSU_R.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js b/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js new file mode 100644 index 0000000..d5532c3 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js @@ -0,0 +1,6 @@ +import"../chunks/Bzak7iHL.js";import{o as Ye}from"../chunks/GG5zm9kr.js";import{p as Ge,s as R,c as Ne,h as M,d as s,g as e,r as t,a as Je,f as Ke,e as n,t as k,u as ae}from"../chunks/CpWkWWOo.js";import{d as Ue,s as w,a as h}from"../chunks/BlVfL1ME.js";import{i as q}from"../chunks/B4yTwGkE.js";import{e as ue,i as je}from"../chunks/CGEBXrjl.js";import{a as c,f as g,b as re}from"../chunks/CHOnp4oo.js";import{s as X,r as qe}from"../chunks/A7po6GxK.js";import{s as Le}from"../chunks/aVbAZ-t7.js";import{s as Z}from"../chunks/Cx-f-Pzo.js";import{b as Qe}from"../chunks/sZcqyNBA.js";import{b as it}from"../chunks/BnXDGOmJ.js";import{a as H}from"../chunks/554JRhq6.js";import{N as lt}from"../chunks/CcUbQ_Wl.js";const dt={created:{label:"Created",color:"#10b981",glyph:"",kind:"ring"},accessed:{label:"Accessed",color:"#3b82f6",glyph:"",kind:"dot"},promoted:{label:"Promoted",color:"#10b981",glyph:"",kind:"arrow-up"},demoted:{label:"Demoted",color:"#f59e0b",glyph:"",kind:"arrow-down"},edited:{label:"Edited",color:"#facc15",glyph:"",kind:"pencil"},suppressed:{label:"Suppressed",color:"#a855f7",glyph:"",kind:"x"},dreamed:{label:"Dreamed",color:"#c084fc",glyph:"",kind:"star"},reconsolidated:{label:"Reconsolidated",color:"#ec4899",glyph:"",kind:"circle-arrow"}},ke=15;function ct(u){let p=0;for(let x=0;x>>0;return p}function vt(u){let p=u>>>0;return()=>(p=p*1664525+1013904223>>>0,p/4294967296)}function pt(u,p=Date.now(),x){if(!u)return[];const _=vt(ct(u)),S=8+Math.floor(_()*8);if(S<=0)return[];const P=[],T=p-(14+_()*21)*864e5;P.push({action:"created",timestamp:new Date(T).toISOString(),reason:"smart_ingest · prediction-error gate opened",triggered_by:"smart_ingest"});let A=T,l=.5+_()*.2;const D=["accessed","accessed","accessed","accessed","promoted","demoted","edited","dreamed","reconsolidated","suppressed"];for(let $=1;$.5?"search":"deep_reference";break}case"promoted":{const E=l;l=Math.min(1,l+.1),d.old_value=E,d.new_value=l,d.reason="confirmed helpful by user",d.triggered_by="memory(action=promote)";break}case"demoted":{const E=l;l=Math.max(0,l-.15),d.old_value=E,d.new_value=l,d.reason="user flagged as outdated",d.triggered_by="memory(action=demote)";break}case"edited":{d.reason="content refined, FSRS state preserved",d.triggered_by="memory(action=edit)";break}case"suppressed":{const E=l;l=Math.max(0,l-.08),d.old_value=E,d.new_value=l,d.reason="top-down inhibition (Anderson 2025)",d.triggered_by="suppress(dashboard)";break}case"dreamed":{const E=l;l=Math.min(1,l+.05),d.old_value=E,d.new_value=l,d.reason="replayed during dream consolidation",d.triggered_by="dream()";break}case"reconsolidated":{d.reason="edited within 5-min labile window (Nader)",d.triggered_by="reconsolidation-manager";break}case"created":d.triggered_by="smart_ingest";break}P.push(d)}return P.reverse()}function ut(u,p=Date.now()){const x=new Date(u).getTime(),_=Math.max(0,p-x),S=Math.floor(_/1e3);if(S<60)return`${S}s ago`;const P=Math.floor(S/60);if(P<60)return`${P}m ago`;const T=Math.floor(P/60);if(T<24)return`${T}h ago`;const A=Math.floor(T/24);if(A<30)return`${A}d ago`;const l=Math.floor(A/30);return l<12?`${l}mo ago`:`${Math.floor(l/12)}y ago`}function ft(u,p){const x=typeof u=="number"&&Number.isFinite(u),_=typeof p=="number"&&Number.isFinite(p);return!x&&!_?null:!x&&_?`set ${p.toFixed(2)}`:x&&!_?`was ${u.toFixed(2)}`:`${u.toFixed(2)} → ${p.toFixed(2)}`}function gt(u,p){return p||u.length<=ke?{visible:u,hiddenCount:Math.max(0,u.length-ke)}:{visible:u.slice(0,ke),hiddenCount:u.length-ke}}var _t=g('
      '),xt=g('
      '),bt=g('

      Audit trail failed to load.

      '),mt=g('

      No memory selected.

      '),ht=g('

      No audit events recorded yet.

      '),kt=g(''),yt=g(''),wt=re(''),Mt=re(''),St=re(''),Pt=re(''),Tt=re(''),At=re(''),Dt=g(' '),Et=g('
      '),Ct=g('
      '),Ft=g('
    • '),It=g(''),Lt=g('
        ',1),Nt=g('
        ');function jt(u,p){Ge(p,!0);let x=R(Ne([])),_=R(!0),S=R(!1),P=R(!1);async function T(m){return pt(m,Date.now())}Ye(async()=>{if(!p.memoryId){M(x,[],!0),M(_,!1);return}try{M(x,await T(p.memoryId),!0)}catch{M(x,[],!0),M(S,!0)}finally{M(_,!1)}});const A=ae(()=>gt(e(x),e(P))),l=ae(()=>e(A).visible),D=ae(()=>e(A).hiddenCount);var $=Nt(),I=s($);{var d=m=>{var y=xt();ue(y,20,()=>Array(5),je,(N,Q)=>{var K=_t();c(N,K)}),t(y),c(m,y)},E=m=>{var y=bt();c(m,y)},oe=m=>{var y=mt();c(m,y)},se=m=>{var y=ht();c(m,y)},fe=m=>{var y=Lt(),N=Ke(y);ue(N,23,()=>e(l),(L,b)=>L.timestamp+b,(L,b,U)=>{const v=ae(()=>dt[e(b).action]),V=ae(()=>ft(e(b).old_value,e(b).new_value));var Y=Ft(),G=s(Y),ge=s(G);{var _e=a=>{var r=kt();k(()=>Z(r,`background: ${e(v).color??""};`)),c(a,r)},ee=a=>{var r=yt();k(()=>Z(r,`border-color: ${e(v).color??""}; background: transparent;`)),c(a,r)},xe=a=>{var r=wt();k(()=>X(r,"stroke",e(v).color)),c(a,r)},ye=a=>{var r=Mt();k(()=>X(r,"stroke",e(v).color)),c(a,r)},we=a=>{var r=St();k(()=>X(r,"stroke",e(v).color)),c(a,r)},Me=a=>{var r=Pt();k(()=>X(r,"stroke",e(v).color)),c(a,r)},Se=a=>{var r=Tt();k(()=>X(r,"fill",e(v).color)),c(a,r)},f=a=>{var r=At();k(()=>X(r,"stroke",e(v).color)),c(a,r)};q(ge,a=>{e(v).kind==="dot"?a(_e):e(v).kind==="ring"?a(ee,1):e(v).kind==="arrow-up"?a(xe,2):e(v).kind==="arrow-down"?a(ye,3):e(v).kind==="pencil"?a(we,4):e(v).kind==="x"?a(Me,5):e(v).kind==="star"?a(Se,6):e(v).kind==="circle-arrow"&&a(f,7)})}t(G);var C=n(G,2),z=s(C),i=s(z),F=s(i),ne=s(F,!0);t(F);var ie=n(F,2);{var le=a=>{var r=Dt(),W=s(r,!0);t(r),k(()=>w(W,e(b).triggered_by)),c(a,r)};q(ie,a=>{e(b).triggered_by&&a(le)})}t(i);var te=n(i,2),de=s(te,!0);t(te),t(z);var be=n(z,2);{var Pe=a=>{var r=Et(),W=s(r);t(r),k(()=>w(W,`retention ${e(V)??""}`)),c(a,r)};q(be,a=>{e(V)&&a(Pe)})}var me=n(be,2);{var Te=a=>{var r=Ct(),W=s(r,!0);t(r),k(()=>w(W,e(b).reason)),c(a,r)};q(me,a=>{e(b).reason&&a(Te)})}t(C),t(Y),k((a,r)=>{Z(Y,`animation-delay: ${e(U)*40}ms;`),Z(G,`background: rgba(10,10,26,0.9); box-shadow: 0 0 10px ${e(v).color??""}88; border: 1px solid ${e(v).color??""};`),Z(F,`color: ${e(v).color??""};`),w(ne,e(v).label),X(te,"title",a),w(de,r)},[()=>new Date(e(b).timestamp).toLocaleString(),()=>ut(e(b).timestamp)]),c(L,Y)}),t(N);var Q=n(N,2);{var K=L=>{var b=It(),U=s(b,!0);t(b),k(()=>w(U,e(P)?"Hide older events":`Show ${e(D)} older event${e(D)===1?"":"s"}…`)),h("click",b,v=>{v.stopPropagation(),M(P,!e(P))}),c(L,b)};q(Q,L=>{e(D)>0&&L(K)})}c(m,y)};q(I,m=>{e(_)?m(d):e(S)?m(E,1):p.memoryId?e(x).length===0?m(se,3):m(fe,!1):m(oe,2)})}t($),c(u,$),Je()}Ue(["click"]);var Bt=g('
        '),Ot=g('
        '),Rt=g(' '),$t=g('

        ',1),zt=g('
        '),Ht=g('
        Content Audit Trail
        Promote Demote Suppress Delete
        '),qt=g(''),Qt=g('
        '),Yt=g(`

        Memories

        Min retention:
        `);function na(u,p){Ge(p,!0);let x=R(Ne([])),_=R(""),S=R(""),P="",T=R(0),A=R(!0),l=R(null),D=Ne({}),$;Ye(()=>I());async function I(){M(A,!0);try{const f={};e(_)&&(f.q=e(_)),e(S)&&(f.node_type=e(S)),e(T)>0&&(f.min_retention=String(e(T)));const C=await H.memories.list(f);M(x,C.memories,!0)}catch{M(x,[],!0)}finally{M(A,!1)}}function d(){clearTimeout($),$=setTimeout(I,300)}function E(f){return f>.7?"#10b981":f>.4?"#f59e0b":"#ef4444"}var oe=Yt(),se=s(oe),fe=n(s(se),2),m=s(fe);t(fe),t(se);var y=n(se,2),N=s(y);qe(N);var Q=n(N,2),K=s(Q);K.value=K.__value="";var L=n(K);L.value=L.__value="fact";var b=n(L);b.value=b.__value="concept";var U=n(b);U.value=U.__value="event";var v=n(U);v.value=v.__value="person";var V=n(v);V.value=V.__value="place";var Y=n(V);Y.value=Y.__value="note";var G=n(Y);G.value=G.__value="pattern";var ge=n(G);ge.value=ge.__value="decision",t(Q);var _e=n(Q,2),ee=n(s(_e),2);qe(ee);var xe=n(ee,2),ye=s(xe);t(xe),t(_e),t(y);var we=n(y,2);{var Me=f=>{var C=Ot();ue(C,20,()=>Array(8),je,(z,i)=>{var F=Bt();c(z,F)}),t(C),c(f,C)},Se=f=>{var C=Qt();ue(C,21,()=>e(x),z=>z.id,(z,i)=>{var F=qt(),ne=s(F),ie=s(ne),le=s(ie),te=s(le),de=n(te,2),be=s(de,!0);t(de);var Pe=n(de,2);ue(Pe,17,()=>e(i).tags.slice(0,3),je,(j,B)=>{var O=Rt(),ce=s(O,!0);t(O),k(()=>w(ce,e(B))),c(j,O)}),t(le);var me=n(le,2),Te=s(me,!0);t(me),t(ie);var a=n(ie,2),r=s(a),W=s(r);t(r);var Be=n(r,2),We=s(Be);t(Be),t(a),t(ne);var Xe=n(ne,2);{var Ze=j=>{const B=ae(()=>D[e(i).id]??"content");var O=Ht(),ce=s(O),he=s(ce),Ae=n(he,2);t(ce);var Oe=n(ce,2);{var Ve=o=>{var J=$t(),ve=Ke(J),pe=s(ve,!0);t(ve);var ze=n(ve,2),Fe=s(ze),tt=s(Fe);t(Fe);var Ie=n(Fe,2),at=s(Ie);t(Ie);var He=n(Ie,2),rt=s(He);t(He),t(ze),k((ot,st,nt)=>{w(pe,e(i).content),w(tt,`Storage: ${ot??""}%`),w(at,`Retrieval: ${st??""}%`),w(rt,`Created: ${nt??""}`)},[()=>(e(i).storageStrength*100).toFixed(1),()=>(e(i).retrievalStrength*100).toFixed(1),()=>new Date(e(i).createdAt).toLocaleDateString()]),c(o,J)},et=o=>{var J=zt(),ve=s(J);jt(ve,{get memoryId(){return e(i).id}}),t(J),h("click",J,pe=>pe.stopPropagation()),h("keydown",J,pe=>pe.stopPropagation()),c(o,J)};q(Oe,o=>{e(B)==="content"?o(Ve):o(et,!1)})}var Re=n(Oe,2),De=s(Re),Ee=n(De,2),Ce=n(Ee,2),$e=n(Ce,2);t(Re),t(O),k(()=>{Le(he,1,`px-3 py-1.5 rounded-lg cursor-pointer select-none transition + ${e(B)==="content"?"bg-synapse/20 text-synapse-glow border border-synapse/40":"bg-white/[0.03] text-dim hover:text-text border border-transparent"}`),Le(Ae,1,`px-3 py-1.5 rounded-lg cursor-pointer select-none transition + ${e(B)==="audit"?"bg-synapse/20 text-synapse-glow border border-synapse/40":"bg-white/[0.03] text-dim hover:text-text border border-transparent"}`)}),h("click",he,o=>{o.stopPropagation(),D[e(i).id]="content"}),h("keydown",he,o=>{(o.key==="Enter"||o.key===" ")&&(o.stopPropagation(),D[e(i).id]="content")}),h("click",Ae,o=>{o.stopPropagation(),D[e(i).id]="audit"}),h("keydown",Ae,o=>{(o.key==="Enter"||o.key===" ")&&(o.stopPropagation(),D[e(i).id]="audit")}),h("click",De,o=>{o.stopPropagation(),H.memories.promote(e(i).id)}),h("keydown",De,o=>{o.key==="Enter"&&(o.stopPropagation(),H.memories.promote(e(i).id))}),h("click",Ee,o=>{o.stopPropagation(),H.memories.demote(e(i).id)}),h("keydown",Ee,o=>{o.key==="Enter"&&(o.stopPropagation(),H.memories.demote(e(i).id))}),h("click",Ce,async o=>{o.stopPropagation(),await H.memories.suppress(e(i).id,"dashboard trigger")}),h("keydown",Ce,async o=>{o.key==="Enter"&&(o.stopPropagation(),await H.memories.suppress(e(i).id,"dashboard trigger"))}),h("click",$e,async o=>{o.stopPropagation(),await H.memories.delete(e(i).id),I()}),h("keydown",$e,async o=>{o.key==="Enter"&&(o.stopPropagation(),await H.memories.delete(e(i).id),I())}),c(j,O)};q(Xe,j=>{var B;((B=e(l))==null?void 0:B.id)===e(i).id&&j(Ze)})}t(F),k((j,B)=>{var O;Le(F,1,`text-left p-4 glass-subtle rounded-xl hover:bg-white/[0.04] + transition-all duration-200 group + ${((O=e(l))==null?void 0:O.id)===e(i).id?"!border-synapse/40 glow-synapse":""}`),Z(te,`background: ${(lt[e(i).nodeType]||"#8B95A5")??""}`),w(be,e(i).nodeType),w(Te,e(i).content),Z(W,`width: ${e(i).retentionStrength*100}%; background: ${j??""}`),w(We,`${B??""}%`)},[()=>E(e(i).retentionStrength),()=>(e(i).retentionStrength*100).toFixed(0)]),h("click",F,()=>{var j;return M(l,((j=e(l))==null?void 0:j.id)===e(i).id?null:e(i),!0)}),c(z,F)}),t(C),c(f,C)};q(we,f=>{e(A)?f(Me):f(Se,!1)})}t(oe),k(f=>{w(m,`${e(x).length??""} results`),w(ye,`${f??""}%`)},[()=>(e(T)*100).toFixed(0)]),h("input",N,d),Qe(N,()=>e(_),f=>M(_,f)),h("change",Q,I),it(Q,()=>e(S),f=>M(S,f)),h("change",ee,I),Qe(ee,()=>e(T),f=>M(T,f)),c(u,oe),Je()}Ue(["input","change","click","keydown"]);export{na as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js.br b/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js.br new file mode 100644 index 0000000..4b24417 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js.gz b/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js.gz new file mode 100644 index 0000000..178247c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/13.C0fh4g_5.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js b/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js new file mode 100644 index 0000000..119993e --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js @@ -0,0 +1,3 @@ +import"../chunks/Bzak7iHL.js";import{o as Ut}from"../chunks/GG5zm9kr.js";import{p as Et,d as r,e as a,r as e,t as N,g as t,u as L,a as Rt,s as rt,h as F,f as Ht,c as Vt}from"../chunks/CpWkWWOo.js";import{d as Lt,s as o,a as at,e as Mt}from"../chunks/BlVfL1ME.js";import{i as Y}from"../chunks/B4yTwGkE.js";import{e as V,i as Yt}from"../chunks/CGEBXrjl.js";import{a as _,c as Jt,f as y}from"../chunks/CHOnp4oo.js";import{s as ht}from"../chunks/A7po6GxK.js";import{s as bt}from"../chunks/aVbAZ-t7.js";import{s as wt}from"../chunks/Cx-f-Pzo.js";function Xt(h,x,A=3){const g={};for(const p of h){g[p]={};for(const u of h)g[p][u]={count:0,topNames:[]}}for(const p of x){const u=p.origin_project;if(g[u])for(const I of p.transferred_to)g[u][I]&&(g[u][I].count+=1,g[u][I].topNames.push(p.name))}const l=Math.max(0,A);for(const p of h)for(const u of h)g[p][u].topNames=g[p][u].topNames.slice(0,l);return g}function te(h,x){let A=0;for(const g of h){const l=x[g];if(l)for(const p of h){const u=l[p];u&&u.count>A&&(A=u.count)}}return A}function ee(h,x){var g;const A=[];for(const l of h)for(const p of h){const u=(g=x[l])==null?void 0:g[p];u&&u.count>0&&A.push({from:l,to:p,count:u.count,topNames:u.topNames})}return A.sort((l,p)=>p.count-l.count)}function re(h){return h?h.length>12?h.slice(0,11)+"…":h:""}var ae=y('
        '),se=y(''),ne=y(' '),oe=y('
        '),ie=y('
        Top patterns
        '),ce=y('
        No transfers recorded
        '),de=y('
        '),le=y('
        No cross-project transfers recorded yet.
        '),pe=y('
        '),ue=y(''),ve=y('
        ');function fe(h,x){Et(x,!0);const A=L(()=>Xt(x.projects,x.patterns)),g=L(()=>te(x.projects,t(A))||1);let l=rt(null);function p(n){if(n===0)return"background: rgba(255,255,255,0.02); border-color: rgba(99,102,241,0.05);";const s=n/t(g),v=.1+s*.7;if(s<.5)return`background: rgba(99, 102, 241, ${v.toFixed(3)}); border-color: rgba(129, 140, 248, ${(v*.6).toFixed(3)}); box-shadow: 0 0 ${(s*14).toFixed(1)}px rgba(129, 140, 248, ${(s*.45).toFixed(3)});`;{const f=(s-.5)*2,d=Math.round(99+69*f),C=Math.round(102+-17*f),b=Math.round(241+6*f);return`background: rgba(${d}, ${C}, ${b}, ${v.toFixed(3)}); border-color: rgba(192, 132, 252, ${(v*.7).toFixed(3)}); box-shadow: 0 0 ${(6+s*18).toFixed(1)}px rgba(192, 132, 252, ${(s*.55).toFixed(3)});`}}function u(n){if(n===0)return"text-muted";const s=n/t(g);return s>=.5?"text-bright font-semibold":s>=.2?"text-text":"text-dim"}function I(n,s,v){const d=n.currentTarget.getBoundingClientRect();F(l,{from:s,to:v,x:d.left+d.width/2,y:d.top},!0)}function j(){F(l,null)}const B=re;function st(n,s){return x.selectedCell!==null&&x.selectedCell.from===n&&x.selectedCell.to===s}const G=L(()=>ee(x.projects,t(A)));var Q=ve(),J=r(Q),X=r(J),nt=a(r(X),2),ot=a(r(nt),4),jt=r(ot,!0);e(ot),e(nt),e(X);var it=a(X,2),gt=r(it),q=r(gt),tt=r(q),ct=a(r(tt));V(ct,16,()=>x.projects,n=>n,(n,s)=>{var v=ae(),f=r(v),d=r(f),C=r(d,!0);e(d),e(f),e(v),N(b=>{ht(v,"title",s),o(C,b)},[()=>B(s)]),_(n,v)}),e(tt),e(q);var xt=a(q);V(xt,20,()=>x.projects,n=>n,(n,s)=>{var v=ne(),f=r(v),d=r(f,!0);e(f);var C=a(f);V(C,16,()=>x.projects,b=>b,(b,$)=>{const P=L(()=>t(A)[s][$]),W=L(()=>s===$);var M=se(),S=r(M),E=r(S),U=r(E,!0);e(E),e(S),e(M),N((R,Z,k)=>{wt(S,`${R??""} ${Z??""} ${t(W)&&t(P).count>0?"border-style: dashed;":""}`),ht(S,"aria-label",`${t(P).count??""} patterns from ${s??""} to ${$??""}`),bt(E,1,`text-[11px] ${k??""}`),o(U,t(P).count||"")},[()=>p(t(P).count),()=>st(s,$)?"outline: 2px solid var(--color-dream-glow); outline-offset: 1px;":"",()=>u(t(P).count)]),at("click",S,()=>x.onCellClick(s,$)),Mt("mouseenter",S,R=>I(R,s,$)),Mt("mouseleave",S,j),_(b,M)}),e(v),N(b=>{ht(f,"title",s),o(d,b)},[()=>B(s)]),_(n,v)}),e(xt),e(gt),e(it);var kt=a(it,2);{var Tt=n=>{const s=L(()=>t(A)[t(l).from][t(l).to]);var v=de(),f=r(v),d=r(f),C=r(d,!0);e(d);var b=a(d,4),$=r(b,!0);e(b),e(f);var P=a(f,2),W=r(P),M=a(W),S=r(M);e(M),e(P);var E=a(P,2);{var U=Z=>{var k=ie(),O=a(r(k),2);V(O,17,()=>t(s).topNames,Yt,(K,lt)=>{var pt=oe(),_t=r(pt);e(pt),N(()=>o(_t,`· ${t(lt)??""}`)),_(K,pt)}),e(k),_(Z,k)},R=Z=>{var k=ce();_(Z,k)};Y(E,Z=>{t(s).topNames.length>0?Z(U):Z(R,!1)})}e(v),N((Z,k)=>{wt(v,`left: ${t(l).x??""}px; top: ${t(l).y-12}px; transform: translate(-50%, -100%);`),o(C,Z),o($,k),o(W,`${t(s).count??""} `),o(S,`${t(s).count===1?"pattern":"patterns"} transferred`)},[()=>B(t(l).from),()=>B(t(l).to)]),_(n,v)};Y(kt,n=>{t(l)&&n(Tt)})}e(J);var mt=a(J,2),dt=r(mt),i=r(dt);e(dt);var c=a(dt,2);{var m=n=>{var s=le();_(n,s)},T=n=>{var s=Jt(),v=Ht(s);V(v,17,()=>t(G),f=>f.from+"->"+f.to,(f,d)=>{var C=ue(),b=r(C),$=r(b),P=r($),W=r(P,!0);e(P);var M=a(P,4),S=r(M,!0);e(M),e($);var E=a($,2);{var U=k=>{var O=pe(),K=r(O,!0);e(O),N(lt=>o(K,lt),[()=>t(d).topNames.join(" · ")]),_(k,O)};Y(E,k=>{t(d).topNames.length>0&&k(U)})}e(b);var R=a(b,2),Z=r(R,!0);e(R),e(C),N((k,O,K)=>{bt(C,1,`flex w-full items-center justify-between rounded-lg border border-synapse/10 bg-white/[0.02] p-3 transition hover:border-synapse/30 hover:bg-white/[0.04] ${k??""}`),o(W,O),o(S,K),o(Z,t(d).count)},[()=>st(t(d).from,t(d).to)?"ring-1 ring-dream-glow":"",()=>B(t(d).from),()=>B(t(d).to)]),at("click",C,()=>x.onCellClick(t(d).from,t(d).to)),_(f,C)}),_(n,s)};Y(c,n=>{t(G).length===0?n(m):n(T,!1)})}e(mt),e(Q),N(()=>{o(jt,t(g)),o(i,`${t(G).length??""} transfer pair${t(G).length===1?"":"s"} · tap to filter`)}),_(h,Q),Rt()}Lt(["click"]);var ge=y(''),xe=y(`
        Couldn't load pattern transfers
        `),me=y('
        '),_e=y('
        Filtered to
        '),ye=y('
        No matching patterns
        '),he=y('
      1. '),be=y('
          '),we=y('
          ',1),je=y('

          Cross-Project Intelligence

          Patterns learned here, applied there.

          ');function Me(h,x){Et(x,!0);const A=["ErrorHandling","AsyncConcurrency","Testing","Architecture","Performance","Security"],g={ErrorHandling:"var(--color-decay)",AsyncConcurrency:"var(--color-synapse-glow)",Testing:"var(--color-recall)",Architecture:"var(--color-dream-glow)",Performance:"var(--color-warning)",Security:"var(--color-node-pattern)"};let l=rt("All"),p=rt(Vt({projects:[],patterns:[]})),u=rt(!0),I=rt(null),j=rt(null);async function B(){return await new Promise(m=>setTimeout(m,420)),{projects:["vestige","api-gateway","desktop-app","model-runner","game-sim","security-dashboard","benchmark-suite"],patterns:[{name:"Result with thiserror context",category:"ErrorHandling",origin_project:"vestige",transferred_to:["api-gateway","desktop-app","model-runner","security-dashboard"],transfer_count:4,last_used:"2026-04-18T14:22:00Z",confidence:.94},{name:"Axum error middleware with tower-http",category:"ErrorHandling",origin_project:"api-gateway",transferred_to:["vestige","security-dashboard"],transfer_count:2,last_used:"2026-04-17T09:10:00Z",confidence:.88},{name:"Graceful shutdown on SIGINT/SIGTERM",category:"ErrorHandling",origin_project:"vestige",transferred_to:["vestige","desktop-app","security-dashboard"],transfer_count:3,last_used:"2026-04-15T22:01:00Z",confidence:.82},{name:"Python try/except with contextual re-raise",category:"ErrorHandling",origin_project:"benchmark-suite",transferred_to:["model-runner"],transfer_count:1,last_used:"2026-04-10T11:30:00Z",confidence:.7},{name:"Arc> reader/writer split",category:"AsyncConcurrency",origin_project:"vestige",transferred_to:["api-gateway","desktop-app"],transfer_count:2,last_used:"2026-04-14T16:42:00Z",confidence:.91},{name:"tokio::select! for cancellation propagation",category:"AsyncConcurrency",origin_project:"desktop-app",transferred_to:["vestige","security-dashboard"],transfer_count:2,last_used:"2026-04-19T08:05:00Z",confidence:.86},{name:"Bounded mpsc channel with backpressure",category:"AsyncConcurrency",origin_project:"desktop-app",transferred_to:["vestige","api-gateway"],transfer_count:2,last_used:"2026-04-12T13:18:00Z",confidence:.83},{name:"asyncio.gather with return_exceptions",category:"AsyncConcurrency",origin_project:"model-runner",transferred_to:["benchmark-suite"],transfer_count:1,last_used:"2026-04-08T20:45:00Z",confidence:.72},{name:"Property-based tests with proptest",category:"Testing",origin_project:"vestige",transferred_to:["api-gateway","desktop-app"],transfer_count:2,last_used:"2026-04-11T10:22:00Z",confidence:.89},{name:"Snapshot testing with insta",category:"Testing",origin_project:"api-gateway",transferred_to:["vestige"],transfer_count:1,last_used:"2026-04-16T14:00:00Z",confidence:.81},{name:"Vitest + Playwright dashboard harness",category:"Testing",origin_project:"vestige",transferred_to:["api-gateway","desktop-app"],transfer_count:2,last_used:"2026-04-19T18:30:00Z",confidence:.87},{name:"One-variable-at-a-time Kaggle submission",category:"Testing",origin_project:"benchmark-suite",transferred_to:["model-runner","game-sim"],transfer_count:2,last_used:"2026-04-20T07:15:00Z",confidence:.95},{name:"Kaggle pre-flight Input-panel screenshot",category:"Testing",origin_project:"benchmark-suite",transferred_to:["model-runner","game-sim"],transfer_count:2,last_used:"2026-04-20T06:50:00Z",confidence:.98},{name:"SvelteKit 2 + Svelte 5 runes dashboard",category:"Architecture",origin_project:"vestige",transferred_to:["api-gateway","security-dashboard"],transfer_count:2,last_used:"2026-04-19T12:10:00Z",confidence:.92},{name:"glass-panel + cosmic-dark design system",category:"Architecture",origin_project:"vestige",transferred_to:["api-gateway","security-dashboard","desktop-app"],transfer_count:3,last_used:"2026-04-20T09:00:00Z",confidence:.9},{name:"Tauri 2 + Rust/Axum sidecar",category:"Architecture",origin_project:"desktop-app",transferred_to:["security-dashboard"],transfer_count:1,last_used:"2026-04-13T19:44:00Z",confidence:.78},{name:"MCP server with 23 stateful tools",category:"Architecture",origin_project:"vestige",transferred_to:["desktop-app"],transfer_count:1,last_used:"2026-04-17T11:05:00Z",confidence:.85},{name:"USearch HNSW index for vector search",category:"Performance",origin_project:"vestige",transferred_to:["api-gateway"],transfer_count:1,last_used:"2026-04-09T15:20:00Z",confidence:.88},{name:"SQLite WAL mode for concurrent reads",category:"Performance",origin_project:"vestige",transferred_to:["api-gateway","desktop-app","security-dashboard"],transfer_count:3,last_used:"2026-04-18T21:33:00Z",confidence:.93},{name:"vLLM prefix caching at 0.35 mem util",category:"Performance",origin_project:"benchmark-suite",transferred_to:["model-runner"],transfer_count:1,last_used:"2026-04-11T08:00:00Z",confidence:.84},{name:"Cross-encoder rerank at k=30",category:"Performance",origin_project:"vestige",transferred_to:["api-gateway"],transfer_count:1,last_used:"2026-04-14T17:55:00Z",confidence:.79},{name:"Rotated auth token in env var",category:"Security",origin_project:"vestige",transferred_to:["api-gateway","desktop-app","security-dashboard"],transfer_count:3,last_used:"2026-04-16T20:12:00Z",confidence:.96},{name:"Parameterized SQL via rusqlite params!",category:"Security",origin_project:"vestige",transferred_to:["api-gateway"],transfer_count:1,last_used:"2026-04-10T13:40:00Z",confidence:.89},{name:"664-pattern secret scanner",category:"Security",origin_project:"api-gateway",transferred_to:["vestige","security-dashboard","desktop-app"],transfer_count:3,last_used:"2026-04-20T05:30:00Z",confidence:.97},{name:"CSP header with nonce-based script allow",category:"Security",origin_project:"api-gateway",transferred_to:["security-dashboard"],transfer_count:1,last_used:"2026-04-05T16:08:00Z",confidence:.8}]}}async function st(){F(u,!0),F(I,null);try{F(p,await B(),!0)}catch(i){F(I,i instanceof Error?i.message:"Failed to load pattern transfers",!0),F(p,{projects:[],patterns:[]},!0)}finally{F(u,!1)}}Ut(()=>st());const G=L(()=>t(l)==="All"?t(p).patterns:t(p).patterns.filter(i=>i.category===t(l))),Q=L(()=>[...t(j)?t(G).filter(c=>c.origin_project===t(j).from&&c.transferred_to.includes(t(j).to)):t(G)].sort((c,m)=>m.transfer_count-c.transfer_count)),J=L(()=>t(G).reduce((i,c)=>i+c.transferred_to.length,0)),X=L(()=>t(p).projects.length),nt=L(()=>t(G).length);function ot(i){F(l,i,!0),F(j,null)}function jt(i,c){t(j)&&t(j).from===i&&t(j).to===c?F(j,null):F(j,{from:i,to:c},!0)}function it(){F(j,null)}function gt(i){const c=new Date(i).getTime(),m=Date.now(),T=Math.floor((m-c)/864e5);return T<=0?"today":T===1?"1d ago":T<30?`${T}d ago`:`${Math.floor(T/30)}mo ago`}var q=je(),tt=a(r(q),2),ct=r(tt),xt=a(ct,2);V(xt,16,()=>A,i=>i,(i,c)=>{var m=ge(),T=r(m),n=a(T);e(m),N(()=>{bt(m,1,`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition ${t(l)===c?"bg-synapse/25 text-synapse-glow":"text-dim hover:bg-white/[0.04] hover:text-text"}`),wt(T,`background: ${g[c]??""}`),o(n,` ${c??""}`)}),at("click",m,()=>ot(c)),_(i,m)}),e(tt);var kt=a(tt,2);{var Tt=i=>{var c=xe(),m=a(r(c),2),T=r(m,!0);e(m);var n=a(m,2);e(c),N(()=>o(T,t(I))),at("click",n,st),_(i,c)},mt=i=>{var c=me();_(i,c)},dt=i=>{var c=we(),m=Ht(c),T=r(m),n=r(T);fe(n,{get projects(){return t(p).projects},get patterns(){return t(G)},get selectedCell(){return t(j)},onCellClick:jt});var s=a(n,2);{var v=D=>{var H=_e(),z=r(H),w=a(r(z),2),ut=r(w,!0);e(w);var vt=a(w,4),ft=r(vt,!0);e(vt),e(z);var et=a(z,2);e(H),N(()=>{o(ut,t(j).from),o(ft,t(j).to)}),at("click",et,it),_(D,H)};Y(s,D=>{t(j)&&D(v)})}e(T);var f=a(T,2),d=r(f),C=a(r(d),2),b=r(C);e(C),e(d);var $=a(d,2);{var P=D=>{var H=ye(),z=a(r(H),2),w=r(z,!0);e(z),e(H),N(()=>o(w,t(j)?"No patterns transferred from this origin to this destination.":"No patterns in this category.")),_(D,H)},W=D=>{var H=be();V(H,21,()=>t(Q),z=>z.name,(z,w)=>{var ut=he(),vt=r(ut),ft=r(vt),et=r(ft),Gt=r(et,!0);e(et);var Ct=a(et,2),yt=r(Ct),Ot=r(yt,!0);e(yt);var Pt=a(yt,2),Dt=r(Pt,!0);e(Pt),e(Ct);var St=a(Ct,2),Zt=r(St),zt=r(Zt,!0);e(Zt);var Nt=a(Zt,4),Kt=r(Nt);e(Nt),e(St),e(ft);var $t=a(ft,2),At=r($t),Bt=r(At,!0);e(At);var Ft=a(At,2),Qt=r(Ft);e(Ft),e($t),e(vt),e(ut),N((Wt,qt)=>{ht(et,"title",t(w).name),o(Gt,t(w).name),wt(yt,`border-color: ${g[t(w).category]??""}66; color: ${g[t(w).category]??""}; background: ${g[t(w).category]??""}1a;`),o(Ot,t(w).category),o(Dt,Wt),o(zt,t(w).origin_project),o(Kt,`${t(w).transferred_to.length??""} + ${t(w).transferred_to.length===1?"project":"projects"}`),o(Bt,t(w).transfer_count),o(Qt,`${qt??""}%`)},[()=>gt(t(w).last_used),()=>(t(w).confidence*100).toFixed(0)]),_(z,ut)}),e(H),_(D,H)};Y($,D=>{t(Q).length===0?D(P):D(W,!1)})}e(f),e(m);var M=a(m,2),S=r(M),E=r(S),U=r(E,!0);e(E);var R=a(E),Z=a(R),k=r(Z,!0);e(Z);var O=a(Z),K=a(O),lt=r(K,!0);e(K);var pt=a(K);e(S);var _t=a(S,2),It=r(_t,!0);e(_t),e(M),N(()=>{o(b,`${t(Q).length??""} + ${t(Q).length===1?"pattern":"patterns"}`),o(U,t(nt)),o(R,` pattern${t(nt)===1?"":"s"} across `),o(k,t(X)),o(O,` project${t(X)===1?"":"s"}, `),o(lt,t(J)),o(pt,` total transfer${t(J)===1?"":"s"}`),o(It,t(l)==="All"?"All categories":t(l))}),_(i,c)};Y(kt,i=>{t(I)?i(Tt):t(u)?i(mt,1):i(dt,!1)})}e(q),N(()=>bt(ct,1,`rounded-lg px-3 py-1.5 text-xs font-medium transition ${t(l)==="All"?"bg-synapse/25 text-synapse-glow":"text-dim hover:bg-white/[0.04] hover:text-text"}`)),at("click",ct,()=>ot("All")),_(h,q),Rt()}Lt(["click"]);export{Me as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js.br b/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js.br new file mode 100644 index 0000000..dc9fa11 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js.gz b/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js.gz new file mode 100644 index 0000000..e903ed0 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/14.DUh3SXOF.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js b/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js new file mode 100644 index 0000000..e6691c6 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js @@ -0,0 +1,5 @@ +import"../chunks/Bzak7iHL.js";import{o as Nt}from"../chunks/GG5zm9kr.js";import{p as We,d as s,g as e,e as a,r as t,n as Fe,t as w,a as Ke,u as Z,s as ce,c as Tt,W as Ft,h as D,aG as jt,f as rt}from"../chunks/CpWkWWOo.js";import{s as v,d as Lt,a as Ge}from"../chunks/BlVfL1ME.js";import{i as O}from"../chunks/B4yTwGkE.js";import{e as re,i as ye}from"../chunks/CGEBXrjl.js";import{a as f,f as _,b as ct}from"../chunks/CHOnp4oo.js";import{h as Dt}from"../chunks/C4h_mRt2.js";import{s as K,r as Ot}from"../chunks/A7po6GxK.js";import{s as b}from"../chunks/Cx-f-Pzo.js";import{b as It}from"../chunks/sZcqyNBA.js";import{b as it}from"../chunks/CJsMJEun.js";import{a as zt}from"../chunks/554JRhq6.js";import{s as ut}from"../chunks/aVbAZ-t7.js";import{p as ie}from"../chunks/V6gjw5Ec.js";import{N as Mt}from"../chunks/CcUbQ_Wl.js";var $t=_('
          '),Bt=_('

          '),Gt=_("
          ");function lt(l,o){We(o,!0);let V=ie(o,"intent",3,"Synthesis"),$=ie(o,"memoriesAnalyzed",3,0),R=ie(o,"evidenceCount",3,0),i=ie(o,"contradictionCount",3,0),B=ie(o,"supersededCount",3,0),E=ie(o,"running",3,!1),G=ie(o,"stageHints",19,()=>({}));const I=[{key:"broad",icon:"◎",label:"Broad Retrieval",base:"Hybrid BM25 + semantic (3x overfetch) then cross-encoder rerank"},{key:"spreading",icon:"⟿",label:"Spreading Activation",base:"Collins & Loftus — expand via graph edges to surface what search missed"},{key:"fsrs",icon:"▲",label:"FSRS Trust Scoring",base:"retention × stability × reps ÷ lapses — which memories have earned trust"},{key:"intent",icon:"◆",label:"Intent Classification",base:"FactCheck / Timeline / RootCause / Comparison / Synthesis"},{key:"supersession",icon:"↗",label:"Temporal Supersession",base:"Newer high-trust memories replace older ones on the same fact"},{key:"contradiction",icon:"⚡",label:"Contradiction Analysis",base:"Only flag conflicts between memories where BOTH have trust > 0.3"},{key:"relation",icon:"⬡",label:"Relation Assessment",base:"Per pair: Supports / Contradicts / Supersedes / Irrelevant"},{key:"template",icon:"❖",label:"Template Reasoning",base:"Build the natural-language reasoning chain you validate"}],Y=Z(()=>({broad:$()?`Analyzed ${$()} memories · ${R()} survived ranking`:void 0,intent:V()?`Classified as ${V()}`:void 0,supersession:B()?`${B()} outdated memor${B()===1?"y":"ies"} superseded`:void 0,contradiction:i()?`${i()} real conflict${i()===1?"":"s"} between trusted memories`:"No conflicts between trusted memories"}));function Q(z,M){return G()[z]??e(Y)[z]??M}var P=Gt();let ue;re(P,23,()=>I,z=>z.key,(z,M,H)=>{var A=Bt(),U=s(A);{var le=n=>{var p=$t();w(()=>b(p,`animation-delay: ${e(H)*140+120}ms;`)),f(n,p)};O(U,n=>{e(H){b(A,`animation-delay: ${e(H)*140}ms;`),b(J,`animation-delay: ${e(H)*140}ms;`),v(te,e(M).icon),v(pe,`0${e(H)+1}`),v(me,e(M).label),v(r,n)},[()=>Q(e(M).key,e(M).base)]),f(z,A)}),t(P),w(()=>ue=ut(P,1,"reasoning-chain space-y-2 svelte-9hm057",null,ue,{running:E()})),f(l,P),Ke()}const Pt="#10b981",Ht="#f59e0b",He="#ef4444",vt="#8B95A5",ot={primary:{label:"Primary",accent:"synapse",icon:"◈"},supporting:{label:"Supporting",accent:"recall",icon:"◇"},contradicting:{label:"Contradicting",accent:"decay",icon:"⚠"},superseded:{label:"Superseded",accent:"muted",icon:"⊘"}};function Wt(l){return ot[l]??ot.supporting}function Te(l){return Number.isFinite(l)?l>75?Pt:l>=40?Ht:He:He}function dt(l){return Number.isFinite(l)?l>75?"HIGH CONFIDENCE":l>=40?"MIXED SIGNAL":"LOW CONFIDENCE":"LOW CONFIDENCE"}function Pe(l){return Number.isFinite(l)?Te(l*100):He}function Kt(l){return!Number.isFinite(l)||l<0?0:l>1?1:l}function Vt(l){return Kt(l)*100}function Yt(l){return l?Mt[l]??vt:vt}function Qt(l,o){if(l==null||typeof l!="string"||l.trim()==="")return"—";const V=new Date(l);if(Number.isNaN(V.getTime()))return l;try{return V.toLocaleDateString(o,{month:"short",day:"numeric",year:"numeric"})}catch{return l}}function Ut(l,o=8){return l?l.length>o?l.slice(0,o):l:""}var Xt=_(' '),Zt=_('

          Trust
          FSRS · reps × retention
          ');function Jt(l,o){We(o,!0);let V=ie(o,"index",3,0);const $=Z(()=>Vt(o.trust)),R=Z(()=>Wt(o.role)),i=Z(()=>Ut(o.id)),B=Z(()=>Yt(o.nodeType));var E=Zt();let G;var I=s(E),Y=s(I),Q=s(Y),P=s(Q),ue=s(P,!0);t(P);var z=a(P,1,!0);t(Q);var M=a(Q,2);{var H=r=>{var n=Xt(),p=s(n,!0);t(n),w(()=>{b(n,`color: ${e(B)??""}`),v(p,o.nodeType)}),f(r,n)};O(M,r=>{o.nodeType&&r(H)})}t(Y);var A=a(Y,2),U=s(A);t(A),t(I);var le=a(I,2),J=s(le,!0);t(le);var ee=a(le,2),te=s(ee),se=a(s(te),2),ae=s(se);t(se),t(te);var ne=a(te,2),pe=s(ne);t(ne),t(ee);var ve=a(ee,2),me=s(ve),xe=s(me,!0);t(me),Fe(2),t(ve),t(E),w((r,n,p,k,x)=>{G=ut(E,1,"evidence-card glass rounded-xl p-4 space-y-3 transition relative svelte-ksja6x",null,G,{contradicting:o.role==="contradicting",primary:o.role==="primary",superseded:o.role==="superseded"}),b(E,`animation-delay: ${V()*80}ms;`),K(E,"data-evidence-id",o.id),v(ue,e(R).icon),v(z,e(R).label),K(A,"title",o.id),v(U,`#${e(i)??""}`),v(J,o.preview),b(se,`color: ${r??""}`),v(ae,`${n??""}%`),b(pe,`width: ${e($)??""}%; background: ${p??""}; box-shadow: 0 0 8px ${k??""}80;`),v(xe,x)},[()=>Pe(o.trust),()=>e($).toFixed(0),()=>Pe(o.trust),()=>Pe(o.trust),()=>Qt(o.date)]),f(l,E),Ke()}var es=_(''),ts=_('
          Try
          '),ss=_('
          Error:
          '),as=_('
          Running cognitive pipeline
          '),ns=_('

          Reasoning

          intent: · ·
          '),rs=ct('',1),is=ct(''),ls=_('

          '),vs=_('

          Contradictions Detected

          '),os=_('
          '),ds=_('

          Superseded

          '),cs=_('
          '),us=_('

          Evolution

          '),ps=_('

          '),ms=_('

          Related Insights

          '),xs=_('
          Confidence
          %
          intent: ·
          Primary Source

          ·

          Cognitive Pipeline

          Evidence

          primary supporting contradicting superseded
          ',1),gs=_(`

          Ask anything. Vestige will run the full reasoning pipeline and show you its work.

          8-stage pipeline: retrieval → rerank → activation → trust-score → supersession → + contradiction → relations → chain. Zero LLM calls, 100% local.

          `),fs=_(`

          Reasoning Theater

          deep_reference

          Watch Vestige reason. Your query runs the 8-stage cognitive pipeline — broad retrieval, + spreading activation, FSRS trust scoring, intent classification, supersession, contradiction + analysis, relation assessment, template reasoning — and returns a pre-built answer with + trust-scored evidence.

          `);function Ls(l,o){We(o,!0);async function V(r){var qe,Ee,we;const n=await zt.deepReference(r,20),k=(Array.isArray(n.evidence)?n.evidence:[]).map(c=>{const T=typeof c.trust=="number"?c.trust:0,ke=T>1?T/100:T,Se=c.role||"supporting";return{id:String(c.id??""),trust:Math.max(0,Math.min(1,ke)),date:String(c.date??""),role:Se,preview:String(c.preview??""),nodeType:c.node_type?String(c.node_type):c.nodeType?String(c.nodeType):void 0}}),x=n.recommended,N={answer_preview:String((x==null?void 0:x.answer_preview)??((qe=k[0])==null?void 0:qe.preview)??""),memory_id:String((x==null?void 0:x.memory_id)??((Ee=k[0])==null?void 0:Ee.id)??""),trust_score:(()=>{var T;const c=x==null?void 0:x.trust_score;return typeof c=="number"?c>1?c/100:c:((T=k[0])==null?void 0:T.trust)??0})(),date:String((x==null?void 0:x.date)??((we=k[0])==null?void 0:we.date)??"")},ge=(Array.isArray(n.contradictions)?n.contradictions:[]).map(c=>({a_id:String(c.a_id??""),b_id:String(c.b_id??""),summary:String(c.summary??c.reason??"Trust-weighted conflict between high-FSRS memories.")})),he=(Array.isArray(n.superseded)?n.superseded:[]).map(c=>({old_id:String(c.old_id??""),new_id:String(c.new_id??N.memory_id??""),reason:String(c.reason??"Superseded by newer memory with higher FSRS trust.")})),fe=(Array.isArray(n.evolution)?n.evolution:[]).map(c=>({date:String(c.date??""),summary:String(c.summary??c.preview??""),trust:(()=>{const T=c.trust;return typeof T=="number"?T>1?T/100:T:0})()})),je=Array.isArray(n.related_insights)?n.related_insights:[],oe=typeof n.confidence=="number"?n.confidence:0,de=oe>1?Math.round(oe):Math.round(oe*100),Le=String(n.intent??"Synthesis"),Re=String(n.reasoning??n.guidance??""),be=typeof n.memoriesAnalyzed=="number"?n.memoriesAnalyzed:k.length;return{intent:Le,reasoning:Re,recommended:N,evidence:k,contradictions:ge,superseded:he,evolution:fe,related_insights:je,confidence:de,memoriesAnalyzed:be}}let $=ce(""),R=ce(!1),i=ce(null),B=ce(null),E=ce(null),G=ce(null),I=ce(Tt([]));async function Y(){const r=e($).trim();if(!(!r||e(R))){D(R,!0),D(B,null),D(i,null),D(I,[],!0);try{D(i,await V(r),!0),requestAnimationFrame(()=>requestAnimationFrame(Q))}catch(n){D(B,n instanceof Error?n.message:"Unknown error",!0)}finally{D(R,!1)}}}function Q(){if(!e(i)||!e(G)||e(i).contradictions.length===0){D(I,[],!0);return}const r=e(G).getBoundingClientRect(),n=[];for(const p of e(i).contradictions){const k=e(G).querySelector(`[data-evidence-id="${p.a_id}"]`),x=e(G).querySelector(`[data-evidence-id="${p.b_id}"]`);if(!k||!x)continue;const N=k.getBoundingClientRect(),W=x.getBoundingClientRect();n.push({x1:N.left-r.left+N.width/2,y1:N.top-r.top+N.height/2,x2:W.left-r.left+W.width/2,y2:W.top-r.top+W.height/2})}D(I,n,!0)}function P(r){var n,p;(r.metaKey||r.ctrlKey)&&r.key.toLowerCase()==="k"&&(r.preventDefault(),(n=e(E))==null||n.focus(),(p=e(E))==null||p.select())}Nt(()=>{var r;return(r=e(E))==null||r.focus(),window.addEventListener("keydown",P),window.addEventListener("resize",Q),()=>{window.removeEventListener("keydown",P),window.removeEventListener("resize",Q)}});const ue=["What port does the dev server use?","Should I enable prefix caching with vLLM?","Why did the benchmark score drop after the parser change?","How does FSRS-6 trust scoring work?"];var z=fs();Dt("q2v96u",r=>{Ft(()=>{jt.title="Reasoning Theater · Vestige"})});var M=a(s(z),2),H=s(M),A=a(s(H),2);Ot(A),it(A,r=>D(E,r),()=>e(E));var U=a(A,4),le=s(U,!0);t(U),t(H);var J=a(H,2);{var ee=r=>{var n=ts(),p=a(s(n),2);re(p,17,()=>ue,ye,(k,x)=>{var N=es(),W=s(N,!0);t(N),w(()=>v(W,e(x))),Ge("click",N,()=>{D($,e(x),!0),Y()}),f(k,N)}),t(n),f(r,n)};O(J,r=>{!e(i)&&!e(R)&&r(ee)})}t(M);var te=a(M,2);{var se=r=>{var n=ss(),p=a(s(n));t(n),w(()=>v(p,` ${e(B)??""}`)),f(r,n)};O(te,r=>{e(B)&&r(se)})}var ae=a(te,2);{var ne=r=>{var n=as(),p=a(s(n),2);lt(p,{running:!0}),t(n),f(r,n)};O(ae,r=>{e(R)&&r(ne)})}var pe=a(ae,2);{var ve=r=>{const n=Z(()=>e(i).confidence),p=Z(()=>Te(e(n)));var k=xs(),x=rt(k);{var N=u=>{var d=ns(),g=s(d),S=a(s(g),2),m=s(S),y=a(s(m)),j=s(y,!0);t(y),t(m);var C=a(m,4),L=s(C);t(C);var h=a(C,4),F=s(h);t(h),t(S),t(g);var q=a(g,2),X=s(q,!0);t(q),t(d),w(_e=>{v(j,e(i).intent),v(L,`${e(i).memoriesAnalyzed??""} analyzed`),b(h,`color: ${e(p)??""}`),v(F,`${e(n)??""}% ${_e??""}`),b(q,`box-shadow: inset 0 1px 0 0 rgba(255,255,255,0.03), 0 0 32px ${e(p)??""}20, 0 8px 32px rgba(0,0,0,0.4); border-color: ${e(p)??""}35;`),v(X,e(i).reasoning)},[()=>dt(e(n))]),f(u,d)};O(x,u=>{e(i).reasoning&&u(N)})}var W=a(x,2),ge=s(W),Ce=a(s(ge),2),he=s(Ce),Ve=s(he,!0);Fe(),t(he),t(Ce);var fe=a(Ce,2),je=s(fe,!0);t(fe);var oe=a(fe,2),de=a(s(oe)),Le=s(de);t(de),t(oe);var Re=a(oe,2),be=s(Re),qe=a(s(be)),Ee=s(qe,!0);t(qe),t(be);var we=a(be,4),c=s(we);t(we),t(Re),t(ge);var T=a(ge,2),ke=s(T),Se=a(s(ke),2),pt=s(Se);t(Se),t(ke);var De=a(ke,2),mt=s(De,!0);t(De);var Ye=a(De,2),Oe=s(Ye),Qe=s(Oe),xt=a(Qe);t(Oe);var Ue=a(Oe,4),gt=s(Ue,!0);t(Ue),t(Ye),t(T),t(W);var Ie=a(W,2),Xe=a(s(Ie),2),ft=s(Xe);lt(ft,{get intent(){return e(i).intent},get memoriesAnalyzed(){return e(i).memoriesAnalyzed},get evidenceCount(){return e(i).evidence.length},get contradictionCount(){return e(i).contradictions.length},get supersededCount(){return e(i).superseded.length}}),t(Xe),t(Ie);var ze=a(Ie,2),Me=s(ze),Ze=s(Me),Je=a(s(Ze),2),_t=s(Je);t(Je),t(Ze),Fe(2),t(Me);var $e=a(Me,2),et=s($e);re(et,19,()=>e(i).evidence,u=>u.id,(u,d,g)=>{Jt(u,{get id(){return e(d).id},get trust(){return e(d).trust},get date(){return e(d).date},get role(){return e(d).role},get preview(){return e(d).preview},get nodeType(){return e(d).nodeType},get index(){return e(g)}})});var yt=a(et,2);{var ht=u=>{var d=is(),g=a(s(d));re(g,17,()=>e(I),ye,(S,m,y)=>{const j=Z(()=>(e(m).x1+e(m).x2)/2),C=Z(()=>Math.min(e(m).y1,e(m).y2)-28);var L=rs(),h=rt(L);b(h,`animation-delay: ${y*120+600}ms;`);var F=a(h);b(F,`animation-delay: ${y*120+600}ms;`);var q=a(F);b(q,`animation-delay: ${y*120+700}ms;`),w(()=>{K(h,"d",`M ${e(m).x1??""} ${e(m).y1??""} Q ${e(j)??""} ${e(C)??""} ${e(m).x2??""} ${e(m).y2??""}`),K(F,"cx",e(m).x1),K(F,"cy",e(m).y1),K(q,"cx",e(m).x2),K(q,"cy",e(m).y2)}),f(S,L)}),t(d),f(u,d)};O(yt,u=>{e(I).length>0&&u(ht)})}t($e),it($e,u=>D(G,u),()=>e(G)),t(ze);var tt=a(ze,2);{var bt=u=>{var d=vs(),g=s(d),S=a(s(g),2),m=s(S);t(S),t(g);var y=a(g,2);re(y,21,()=>e(i).contradictions,ye,(j,C,L)=>{var h=ls(),F=a(s(h),2),q=s(F),X=s(q),_e=s(X);t(X);var Ae=a(X,4),Be=s(Ae);t(Ae),t(q);var Ne=a(q,2),Ct=s(Ne,!0);t(Ne),t(F);var Rt=a(F,2);Rt.textContent=`pair ${L+1}`,t(h),w((Et,At)=>{v(_e,`#${Et??""}`),v(Be,`#${At??""}`),v(Ct,e(C).summary)},[()=>e(C).a_id.slice(0,8),()=>e(C).b_id.slice(0,8)]),f(j,h)}),t(y),t(d),w(()=>v(m,`(${e(i).contradictions.length??""})`)),f(u,d)};O(tt,u=>{e(i).contradictions.length>0&&u(bt)})}var st=a(tt,2);{var qt=u=>{var d=ds(),g=s(d),S=a(s(g),2),m=s(S);t(S),t(g);var y=a(g,2);re(y,21,()=>e(i).superseded,ye,(j,C)=>{var L=os(),h=s(L),F=s(h);t(h);var q=a(h,4),X=s(q);t(q);var _e=a(q,2),Ae=s(_e,!0);t(_e),t(L),w((Be,Ne)=>{v(F,`#${Be??""}`),v(X,`#${Ne??""}`),v(Ae,e(C).reason)},[()=>e(C).old_id.slice(0,8),()=>e(C).new_id.slice(0,8)]),f(j,L)}),t(y),t(d),w(()=>v(m,`(${e(i).superseded.length??""})`)),f(u,d)};O(st,u=>{e(i).superseded.length>0&&u(qt)})}var at=a(st,2),nt=s(at);{var wt=u=>{var d=us(),g=a(s(d),2);re(g,21,()=>e(i).evolution,ye,(S,m)=>{var y=cs(),j=s(y),C=s(j,!0);t(j);var L=a(j,2),h=a(L,2),F=s(h,!0);t(h),t(y),w((q,X)=>{v(C,q),b(L,`background: ${X??""}`),v(F,e(m).summary)},[()=>new Date(e(m).date).toLocaleDateString(void 0,{month:"short",day:"numeric"}),()=>Te(e(m).trust*100)]),f(S,y)}),t(g),t(d),f(u,d)};O(nt,u=>{e(i).evolution.length>0&&u(wt)})}var kt=a(nt,2);{var St=u=>{var d=ms(),g=a(s(d),2);re(g,21,()=>e(i).related_insights,ye,(S,m)=>{var y=ps(),j=a(s(y),1,!0);t(y),w(()=>v(j,e(m))),f(S,y)}),t(g),t(d),f(u,d)};O(kt,u=>{e(i).related_insights.length>0&&u(St)})}t(at),w((u,d,g,S,m)=>{b(ge,`box-shadow: inset 0 1px 0 0 rgba(255,255,255,0.03), 0 0 32px ${e(p)??""}30, 0 8px 32px rgba(0,0,0,0.4); border-color: ${e(p)??""}40;`),b(he,`color: ${e(p)??""}; text-shadow: 0 0 24px ${e(p)??""}80;`),v(Ve,e(n)),b(fe,`color: ${e(p)??""}`),v(je,u),K(de,"width",e(n)/100*220),K(de,"fill",e(p)),b(de,`filter: drop-shadow(0 0 6px ${e(p)??""});`),K(Le,"to",e(n)/100*220),v(Ee,e(i).intent),v(c,`${e(i).memoriesAnalyzed??""} analyzed`),K(Se,"title",e(i).recommended.memory_id),v(pt,`#${d??""}`),v(mt,e(i).recommended.answer_preview),b(Qe,`background: ${g??""}`),v(xt,` Trust ${S??""}%`),v(gt,m),v(_t,`(${e(i).evidence.length??""})`)},[()=>dt(e(n)),()=>e(i).recommended.memory_id.slice(0,8),()=>Te(e(i).recommended.trust_score*100),()=>(e(i).recommended.trust_score*100).toFixed(0),()=>new Date(e(i).recommended.date).toLocaleDateString()]),f(r,k)};O(pe,r=>{e(i)&&!e(R)&&r(ve)})}var me=a(pe,2);{var xe=r=>{var n=gs();f(r,n)};O(me,r=>{!e(i)&&!e(R)&&!e(B)&&r(xe)})}t(z),w(r=>{U.disabled=r,v(le,e(R)?"Reasoning…":"Reason")},[()=>!e($).trim()||e(R)]),Ge("keydown",A,r=>r.key==="Enter"&&Y()),It(A,()=>e($),r=>D($,r)),Ge("click",U,Y),f(l,z),Ke()}Lt(["keydown","click"]);export{Ls as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js.br b/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js.br new file mode 100644 index 0000000..0ec0f08 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js.gz b/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js.gz new file mode 100644 index 0000000..4e37900 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/15.QNRJhGzj.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js b/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js new file mode 100644 index 0000000..84f16e9 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js @@ -0,0 +1,9 @@ +import"../chunks/Bzak7iHL.js";import{o as Ve}from"../chunks/GG5zm9kr.js";import{p as Ye,d as a,e as n,g as e,f as Ze,t as y,r as t,u as S,a as qe,s as ue,h as I,c as et,n as tt}from"../chunks/CpWkWWOo.js";import{d as Ue,s as u,a as Ae}from"../chunks/BlVfL1ME.js";import{i as L}from"../chunks/B4yTwGkE.js";import{e as Q,i as pe}from"../chunks/CGEBXrjl.js";import{c as at,a as v,f as m,b as Oe}from"../chunks/CHOnp4oo.js";import{s as Fe}from"../chunks/aVbAZ-t7.js";import{a as Ee}from"../chunks/554JRhq6.js";import{s as M}from"../chunks/A7po6GxK.js";import{s as Re}from"../chunks/Cx-f-Pzo.js";import{p as st}from"../chunks/V6gjw5Ec.js";import{N as rt}from"../chunks/CcUbQ_Wl.js";const ze=1440*60*1e3;function xe(p){const o=typeof p=="string"?new Date(p):new Date(p);return o.setHours(0,0,0,0),o}function we(p,o){return Math.floor((xe(p).getTime()-xe(o).getTime())/ze)}function Be(p){const o=p.getFullYear(),f=String(p.getMonth()+1).padStart(2,"0"),N=String(p.getDate()).padStart(2,"0");return`${o}-${f}-${N}`}function He(p,o){if(!o)return"none";const f=new Date(o);if(Number.isNaN(f.getTime()))return"none";const N=we(f,p);return N<0?"overdue":N===0?"today":N<=7?"week":"future"}function Ke(p,o){if(!o)return null;const f=new Date(o);return Number.isNaN(f.getTime())?null:we(f,p)}function nt(p){if(p.length===0)return 0;let o=0;for(const f of p)o+=f.retentionStrength??0;return o/p.length}function it(p){const o=xe(p);return o.setDate(o.getDate()-14),o.setDate(o.getDate()-o.getDay()),o}function ot(p,o){let f=0,N=0,P=0,E=0,j=0,Z=0;const B=xe(p);for(const H of o){if(!H.nextReviewAt)continue;const K=new Date(H.nextReviewAt);if(Number.isNaN(K.getTime()))continue;const _=we(K,p);_<0&&f++,_<=0&&N++,_<=7&&P++,_<=30&&E++,_>=0&&(j+=(K.getTime()-B.getTime())/ze,Z++)}const w=Z>0?j/Z:0;return{overdue:f,dueToday:N,dueThisWeek:P,dueThisMonth:E,avgDays:w}}var dt=Oe(''),lt=Oe(''),vt=Oe(''),ct=m('
          '),ut=m(' '),pt=m(' '),xt=m('
          '),gt=m(''),mt=m(" "),ft=m(' '),_t=m('

          '),bt=m('

          '),ht=m('

          '),yt=m('
          Avg retention of memories due — last 2 weeks → next 4
          retention today
          Overdue Due today Within 7 days Future (8+ days)
          ');function wt(p,o){Ye(o,!0);let f=st(o,"anchor",19,()=>new Date),N=S(()=>xe(f())),P=S(()=>it(f())),E=S(()=>(()=>{const r=new Map;for(const s of o.memories){if(!s.nextReviewAt)continue;const i=new Date(s.nextReviewAt);if(Number.isNaN(i.getTime()))continue;const c=Be(xe(i)),k=r.get(c);k?k.push(s):r.set(c,[s])}return r})()),j=S(()=>(()=>{const r=[];for(let s=0;s<42;s++){const i=new Date(e(P));i.setDate(i.getDate()+s);const c=Be(i),k=e(E).get(c)??[],h=we(i,e(N));r.push({date:i,key:c,isToday:h===0,inWindow:h>=-14&&h<=28,memories:k,avgRetention:nt(k)})}return r})());function Z(r){if(r.memories.length===0)return{bg:"rgba(255,255,255,0.02)",border:"rgba(99,102,241,0.06)",text:"#4a4a7a"};const s=we(r.date,e(N));return s<-1?{bg:"rgba(239,68,68,0.16)",border:"rgba(239,68,68,0.45)",text:"#fca5a5"}:s>=-1&&s<=0?{bg:"rgba(245,158,11,0.18)",border:"rgba(245,158,11,0.5)",text:"#fcd34d"}:s>0&&s<=7?{bg:"rgba(99,102,241,0.16)",border:"rgba(99,102,241,0.45)",text:"#a5b4fc"}:{bg:"rgba(168,85,247,0.08)",border:"rgba(168,85,247,0.2)",text:"#c084fc"}}let B=ue(null),w=S(()=>e(j).find(r=>r.key===e(B))??null);function H(r){I(B,e(B)===r?null:r,!0)}const K=600,_=56;let Y=S(()=>(()=>{const r=[],s=e(j).length;for(let i=0;i(()=>{const r=e(Y).filter(s=>s.count>0);return r.length===0?"":r.map((s,i)=>`${i===0?"M":"L"} ${s.x.toFixed(1)} ${s.y.toFixed(1)}`).join(" ")})()),ge=S(()=>e(j).findIndex(r=>r.isToday)),ee=S(()=>e(ge)>=0?e(ge)/(e(j).length-1)*K:-1);const me=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"];function fe(r){return r.toLocaleDateString(void 0,{weekday:"long",month:"long",day:"numeric",year:"numeric"})}var ne=yt(),_e=a(ne),be=n(a(_e),2);M(be,"viewBox","0 0 600 56");var ie=a(be);M(ie,"x2",K),M(ie,"y1",_-6-.3*(_-12)),M(ie,"y2",_-6-.3*(_-12));var oe=n(ie);M(oe,"x2",K),M(oe,"y1",_-6-.7*(_-12)),M(oe,"y2",_-6-.7*(_-12));var $e=n(oe);{var Ne=r=>{var s=dt();M(s,"y2",_),y(()=>{M(s,"x1",e(ee)),M(s,"x2",e(ee))}),v(r,s)};L($e,r=>{e(ee)>=0&&r(Ne)})}var d=n($e);{var x=r=>{var s=lt();y(()=>M(s,"d",e(ke))),v(r,s)};L(d,r=>{e(ke)&&r(x)})}var g=n(d);Q(g,17,()=>e(Y),pe,(r,s)=>{var i=at(),c=Ze(i);{var k=h=>{var A=vt();y(()=>{M(A,"cx",e(s).x),M(A,"cy",e(s).y)}),v(h,A)};L(c,h=>{e(s).count>0&&h(k)})}v(r,i)}),t(be),t(_e);var D=n(_e,2);Q(D,21,()=>me,pe,(r,s)=>{var i=ct(),c=a(i,!0);t(i),y(()=>u(c,e(s))),v(r,i)}),t(D);var W=n(D,2);Q(W,21,()=>e(j),r=>r.key,(r,s)=>{const i=S(()=>Z(e(s)));var c=gt(),k=a(c),h=a(k),A=a(h),he=a(A,!0);t(A);var de=n(A,2);{var ae=b=>{var l=ut(),C=a(l,!0);t(l),y(X=>u(C,X),[()=>e(s).date.toLocaleDateString(void 0,{month:"short"})]),v(b,l)},le=S(()=>e(s).date.getDate()===1);L(de,b=>{e(le)&&b(ae)})}t(h);var ve=n(h,2);{var se=b=>{var l=xt(),C=a(l),X=a(C,!0);t(C);var G=n(C,2);{var U=z=>{var J=pt(),V=a(J);t(J),y(ye=>u(V,`${ye??""}%`),[()=>(e(s).avgRetention*100).toFixed(0)]),v(z,J)};L(G,z=>{e(s).avgRetention>0&&z(U)})}t(l),y(()=>{Re(C,`color: ${e(i).text??""}`),u(X,e(s).memories.length)}),v(b,l)};L(ve,b=>{e(s).memories.length>0&&b(se)})}t(k),t(c),y((b,l)=>{c.disabled=e(s).memories.length===0,Fe(c,1,`relative aspect-square rounded-lg p-2 text-left transition-all duration-200 + ${e(s).inWindow?"opacity-100":"opacity-35"} + ${e(s).memories.length>0?"hover:scale-[1.03] cursor-pointer":"cursor-default"} + ${e(s).isToday?"ring-2 ring-synapse/60 shadow-[0_0_16px_rgba(99,102,241,0.3)]":""} + ${e(B)===e(s).key?"ring-2 ring-dream/60 shadow-[0_0_16px_rgba(168,85,247,0.3)]":""}`),Re(c,`background: ${e(i).bg??""}; border: 1px solid ${e(i).border??""};`),M(c,"title",b),Fe(A,1,`text-[10px] font-mono ${e(s).isToday?"text-synapse-glow font-bold":"text-dim"}`),u(he,l)},[()=>`${fe(e(s).date)} — ${e(s).memories.length} due`,()=>e(s).date.getDate()]),Ae("click",c,()=>H(e(s).key)),v(r,c)}),t(W);var q=n(W,4);{var te=r=>{var s=ht(),i=a(s),c=a(i),k=a(c),h=a(k,!0);t(k);var A=n(k,2),he=a(A);t(A),t(c);var de=n(c,2);t(i);var ae=n(i,2),le=a(ae);Q(le,17,()=>e(w).memories.slice(0,100),b=>b.id,(b,l)=>{var C=_t(),X=a(C),G=n(X,2),U=a(G),z=a(U,!0);t(U);var J=n(U,2),V=a(J),ye=a(V,!0);t(V);var Se=n(V,2);{var Ce=R=>{var F=mt(),O=a(F);t(F),y(()=>u(O,`· ${e(l).reviewCount??""} review${e(l).reviewCount===1?"":"s"}`)),v(R,F)};L(Se,R=>{e(l).reviewCount!==void 0&&R(Ce)})}var je=n(Se,2);Q(je,17,()=>e(l).tags.slice(0,2),pe,(R,F)=>{var O=ft(),De=a(O,!0);t(O),y(()=>u(De,e(F))),v(R,O)}),t(J),t(G);var $=n(G,2),T=a($),re=a(T);t(T);var ce=n(T,2),Me=a(ce);t(ce),t($),t(C),y(R=>{Re(X,`background: ${(rt[e(l).nodeType]||"#8B95A5")??""}`),u(z,e(l).content),u(ye,e(l).nodeType),Re(re,`width: ${e(l).retentionStrength*100}%; background: ${e(l).retentionStrength>.7?"var(--color-recall)":e(l).retentionStrength>.4?"var(--color-warning)":"var(--color-decay)"}`),u(Me,`${R??""}%`)},[()=>(e(l).retentionStrength*100).toFixed(0)]),v(b,C)});var ve=n(le,2);{var se=b=>{var l=bt(),C=a(l);t(l),y(()=>u(C,`+${e(w).memories.length-100} more`)),v(b,l)};L(ve,b=>{e(w).memories.length>100&&b(se)})}t(ae),t(s),y((b,l)=>{u(h,b),u(he,`${e(w).memories.length??""} memor${e(w).memories.length===1?"y":"ies"} due + · avg retention ${l??""}%`)},[()=>fe(e(w).date),()=>(e(w).avgRetention*100).toFixed(0)]),Ae("click",de,()=>I(B,null)),v(r,s)};L(q,r=>{e(w)&&e(w).memories.length>0&&r(te)})}t(ne),v(p,ne),qe()}Ue(["click"]);var kt=m(''),$t=m('
          '),St=m('
          '),Dt=m('
          '),Tt=m('
          '),Rt=m('

          API unavailable.

          Could not fetch memories from /api/memories.

          '),At=m(`

          FSRS review schedule not yet populated.

          nextReviewAt timestamp yet. Run consolidation to compute + next-review dates via FSRS-6.

          `),Ft=m('
          Overdue
          '),Nt=m('

          Nothing in this window.

          '),Ct=m('

          '),jt=m('

          '),Mt=m('
          '),Lt=m('
          '),Wt=m('

          Review Schedule

          FSRS-6 next-review dates across your memory corpus

          ');function Gt(p,o){Ye(o,!0);let f=ue(et([])),N=ue(0),P=ue(!0),E=ue(!1),j=ue("week");const Z=2e3;async function B(){const d=await Ee.memories.list({limit:String(Z)});I(f,d.memories,!0),I(N,d.total,!0)}Ve(async()=>{try{await B()}catch{I(E,!0),I(f,[],!0)}finally{I(P,!1)}});let w=S(()=>e(f).filter(d=>!!d.nextReviewAt)),H=S(()=>new Date),K=S(()=>e(N)>e(f).length),_=S(()=>(()=>{const d=e(j);return d==="all"?e(w):e(w).filter(x=>{const g=He(e(H),x.nextReviewAt);if(g==="none")return!1;if(d==="today")return g==="overdue"||g==="today";if(d==="week")return g!=="future";const D=Ke(e(H),x.nextReviewAt);return D!==null&&D<=30})})()),Y=S(()=>ot(e(H),e(w)));async function ke(){I(P,!0);try{await Ee.consolidate(),await B(),I(E,!1)}catch{I(E,!0)}finally{I(P,!1)}}const ge=[{key:"today",label:"Due today"},{key:"week",label:"This week"},{key:"month",label:"This month"},{key:"all",label:"All upcoming"}];var ee=Wt(),me=a(ee),fe=n(a(me),2);Q(fe,21,()=>ge,pe,(d,x)=>{var g=kt(),D=a(g,!0);t(g),y(()=>{Fe(g,1,`px-3 py-1.5 text-xs rounded-lg transition-all + ${e(j)===e(x).key?"bg-synapse/20 text-synapse-glow border border-synapse/30":"text-dim hover:text-text hover:bg-white/[0.03] border border-transparent"}`),u(D,e(x).label)}),Ae("click",g,()=>I(j,e(x).key,!0)),v(d,g)}),t(fe),t(me);var ne=n(me,2);{var _e=d=>{var x=$t(),g=a(x);t(x),y((D,W)=>u(g,`Showing the first ${D??""} of ${W??""} memories. + Schedule reflects this slice only.`),[()=>e(f).length.toLocaleString(),()=>e(N).toLocaleString()]),v(d,x)};L(ne,d=>{!e(P)&&!e(E)&&e(K)&&d(_e)})}var be=n(ne,2);{var ie=d=>{var x=Tt(),g=a(x),D=n(a(g),2);Q(D,20,()=>Array(42),pe,(q,te)=>{var r=St();v(q,r)}),t(D),t(g);var W=n(g,2);Q(W,20,()=>Array(5),pe,(q,te)=>{var r=Dt();v(q,r)}),t(W),t(x),v(d,x)},oe=d=>{var x=Rt();v(d,x)},$e=d=>{var x=At(),g=n(a(x),4),D=a(g);tt(2),t(g);var W=n(g,2);t(x),y(()=>u(D,`None of your ${e(f).length??""} memor${e(f).length===1?"y has":"ies have"} a `)),Ae("click",W,ke),v(d,x)},Ne=d=>{var x=Lt(),g=a(x),D=a(g);wt(D,{get memories(){return e(w)}}),t(g);var W=n(g,2),q=a(W),te=n(a(q),2),r=a(te);{var s=$=>{var T=Ft(),re=n(a(T),2),ce=a(re,!0);t(re),t(T),y(()=>u(ce,e(Y).overdue)),v($,T)};L(r,$=>{e(Y).overdue>0&&$(s)})}var i=n(r,2),c=n(a(i),2),k=a(c,!0);t(c),t(i);var h=n(i,2),A=n(a(h),2),he=a(A,!0);t(A),t(h);var de=n(h,2),ae=n(a(de),2),le=a(ae,!0);t(ae),t(de),t(te);var ve=n(te,2),se=a(ve),b=n(a(se),2),l=a(b,!0);t(b),t(se);var C=n(se,2),X=a(C);t(C),t(ve),t(q);var G=n(q,2),U=a(G),z=a(U),J=a(z,!0);t(z);var V=n(z,2),ye=a(V,!0);t(V),t(U);var Se=n(U,2);{var Ce=$=>{var T=Nt();v($,T)},je=$=>{var T=Mt(),re=a(T);Q(re,17,()=>e(_).slice().sort((R,F)=>(R.nextReviewAt??"").localeCompare(F.nextReviewAt??"")).slice(0,50),R=>R.id,(R,F)=>{const O=S(()=>He(e(H),e(F).nextReviewAt)),De=S(()=>Ke(e(H),e(F).nextReviewAt)??0);var Le=Ct(),We=a(Le),Qe=a(We,!0);t(We);var Ie=n(We,2),Te=a(Ie),Xe=a(Te,!0);t(Te);var Pe=n(Te,2),Ge=a(Pe);t(Pe),t(Ie),t(Le),y(Je=>{u(Qe,e(F).content),Fe(Te,1,e(O)==="overdue"?"text-decay":e(O)==="today"?"text-warning":e(O)==="week"?"text-synapse-glow":"text-dream-glow"),u(Xe,e(O)==="overdue"?`${-e(De)}d overdue`:e(O)==="today"?"today":`in ${e(De)}d`),u(Ge,`· ${Je??""}%`)},[()=>(e(F).retentionStrength*100).toFixed(0)]),v(R,Le)});var ce=n(re,2);{var Me=R=>{var F=jt(),O=a(F);t(F),y(()=>u(O,`+${e(_).length-50} more`)),v(R,F)};L(ce,R=>{e(_).length>50&&R(Me)})}t(T),v($,T)};L(Se,$=>{e(_).length===0?$(Ce):$(je,!1)})}t(G),t(W),t(x),y(($,T)=>{u(k,e(Y).dueToday),u(he,e(Y).dueThisWeek),u(le,e(Y).dueThisMonth),u(l,$),u(X,`Across ${e(w).length??""} scheduled memor${e(w).length===1?"y":"ies"}`),u(J,T),u(ye,e(_).length)},[()=>e(Y).avgDays.toFixed(1),()=>{var $;return($=ge.find(T=>T.key===e(j)))==null?void 0:$.label}]),v(d,x)};L(be,d=>{e(P)?d(ie):e(E)?d(oe,1):e(w).length===0?d($e,2):d(Ne,!1)})}t(ee),v(p,ee),qe()}Ue(["click"]);export{Gt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js.br b/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js.br new file mode 100644 index 0000000..95eb289 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js.gz b/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js.gz new file mode 100644 index 0000000..017439b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/16.QhdtMP78.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js b/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js new file mode 100644 index 0000000..2791a77 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js @@ -0,0 +1,2 @@ +import"../chunks/Bzak7iHL.js";import{o as Qe}from"../chunks/GG5zm9kr.js";import{p as Xe,t as w,a as Ze,g as s,d as t,e as i,s as k,h as m,C as et,r as e,n as _,f as tt,u as P}from"../chunks/CpWkWWOo.js";import{d as st,a as E,s as p}from"../chunks/BlVfL1ME.js";import{i as u}from"../chunks/B4yTwGkE.js";import{e as se,i as ae}from"../chunks/CGEBXrjl.js";import{a as v,f as l,t as he}from"../chunks/CHOnp4oo.js";import{s as we}from"../chunks/aVbAZ-t7.js";import{s as ke}from"../chunks/Cx-f-Pzo.js";import{s as at,a as ie}from"../chunks/C6HuKgyx.js";import{a as F}from"../chunks/554JRhq6.js";import{w as it,m as dt,a as rt,i as ot}from"../chunks/MAY1QfFZ.js";import{f as nt}from"../chunks/D4ymNiig.js";var vt=l(' Running...',1),lt=l('
          Processed
          '),ct=l('
          Decayed
          '),xt=l('
          Embedded
          '),mt=l('
          '),pt=l(' Dreaming...',1),ut=l('
          '),ft=l('
          Insights Discovered:
          ',1),gt=l('
          Connections found:
          '),bt=l('
          Memories replayed:
          '),_t=l('
          '),yt=l('
          '),ht=l('
          '),wt=l('

          Retention Distribution

          '),kt=l('
          '),St=l(`

          Settings & System

          Memories
          Avg Retention
          WebSocket
          v2.1
          Vestige

          Cognitive Operations

          Pulse Toast Preview
          Fire a synthetic event sequence — useful for UI demos
          Birth Ritual Preview
          Inject a synthetic memory — switch to Graph to watch the orb fly in
          FSRS-6 Consolidation
          Apply spaced-repetition decay, regenerate embeddings, run maintenance
          Memory Dream Cycle
          Replay memories, discover hidden connections, synthesize insights

          Keyboard Shortcuts

          About

          V
          Vestige v2.1 "Nuclear Dashboard"
          Your AI's long-term memory system
          29 cognitive modules
          FSRS-6 spaced repetition
          Nomic Embed v1.5 (256d)
          Jina Reranker v1 Turbo
          USearch HNSW (20x FAISS)
          Local-first, zero cloud
          Built with Rust + Axum + SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4
          `);function It(Se,Ce){Xe(Ce,!0);const De=()=>ie(dt,"$memoryCount",B),N=()=>ie(rt,"$avgRetention",B),de=()=>ie(ot,"$isConnected",B),[B,$e]=at(),re=["fact","concept","pattern","decision","person","place"];let K=k(0);function Re(){const a=re[s(K)%re.length];et(K),it.injectEvent({type:"MemoryCreated",data:{id:`demo-birth-${Date.now()}`,content:`Demo memory #${s(K)} — ${a}`,node_type:a,tags:["demo","v2.3-birth-ritual"],retention:.9}})}let T=k(!1),R=k(!1),y=k(null),f=k(null),Ae=k(null),$=k(null),oe=k(!0),Ge=k(null);Qe(()=>{O()});async function O(){m(oe,!0);try{const[a,d,c]=await Promise.all([F.stats().catch(()=>null),F.health().catch(()=>null),F.retentionDistribution().catch(()=>null)]);m(Ae,a,!0),m(Ge,d,!0),m($,c,!0)}finally{m(oe,!1)}}async function je(){m(T,!0),m(y,null);try{m(y,await F.consolidate(),!0),await O()}catch{}finally{m(T,!1)}}async function Me(){m(R,!0),m(f,null);try{m(f,await F.dream(),!0),await O()}catch{}finally{m(R,!1)}}var V=St(),q=t(V),Pe=i(t(q),2);e(q);var z=i(q,2),J=t(z),ne=t(J),Ee=t(ne,!0);e(ne),_(2),e(J);var U=i(J,2),W=t(U),Fe=t(W);e(W),_(2),e(U);var ve=i(U,2),le=t(ve),ce=t(le),xe=i(ce,2),Te=t(xe,!0);e(xe),e(le),_(2),e(ve),_(2),e(z);var Y=i(z,2),H=i(t(Y),2),me=t(H),Oe=i(t(me),2);e(me),e(H);var L=i(H,2),pe=t(L),Ie=i(t(pe),2);e(pe),e(L);var Q=i(L,2),X=t(Q),I=i(t(X),2),Ne=t(I);{var Be=a=>{var d=vt();_(),v(a,d)},Ke=a=>{var d=he("Consolidate");v(a,d)};u(Ne,a=>{s(T)?a(Be):a(Ke,!1)})}e(I),e(X);var Ve=i(X,2);{var qe=a=>{var d=mt(),c=t(d),g=t(c);{var S=o=>{var r=lt(),n=t(r),x=t(n,!0);e(n),_(2),e(r),w(()=>p(x,s(y).nodesProcessed)),v(o,r)};u(g,o=>{s(y).nodesProcessed!==void 0&&o(S)})}var b=i(g,2);{var h=o=>{var r=ct(),n=t(r),x=t(n,!0);e(n),_(2),e(r),w(()=>p(x,s(y).decayApplied)),v(o,r)};u(b,o=>{s(y).decayApplied!==void 0&&o(h)})}var C=i(b,2);{var G=o=>{var r=xt(),n=t(r),x=t(n,!0);e(n),_(2),e(r),w(()=>p(x,s(y).embeddingsGenerated)),v(o,r)};u(C,o=>{s(y).embeddingsGenerated!==void 0&&o(G)})}e(c),e(d),v(a,d)};u(Ve,a=>{s(y)&&a(qe)})}e(Q);var ue=i(Q,2),Z=t(ue),A=i(t(Z),2),ze=t(A);{var Je=a=>{var d=pt();_(),v(a,d)},Ue=a=>{var d=he("Dream");v(a,d)};u(ze,a=>{s(R)?a(Je):a(Ue,!1)})}e(A),e(Z);var We=i(Z,2);{var Ye=a=>{var d=_t(),c=t(d);{var g=o=>{var r=ft(),n=i(tt(r),2);se(n,17,()=>s(f).insights,ae,(x,j)=>{var D=ut(),M=t(D,!0);e(D),w(ee=>p(M,ee),[()=>typeof s(j)=="string"?s(j):JSON.stringify(s(j))]),v(x,D)}),v(o,r)},S=P(()=>s(f).insights&&Array.isArray(s(f).insights));u(c,o=>{s(S)&&o(g)})}var b=i(c,2);{var h=o=>{var r=gt(),n=i(t(r)),x=t(n,!0);e(n),e(r),w(()=>p(x,s(f).connections_found)),v(o,r)};u(b,o=>{s(f).connections_found!==void 0&&o(h)})}var C=i(b,2);{var G=o=>{var r=bt(),n=i(t(r)),x=t(n,!0);e(n),e(r),w(()=>p(x,s(f).memories_replayed)),v(o,r)};u(C,o=>{s(f).memories_replayed!==void 0&&o(G)})}e(d),v(a,d)};u(We,a=>{s(f)&&a(Ye)})}e(ue),e(Y);var fe=i(Y,2);{var He=a=>{var d=wt(),c=i(t(d),2),g=t(c);{var S=h=>{var C=ht();se(C,21,()=>s($).distribution,ae,(G,o,r)=>{const n=P(()=>Math.max(...s($).distribution.map(te=>te.count),1)),x=P(()=>s(o).count/s(n)*100),j=P(()=>r<2?"#ef4444":r<4?"#f59e0b":r<7?"#6366f1":"#10b981");var D=yt(),M=t(D),ee=t(M,!0);e(M);var ye=i(M,2),Le=i(ye,2);Le.textContent=`${r*10}%`,e(D),w(te=>{p(ee,s(o).count),ke(ye,`height: ${te??""}%; background: ${s(j)??""}; opacity: 0.7`)},[()=>Math.max(s(x),2)]),v(G,D)}),e(C),v(h,C)},b=P(()=>s($).distribution&&Array.isArray(s($).distribution));u(g,h=>{s(b)&&h(S)})}e(c),e(d),v(a,d)};u(fe,a=>{s($)&&a(He)})}var ge=i(fe,2),be=i(t(ge),2),_e=t(be);se(_e,20,()=>[{key:"⌘ K",desc:"Command palette"},{key:"/",desc:"Focus search"},{key:"G",desc:"Go to Graph"},{key:"M",desc:"Go to Memories"},{key:"T",desc:"Go to Timeline"},{key:"F",desc:"Go to Feed"},{key:"E",desc:"Go to Explore"},{key:"S",desc:"Go to Stats"}],ae,(a,d)=>{var c=kt(),g=t(c),S=t(g,!0);e(g);var b=i(g,2),h=t(b,!0);e(b),e(c),w(()=>{p(S,d.key),p(h,d.desc)}),v(a,c)}),e(_e),e(be),e(ge),_(2),e(V),w(a=>{p(Ee,De()),ke(W,`color: ${N()>.7?"#10b981":N()>.4?"#f59e0b":"#ef4444"}`),p(Fe,`${a??""}%`),we(ce,1,`w-2.5 h-2.5 rounded-full ${de()?"bg-recall animate-pulse-glow":"bg-decay"}`),p(Te,de()?"Online":"Offline"),I.disabled=s(T),A.disabled=s(R),we(A,1,`px-4 py-2 bg-dream/20 border border-dream/40 text-dream-glow text-sm rounded-xl hover:bg-dream/30 transition disabled:opacity-50 flex items-center gap-2 + ${s(R)?"glow-dream animate-pulse-glow":""}`)},[()=>(N()*100).toFixed(1)]),E("click",Pe,O),E("click",Oe,function(...a){var d;(d=nt)==null||d.apply(this,a)}),E("click",Ie,Re),E("click",I,je),E("click",A,Me),v(Se,V),Ze(),$e()}st(["click"]);export{It as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js.br b/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js.br new file mode 100644 index 0000000..61c7743 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js.gz b/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js.gz new file mode 100644 index 0000000..343f821 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/17.DlzXJVdF.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js b/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js new file mode 100644 index 0000000..3c88c41 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{o as Ft}from"../chunks/GG5zm9kr.js";import{p as $t,a as Ct,j as W,h as y,e as s,d as e,g as t,r as a,s as E,f as dt,n as P,t as B,u as O}from"../chunks/CpWkWWOo.js";import{d as Rt,s as i,a as At}from"../chunks/BlVfL1ME.js";import{i as X}from"../chunks/B4yTwGkE.js";import{e as U,i as q}from"../chunks/CGEBXrjl.js";import{a as p,f as u}from"../chunks/CHOnp4oo.js";import{s as A}from"../chunks/Cx-f-Pzo.js";import{a as w}from"../chunks/554JRhq6.js";var Dt=u('
          '),Mt=u('
          '),kt=u('
          '),Bt=u('
          '),St=u('
          '),Tt=u('

          '),jt=u('

          Retention Distribution

          Memory Types

          ',1),Et=u('
          Total Memories
          Avg Retention
          Due for Review
          Embedding Coverage
          ',1),Pt=u('

          System Stats

          ');function Lt(ot,vt){$t(vt,!0);let n=E(null),m=E(null),l=E(null),Y=E(!0);Ft(async()=>{try{await(async d=>{var r=W(d,3);y(n,r[0],!0),y(m,r[1],!0),y(l,r[2],!0)})(await Promise.all([w.stats(),w.health(),w.retentionDistribution()]))}catch{}finally{y(Y,!1)}});function z(d){return{healthy:"#10b981",degraded:"#f59e0b",critical:"#ef4444",empty:"#6b7280"}[d]||"#6b7280"}async function nt(){try{await w.consolidate(),await(async d=>{var r=W(d,3);y(n,r[0],!0),y(m,r[1],!0),y(l,r[2],!0)})(await Promise.all([w.stats(),w.health(),w.retentionDistribution()]))}catch{}}var G=Pt(),lt=s(e(G),2);{var ct=d=>{var r=Mt();U(r,20,()=>Array(8),q,(F,H)=>{var $=Dt();p(F,$)}),a(r),p(d,r)},xt=d=>{var r=Et(),F=dt(r),H=e(F),$=s(H,2),pt=e($,!0);a($);var Z=s($,2),ut=e(Z);a(Z),a(F);var I=s(F,2),J=e(I),tt=e(J),mt=e(tt,!0);a(tt),P(2),a(J);var K=s(J,2),L=e(K),gt=e(L);a(L),P(2),a(K);var N=s(K,2),at=e(N),_t=e(at,!0);a(at),P(2),a(N);var et=s(N,2),st=e(et),ft=e(st);a(st),P(2),a(et),a(I);var rt=s(I,2);{var bt=D=>{var S=jt(),M=dt(S),T=s(e(M),2);U(T,21,()=>t(l).distribution,q,(g,c,v)=>{const C=O(()=>Math.max(...t(l).distribution.map(V=>V.count),1)),R=O(()=>t(c).count/t(C)*100),_=O(()=>v<3?"#ef4444":v<5?"#f59e0b":v<7?"#10b981":"#6366f1");var x=kt(),o=e(x),f=e(o,!0);a(o);var b=s(o,2),h=s(b,2),Q=e(h,!0);a(h),a(x),B(()=>{i(f,t(c).count),A(b,`height: ${t(R)??""}%; background: ${t(_)??""}; opacity: 0.7; min-height: 2px`),i(Q,t(c).range)}),p(g,x)}),a(T),a(M);var k=s(M,2),j=s(e(k),2);U(j,21,()=>Object.entries(t(l).byType),q,(g,c)=>{var v=O(()=>W(t(c),2));let C=()=>t(v)[0],R=()=>t(v)[1];var _=Bt(),x=e(_),o=s(x,2),f=e(o,!0);a(o);var b=s(o,2),h=e(b,!0);a(b),a(_),B(()=>{A(x,`background: ${({fact:"#00A8FF",concept:"#9D00FF",event:"#FFB800",person:"#00FFD1",note:"#8B95A5",pattern:"#FF3CAC",decision:"#FF4757"}[C()]||"#8B95A5")??""}`),i(f,C()),i(h,R())}),p(g,_)}),a(j),a(k);var yt=s(k,2);{var wt=g=>{var c=Tt(),v=e(c),C=e(v);a(v);var R=s(v,2);U(R,21,()=>t(l).endangered.slice(0,20),q,(_,x)=>{var o=St(),f=e(o),b=e(f);a(f);var h=s(f,2),Q=e(h,!0);a(h),a(o),B(V=>{i(b,`${V??""}%`),i(Q,t(x).content)},[()=>(t(x).retentionStrength*100).toFixed(0)]),p(_,o)}),a(R),a(c),B(()=>i(C,`Endangered Memories (${t(l).endangered.length??""})`)),p(g,c)};X(yt,g=>{t(l).endangered.length>0&&g(wt)})}p(D,S)};X(rt,D=>{t(l)&&D(bt)})}var it=s(rt,2),ht=e(it);a(it),B((D,S,M,T,k,j)=>{A(F,`border-color: ${D??""}30`),A(H,`background: ${S??""}`),A($,`color: ${M??""}`),i(pt,T),i(ut,`v${t(m).version??""}`),i(mt,t(n).totalMemories),A(L,`color: ${t(n).averageRetention>.7?"#10b981":t(n).averageRetention>.4?"#f59e0b":"#ef4444"}`),i(gt,`${k??""}%`),i(_t,t(n).dueForReview),i(ft,`${j??""}%`)},[()=>z(t(m).status),()=>z(t(m).status),()=>z(t(m).status),()=>t(m).status.toUpperCase(),()=>(t(n).averageRetention*100).toFixed(1),()=>t(n).embeddingCoverage.toFixed(0)]),At("click",ht,nt),p(d,r)};X(lt,d=>{t(Y)?d(ct):t(n)&&t(m)&&d(xt,1)})}a(G),p(ot,G),Ct()}Rt(["click"]);export{Lt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js.br b/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js.br new file mode 100644 index 0000000..dc4ad5a Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js.gz b/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js.gz new file mode 100644 index 0000000..b676e7f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/18.Bmu4OWRV.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js b/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js similarity index 85% rename from apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js rename to apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js index c8c3513..cc6fd79 100644 --- a/apps/dashboard/build/_app/immutable/nodes/11.BOa24N9o.js +++ b/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js @@ -1 +1 @@ -import"../chunks/Bzak7iHL.js";import{o as pe}from"../chunks/DWVWfZUn.js";import{p as ce,s as b,c as me,g as e,a as _e,d as i,e as a,h as c,r as t,t as g}from"../chunks/VE8Jor13.js";import{d as ue,a as K,s as m}from"../chunks/DHnEMX8z.js";import{i as M}from"../chunks/JkhlGLjU.js";import{e as h,i as P}from"../chunks/ByItJEsC.js";import{a as l,f as v}from"../chunks/7UNxJI5L.js";import{s as Q}from"../chunks/ussr1V5_.js";import{b as xe}from"../chunks/B5Pq2mnD.js";import{a as fe}from"../chunks/DcQGRi49.js";import{N as U}from"../chunks/BNytumrp.js";var be=v('
          '),ge=v('
          '),he=v('

          No memories in the selected time range.

          '),ye=v('
          '),we=v(' '),ke=v('
          '),Te=v('
          '),je=v('
          '),Ae=v('
          '),Ne=v('

          Timeline

          ');function Re(V,W){ce(W,!0);let _=b(me([])),y=b(!0),w=b(14),k=b(null);pe(()=>R());async function R(){c(y,!0);try{const s=await fe.timeline(e(w),500);c(_,s.timeline,!0)}catch{c(_,[],!0)}finally{c(y,!1)}}var T=Ne(),j=a(T),u=i(a(j),2),A=a(u);A.value=A.__value=7;var N=i(A);N.value=N.__value=14;var O=i(N);O.value=O.__value=30;var Y=i(O);Y.value=Y.__value=90,t(u),t(j);var X=i(j,2);{var Z=s=>{var d=ge();h(d,20,()=>Array(7),P,(x,f)=>{var r=be();l(x,r)}),t(d),l(s,d)},ee=s=>{var d=he();l(s,d)},te=s=>{var d=Ae(),x=i(a(d),2);h(x,21,()=>e(_),f=>f.date,(f,r)=>{var S=je(),B=i(a(S),2),D=a(B),E=a(D),$=a(E),ae=a($,!0);t($);var q=i($,2),se=a(q);t(q),t(E);var z=i(E,2),G=a(z);h(G,17,()=>e(r).memories.slice(0,10),P,(n,o)=>{var p=ye();g(()=>Q(p,`background: ${(U[e(o).nodeType]||"#8B95A5")??""}; opacity: ${.3+e(o).retentionStrength*.7}`)),l(n,p)});var ie=i(G,2);{var re=n=>{var o=we(),p=a(o);t(o),g(()=>m(p,`+${e(r).memories.length-10}`)),l(n,o)};M(ie,n=>{e(r).memories.length>10&&n(re)})}t(z),t(D);var oe=i(D,2);{var le=n=>{var o=Te();h(o,21,()=>e(r).memories,P,(p,C)=>{var F=ke(),H=a(F),L=i(H,2),I=a(L),ve=a(I,!0);t(I),t(L);var J=i(L,2),de=a(J);t(J),t(F),g(ne=>{Q(H,`background: ${(U[e(C).nodeType]||"#8B95A5")??""}`),m(ve,e(C).content),m(de,`${ne??""}%`)},[()=>(e(C).retentionStrength*100).toFixed(0)]),l(p,F)}),t(o),l(n,o)};M(oe,n=>{e(k)===e(r).date&&n(le)})}t(B),t(S),g(()=>{m(ae,e(r).date),m(se,`${e(r).count??""} memories`)}),K("click",B,()=>c(k,e(k)===e(r).date?null:e(r).date,!0)),l(f,S)}),t(x),t(d),l(s,d)};M(X,s=>{e(y)?s(Z):e(_).length===0?s(ee,1):s(te,!1)})}t(T),K("change",u,R),xe(u,()=>e(w),s=>c(w,s)),l(V,T),_e()}ue(["change","click"]);export{Re as component}; +import"../chunks/Bzak7iHL.js";import{o as pe}from"../chunks/GG5zm9kr.js";import{p as ce,s as b,c as me,g as e,a as _e,e as i,d as a,h as c,r as t,t as g}from"../chunks/CpWkWWOo.js";import{d as ue,a as K,s as m}from"../chunks/BlVfL1ME.js";import{i as M}from"../chunks/B4yTwGkE.js";import{e as h,i as P}from"../chunks/CGEBXrjl.js";import{a as l,f as v}from"../chunks/CHOnp4oo.js";import{s as Q}from"../chunks/Cx-f-Pzo.js";import{b as xe}from"../chunks/BnXDGOmJ.js";import{a as fe}from"../chunks/554JRhq6.js";import{N as U}from"../chunks/CcUbQ_Wl.js";var be=v('
          '),ge=v('
          '),he=v('

          No memories in the selected time range.

          '),ye=v('
          '),we=v(' '),ke=v('
          '),Te=v('
          '),je=v('
          '),Ae=v('
          '),Ne=v('

          Timeline

          ');function Re(V,W){ce(W,!0);let _=b(me([])),y=b(!0),w=b(14),k=b(null);pe(()=>R());async function R(){c(y,!0);try{const s=await fe.timeline(e(w),500);c(_,s.timeline,!0)}catch{c(_,[],!0)}finally{c(y,!1)}}var T=Ne(),j=a(T),u=i(a(j),2),A=a(u);A.value=A.__value=7;var N=i(A);N.value=N.__value=14;var O=i(N);O.value=O.__value=30;var Y=i(O);Y.value=Y.__value=90,t(u),t(j);var X=i(j,2);{var Z=s=>{var d=ge();h(d,20,()=>Array(7),P,(x,f)=>{var r=be();l(x,r)}),t(d),l(s,d)},ee=s=>{var d=he();l(s,d)},te=s=>{var d=Ae(),x=i(a(d),2);h(x,21,()=>e(_),f=>f.date,(f,r)=>{var S=je(),B=i(a(S),2),D=a(B),E=a(D),$=a(E),ae=a($,!0);t($);var q=i($,2),se=a(q);t(q),t(E);var z=i(E,2),G=a(z);h(G,17,()=>e(r).memories.slice(0,10),P,(n,o)=>{var p=ye();g(()=>Q(p,`background: ${(U[e(o).nodeType]||"#8B95A5")??""}; opacity: ${.3+e(o).retentionStrength*.7}`)),l(n,p)});var ie=i(G,2);{var re=n=>{var o=we(),p=a(o);t(o),g(()=>m(p,`+${e(r).memories.length-10}`)),l(n,o)};M(ie,n=>{e(r).memories.length>10&&n(re)})}t(z),t(D);var oe=i(D,2);{var le=n=>{var o=Te();h(o,21,()=>e(r).memories,P,(p,C)=>{var F=ke(),H=a(F),L=i(H,2),I=a(L),ve=a(I,!0);t(I),t(L);var J=i(L,2),de=a(J);t(J),t(F),g(ne=>{Q(H,`background: ${(U[e(C).nodeType]||"#8B95A5")??""}`),m(ve,e(C).content),m(de,`${ne??""}%`)},[()=>(e(C).retentionStrength*100).toFixed(0)]),l(p,F)}),t(o),l(n,o)};M(oe,n=>{e(k)===e(r).date&&n(le)})}t(B),t(S),g(()=>{m(ae,e(r).date),m(se,`${e(r).count??""} memories`)}),K("click",B,()=>c(k,e(k)===e(r).date?null:e(r).date,!0)),l(f,S)}),t(x),t(d),l(s,d)};M(X,s=>{e(y)?s(Z):e(_).length===0?s(ee,1):s(te,!1)})}t(T),K("change",u,R),xe(u,()=>e(w),s=>c(w,s)),l(V,T),_e()}ue(["change","click"]);export{Re as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js.br b/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js.br new file mode 100644 index 0000000..8949bb5 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js.gz b/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js.gz new file mode 100644 index 0000000..d782c38 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/19.Baocji37.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js b/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js deleted file mode 100644 index f2b50dc..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{f as m}from"../chunks/VE8Jor13.js";import{c as n,a as p}from"../chunks/7UNxJI5L.js";import{s as i}from"../chunks/BZYVQ1d5.js";function d(r,t){var o=n(),a=m(o);i(a,()=>t.children),p(r,o)}export{d as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js.br b/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js.br deleted file mode 100644 index 982eff8..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js.gz b/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js.gz deleted file mode 100644 index 827659b..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/2.BFaWefTK.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js b/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js new file mode 100644 index 0000000..ad009e1 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{f as m}from"../chunks/CpWkWWOo.js";import{c as n,a as p}from"../chunks/CHOnp4oo.js";import{s as i}from"../chunks/CJCPY1OL.js";function d(r,t){var o=n(),a=m(o);i(a,()=>t.children),p(r,o)}export{d as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js.br b/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js.br new file mode 100644 index 0000000..b1be6ec Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js.gz b/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js.gz new file mode 100644 index 0000000..ba053e9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/2.D-vKwnTC.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js b/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js new file mode 100644 index 0000000..c998c61 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js @@ -0,0 +1,6 @@ +import"../chunks/Bzak7iHL.js";import{o as it}from"../chunks/GG5zm9kr.js";import{p as ct,s as p,c as mt,t as f,g as a,a as dt,W as pt,e as s,d as l,h as c,aG as vt,n as $e,r as o,i as ut}from"../chunks/CpWkWWOo.js";import{d as ht,e as Qe,s as v,a as bt}from"../chunks/BlVfL1ME.js";import{i as We}from"../chunks/B4yTwGkE.js";import{e as P,i as k}from"../chunks/CGEBXrjl.js";import{a as u,f as h}from"../chunks/CHOnp4oo.js";import{h as yt}from"../chunks/C4h_mRt2.js";import{s as Ge,r as W}from"../chunks/A7po6GxK.js";import{s as He}from"../chunks/aVbAZ-t7.js";import{s as gt}from"../chunks/Cx-f-Pzo.js";import{b as V}from"../chunks/sZcqyNBA.js";import{b as Re}from"../chunks/BnXDGOmJ.js";import{b as ft}from"../chunks/CJsMJEun.js";import{b as Ue}from"../chunks/DGcYlAAw.js";var qt=h(''),_t=h('
          '),wt=h("

          "),Pt=h('
          '),kt=h('

          '),xt=h('

          '),St=h("
          "),Tt=h('

          Checking the onboarding notes...

          '),Ct=h(''),Mt=h(`
          V Vestige Pro

          June early access

          Vestige Pro

          The paid layer for developers and teams who already trust Vestige as local memory for AI agents. + Sync, backups, team memory, and bot-assisted support come next.

          Early access

          Reserve a Pro seat

          Why Pro exists

          Two plans. One promise: agent memory you can depend on.

          Always-on answers

          The support bot handles the first wave.

          This is the first support layer: instant onboarding answers before anyone has to write an email. + It can run locally from the FAQ now and call a hosted support endpoint later.

          Onboarding bot

          May to June

          The plan is simple.

          1. May Get Vestige into every MCP, Claude Code, Cursor, local AI, Rust, and self-hosted channel that cares about agent memory.
          2. June Invite the first Solo Pro and Team Pro users into sync, backups, shared memory, PostgreSQL-backed deployments, and bot-assisted support.
          3. After Use paid feedback to turn Vestige from a beloved local tool into durable agent-memory infrastructure.
          `);function Ft(Be,Ee){ct(Ee,!0);let q,G=p(""),j=p(""),I=p("solo"),J=p("sync"),x=p(""),H=p(""),y=p("idle"),_=p(""),S=p(""),T=p(!1),C=p(mt([{role:"bot",content:"Ask me about installing Vestige, whether heavy models are required, Solo vs Team Pro, sync, pricing, or what happens after you join the June list."}]));const Fe=[{value:"Local",label:"SQLite memory, no hosted memory service"},{value:"MCP",label:"Claude Code, Cursor, Cline, Codex, Goose"},{value:"June",label:"Pro sync, backup, team memory early access"}],De=[{name:"Solo Pro",accent:"#22c55e",copy:"Multi-device sync, encrypted backups, managed updates, and a cleaner memory dashboard for developers living inside AI coding agents."},{name:"Team Pro",accent:"#06b6d4",copy:"Shared project memory, admin review, audit trails, PostgreSQL-backed deployments, and async support for engineering teams."}],Oe=["Private by default","Sync without lock-in","Team memory controls","Bot-assisted support"],ze=[{label:"Install",prompt:"How do I install Vestige and connect it to Claude Code?"},{label:"No 20GB?",prompt:"Do I need the Sanhedrin model or 20GB of RAM?"},{label:"Solo vs Team",prompt:"Should I choose Solo Pro or Team Pro?"},{label:"Sync",prompt:"How will Pro sync and backups work?"},{label:"Pricing",prompt:"How much will Vestige Pro cost?"},{label:"Human help",prompt:"When does a human get involved?"}];it(()=>{const t=q.getContext("2d");if(!t)return;const e=t;let r=0,i=0,m=0;const d=Array.from({length:62},(b,w)=>({x:Math.random(),y:Math.random(),vx:(Math.random()-.5)*16e-5,vy:(Math.random()-.5)*16e-5,phase:Math.random()*Math.PI*2,kind:w%5}));function M(){const b=Math.min(window.devicePixelRatio||1,2);i=window.innerWidth,m=window.innerHeight,q.width=Math.floor(i*b),q.height=Math.floor(m*b),q.style.width=`${i}px`,q.style.height=`${m}px`,e.setTransform(b,0,0,b,0,0)}function Me(b){e.clearRect(0,0,i,m);const w=e.createLinearGradient(0,0,i,m);w.addColorStop(0,"#07100f"),w.addColorStop(.45,"#0b1221"),w.addColorStop(1,"#15100a"),e.fillStyle=w,e.fillRect(0,0,i,m),e.strokeStyle="rgba(148, 163, 184, 0.08)",e.lineWidth=1;for(let n=0;n.96)&&(n.vx*=-1),(n.y<.06||n.y>.94)&&(n.vy*=-1);for(let n=0;n{cancelAnimationFrame(r),window.removeEventListener("resize",M)}});function Ne(){const t=["## Vestige Pro waitlist","",`Plan: ${a(I)}`,`Priority: ${a(J)}`,a(x).trim()?`Use case: ${a(x).trim()}`:"Use case:","","Please do not include private email addresses in this public issue."].join(` +`);return`https://github.com/samvallad33/vestige/issues/new?title=${encodeURIComponent("Vestige Pro waitlist")}&body=${encodeURIComponent(t)}`}async function Ye(t){if(t.preventDefault(),c(y,"submitting"),c(_,""),a(H).trim()){c(y,"success"),c(_,"You are on the list.");return}if(!a(j).includes("@")){c(y,"error"),c(_,"Enter an email so the early-access invite can reach you.");return}a(G).trim(),a(j).trim(),a(I),a(J),a(x).trim(),new Date().toISOString();{c(y,"success"),c(_,"Email capture is ready for an endpoint. Opening the GitHub waitlist fallback with your email omitted."),window.open(Ne(),"_blank","noopener,noreferrer");return}}function Ke(t){const e=t.toLowerCase();return/(install|setup|onboard|claude|cursor|cline|codex|connect)/.test(e)?["Start with the open-source install:","1. `npm install -g vestige-mcp-server@latest`","2. Claude Code: `claude mcp add vestige vestige-mcp -s user`","3. Codex: `codex mcp add vestige -- vestige-mcp`","Then test it by asking your agent to remember a preference, opening a fresh session, and asking for that preference back."].join(` +`):/(sanhedrin|20gb|20 gb|ram|heavy|model|mlx|preflight|hook)/.test(e)?"No. The default Vestige path is the local MCP memory server. Sanhedrin, preflight hooks, and large local verifier models are optional. Pro should keep that promise: nobody should need a 20GB machine just to use memory.":/(solo|team|plan|seat|buying)/.test(e)?"Choose Solo Pro if you want your own multi-device memory, backups, smoother updates, and personal support. Choose Team Pro if multiple people need shared project memory, admin controls, PostgreSQL-backed storage, audit trails, or team onboarding.":/(sync|backup|device|dropbox|icloud|syncthing|postgres|postgresql|pg|central)/.test(e)?"Open-source Vestige should stay local-first. Pro is where guided sync, encrypted backups, conflict handling, and Team Pro PostgreSQL-backed storage belong. The important design rule: users own memory and can export it.":/(price|pricing|cost|pay|billing|stripe|lemon|subscription|monthly|yearly)/.test(e)?"Pricing is not final yet. The current plan is simple: Solo Pro for individual developers, Team Pro for engineering teams. Join the waitlist so early users can shape pricing before the June launch.":/(update|upgrade|curl|reinstall|version)/.test(e)?"Use `vestige update` for existing installs. The goal is that users should not need to keep copying curl commands just to stay current.":/(privacy|local|cloud|telemetry|data|where.*stored|sqlite)/.test(e)?"Vestige core stores memory locally in SQLite and does not need a hosted memory service. Pro should add convenience around sync, backup, and teams without turning private local memory into a black box.":/(support|bot|human|email|question|help|available|awake|discord)/.test(e)?"The support bot should answer common install, sync, plan, and onboarding questions instantly. Hard cases should escalate with context so a human teammate only handles the issues that actually need human judgment.":/(waitlist|june|early|launch|invite|after)/.test(e)?"After you join the waitlist, the June early-access flow should invite you into the right lane: Solo Pro for personal memory, Team Pro for shared memory and admin controls. The bot will keep onboarding answers available while the launch scales.":"I can help with install, updates, optional heavy models, Solo vs Team Pro, sync, backups, privacy, pricing, and support escalation. For now, the fastest next step is to join the waitlist and include your use case so the June onboarding can prioritize the right workflows."}async function pe(t,e){t==null||t.preventDefault();const r=(e??a(S)).trim();if(!(!r||a(T))){c(S,""),c(T,!0),c(C,[...a(C),{role:"user",content:r}],!0);{c(C,[...a(C),{role:"bot",content:Ke(r)}],!0),c(T,!1);return}}}var R=Mt();yt("1375qm6",t=>{var e=qt();pt(()=>{vt.title="Vestige Pro Waitlist"}),u(t,e)});var ve=l(R);ft(ve,t=>q=t,()=>q);var U=s(ve,4),ue=l(U),he=s(ue,2),Xe=s(l(he),2);$e(2),o(he),o(U);var be=s(U,2),B=l(be),E=l(B),ye=s(l(E),8);P(ye,21,()=>Fe,k,(t,e)=>{var r=_t(),i=l(r),m=l(i,!0);o(i);var g=s(i,2),d=l(g,!0);o(g),o(r),f(()=>{v(m,a(e).value),v(d,a(e).label)}),u(t,r)}),o(ye),o(E);var F=s(E,2),D=s(l(F),2),ge=s(l(D),2);W(ge),o(D);var O=s(D,2),fe=s(l(O),2);W(fe),o(O);var z=s(O,2),N=s(l(z),2),Y=l(N);Y.value=Y.__value="solo";var qe=s(Y);qe.value=qe.__value="team",o(N),o(z);var K=s(z,2),X=s(l(K),2),Z=l(X);Z.value=Z.__value="sync";var ee=s(Z);ee.value=ee.__value="team-memory";var te=s(ee);te.value=te.__value="postgres";var _e=s(te);_e.value=_e.__value="support-bot",o(X),o(K);var ae=s(K,2),we=s(l(ae),2);ut(we),o(ae);var se=s(ae,2),Pe=s(l(se),2);W(Pe),o(se);var L=s(se,2),Ze=l(L,!0);o(L);var et=s(L,2);{var tt=t=>{var e=wt();let r;var i=l(e,!0);o(e),f(()=>{r=He(e,1,"submit-message svelte-1375qm6",null,r,{success:a(y)==="success",error:a(y)==="error"}),v(i,a(_))}),u(t,e)};We(et,t=>{a(_)&&t(tt)})}o(F),o(B);var oe=s(B,2);P(oe,21,()=>Oe,k,(t,e)=>{var r=Pt(),i=l(r,!0);o(r),f(()=>v(i,a(e))),u(t,r)}),o(oe);var re=s(oe,2),ke=s(l(re),2);P(ke,21,()=>De,k,(t,e)=>{var r=kt(),i=s(l(r),2),m=l(i,!0);o(i);var g=s(i,2),d=l(g,!0);o(g),o(r),f(()=>{gt(r,`--track-color: ${a(e).accent}`),v(m,a(e).name),v(d,a(e).copy)}),u(t,r)}),o(ke),o(re);var xe=s(re,2),Se=s(l(xe),2),le=l(Se),Te=s(l(le),4),at=l(Te,!0);o(Te),o(le);var ne=s(le,2),Ce=l(ne);P(Ce,17,()=>a(C),k,(t,e)=>{var r=St();let i;P(r,21,()=>a(e).content.split(` +`),k,(m,g)=>{var d=xt(),M=l(d,!0);o(d),f(()=>v(M,a(g))),u(m,d)}),o(r),f(()=>i=He(r,1,"svelte-1375qm6",null,i,{"bot-bubble":a(e).role==="bot","user-bubble":a(e).role==="user"})),u(t,r)});var st=s(Ce,2);{var ot=t=>{var e=Tt();u(t,e)};We(st,t=>{a(T)&&t(ot)})}o(ne);var ie=s(ne,2);P(ie,21,()=>ze,k,(t,e)=>{var r=Ct(),i=l(r,!0);o(r),f(()=>v(i,a(e).label)),bt("click",r,()=>pe(void 0,a(e).prompt)),u(t,r)}),o(ie);var ce=s(ie,2),me=l(ce);W(me);var rt=s(me,2);o(ce),o(Se),o(xe),$e(2),o(be),o(R),f(t=>{Ge(ue,"href",`${Ue}/waitlist`),Ge(Xe,"href",`${Ue}/graph`),L.disabled=a(y)==="submitting",v(Ze,a(y)==="submitting"?"Saving...":"Join June early access"),v(at,"FAQ mode"),rt.disabled=t},[()=>a(T)||!a(S).trim()]),Qe("submit",F,Ye),V(ge,()=>a(G),t=>c(G,t)),V(fe,()=>a(j),t=>c(j,t)),Re(N,()=>a(I),t=>c(I,t)),Re(X,()=>a(J),t=>c(J,t)),V(we,()=>a(x),t=>c(x,t)),V(Pe,()=>a(H),t=>c(H,t)),Qe("submit",ce,pe),V(me,()=>a(S),t=>c(S,t)),u(Be,R),dt()}ht(["click"]);export{Ft as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js.br b/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js.br new file mode 100644 index 0000000..183b239 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js.gz b/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js.gz new file mode 100644 index 0000000..ba4845c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/20.CJQuAMkU.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js b/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js deleted file mode 100644 index a3aaf90..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import"../chunks/CrlWs-6R.js";import{o as p}from"../chunks/DWVWfZUn.js";import{p as r,a as t}from"../chunks/VE8Jor13.js";import{i as a}from"../chunks/jyeIy8pa.js";import{g as m}from"../chunks/CK5Nmlyf.js";function u(i,o){r(o,!1),p(()=>m("/graph",{replaceState:!0})),a(),t()}export{u as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js.br b/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js.br deleted file mode 100644 index a72f272..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js.gz b/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js.gz deleted file mode 100644 index 52fbb7a..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/3.BbrO3ed8.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js b/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js new file mode 100644 index 0000000..28965de --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{i as p}from"../chunks/BUoSzNdg.js";import{o as r}from"../chunks/GG5zm9kr.js";import{p as t,a}from"../chunks/CpWkWWOo.js";import{g as m}from"../chunks/D-gDZzN6.js";function g(i,o){t(o,!1),r(()=>m("/graph",{replaceState:!0})),p(),a()}export{g as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js.br b/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js.br new file mode 100644 index 0000000..840c5ed Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js.gz b/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js.gz new file mode 100644 index 0000000..3482e9c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/3.mK8D6pz1.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js.br b/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js.br deleted file mode 100644 index 43b45ef..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js.gz b/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js.gz deleted file mode 100644 index 28560f6..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js b/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js new file mode 100644 index 0000000..eb053c8 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js @@ -0,0 +1,8 @@ +import"../chunks/Bzak7iHL.js";import{o as we,a as Ne}from"../chunks/GG5zm9kr.js";import{p as Me,s as A,c as le,aB as ve,e as y,d as p,t as L,g as e,f as Ee,u as de,r as v,a as Se,h as d,n as me}from"../chunks/CpWkWWOo.js";import{s as K,d as Ie,a as xe}from"../chunks/BlVfL1ME.js";import{i as ue}from"../chunks/B4yTwGkE.js";import{a as E,c as Fe,b as oe,f as X}from"../chunks/CHOnp4oo.js";import{s as o,r as ge}from"../chunks/A7po6GxK.js";import{b as Ge,a as Pe}from"../chunks/sZcqyNBA.js";import{a as he}from"../chunks/554JRhq6.js";import{e as ke}from"../chunks/MAY1QfFZ.js";import{e as fe,i as ye}from"../chunks/CGEBXrjl.js";import{p as W}from"../chunks/V6gjw5Ec.js";import{N as Ce}from"../chunks/CcUbQ_Wl.js";const Re=.93,Te=.05,be="#8B95A5",Le="#818cf8",Oe=140,pe=8,De=4,Ke=12;function je(s){return!Number.isFinite(s)||s<=0?0:s*Re}function Ue(s){return Number.isFinite(s)?s>=Te:!1}function Ae(s){return!Number.isFinite(s)||stypeof b=="string");M.length!==0&&u.push({source_id:N.source_id,target_ids:M})}return u.reverse()}var Qe=oe(''),We=oe(''),Xe=oe(' '),Je=oe(''),Ze=oe('');function $e(s,f){Me(f,!0);let u=W(f,"width",3,900),x=W(f,"height",3,560),N=W(f,"source",3,null),M=W(f,"neighbours",19,()=>[]),b=W(f,"liveBurstKey",3,0),S=W(f,"liveBurst",3,null);const F=22,J=14;let B=A(le([])),G=A(le([])),T=A(le([])),U=0,O=null,ne=null,Z=0;function $(n,t,c,_){U+=1;const i=U,l=b()>0&&e(B).length>0?40:0,m=c+(Math.random()-.5)*l,g=_+(Math.random()-.5)*l;d(T,[...e(T),{burstId:i,x:m,y:g,radius:F,opacity:.75},{burstId:i,x:m,y:g,radius:F,opacity:.5}],!0);const V={id:`${n.id}::${i}`,label:n.label,nodeType:"source",x:m,y:g,activation:1,isSource:!0,sourceBurstId:i},H=[],k=[],C=U*.37%(Math.PI*2),re=qe(m,g,t.length,C);t.forEach((D,ie)=>{const se=re[ie];se&&(H.push({id:`${D.id}::${i}`,label:D.label,nodeType:D.nodeType,x:se.x,y:se.y,activation:Ye(ie,t.length),isSource:!1,sourceBurstId:i}),k.push({burstId:i,sourceNodeId:V.id,targetNodeId:`${D.id}::${i}`,drawProgress:0,staggerDelay:ze(ie),framesElapsed:0}))}),d(B,[...e(B),V,...H],!0),d(G,[...e(G),...k],!0)}function q(){let n=[];for(const i of e(B)){const l=je(i.activation);Ue(l)&&n.push({...i,activation:l})}d(B,n,!0);const t=new Set(n.map(i=>i.id));let c=[];for(const i of e(G)){if(!t.has(i.sourceNodeId)||!t.has(i.targetNodeId))continue;const l=i.framesElapsed+1;let m=i.drawProgress;l>=i.staggerDelay&&(m=Math.min(1,m+1/15)),c.push({...i,framesElapsed:l,drawProgress:m})}d(G,c,!0);let _=[];for(const i of e(T)){const l=i.radius+6,m=i.opacity*.96;m<.02||l>Math.max(u(),x())||_.push({...i,radius:l,opacity:m})}d(T,_,!0),O=requestAnimationFrame(q)}function Y(){d(B,[],!0),d(G,[],!0),d(T,[],!0)}ve(()=>{if(!N())return;const n=N().id;n!==ne&&(ne=n,Y(),$(N(),M(),u()/2,x()/2))}),ve(()=>{if(!S()||b()===0||b()===Z)return;Z=b();const n=(Math.random()-.5)*120,t=(Math.random()-.5)*120;$(S().source,S().neighbours,u()/2+n,x()/2+t)}),we(()=>{O=requestAnimationFrame(q)}),Ne(()=>{O!==null&&cancelAnimationFrame(O)});function ce(n,t){return Ve(n,t)}function ee(n){const t=e(B).find(l=>l.id===n.sourceNodeId),c=e(B).find(l=>l.id===n.targetNodeId);if(!t||!c)return null;const _=t.x+(c.x-t.x)*n.drawProgress,i=t.y+(c.y-t.y)*n.drawProgress;return{x1:t.x,y1:t.y,x2:_,y2:i}}var P=Ze(),te=y(p(P));fe(te,17,()=>e(T),ye,(n,t)=>{var c=Qe();L(()=>{o(c,"cx",e(t).x),o(c,"cy",e(t).y),o(c,"r",e(t).radius),o(c,"opacity",e(t).opacity)}),E(n,c)});var j=y(te);fe(j,17,()=>e(G),ye,(n,t)=>{const c=de(()=>ee(e(t)));var _=Fe(),i=Ee(_);{var l=m=>{var g=We();L(()=>{o(g,"x1",e(c).x1),o(g,"y1",e(c).y1),o(g,"x2",e(c).x2),o(g,"y2",e(c).y2),o(g,"opacity",.35*e(t).drawProgress)}),E(m,g)};ue(i,m=>{e(c)&&m(l)})}E(n,_)});var z=y(j);fe(z,17,()=>e(B),n=>n.id,(n,t)=>{const c=de(()=>ce(e(t).nodeType,e(t).isSource)),_=de(()=>e(t).isSource?F*(.7+.3*e(t).activation):J*(.5+.8*e(t).activation));var i=Je(),l=p(i),m=y(l),g=y(m),V=y(g);{var H=k=>{var C=Xe(),re=p(C,!0);v(C),L(D=>{o(C,"x",e(t).x),o(C,"y",e(t).y+e(_)+18),o(C,"opacity",.9*e(t).activation),K(re,D)},[()=>e(t).label.length>40?e(t).label.slice(0,40)+"…":e(t).label]),E(k,C)};ue(V,k=>{e(t).isSource&&e(t).label&&k(H)})}v(i),L(k=>{o(i,"opacity",k),o(l,"cx",e(t).x),o(l,"cy",e(t).y),o(l,"r",e(_)*1.9),o(l,"fill",e(c)),o(l,"opacity",.18*e(t).activation),o(m,"cx",e(t).x),o(m,"cy",e(t).y),o(m,"r",e(_)),o(m,"fill",e(c)),o(g,"cx",e(t).x-e(_)*.3),o(g,"cy",e(t).y-e(_)*.3),o(g,"r",e(_)*.35),o(g,"opacity",.35*e(t).activation)},[()=>Math.min(1,e(t).activation*1.25)]),E(n,i)}),v(P),L(()=>{o(P,"width",u()),o(P,"height",x()),o(P,"viewBox",`0 0 ${u()??""} ${x()??""}`)}),E(s,P),Se()}var et=X('

          Computing activation...

          '),tt=X('

          Activation failed

          '),rt=X(`

          No matching memory

          Nothing in the graph matches . Try a broader + query or switch on live mode to watch the engine fire its own + bursts.

          `),it=X(`

          Waiting for activation

          Seed a burst with the search bar above, or enable live mode to + overlay bursts from the cognitive engine as they happen.

          `),st=X('
          Seed

          '),at=X(`

          Spreading Activation

          Collins & Loftus 1975 — activation spreads from a seed memory to + neighbours along semantic edges, decaying by 0.93 per animation frame + until it drops below 0.05. Search seeds a focused burst; live mode + overlays every spread event fired by the cognitive engine in real time.

          Seed Memory
          Live bursts fired:
          `);function yt(s,f){Me(f,!0);let u=A(""),x=A(!1),N=A(!1),M=A(null),b=A(null),S=A(le([])),F=A(!0),J=A(0),B=A(null),G=A(0);const T=new Map;function U(r){T.set(r.id,r)}function O(r){return{id:r.id,label:ne(r.content,r.id),nodeType:r.nodeType}}function ne(r,a){if(r&&r.trim().length>0){const h=r.trim();return h.length>60?h.slice(0,60)+"…":h}return a.slice(0,8)}function Z(r){const a=T.get(r);return a?O(a):{id:r,label:r.slice(0,8),nodeType:"note"}}async function $(){const r=e(u).trim();if(!r){d(M,null);return}d(x,!0),d(N,!0),d(M,null),d(b,null),d(S,[],!0);try{const a=await he.search(r,1);if(!a.results||a.results.length===0)return;const h=a.results[0];U(h),d(b,O(h),!0);const w=await he.explore(h.id,"associations",void 0,15),I=(w==null?void 0:w.results)??(w==null?void 0:w.nodes)??(w==null?void 0:w.associations)??[],R=[];for(const Q of I){if(!Q||typeof Q!="object"||!("id"in Q))continue;const ae=Q;U(ae),R.push(O(ae))}d(S,R,!0)}catch(a){d(M,a instanceof Error?a.message:String(a),!0),d(b,null),d(S,[],!0)}finally{d(x,!1)}}let q=null,Y=null,ce=!1;we(()=>{q=ke.subscribe(r=>{if(!r||r.length===0)return;if(!ce){Y=r[0],ce=!0;return}if(!e(F)){Y=r[0];return}const a=He(r,Y);if(Y=r[0],a.length!==0)for(const h of a){const w=Z(h.source_id),I=h.target_ids.map(R=>Z(R));d(J,e(J)+1),d(B,{source:w,neighbours:I},!0),d(G,e(G)+1)}})}),Ne(()=>{q&&q()});var ee=at(),P=y(p(ee),2),te=y(p(P),2),j=p(te);ge(j);var z=y(j,2),n=p(z,!0);v(z),v(te),v(P);var t=y(P,2),c=p(t),_=p(c);ge(_),me(2),v(c);var i=y(c,2),l=y(p(i)),m=p(l,!0);v(l),v(i),v(t);var g=y(t,2),V=p(g);{var H=r=>{var a=et();E(r,a)},k=r=>{var a=tt(),h=p(a),w=y(p(h),4),I=p(w,!0);v(w),v(h),v(a),L(()=>K(I,e(M))),E(r,a)},C=r=>{var a=rt(),h=p(a),w=y(p(h),4),I=y(p(w)),R=p(I);v(I),me(),v(w),v(h),v(a),L(()=>K(R,`"${e(u)??""}"`)),E(r,a)},re=r=>{var a=it();E(r,a)},D=r=>{$e(r,{width:1040,height:560,get source(){return e(b)},get neighbours(){return e(S)},get liveBurstKey(){return e(J)},get liveBurst(){return e(B)}})};ue(V,r=>{e(x)?r(H):e(M)?r(k,1):!e(b)&&e(N)?r(C,2):e(b)?r(D,!1):r(re,3)})}v(g);var ie=y(g,2);{var se=r=>{var a=st(),h=y(p(a),2),w=p(h,!0);v(h);var I=y(h,2),R=p(I),Q=p(R,!0);v(R);var ae=y(R,2),Be=p(ae);v(ae),v(I),v(a),L(()=>{K(w,e(b).label),K(Q,e(b).nodeType),K(Be,`${e(S).length??""} neighbours`)}),E(r,a)};ue(ie,r=>{e(b)&&r(se)})}v(ee),L(()=>{z.disabled=e(x),K(n,e(x)?"Activating…":"Activate"),K(m,e(G))}),xe("keydown",j,r=>r.key==="Enter"&&$()),Ge(j,()=>e(u),r=>d(u,r)),xe("click",z,$),Pe(_,()=>e(F),r=>d(F,r)),E(s,ee),Se()}Ie(["keydown","click"]);export{yt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js.br b/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js.br new file mode 100644 index 0000000..715e2ab Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js.gz b/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js.gz new file mode 100644 index 0000000..6cf01ba Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/4.DPhwyLUO.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js.br b/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js.br deleted file mode 100644 index 1bfb8c8..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js.gz b/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js.gz deleted file mode 100644 index 0f242c1..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js b/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js new file mode 100644 index 0000000..65cdcf5 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js @@ -0,0 +1,3 @@ +import"../chunks/Bzak7iHL.js";import{p as Qe,d as a,e as o,f as He,t as A,g as e,h as D,u as E,r,n as ke,a as Je,s as re}from"../chunks/CpWkWWOo.js";import{d as Ze,a as H,e as ue,s as v}from"../chunks/BlVfL1ME.js";import{a as w,f as I,b as Le}from"../chunks/CHOnp4oo.js";import{i as B}from"../chunks/B4yTwGkE.js";import{e as te,i as Ce}from"../chunks/CGEBXrjl.js";import{s as Ue}from"../chunks/aVbAZ-t7.js";import{s as X}from"../chunks/Cx-f-Pzo.js";import{b as ut}from"../chunks/BnXDGOmJ.js";import{s as i}from"../chunks/A7po6GxK.js";import{p as Ne}from"../chunks/V6gjw5Ec.js";const et=.7,tt=.5,ft="#ef4444",xt="#f59e0b",bt="#fde047";function Pe(n){return n>et?ft:n>tt?xt:bt}function rt(n){return n>et?"strong":n>tt?"moderate":"mild"}const We="#8b5cf6",gt={fact:"#3b82f6",concept:"#8b5cf6",event:"#f59e0b",person:"#10b981",place:"#06b6d4",note:"#6b7280",pattern:"#ec4899",decision:"#ef4444"};function qe(n){return n?gt[n]??We:We}const Xe=5,kt=9;function ht(n){if(!Number.isFinite(n))return Xe;const _=n<0?0:n>1?1:n;return Xe+_*kt}const wt=.12;function Ke(n,_){return _==null||_===n?1:wt}function he(n,_=60){return n==null||typeof n!="string"||_<=0?"":n.length<=_?n:n.slice(0,_-1)+"…"}function jt(n){if(!n||n.length===0)return 0;const _=new Set;for(const x of n)x.memory_a_id&&_.add(x.memory_a_id),x.memory_b_id&&_.add(x.memory_b_id);return _.size}function Mt(n){if(!n||n.length===0)return 0;let _=0;for(const x of n)_+=Math.abs((x.trust_a??0)-(x.trust_b??0));return _/n.length}var It=Le('',1),St=Le(' '),Rt=Le('',1),Tt=I('
          '),At=I('
          '),Et=I('
          '),Dt=I('
          topic:
          '),Ft=I('
          SEVERITYstrong (>0.7)moderate (0.5-0.7)mild (0.3-0.5)
          ');function Gt(n,_){Qe(_,!0);let x=Ne(_,"focusedPairIndex",3,null),F=Ne(_,"width",3,800),G=Ne(_,"height",3,600);const R=E(()=>{const m=F()/2,t=G()/2,k=Math.min(F(),G())*.38;return{cx:m,cy:t,R:k}}),U=E(()=>{const m=[],t=[],k=_.contradictions.length||1;return _.contradictions.forEach((l,y)=>{const j=y/k*Math.PI*2-Math.PI/2,p=.18+l.similarity*.22,g=j-p,d=j+p,L=e(R).R+Math.sin(y*2.3)*18,C=e(R).R+Math.cos(y*1.7)*18,u={x:e(R).cx+Math.cos(g)*L,y:e(R).cy+Math.sin(g)*L,trust:l.trust_a,preview:l.memory_a_preview,type:l.memory_a_type,created:l.memory_a_created,tags:l.memory_a_tags,memoryId:l.memory_a_id,pairIndex:y,side:"a"},M={x:e(R).cx+Math.cos(d)*C,y:e(R).cy+Math.sin(d)*C,trust:l.trust_b,preview:l.memory_b_preview,type:l.memory_b_type,created:l.memory_b_created,tags:l.memory_b_tags,memoryId:l.memory_b_id,pairIndex:y,side:"b"};m.push(u,M);const $=(u.x+M.x)/2,S=(u.y+M.y)/2,T=.55-l.similarity*.25,N=$+(e(R).cx-$)*T,Y=S+(e(R).cy-S)*T,je=1+Math.min(l.trust_a,l.trust_b)*4;t.push({pairIndex:y,path:`M ${u.x.toFixed(1)} ${u.y.toFixed(1)} Q ${N.toFixed(1)} ${Y.toFixed(1)} ${M.x.toFixed(1)} ${M.y.toFixed(1)}`,color:Pe(l.similarity),thickness:je,severity:rt(l.similarity),topic:l.topic,similarity:l.similarity,dateDiff:l.date_diff_days,aPoint:u,bPoint:M,midX:N,midY:Y})}),{nodes:m,arcs:t}});let b=re(null),P=re(null),ie=re(0),le=re(0);function ne(m){const t=m.currentTarget.getBoundingClientRect();D(ie,m.clientX-t.left),D(le,m.clientY-t.top)}function ae(m){_.onSelectPair&&_.onSelectPair(x()===m?null:m)}function me(){var m;(m=_.onSelectPair)==null||m.call(_,null)}var W=Ft(),O=a(W),se=o(a(O)),K=o(se),_e=o(K),oe=o(_e);te(oe,17,()=>e(U).arcs,m=>m.pairIndex,(m,t)=>{const k=E(()=>Ke(e(t).pairIndex,x())),l=E(()=>x()===e(t).pairIndex);var y=It(),j=He(y),p=o(j),g=o(p);A(d=>{i(j,"d",e(t).path),i(j,"stroke",e(t).color),i(j,"stroke-width",e(t).thickness*3),i(j,"stroke-opacity",.08*e(k)),i(p,"d",e(t).path),i(p,"stroke",e(t).color),i(p,"stroke-width",e(t).thickness*(e(l)?1.6:1)),i(p,"stroke-opacity",(e(l)?1:.72)*e(k)),i(p,"aria-label",`contradiction ${e(t).pairIndex+1}: ${e(t).topic??""}`),i(g,"d",e(t).path),i(g,"stroke",e(t).color),i(g,"stroke-width",d),i(g,"stroke-opacity",.85*e(k)),X(g,`animation-duration: ${4+e(t).pairIndex%5}s`)},[()=>Math.max(1,e(t).thickness*.6)]),H("click",p,d=>{d.stopPropagation(),ae(e(t).pairIndex)}),ue("mouseenter",p,()=>D(P,e(t),!0)),ue("mouseleave",p,()=>D(P,null)),H("keydown",p,d=>{d.key==="Enter"&&ae(e(t).pairIndex)}),w(m,y)});var fe=o(oe);te(fe,19,()=>e(U).nodes,(m,t)=>m.memoryId+"-"+m.side+"-"+t,(m,t)=>{const k=E(()=>Ke(e(t).pairIndex,x())),l=E(()=>x()===e(t).pairIndex),y=E(()=>ht(e(t).trust)),j=E(()=>qe(e(t).type));var p=Rt(),g=He(p),d=o(g),L=o(d);{var C=u=>{var M=St(),$=a(M,!0);r(M),A(S=>{i(M,"x",e(t).x),i(M,"y",e(t).y-e(y)-8),v($,S)},[()=>he(e(t).preview,40)]),w(u,M)};B(L,u=>{e(l)&&u(C)})}A(u=>{i(g,"cx",e(t).x),i(g,"cy",e(t).y),i(g,"r",e(y)*2.2),i(g,"fill",e(j)),i(g,"opacity",.12*e(k)),i(d,"cx",e(t).x),i(d,"cy",e(t).y),i(d,"r",e(y)),i(d,"fill",e(j)),i(d,"opacity",e(k)),i(d,"stroke-opacity",e(l)?.85:.25),i(d,"stroke-width",e(l)?2:1),i(d,"aria-label",`memory ${u??""}`)},[()=>he(e(t).preview,40)]),ue("mouseenter",d,()=>D(b,e(t),!0)),ue("mouseleave",d,()=>D(b,null)),H("click",d,u=>{u.stopPropagation(),ae(e(t).pairIndex)}),H("keydown",d,u=>{u.key==="Enter"&&ae(e(t).pairIndex)}),w(m,p)}),ke(),r(O);var we=o(O,2);{var de=m=>{var t=Et(),k=a(t),l=a(k),y=o(l,2),j=a(y,!0);r(y);var p=o(y,2),g=a(p);r(p),r(k);var d=o(k,2),L=a(d,!0);r(d);var C=o(d,2);{var u=S=>{var T=Tt(),N=a(T);r(T),A(()=>v(N,`created ${e(b).created??""}`)),w(S,T)};B(C,S=>{e(b).created&&S(u)})}var M=o(C,2);{var $=S=>{var T=At(),N=a(T,!0);r(T),A(Y=>v(N,Y),[()=>e(b).tags.slice(0,4).join(" · ")]),w(S,T)};B(M,S=>{e(b).tags&&e(b).tags.length>0&&S($)})}r(t),A((S,T,N,Y)=>{X(t,`left: ${S??""}px; top: ${T??""}px;`),X(l,`background: ${N??""}`),v(j,e(b).type??"memory"),v(g,`trust ${Y??""}%`),v(L,e(b).preview)},[()=>Math.max(0,Math.min(e(ie)+12,F()-240)),()=>Math.max(0,Math.min(e(le)-8,G()-120)),()=>qe(e(b).type),()=>(e(b).trust*100).toFixed(0)]),w(m,t)},xe=m=>{var t=Dt(),k=a(t),l=a(k),y=o(l,2),j=a(y);r(y),r(k);var p=o(k,2),g=o(a(p)),d=a(g,!0);r(g),r(p);var L=o(p,2),C=a(L);r(L),r(t),A((u,M,$)=>{X(t,`left: ${u??""}px; top: ${M??""}px;`),X(l,`background: ${e(P).color??""}`),v(j,`${e(P).severity??""} conflict`),v(d,e(P).topic),v(C,`similarity ${$??""}% · ${e(P).dateDiff??""}d apart`)},[()=>Math.max(0,Math.min(e(ie)+12,F()-240)),()=>Math.max(0,Math.min(e(le)-8,G()-120)),()=>(e(P).similarity*100).toFixed(0)]),w(m,t)};B(we,m=>{e(b)?m(de):e(P)&&m(xe,1)})}r(W),A(()=>{X(W,`aspect-ratio: ${F()??""} / ${G()??""};`),i(O,"width",F()),i(O,"height",G()),i(O,"viewBox",`0 0 ${F()??""} ${G()??""}`),i(se,"width",F()),i(se,"height",G()),i(K,"cx",e(R).cx),i(K,"cy",e(R).cy),i(K,"r",e(R).R),i(_e,"cx",e(R).cx),i(_e,"cy",e(R).cy)}),H("mousemove",O,ne),ue("mouseleave",O,()=>{D(b,null),D(P,null)}),H("click",O,me),w(n,W),Je()}Ze(["mousemove","click","keydown"]);var Ot=I(""),Ct=I(""),Nt=I(''),Pt=I(''),Lt=I('
          No contradictions match this filter.
          '),Bt=I('
          No pairs visible.
          '),$t=I(' '),Yt=I('
          '),zt=I(' '),Vt=I('
          '),Ht=I('
          Full memory A
          Full memory B
          '),Ut=I(''),Wt=I('

          Contradiction Constellation

          Where your memory disagrees with itself

          average trust delta
          visible in current filter
          strong conflicts
          ');function or(n,_){Qe(_,!0);const x=[{memory_a_id:"a1",memory_b_id:"b1",memory_a_preview:"Dev server runs on port 3000 (default Vite config)",memory_b_preview:"Dev server moved to port 3002 to avoid conflict",memory_a_type:"fact",memory_b_type:"decision",memory_a_created:"2026-01-14",memory_b_created:"2026-03-22",memory_a_tags:["dev","vite"],memory_b_tags:["dev","vite","decision"],trust_a:.42,trust_b:.91,similarity:.88,date_diff_days:67,topic:"dev server port"},{memory_a_id:"a2",memory_b_id:"b2",memory_a_preview:"Prompt variation helps at higher sampling temperatures",memory_b_preview:"Prompt variation reduced accuracy in the latest benchmark run",memory_a_type:"concept",memory_b_type:"fact",memory_a_created:"2026-03-30",memory_b_created:"2026-04-03",memory_a_tags:["research","prompting"],memory_b_tags:["research","prompting","evidence"],trust_a:.35,trust_b:.88,similarity:.92,date_diff_days:4,topic:"prompt diversity"},{memory_a_id:"a3",memory_b_id:"b3",memory_a_preview:"Use min_p=0.05 for long-form sampling",memory_b_preview:"min_p scheduling failed at high sampling temperatures",memory_a_type:"pattern",memory_b_type:"fact",memory_a_created:"2026-04-01",memory_b_created:"2026-04-05",memory_a_tags:["sampling"],memory_b_tags:["sampling"],trust_a:.58,trust_b:.74,similarity:.81,date_diff_days:4,topic:"min_p sampling"},{memory_a_id:"a4",memory_b_id:"b4",memory_a_preview:"LoRA rank 16 is enough for domain adaptation",memory_b_preview:"LoRA rank 32 consistently outperforms rank 16 on math",memory_a_type:"concept",memory_b_type:"fact",memory_a_created:"2026-02-10",memory_b_created:"2026-04-12",memory_a_tags:["lora","training"],memory_b_tags:["lora","training","nemotron"],trust_a:.48,trust_b:.76,similarity:.74,date_diff_days:61,topic:"LoRA rank"},{memory_a_id:"a5",memory_b_id:"b5",memory_a_preview:"Team prefers Rust for backend services",memory_b_preview:"Project chose Axum + Rust for the dashboard backend",memory_a_type:"note",memory_b_type:"decision",memory_a_created:"2026-01-05",memory_b_created:"2026-02-18",memory_a_tags:["preference","backend"],memory_b_tags:["backend","decision"],trust_a:.81,trust_b:.88,similarity:.42,date_diff_days:44,topic:"backend language"},{memory_a_id:"a6",memory_b_id:"b6",memory_a_preview:"Warm-start from checkpoint saves 8h of training",memory_b_preview:"Warm-start code never loaded the PEFT adapter correctly",memory_a_type:"pattern",memory_b_type:"fact",memory_a_created:"2026-03-11",memory_b_created:"2026-04-16",memory_a_tags:["training","warm-start"],memory_b_tags:["training","warm-start","bug-fix"],trust_a:.55,trust_b:.93,similarity:.79,date_diff_days:36,topic:"warm-start correctness"},{memory_a_id:"a7",memory_b_id:"b7",memory_a_preview:"Three.js force-directed graph runs fine at 5k nodes",memory_b_preview:"WebGL graph stutters above 2k nodes on M1 MacBook Air",memory_a_type:"fact",memory_b_type:"fact",memory_a_created:"2025-12-02",memory_b_created:"2026-03-29",memory_a_tags:["vestige","graph","perf"],memory_b_tags:["vestige","graph","perf"],trust_a:.39,trust_b:.72,similarity:.67,date_diff_days:117,topic:"graph performance"},{memory_a_id:"a8",memory_b_id:"b8",memory_a_preview:"Submit benchmark runs with a 16384 token budget",memory_b_preview:"Latest baseline improved when token budget increased to 32768",memory_a_type:"pattern",memory_b_type:"event",memory_a_created:"2026-04-04",memory_b_created:"2026-04-10",memory_a_tags:["benchmark","tokens"],memory_b_tags:["benchmark","baseline"],trust_a:.31,trust_b:.85,similarity:.73,date_diff_days:6,topic:"token budget"},{memory_a_id:"a9",memory_b_id:"b9",memory_a_preview:"FSRS-6 parameters require ~1k reviews to train",memory_b_preview:"FSRS-6 default parameters work fine out of the box",memory_a_type:"concept",memory_b_type:"concept",memory_a_created:"2026-01-22",memory_b_created:"2026-02-28",memory_a_tags:["fsrs","training"],memory_b_tags:["fsrs"],trust_a:.62,trust_b:.54,similarity:.57,date_diff_days:37,topic:"FSRS parameter tuning"},{memory_a_id:"a10",memory_b_id:"b10",memory_a_preview:"Tailwind 4 requires explicit CSS import only",memory_b_preview:"Tailwind 4 config still supports tailwind.config.js",memory_a_type:"fact",memory_b_type:"fact",memory_a_created:"2026-01-30",memory_b_created:"2026-02-14",memory_a_tags:["tailwind","config"],memory_b_tags:["tailwind","config"],trust_a:.47,trust_b:.33,similarity:.85,date_diff_days:15,topic:"Tailwind 4 config"},{memory_a_id:"a11",memory_b_id:"b11",memory_a_preview:"Dataset API silently ignores invalid source slugs",memory_b_preview:"Dataset API throws an error when source slug is invalid",memory_a_type:"fact",memory_b_type:"concept",memory_a_created:"2026-04-07",memory_b_created:"2026-02-20",memory_a_tags:["api","bug-fix"],memory_b_tags:["api"],trust_a:.89,trust_b:.28,similarity:.91,date_diff_days:46,topic:"API validation"},{memory_a_id:"a12",memory_b_id:"b12",memory_a_preview:"USearch HNSW is 20x faster than FAISS for embeddings",memory_b_preview:"FAISS IVF is the fastest vector index at scale",memory_a_type:"fact",memory_b_type:"concept",memory_a_created:"2026-02-01",memory_b_created:"2025-11-15",memory_a_tags:["vectors","perf"],memory_b_tags:["vectors","perf"],trust_a:.78,trust_b:.36,similarity:.69,date_diff_days:78,topic:"vector index perf"},{memory_a_id:"a13",memory_b_id:"b13",memory_a_preview:"Leaderboard scores weight by top-10 consistency",memory_b_preview:"Leaderboard uses single-best-episode scoring",memory_a_type:"fact",memory_b_type:"fact",memory_a_created:"2026-04-18",memory_b_created:"2026-04-10",memory_a_tags:["leaderboard","scoring"],memory_b_tags:["leaderboard","scoring"],trust_a:.64,trust_b:.52,similarity:.82,date_diff_days:8,topic:"leaderboard scoring"},{memory_a_id:"a14",memory_b_id:"b14",memory_a_preview:"Release notes were planned for 8am ET",memory_b_preview:"Release notes moved to 9am ET after schedule review",memory_a_type:"decision",memory_b_type:"decision",memory_a_created:"2026-03-01",memory_b_created:"2026-04-15",memory_a_tags:["cadence","content"],memory_b_tags:["cadence","content"],trust_a:.5,trust_b:.81,similarity:.58,date_diff_days:45,topic:"posting cadence"},{memory_a_id:"a15",memory_b_id:"b15",memory_a_preview:"Dream cycle consolidates ~50 memories per run",memory_b_preview:"Dream cycle replays closer to 120 memories in practice",memory_a_type:"fact",memory_b_type:"fact",memory_a_created:"2026-02-15",memory_b_created:"2026-04-08",memory_a_tags:["vestige","dream"],memory_b_tags:["vestige","dream"],trust_a:.44,trust_b:.79,similarity:.76,date_diff_days:52,topic:"dream cycle count"},{memory_a_id:"a16",memory_b_id:"b16",memory_a_preview:"Never commit API keys to git; use .env files",memory_b_preview:"Environment secrets should live in a 1Password vault",memory_a_type:"pattern",memory_b_type:"pattern",memory_a_created:"2025-10-11",memory_b_created:"2026-03-20",memory_a_tags:["security","secrets"],memory_b_tags:["security","secrets"],trust_a:.72,trust_b:.64,similarity:.48,date_diff_days:160,topic:"secret storage"}];let F=re("all"),G=re("");const R=E(()=>Array.from(new Set(x.map(s=>s.topic))).sort()),U=E(()=>{switch(e(F)){case"recent":{const s=new Date("2026-04-20").getTime(),c=10080*60*1e3;return x.filter(h=>{const f=h.memory_a_created?new Date(h.memory_a_created).getTime():0,q=h.memory_b_created?new Date(h.memory_b_created).getTime():0;return s-Math.max(f,q)<=c})}case"high-trust":return x.filter(s=>Math.min(s.trust_a,s.trust_b)>.6);case"topic":return e(G)?x.filter(s=>s.topic===e(G)):x;case"all":default:return x}});let b=re(null);function P(s){D(b,s,!0)}const ie=E(()=>jt(x)),le=E(()=>Mt(x)),ne=E(()=>{const s=new Map(x.map((c,h)=>[c.memory_a_id+"|"+c.memory_b_id,h]));return e(U).map(c=>({orig:s.get(c.memory_a_id+"|"+c.memory_b_id)??0,c}))});function ae(s){D(b,e(b)===s?null:s,!0)}var me=Wt(),W=o(a(me),2),O=a(W),se=a(O);se.textContent="47";var K=o(se,2),_e=a(K);r(K),r(O);var oe=o(O,2),fe=a(oe),we=a(fe,!0);r(fe),ke(2),r(oe);var de=o(oe,2),xe=a(de),m=a(xe,!0);r(xe),ke(2),r(de);var t=o(de,2),k=a(t),l=a(k,!0);r(k),ke(2),r(t),r(W);var y=o(W,2),j=a(y);te(j,16,()=>[{id:"all",label:"All"},{id:"recent",label:"Recent (7d)"},{id:"high-trust",label:"High trust (>60%)"},{id:"topic",label:"By topic"}],s=>s.id,(s,c)=>{var h=Ot(),f=a(h,!0);r(h),A(()=>{Ue(h,1,`px-3 py-1.5 rounded-lg text-xs border transition + ${e(F)===c.id?"bg-synapse/15 border-synapse/40 text-synapse-glow":"border-subtle/30 text-dim hover:text-text hover:bg-white/[0.03]"}`),v(f,c.label)}),H("click",h,()=>{D(F,c.id,!0),D(b,null)}),w(s,h)});var p=o(j,2);{var g=s=>{var c=Nt(),h=a(c);h.value=h.__value="";var f=o(h);te(f,17,()=>e(R),Ce,(q,z)=>{var V=Ct(),be=a(V,!0);r(V);var Q={};A(()=>{v(be,e(z)),Q!==(Q=e(z))&&(V.value=(V.__value=e(z))??"")}),w(q,V)}),r(c),ut(c,()=>e(G),q=>D(G,q)),w(s,c)};B(p,s=>{e(F)==="topic"&&s(g)})}var d=o(p,2);{var L=s=>{var c=Pt();H("click",c,()=>D(b,null)),w(s,c)};B(d,s=>{e(b)!==null&&s(L)})}r(y);var C=o(y,2),u=a(C),M=a(u);{var $=s=>{var c=Lt();w(s,c)},S=s=>{Gt(s,{get contradictions(){return e(U)},get focusedPairIndex(){return e(b)},onSelectPair:P,width:800,height:600})};B(M,s=>{e(U).length===0?s($):s(S,!1)})}r(u);var T=o(u,2),N=a(T),Y=o(a(N),2),je=a(Y,!0);r(Y),r(N);var Be=o(N,2);{var at=s=>{var c=Bt();w(s,c)};B(Be,s=>{e(ne).length===0&&s(at)})}var st=o(Be,2);te(st,19,()=>e(ne),s=>s.c.memory_a_id+"|"+s.c.memory_b_id,(s,c,h)=>{const f=E(()=>e(c).c),q=E(()=>e(b)===e(h));var z=Ut(),V=a(z),be=a(V),Q=o(be,2),ot=a(Q,!0);r(Q);var $e=o(Q,2),it=a($e);r($e),r(V);var Me=o(V,2),lt=a(Me,!0);r(Me);var Ie=o(Me,2),Se=a(Ie),Re=o(a(Se),2),nt=a(Re,!0);r(Re);var Ye=o(Re,2),mt=a(Ye);r(Ye),r(Se);var ze=o(Se,2),Te=o(a(ze),2),_t=a(Te,!0);r(Te);var Ve=o(Te,2),dt=a(Ve);r(Ve),r(ze),r(Ie);var ct=o(Ie,2);{var vt=ce=>{var ve=Ht(),pe=o(a(ve),2),Ae=a(pe,!0);r(pe);var ge=o(pe,2);{var Ee=J=>{var Z=Yt();te(Z,21,()=>e(f).memory_a_tags,Ce,(Fe,Ge)=>{var ee=$t(),Oe=a(ee,!0);r(ee),A(()=>v(Oe,e(Ge))),w(Fe,ee)}),r(Z),w(J,Z)};B(ge,J=>{e(f).memory_a_tags&&e(f).memory_a_tags.length>0&&J(Ee)})}var ye=o(ge,4),De=a(ye,!0);r(ye);var pt=o(ye,2);{var yt=J=>{var Z=Vt();te(Z,21,()=>e(f).memory_b_tags,Ce,(Fe,Ge)=>{var ee=zt(),Oe=a(ee,!0);r(ee),A(()=>v(Oe,e(Ge))),w(Fe,ee)}),r(Z),w(J,Z)};B(pt,J=>{e(f).memory_b_tags&&e(f).memory_b_tags.length>0&&J(yt)})}r(ve),A(()=>{v(Ae,e(f).memory_a_preview),v(De,e(f).memory_b_preview)}),w(ce,ve)};B(ct,ce=>{e(q)&&ce(vt)})}r(z),A((ce,ve,pe,Ae,ge,Ee,ye,De)=>{Ue(z,1,`w-full text-left p-3 rounded-xl border transition + ${e(q)?"bg-synapse/10 border-synapse/40 shadow-[0_0_12px_rgba(99,102,241,0.18)]":"border-subtle/20 hover:border-synapse/30 hover:bg-white/[0.02]"}`),X(be,`background: ${ce??""}`),X(Q,`color: ${ve??""}`),v(ot,pe),v(it,`${Ae??""}% sim · ${e(f).date_diff_days??""}d`),v(lt,e(f).topic),v(nt,ge),v(mt,`${Ee??""}%`),v(_t,ye),v(dt,`${De??""}%`)},[()=>Pe(e(f).similarity),()=>Pe(e(f).similarity),()=>rt(e(f).similarity),()=>(e(f).similarity*100).toFixed(0),()=>he(e(f).memory_a_preview),()=>(e(f).trust_a*100).toFixed(0),()=>he(e(f).memory_b_preview),()=>(e(f).trust_b*100).toFixed(0)]),H("click",z,()=>ae(e(h))),w(s,z)}),r(T),r(C),r(me),A((s,c,h)=>{v(_e,`contradictions across ${s??""} memories`),v(we,c),v(m,e(U).length),v(l,h),v(je,e(ne).length)},[()=>e(ie).toLocaleString(),()=>e(le).toFixed(2),()=>e(U).filter(s=>s.similarity>.7).length]),w(n,me),Je()}Ze(["click"]);export{or as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js.br b/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js.br new file mode 100644 index 0000000..d2b9243 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js.gz b/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js.gz new file mode 100644 index 0000000..69c8947 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/5.C0AYWqwr.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js b/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js new file mode 100644 index 0000000..8d4e000 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js @@ -0,0 +1,14 @@ +import"../chunks/Bzak7iHL.js";import{p as qe,d as a,r as t,e as i,g as e,f as he,u as b,t as R,a as Se,n as X,h as de,s as be,W as Ge,aG as Ae}from"../chunks/CpWkWWOo.js";import{s as x,d as Ye,a as pe}from"../chunks/BlVfL1ME.js";import{c as Fe,a as m,f as _,b as Ve,t as Ie}from"../chunks/CHOnp4oo.js";import{i as j}from"../chunks/B4yTwGkE.js";import{e as ge}from"../chunks/CGEBXrjl.js";import{h as We}from"../chunks/C4h_mRt2.js";import{s as A,r as Be,a as Xe}from"../chunks/A7po6GxK.js";import{s as _e}from"../chunks/aVbAZ-t7.js";import{a as ze}from"../chunks/554JRhq6.js";import{s as ae}from"../chunks/Cx-f-Pzo.js";import{p as Ue}from"../chunks/V6gjw5Ec.js";import{b as Je}from"../chunks/DGcYlAAw.js";const Te=5,je=["Replay","Cross-reference","Strengthen","Prune","Transfer"];function Oe(v){if(!Number.isFinite(v))return 1;const n=Math.floor(v);return n<1?1:n>Te?Te:n}const Ke=.3,Qe=.7;function Ze(v){const n=ye(v);return n>Qe?"high":n1?1:v}function $e(v){return v==null||!Number.isFinite(v)||v<0?"0ms":v<1e3?`${Math.round(v)}ms`:`${(v/1e3).toFixed(2)}s`}function et(v){const n=ye(v);return`${Math.round(n*100)}%`}function tt(v,n=""){return`${n}/memories/${v}`}function st(v,n=2){return!v||v.length===0?[]:v.slice(0,Math.max(0,n))}function at(v,n=2){return v?Math.max(0,v.length-n):0}function rt(v){return v?v.length>8?v.slice(0,8):v:""}var nt=_('
          Episodic hippocampus
          Semantic cortex
          ',1),it=Ve(''),vt=_('
          '),lt=_(''),ot=_('Replaying memories'),ct=_('New connections found: '),dt=_('Strengthened: '),ut=_('Compressed: '),ft=_('Connections persisted: Insights: ',1),mt=_('
          ');function pt(v,n){qe(n,!0);const N=[{num:1,name:"Replay",color:"#818cf8",desc:"Hippocampal replay: tagged memories surface for consolidation."},{num:2,name:"Cross-reference",color:"#a855f7",desc:"Semantic proximity check — new edges discovered across memories."},{num:3,name:"Strengthen",color:"#c084fc",desc:"Co-activated memories strengthen; FSRS stability grows."},{num:4,name:"Prune",color:"#ef4444",desc:"Low-retention redundant memories compressed or released."},{num:5,name:"Transfer",color:"#10b981",desc:"Episodic → semantic consolidation (hippocampus → cortex)."}];let l=b(()=>Oe(n.stage)),f=b(()=>N[e(l)-1]),q=b(()=>{if(!n.dreamResult)return 8;const s=n.dreamResult.memoriesReplayed??8;return Math.max(6,Math.min(12,s))}),re=b(()=>{var r;if(!n.dreamResult)return 5;const s=((r=n.dreamResult.stats)==null?void 0:r.newConnectionsFound)??5;return Math.max(3,Math.min(e(q),s))}),z=b(()=>{var r;if(!n.dreamResult)return Math.ceil(e(q)*.5);const s=((r=n.dreamResult.stats)==null?void 0:r.memoriesStrengthened)??Math.ceil(e(q)*.5);return Math.max(1,Math.min(e(q),s))}),ne=b(()=>{var r;if(!n.dreamResult)return Math.ceil(e(q)*.25);const s=((r=n.dreamResult.stats)==null?void 0:r.memoriesCompressed)??Math.ceil(e(q)*.25);return Math.max(1,Math.min(Math.floor(e(q)/2),s))});function C(s,r=0){const d=Math.sin((s+1)*9301+49297+r*233)*233280;return d-Math.floor(d)}let U=b(()=>{const s=[],r=Math.ceil(Math.sqrt(e(q))),d=Math.ceil(e(q)/r);for(let c=0;c{const s=[],r=e(U).length;for(let d=0;d{var r=nt();X(4),m(s,r)};j(le,s=>{e(l)===5&&s(oe)})}var ee=i(le,2);ge(ee,23,()=>e(L),(s,r)=>s.a+"-"+s.b+"-"+r,(s,r,d)=>{const c=b(()=>e(U)[e(r).a]),p=b(()=>e(U)[e(r).b]);var u=Fe(),k=he(u);{var w=g=>{const M=b(()=>I(e(c))),h=b(()=>F(e(c))),V=b(()=>I(e(p))),ke=b(()=>F(e(p)));var E=it();R(()=>{A(E,"x1",e(M)),A(E,"y1",e(h)),A(E,"x2",e(V)),A(E,"y2",e(ke)),A(E,"stroke",e(f).color),A(E,"stroke-width",e(l)===2?.25:e(l)===3?.35:.2),A(E,"stroke-opacity",e(l)<2?0:e(l)===4?.25:e(l)===5?.15:.6),A(E,"stroke-dasharray",e(l)===2?"1.2 0.8":"none"),ae(E,`--edge-delay: ${e(d)*80}ms`)}),m(g,E)};j(k,g=>{e(c)&&e(p)&&g(w)})}m(s,u)}),t(ee);var S=i(ee,2);ge(S,17,()=>e(U),s=>s.id,(s,r)=>{var d=vt();let c;R((p,u,k,w,g)=>{c=_e(d,1,"memory-card svelte-1cq1ntk",null,c,{"is-pulsing":e(l)===3&&e(r).strengthened,"is-pruning":e(l)===4&&e(r).pruned,"is-transferring":e(l)===5,"semantic-side":e(l)===5&&e(r).transferIsSemantic}),ae(d,` + left: ${p??""}%; + top: ${u??""}%; + opacity: ${k??""}; + --card-scale: ${w??""}; + --card-delay: ${e(r).id*40}ms; + --card-hue: ${g??""}deg; + `)},[()=>I(e(r)),()=>F(e(r)),()=>J(e(r)),()=>K(e(r)),()=>C(e(r).id,3)*60-30]),m(s,d)});var D=i(S,2);{var te=s=>{var r=lt();m(s,r)};j(D,s=>{e(l)===1&&s(te)})}t(P);var se=i(P,2),ue=a(se);{var ce=s=>{var r=ot(),d=i(a(r)),c=a(d,!0);t(d),X(),t(r),R(()=>{var p;return x(c,((p=n.dreamResult)==null?void 0:p.memoriesReplayed)??e(q))}),m(s,r)},T=s=>{var r=ct(),d=i(a(r)),c=a(d,!0);t(d),t(r),R(()=>{var p,u;return x(c,((u=(p=n.dreamResult)==null?void 0:p.stats)==null?void 0:u.newConnectionsFound)??e(re))}),m(s,r)},H=s=>{var r=dt(),d=i(a(r)),c=a(d,!0);t(d),t(r),R(()=>{var p,u;return x(c,((u=(p=n.dreamResult)==null?void 0:p.stats)==null?void 0:u.memoriesStrengthened)??e(z))}),m(s,r)},G=s=>{var r=ut(),d=i(a(r)),c=a(d,!0);t(d),t(r),R(()=>{var p,u;return x(c,((u=(p=n.dreamResult)==null?void 0:p.stats)==null?void 0:u.memoriesCompressed)??e(ne))}),m(s,r)},fe=s=>{var r=ft(),d=he(r),c=i(a(d)),p=a(c,!0);t(c),t(d);var u=i(d,2),k=i(a(u)),w=a(k,!0);t(k),t(u),R(()=>{var g,M,h;x(p,((g=n.dreamResult)==null?void 0:g.connectionsPersisted)??0),x(w,((h=(M=n.dreamResult)==null?void 0:M.stats)==null?void 0:h.insightsGenerated)??0)}),m(s,r)};j(ue,s=>{e(l)===1?s(ce):e(l)===2?s(T,1):e(l)===3?s(H,2):e(l)===4?s(G,3):e(l)===5&&s(fe,4)})}t(se),t(ie),R(()=>{ae(Y,` + background: color-mix(in srgb, ${e(f).color??""} 20%, transparent); + color: ${e(f).color??""}; + border: 1.5px solid ${e(f).color??""}; + box-shadow: 0 0 16px color-mix(in srgb, ${e(f).color??""} 40%, transparent); + `),x(me,e(f).num),x(o,e(f).name),x(O,e(f).desc),x(B,`Stage ${e(f).num??""} / 5`),ae(P,`--stage-color: ${e(f).color??""}`),A(P,"aria-label",`Dream stage ${e(f).num??""} — ${e(f).name??""}`)}),m(v,ie),Se()}var gt=_(' novel'),xt=_(' '),bt=_(' '),ht=_('
          Sources
          '),_t=_('

          Novelty
          Confidence
          ');function yt(v,n){qe(n,!0);let N=Ue(n,"index",3,0),l=b(()=>ye(n.insight.noveltyScore)),f=b(()=>ye(n.insight.confidence)),q=b(()=>Ze(n.insight.noveltyScore)),re=b(()=>e(q)==="high"),z=b(()=>e(q)==="low"),ne=b(()=>st(n.insight.sourceMemories,2)),C=b(()=>at(n.insight.sourceMemories,2));const U={connection:"#818cf8",pattern:"#ec4899",contradiction:"#ef4444",synthesis:"#c084fc",emergence:"#f59e0b",cluster:"#06b6d4"};let L=b(()=>{var S;return U[((S=n.insight.type)==null?void 0:S.toLowerCase())??""]??"#a855f7"});var I=_t();let F;var J=a(I),K=a(J),ie=a(K,!0);t(K);var ve=i(K,2);{var Q=S=>{var D=gt();m(S,D)};j(ve,S=>{e(re)&&S(Q)})}t(J);var Y=i(J,2),me=a(Y,!0);t(Y);var Z=i(Y,2),W=a(Z),o=i(a(W),2),y=a(o,!0);t(o),t(W);var O=i(W,2),$=a(O);t(O),t(Z);var B=i(Z,2),P=i(a(B),2),le=a(P,!0);t(P),t(B);var oe=i(B,2);{var ee=S=>{var D=ht(),te=a(D),se=i(a(te));{var ue=T=>{var H=xt(),G=a(H);t(H),R(()=>x(G,`(+${e(C)??""})`)),m(T,H)};j(se,T=>{e(C)>0&&T(ue)})}t(te);var ce=i(te,2);ge(ce,20,()=>e(ne),T=>T,(T,H)=>{var G=bt(),fe=a(G,!0);t(G),R((s,r)=>{A(G,"href",s),A(G,"title",`Open memory ${H??""}`),x(fe,r)},[()=>tt(H,Je),()=>rt(H)]),m(T,G)}),t(ce),t(D),m(S,D)};j(oe,S=>{e(ne).length>0&&S(ee)})}t(I),R((S,D)=>{F=_e(I,1,"insight-card glass-panel rounded-xl p-4 space-y-3 svelte-1y17hsl",null,F,{"high-novelty":e(re),"low-novelty":e(z)}),ae(I,`--insight-color: ${e(L)??""}; --enter-delay: ${N()*60}ms`),ae(K,`background: ${e(L)??""}22; color: ${e(L)??""}; border: 1px solid ${e(L)??""}55`),x(ie,n.insight.type??"insight"),x(me,n.insight.insight),x(y,S),ae($,`width: ${e(l)*100}%; background: linear-gradient(90deg, ${e(L)??""}, var(--color-dream-glow))`),ae(P,`color: ${e(f)>.7?"#10b981":e(f)>.4?"#f59e0b":"#ef4444"}`),x(le,D)},[()=>e(l).toFixed(2),()=>et(e(f))]),m(v,I),Se()}var kt=_(' Dreaming...',1),wt=_(' Dream Now',1),qt=_('
          '),St=_('

          No dream yet.

          Click Dream Now to begin.

          '),Mt=_(''),Rt=_('
          '),Ct=_('
          Replayed
          Connections Found
          Connections Persisted
          Insights
          Duration
          '),Dt=_('
          ',1),Nt=_(`

          Dream Cinema

          Scrub through Vestige's 5-stage consolidation cycle. Replay, cross-reference, + strengthen, prune, transfer. Watch episodic become semantic.

          `);function Wt(v,n){qe(n,!0);let N=be(null),l=be(1),f=be(!1),q=be(null),re=b(()=>e(N)!==null),z=b(()=>{const o=e(N);return o?[...o.insights].sort((y,O)=>(O.noveltyScore??0)-(y.noveltyScore??0)):[]});async function ne(){if(!e(f)){de(f,!0),de(q,null);try{const o=await ze.dream();de(N,o,!0),de(l,1)}catch(o){de(q,o instanceof Error?o.message:"Dream failed",!0)}finally{de(f,!1)}}}function C(o){de(l,Oe(o),!0)}function U(o){const y=Number(o.currentTarget.value);C(y)}var L=Nt();We("1fv2vo0",o=>{Ge(()=>{Ae.title="Dream Cinema · Vestige"})});var I=a(L),F=i(a(I),2);let J;var K=a(F);{var ie=o=>{var y=kt();X(2),m(o,y)},ve=o=>{var y=wt();X(2),m(o,y)};j(K,o=>{e(f)?o(ie):o(ve,!1)})}t(F),t(I);var Q=i(I,2);{var Y=o=>{var y=qt(),O=a(y,!0);t(y),R(()=>x(O,e(q))),m(o,y)};j(Q,o=>{e(q)&&o(Y)})}var me=i(Q,2);{var Z=o=>{var y=St();m(o,y)},W=o=>{var y=Dt(),O=he(y),$=a(O),B=a($),P=a(B);t(B);var le=i(B,2),oe=a(le),ee=i(oe,2);t(le),t($);var S=i($,2),D=a(S);Be(D);var te=i(D,2);ge(te,22,()=>je,u=>u,(u,k,w)=>{var g=Mt();let M;var h=i(a(g),2),V=a(h);t(h),t(g),R(()=>{M=_e(g,1,"tick svelte-1fv2vo0",null,M,{active:e(l)===e(w)+1,passed:e(l)>e(w)+1}),g.disabled=e(f),x(V,`${e(w)+1}. ${k??""}`)}),pe("click",g,()=>C(e(w)+1)),m(u,g)}),t(te),t(S),t(O);var se=i(O,2),ue=a(se);pt(ue,{get stage(){return e(l)},get dreamResult(){return e(N)}});var ce=i(ue,2),T=a(ce),H=i(a(T),2),G=a(H);t(H),t(T);var fe=i(T,2),s=a(fe);{var r=u=>{var k=Rt(),w=a(k);{var g=h=>{var V=Ie("Dreaming...");m(h,V)},M=h=>{var V=Ie("No insights generated this cycle.");m(h,V)};j(w,h=>{e(f)?h(g):h(M,!1)})}t(k),m(u,k)},d=u=>{var k=Fe(),w=he(k);ge(w,19,()=>e(z),(g,M)=>{var h;return M+"-"+(((h=g.insight)==null?void 0:h.slice(0,32))??"")},(g,M,h)=>{yt(g,{get insight(){return e(M)},get index(){return e(h)}})}),m(u,k)};j(s,u=>{e(z).length===0?u(r):u(d,!1)})}t(fe),t(ce),t(se);var c=i(se,2);{var p=u=>{var k=Ct(),w=a(k),g=a(w),M=a(g,!0);t(g),X(2),t(w);var h=i(w,2),V=a(h),ke=a(V,!0);t(V),X(2),t(h);var E=i(h,2),Me=a(E),He=a(Me,!0);t(Me),X(2),t(E);var we=i(E,2),Re=a(we),Le=a(Re,!0);t(Re),X(2),t(we);var Ce=i(we,2),De=a(Ce),Pe=a(De,!0);t(De),X(2),t(Ce),t(k),R(xe=>{var Ne,Ee;x(M,e(N).memoriesReplayed??0),x(ke,((Ne=e(N).stats)==null?void 0:Ne.newConnectionsFound)??0),x(He,e(N).connectionsPersisted??0),x(Le,((Ee=e(N).stats)==null?void 0:Ee.insightsGenerated)??0),x(Pe,xe)},[()=>{var xe;return $e((xe=e(N).stats)==null?void 0:xe.durationMs)}]),m(u,k)};j(c,u=>{e(N)&&u(p)})}R(()=>{x(P,`Stage ${e(l)??""} · ${je[e(l)-1]??""}`),oe.disabled=e(l)<=1||e(f),ee.disabled=e(l)>=5||e(f),Xe(D,e(l)),D.disabled=e(f),x(G,`${e(z).length??""} total · by novelty`)}),pe("click",oe,()=>C(e(l)-1)),pe("click",ee,()=>C(e(l)+1)),pe("input",D,U),m(o,y)};j(me,o=>{!e(re)&&!e(f)?o(Z):o(W,!1)})}t(L),R(()=>{F.disabled=e(f),J=_e(F,1,"dream-button svelte-1fv2vo0",null,J,{"is-dreaming":e(f)})}),pe("click",F,ne),m(v,L),Se()}Ye(["click","input"]);export{Wt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js.br b/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js.br new file mode 100644 index 0000000..c2dd190 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js.gz b/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js.gz new file mode 100644 index 0000000..b1295da Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/6.BD0AetaD.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js b/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js deleted file mode 100644 index 4c90c4f..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js +++ /dev/null @@ -1,4113 +0,0 @@ -var Cc=Object.defineProperty;var Pc=(i,t,e)=>t in i?Cc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var zt=(i,t,e)=>Pc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as Ll,a as Ul}from"../chunks/DWVWfZUn.js";import{p as fs,I as Dc,a as ps,e as At,d as Dt,O as Lc,r as bt,t as on,g as V,u as ei,f as Il,s as De,h as te,c as Uc}from"../chunks/VE8Jor13.js";import{s as me,d as Nl,a as Ge}from"../chunks/DHnEMX8z.js";import{i as Zn}from"../chunks/JkhlGLjU.js";import{e as cr,i as sa}from"../chunks/ByItJEsC.js";import{a as we,f as Ue,c as Ic}from"../chunks/7UNxJI5L.js";import{s as Pe,r as Fl}from"../chunks/Cu3VmnGp.js";import{s as Er}from"../chunks/BR2EHpd7.js";import{s as $a}from"../chunks/ussr1V5_.js";import{b as Ol}from"../chunks/BRHZEveZ.js";import{b as Bl}from"../chunks/B5Pq2mnD.js";import{s as Nc,a as Fc}from"../chunks/AcZBvMXu.js";import{b as Oc}from"../chunks/DUtaznkq.js";import{b as Bc}from"../chunks/DHakDdar.js";import{p as cs}from"../chunks/ykT2B6d3.js";import{N as zl}from"../chunks/BNytumrp.js";import"../chunks/CrlWs-6R.js";import{i as zc}from"../chunks/jyeIy8pa.js";import{a as Zi}from"../chunks/DcQGRi49.js";import{e as kc}from"../chunks/XIUN5r_Y.js";/** - * @license - * Copyright 2010-2024 Three.js Authors - * SPDX-License-Identifier: MIT - */const Ja="172",Li={ROTATE:0,DOLLY:1,PAN:2},Ci={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},Hc=0,go=1,Vc=2,kl=1,Gc=2,En=3,kn=0,We=1,dn=2,Tn=0,Ui=1,Be=2,_o=3,vo=4,Wc=5,$n=100,Xc=101,Yc=102,qc=103,jc=104,Zc=200,Kc=201,$c=202,Jc=203,ra=204,aa=205,Qc=206,th=207,eh=208,nh=209,ih=210,sh=211,rh=212,ah=213,oh=214,oa=0,la=1,ca=2,Oi=3,ha=4,ua=5,da=6,fa=7,Hl=0,lh=1,ch=2,Bn=0,hh=1,uh=2,dh=3,Vl=4,fh=5,ph=6,mh=7,Gl=300,Bi=301,zi=302,pa=303,ma=304,vr=306,ga=1e3,Qn=1001,_a=1002,Ze=1003,gh=1004,Es=1005,pn=1006,br=1007,ti=1008,Rn=1009,Wl=1010,Xl=1011,us=1012,Qa=1013,ii=1014,mn=1015,wn=1016,to=1017,eo=1018,ki=1020,Yl=35902,ql=1021,jl=1022,ln=1023,Zl=1024,Kl=1025,Ii=1026,Hi=1027,no=1028,io=1029,$l=1030,so=1031,ro=1033,tr=33776,er=33777,nr=33778,ir=33779,va=35840,xa=35841,Ma=35842,Sa=35843,ya=36196,Ea=37492,ba=37496,Ta=37808,wa=37809,Aa=37810,Ra=37811,Ca=37812,Pa=37813,Da=37814,La=37815,Ua=37816,Ia=37817,Na=37818,Fa=37819,Oa=37820,Ba=37821,sr=36492,za=36494,ka=36495,Jl=36283,Ha=36284,Va=36285,Ga=36286,_h=3200,vh=3201,Ql=0,xh=1,On="",Qe="srgb",Vi="srgb-linear",hr="linear",re="srgb",ci=7680,xo=519,Mh=512,Sh=513,yh=514,tc=515,Eh=516,bh=517,Th=518,wh=519,Wa=35044,Mo="300 es",bn=2e3,ur=2001;class ri{addEventListener(t,e){this._listeners===void 0&&(this._listeners={});const n=this._listeners;n[t]===void 0&&(n[t]=[]),n[t].indexOf(e)===-1&&n[t].push(e)}hasEventListener(t,e){if(this._listeners===void 0)return!1;const n=this._listeners;return n[t]!==void 0&&n[t].indexOf(e)!==-1}removeEventListener(t,e){if(this._listeners===void 0)return;const s=this._listeners[t];if(s!==void 0){const r=s.indexOf(e);r!==-1&&s.splice(r,1)}}dispatchEvent(t){if(this._listeners===void 0)return;const n=this._listeners[t.type];if(n!==void 0){t.target=this;const s=n.slice(0);for(let r=0,a=s.length;r>8&255]+Re[i>>16&255]+Re[i>>24&255]+"-"+Re[t&255]+Re[t>>8&255]+"-"+Re[t>>16&15|64]+Re[t>>24&255]+"-"+Re[e&63|128]+Re[e>>8&255]+"-"+Re[e>>16&255]+Re[e>>24&255]+Re[n&255]+Re[n>>8&255]+Re[n>>16&255]+Re[n>>24&255]).toLowerCase()}function Xt(i,t,e){return Math.max(t,Math.min(e,i))}function Ah(i,t){return(i%t+t)%t}function Tr(i,t,e){return(1-e)*i+e*t}function fn(i,t){switch(t.constructor){case Float32Array:return i;case Uint32Array:return i/4294967295;case Uint16Array:return i/65535;case Uint8Array:return i/255;case Int32Array:return Math.max(i/2147483647,-1);case Int16Array:return Math.max(i/32767,-1);case Int8Array:return Math.max(i/127,-1);default:throw new Error("Invalid component type.")}}function ae(i,t){switch(t.constructor){case Float32Array:return i;case Uint32Array:return Math.round(i*4294967295);case Uint16Array:return Math.round(i*65535);case Uint8Array:return Math.round(i*255);case Int32Array:return Math.round(i*2147483647);case Int16Array:return Math.round(i*32767);case Int8Array:return Math.round(i*127);default:throw new Error("Invalid component type.")}}const Rh={DEG2RAD:rr};class Mt{constructor(t=0,e=0){Mt.prototype.isVector2=!0,this.x=t,this.y=e}get width(){return this.x}set width(t){this.x=t}get height(){return this.y}set height(t){this.y=t}set(t,e){return this.x=t,this.y=e,this}setScalar(t){return this.x=t,this.y=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y)}copy(t){return this.x=t.x,this.y=t.y,this}add(t){return this.x+=t.x,this.y+=t.y,this}addScalar(t){return this.x+=t,this.y+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this}subScalar(t){return this.x-=t,this.y-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this}multiply(t){return this.x*=t.x,this.y*=t.y,this}multiplyScalar(t){return this.x*=t,this.y*=t,this}divide(t){return this.x/=t.x,this.y/=t.y,this}divideScalar(t){return this.multiplyScalar(1/t)}applyMatrix3(t){const e=this.x,n=this.y,s=t.elements;return this.x=s[0]*e+s[3]*n+s[6],this.y=s[1]*e+s[4]*n+s[7],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this}clamp(t,e){return this.x=Xt(this.x,t.x,e.x),this.y=Xt(this.y,t.y,e.y),this}clampScalar(t,e){return this.x=Xt(this.x,t,e),this.y=Xt(this.y,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Xt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this}negate(){return this.x=-this.x,this.y=-this.y,this}dot(t){return this.x*t.x+this.y*t.y}cross(t){return this.x*t.y-this.y*t.x}lengthSq(){return this.x*this.x+this.y*this.y}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)}normalize(){return this.divideScalar(this.length()||1)}angle(){return Math.atan2(-this.y,-this.x)+Math.PI}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(e===0)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Xt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y;return e*e+n*n}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this}equals(t){return t.x===this.x&&t.y===this.y}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this}rotateAround(t,e){const n=Math.cos(e),s=Math.sin(e),r=this.x-t.x,a=this.y-t.y;return this.x=r*n-a*s+t.x,this.y=r*s+a*n+t.y,this}random(){return this.x=Math.random(),this.y=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y}}class Bt{constructor(t,e,n,s,r,a,o,l,c){Bt.prototype.isMatrix3=!0,this.elements=[1,0,0,0,1,0,0,0,1],t!==void 0&&this.set(t,e,n,s,r,a,o,l,c)}set(t,e,n,s,r,a,o,l,c){const h=this.elements;return h[0]=t,h[1]=s,h[2]=o,h[3]=e,h[4]=r,h[5]=l,h[6]=n,h[7]=a,h[8]=c,this}identity(){return this.set(1,0,0,0,1,0,0,0,1),this}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],this}extractBasis(t,e,n){return t.setFromMatrix3Column(this,0),e.setFromMatrix3Column(this,1),n.setFromMatrix3Column(this,2),this}setFromMatrix4(t){const e=t.elements;return this.set(e[0],e[4],e[8],e[1],e[5],e[9],e[2],e[6],e[10]),this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,s=e.elements,r=this.elements,a=n[0],o=n[3],l=n[6],c=n[1],h=n[4],d=n[7],p=n[2],u=n[5],g=n[8],v=s[0],m=s[3],f=s[6],T=s[1],b=s[4],S=s[7],D=s[2],w=s[5],R=s[8];return r[0]=a*v+o*T+l*D,r[3]=a*m+o*b+l*w,r[6]=a*f+o*S+l*R,r[1]=c*v+h*T+d*D,r[4]=c*m+h*b+d*w,r[7]=c*f+h*S+d*R,r[2]=p*v+u*T+g*D,r[5]=p*m+u*b+g*w,r[8]=p*f+u*S+g*R,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[3]*=t,e[6]*=t,e[1]*=t,e[4]*=t,e[7]*=t,e[2]*=t,e[5]*=t,e[8]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8];return e*a*h-e*o*c-n*r*h+n*o*l+s*r*c-s*a*l}invert(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8],d=h*a-o*c,p=o*l-h*r,u=c*r-a*l,g=e*d+n*p+s*u;if(g===0)return this.set(0,0,0,0,0,0,0,0,0);const v=1/g;return t[0]=d*v,t[1]=(s*c-h*n)*v,t[2]=(o*n-s*a)*v,t[3]=p*v,t[4]=(h*e-s*l)*v,t[5]=(s*r-o*e)*v,t[6]=u*v,t[7]=(n*l-c*e)*v,t[8]=(a*e-n*r)*v,this}transpose(){let t;const e=this.elements;return t=e[1],e[1]=e[3],e[3]=t,t=e[2],e[2]=e[6],e[6]=t,t=e[5],e[5]=e[7],e[7]=t,this}getNormalMatrix(t){return this.setFromMatrix4(t).invert().transpose()}transposeIntoArray(t){const e=this.elements;return t[0]=e[0],t[1]=e[3],t[2]=e[6],t[3]=e[1],t[4]=e[4],t[5]=e[7],t[6]=e[2],t[7]=e[5],t[8]=e[8],this}setUvTransform(t,e,n,s,r,a,o){const l=Math.cos(r),c=Math.sin(r);return this.set(n*l,n*c,-n*(l*a+c*o)+a+t,-s*c,s*l,-s*(-c*a+l*o)+o+e,0,0,1),this}scale(t,e){return this.premultiply(wr.makeScale(t,e)),this}rotate(t){return this.premultiply(wr.makeRotation(-t)),this}translate(t,e){return this.premultiply(wr.makeTranslation(t,e)),this}makeTranslation(t,e){return t.isVector2?this.set(1,0,t.x,0,1,t.y,0,0,1):this.set(1,0,t,0,1,e,0,0,1),this}makeRotation(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,n,e,0,0,0,1),this}makeScale(t,e){return this.set(t,0,0,0,e,0,0,0,1),this}equals(t){const e=this.elements,n=t.elements;for(let s=0;s<9;s++)if(e[s]!==n[s])return!1;return!0}fromArray(t,e=0){for(let n=0;n<9;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t}clone(){return new this.constructor().fromArray(this.elements)}}const wr=new Bt;function ec(i){for(let t=i.length-1;t>=0;--t)if(i[t]>=65535)return!0;return!1}function dr(i){return document.createElementNS("http://www.w3.org/1999/xhtml",i)}function Ch(){const i=dr("canvas");return i.style.display="block",i}const So={};function Ri(i){i in So||(So[i]=!0,console.warn(i))}function Ph(i,t,e){return new Promise(function(n,s){function r(){switch(i.clientWaitSync(t,i.SYNC_FLUSH_COMMANDS_BIT,0)){case i.WAIT_FAILED:s();break;case i.TIMEOUT_EXPIRED:setTimeout(r,e);break;default:n()}}setTimeout(r,e)})}function Dh(i){const t=i.elements;t[2]=.5*t[2]+.5*t[3],t[6]=.5*t[6]+.5*t[7],t[10]=.5*t[10]+.5*t[11],t[14]=.5*t[14]+.5*t[15]}function Lh(i){const t=i.elements;t[11]===-1?(t[10]=-t[10]-1,t[14]=-t[14]):(t[10]=-t[10],t[14]=-t[14]+1)}const yo=new Bt().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),Eo=new Bt().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function Uh(){const i={enabled:!0,workingColorSpace:Vi,spaces:{},convert:function(s,r,a){return this.enabled===!1||r===a||!r||!a||(this.spaces[r].transfer===re&&(s.r=An(s.r),s.g=An(s.g),s.b=An(s.b)),this.spaces[r].primaries!==this.spaces[a].primaries&&(s.applyMatrix3(this.spaces[r].toXYZ),s.applyMatrix3(this.spaces[a].fromXYZ)),this.spaces[a].transfer===re&&(s.r=Ni(s.r),s.g=Ni(s.g),s.b=Ni(s.b))),s},fromWorkingColorSpace:function(s,r){return this.convert(s,this.workingColorSpace,r)},toWorkingColorSpace:function(s,r){return this.convert(s,r,this.workingColorSpace)},getPrimaries:function(s){return this.spaces[s].primaries},getTransfer:function(s){return s===On?hr:this.spaces[s].transfer},getLuminanceCoefficients:function(s,r=this.workingColorSpace){return s.fromArray(this.spaces[r].luminanceCoefficients)},define:function(s){Object.assign(this.spaces,s)},_getMatrix:function(s,r,a){return s.copy(this.spaces[r].toXYZ).multiply(this.spaces[a].fromXYZ)},_getDrawingBufferColorSpace:function(s){return this.spaces[s].outputColorSpaceConfig.drawingBufferColorSpace},_getUnpackColorSpace:function(s=this.workingColorSpace){return this.spaces[s].workingColorSpaceConfig.unpackColorSpace}},t=[.64,.33,.3,.6,.15,.06],e=[.2126,.7152,.0722],n=[.3127,.329];return i.define({[Vi]:{primaries:t,whitePoint:n,transfer:hr,toXYZ:yo,fromXYZ:Eo,luminanceCoefficients:e,workingColorSpaceConfig:{unpackColorSpace:Qe},outputColorSpaceConfig:{drawingBufferColorSpace:Qe}},[Qe]:{primaries:t,whitePoint:n,transfer:re,toXYZ:yo,fromXYZ:Eo,luminanceCoefficients:e,outputColorSpaceConfig:{drawingBufferColorSpace:Qe}}}),i}const Qt=Uh();function An(i){return i<.04045?i*.0773993808:Math.pow(i*.9478672986+.0521327014,2.4)}function Ni(i){return i<.0031308?i*12.92:1.055*Math.pow(i,.41666)-.055}let hi;class Ih{static getDataURL(t){if(/^data:/i.test(t.src)||typeof HTMLCanvasElement>"u")return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{hi===void 0&&(hi=dr("canvas")),hi.width=t.width,hi.height=t.height;const n=hi.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=hi}return e.width>2048||e.height>2048?(console.warn("THREE.ImageUtils.getDataURL: Image converted to jpg for performance reasons",t),e.toDataURL("image/jpeg",.6)):e.toDataURL("image/png")}static sRGBToLinear(t){if(typeof HTMLImageElement<"u"&&t instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&t instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&t instanceof ImageBitmap){const e=dr("canvas");e.width=t.width,e.height=t.height;const n=e.getContext("2d");n.drawImage(t,0,0,t.width,t.height);const s=n.getImageData(0,0,t.width,t.height),r=s.data;for(let a=0;a0&&(n.userData=this.userData),e||(t.textures[this.uuid]=n),n}dispose(){this.dispatchEvent({type:"dispose"})}transformUv(t){if(this.mapping!==Gl)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case ga:t.x=t.x-Math.floor(t.x);break;case Qn:t.x=t.x<0?0:1;break;case _a:Math.abs(Math.floor(t.x)%2)===1?t.x=Math.ceil(t.x)-t.x:t.x=t.x-Math.floor(t.x);break}if(t.y<0||t.y>1)switch(this.wrapT){case ga:t.y=t.y-Math.floor(t.y);break;case Qn:t.y=t.y<0?0:1;break;case _a:Math.abs(Math.floor(t.y)%2)===1?t.y=Math.ceil(t.y)-t.y:t.y=t.y-Math.floor(t.y);break}return this.flipY&&(t.y=1-t.y),t}set needsUpdate(t){t===!0&&(this.version++,this.source.needsUpdate=!0)}set needsPMREMUpdate(t){t===!0&&this.pmremVersion++}}be.DEFAULT_IMAGE=null;be.DEFAULT_MAPPING=Gl;be.DEFAULT_ANISOTROPY=1;class le{constructor(t=0,e=0,n=0,s=1){le.prototype.isVector4=!0,this.x=t,this.y=e,this.z=n,this.w=s}get width(){return this.z}set width(t){this.z=t}get height(){return this.w}set height(t){this.w=t}set(t,e,n,s){return this.x=t,this.y=e,this.z=n,this.w=s,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this.w=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setW(t){return this.w=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;case 3:this.w=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;case 3:return this.w;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z,this.w)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this.w=t.w!==void 0?t.w:1,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this.w+=t.w,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this.w+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this.w=t.w+e.w,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this.w+=t.w*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this.w-=t.w,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this.w-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this.w=t.w-e.w,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this.w*=t.w,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this.w*=t,this}applyMatrix4(t){const e=this.x,n=this.y,s=this.z,r=this.w,a=t.elements;return this.x=a[0]*e+a[4]*n+a[8]*s+a[12]*r,this.y=a[1]*e+a[5]*n+a[9]*s+a[13]*r,this.z=a[2]*e+a[6]*n+a[10]*s+a[14]*r,this.w=a[3]*e+a[7]*n+a[11]*s+a[15]*r,this}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this.w/=t.w,this}divideScalar(t){return this.multiplyScalar(1/t)}setAxisAngleFromQuaternion(t){this.w=2*Math.acos(t.w);const e=Math.sqrt(1-t.w*t.w);return e<1e-4?(this.x=1,this.y=0,this.z=0):(this.x=t.x/e,this.y=t.y/e,this.z=t.z/e),this}setAxisAngleFromRotationMatrix(t){let e,n,s,r;const l=t.elements,c=l[0],h=l[4],d=l[8],p=l[1],u=l[5],g=l[9],v=l[2],m=l[6],f=l[10];if(Math.abs(h-p)<.01&&Math.abs(d-v)<.01&&Math.abs(g-m)<.01){if(Math.abs(h+p)<.1&&Math.abs(d+v)<.1&&Math.abs(g+m)<.1&&Math.abs(c+u+f-3)<.1)return this.set(1,0,0,0),this;e=Math.PI;const b=(c+1)/2,S=(u+1)/2,D=(f+1)/2,w=(h+p)/4,R=(d+v)/4,U=(g+m)/4;return b>S&&b>D?b<.01?(n=0,s=.707106781,r=.707106781):(n=Math.sqrt(b),s=w/n,r=R/n):S>D?S<.01?(n=.707106781,s=0,r=.707106781):(s=Math.sqrt(S),n=w/s,r=U/s):D<.01?(n=.707106781,s=.707106781,r=0):(r=Math.sqrt(D),n=R/r,s=U/r),this.set(n,s,r,e),this}let T=Math.sqrt((m-g)*(m-g)+(d-v)*(d-v)+(p-h)*(p-h));return Math.abs(T)<.001&&(T=1),this.x=(m-g)/T,this.y=(d-v)/T,this.z=(p-h)/T,this.w=Math.acos((c+u+f-1)/2),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this.w=e[15],this}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this.w=Math.min(this.w,t.w),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this.w=Math.max(this.w,t.w),this}clamp(t,e){return this.x=Xt(this.x,t.x,e.x),this.y=Xt(this.y,t.y,e.y),this.z=Xt(this.z,t.z,e.z),this.w=Xt(this.w,t.w,e.w),this}clampScalar(t,e){return this.x=Xt(this.x,t,e),this.y=Xt(this.y,t,e),this.z=Xt(this.z,t,e),this.w=Xt(this.w,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Xt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this.w=Math.floor(this.w),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this.w=Math.ceil(this.w),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this.w=Math.round(this.w),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this.w=Math.trunc(this.w),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this.w=-this.w,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z+this.w*t.w}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z+this.w*this.w)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)+Math.abs(this.w)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this.w+=(t.w-this.w)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this.w=t.w+(e.w-t.w)*n,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z&&t.w===this.w}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this.w=t[e+3],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t[e+3]=this.w,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this.w=t.getW(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this.w=Math.random(),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z,yield this.w}}class Oh extends ri{constructor(t=1,e=1,n={}){super(),this.isRenderTarget=!0,this.width=t,this.height=e,this.depth=1,this.scissor=new le(0,0,t,e),this.scissorTest=!1,this.viewport=new le(0,0,t,e);const s={width:t,height:e,depth:1};n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:pn,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1},n);const r=new be(s,n.mapping,n.wrapS,n.wrapT,n.magFilter,n.minFilter,n.format,n.type,n.anisotropy,n.colorSpace);r.flipY=!1,r.generateMipmaps=n.generateMipmaps,r.internalFormat=n.internalFormat,this.textures=[];const a=n.count;for(let o=0;o=0?1:-1,b=1-f*f;if(b>Number.EPSILON){const D=Math.sqrt(b),w=Math.atan2(D,f*T);m=Math.sin(m*w)/D,o=Math.sin(o*w)/D}const S=o*T;if(l=l*m+p*S,c=c*m+u*S,h=h*m+g*S,d=d*m+v*S,m===1-o){const D=1/Math.sqrt(l*l+c*c+h*h+d*d);l*=D,c*=D,h*=D,d*=D}}t[e]=l,t[e+1]=c,t[e+2]=h,t[e+3]=d}static multiplyQuaternionsFlat(t,e,n,s,r,a){const o=n[s],l=n[s+1],c=n[s+2],h=n[s+3],d=r[a],p=r[a+1],u=r[a+2],g=r[a+3];return t[e]=o*g+h*d+l*u-c*p,t[e+1]=l*g+h*p+c*d-o*u,t[e+2]=c*g+h*u+o*p-l*d,t[e+3]=h*g-o*d-l*p-c*u,t}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get w(){return this._w}set w(t){this._w=t,this._onChangeCallback()}set(t,e,n,s){return this._x=t,this._y=e,this._z=n,this._w=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._w)}copy(t){return this._x=t.x,this._y=t.y,this._z=t.z,this._w=t.w,this._onChangeCallback(),this}setFromEuler(t,e=!0){const n=t._x,s=t._y,r=t._z,a=t._order,o=Math.cos,l=Math.sin,c=o(n/2),h=o(s/2),d=o(r/2),p=l(n/2),u=l(s/2),g=l(r/2);switch(a){case"XYZ":this._x=p*h*d+c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d-p*u*g;break;case"YXZ":this._x=p*h*d+c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d+p*u*g;break;case"ZXY":this._x=p*h*d-c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d-p*u*g;break;case"ZYX":this._x=p*h*d-c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d+p*u*g;break;case"YZX":this._x=p*h*d+c*u*g,this._y=c*u*d+p*h*g,this._z=c*h*g-p*u*d,this._w=c*h*d-p*u*g;break;case"XZY":this._x=p*h*d-c*u*g,this._y=c*u*d-p*h*g,this._z=c*h*g+p*u*d,this._w=c*h*d+p*u*g;break;default:console.warn("THREE.Quaternion: .setFromEuler() encountered an unknown order: "+a)}return e===!0&&this._onChangeCallback(),this}setFromAxisAngle(t,e){const n=e/2,s=Math.sin(n);return this._x=t.x*s,this._y=t.y*s,this._z=t.z*s,this._w=Math.cos(n),this._onChangeCallback(),this}setFromRotationMatrix(t){const e=t.elements,n=e[0],s=e[4],r=e[8],a=e[1],o=e[5],l=e[9],c=e[2],h=e[6],d=e[10],p=n+o+d;if(p>0){const u=.5/Math.sqrt(p+1);this._w=.25/u,this._x=(h-l)*u,this._y=(r-c)*u,this._z=(a-s)*u}else if(n>o&&n>d){const u=2*Math.sqrt(1+n-o-d);this._w=(h-l)/u,this._x=.25*u,this._y=(s+a)/u,this._z=(r+c)/u}else if(o>d){const u=2*Math.sqrt(1+o-n-d);this._w=(r-c)/u,this._x=(s+a)/u,this._y=.25*u,this._z=(l+h)/u}else{const u=2*Math.sqrt(1+d-n-o);this._w=(a-s)/u,this._x=(r+c)/u,this._y=(l+h)/u,this._z=.25*u}return this._onChangeCallback(),this}setFromUnitVectors(t,e){let n=t.dot(e)+1;return nMath.abs(t.z)?(this._x=-t.y,this._y=t.x,this._z=0,this._w=n):(this._x=0,this._y=-t.z,this._z=t.y,this._w=n)):(this._x=t.y*e.z-t.z*e.y,this._y=t.z*e.x-t.x*e.z,this._z=t.x*e.y-t.y*e.x,this._w=n),this.normalize()}angleTo(t){return 2*Math.acos(Math.abs(Xt(this.dot(t),-1,1)))}rotateTowards(t,e){const n=this.angleTo(t);if(n===0)return this;const s=Math.min(1,e/n);return this.slerp(t,s),this}identity(){return this.set(0,0,0,1)}invert(){return this.conjugate()}conjugate(){return this._x*=-1,this._y*=-1,this._z*=-1,this._onChangeCallback(),this}dot(t){return this._x*t._x+this._y*t._y+this._z*t._z+this._w*t._w}lengthSq(){return this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w}length(){return Math.sqrt(this._x*this._x+this._y*this._y+this._z*this._z+this._w*this._w)}normalize(){let t=this.length();return t===0?(this._x=0,this._y=0,this._z=0,this._w=1):(t=1/t,this._x=this._x*t,this._y=this._y*t,this._z=this._z*t,this._w=this._w*t),this._onChangeCallback(),this}multiply(t){return this.multiplyQuaternions(this,t)}premultiply(t){return this.multiplyQuaternions(t,this)}multiplyQuaternions(t,e){const n=t._x,s=t._y,r=t._z,a=t._w,o=e._x,l=e._y,c=e._z,h=e._w;return this._x=n*h+a*o+s*c-r*l,this._y=s*h+a*l+r*o-n*c,this._z=r*h+a*c+n*l-s*o,this._w=a*h-n*o-s*l-r*c,this._onChangeCallback(),this}slerp(t,e){if(e===0)return this;if(e===1)return this.copy(t);const n=this._x,s=this._y,r=this._z,a=this._w;let o=a*t._w+n*t._x+s*t._y+r*t._z;if(o<0?(this._w=-t._w,this._x=-t._x,this._y=-t._y,this._z=-t._z,o=-o):this.copy(t),o>=1)return this._w=a,this._x=n,this._y=s,this._z=r,this;const l=1-o*o;if(l<=Number.EPSILON){const u=1-e;return this._w=u*a+e*this._w,this._x=u*n+e*this._x,this._y=u*s+e*this._y,this._z=u*r+e*this._z,this.normalize(),this}const c=Math.sqrt(l),h=Math.atan2(c,o),d=Math.sin((1-e)*h)/c,p=Math.sin(e*h)/c;return this._w=a*d+this._w*p,this._x=n*d+this._x*p,this._y=s*d+this._y*p,this._z=r*d+this._z*p,this._onChangeCallback(),this}slerpQuaternions(t,e,n){return this.copy(t).slerp(e,n)}random(){const t=2*Math.PI*Math.random(),e=2*Math.PI*Math.random(),n=Math.random(),s=Math.sqrt(1-n),r=Math.sqrt(n);return this.set(s*Math.sin(t),s*Math.cos(t),r*Math.sin(e),r*Math.cos(e))}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._w===this._w}fromArray(t,e=0){return this._x=t[e],this._y=t[e+1],this._z=t[e+2],this._w=t[e+3],this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._w,t}fromBufferAttribute(t,e){return this._x=t.getX(e),this._y=t.getY(e),this._z=t.getZ(e),this._w=t.getW(e),this._onChangeCallback(),this}toJSON(){return this.toArray()}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._w}}class C{constructor(t=0,e=0,n=0){C.prototype.isVector3=!0,this.x=t,this.y=e,this.z=n}set(t,e,n){return n===void 0&&(n=this.z),this.x=t,this.y=e,this.z=n,this}setScalar(t){return this.x=t,this.y=t,this.z=t,this}setX(t){return this.x=t,this}setY(t){return this.y=t,this}setZ(t){return this.z=t,this}setComponent(t,e){switch(t){case 0:this.x=e;break;case 1:this.y=e;break;case 2:this.z=e;break;default:throw new Error("index is out of range: "+t)}return this}getComponent(t){switch(t){case 0:return this.x;case 1:return this.y;case 2:return this.z;default:throw new Error("index is out of range: "+t)}}clone(){return new this.constructor(this.x,this.y,this.z)}copy(t){return this.x=t.x,this.y=t.y,this.z=t.z,this}add(t){return this.x+=t.x,this.y+=t.y,this.z+=t.z,this}addScalar(t){return this.x+=t,this.y+=t,this.z+=t,this}addVectors(t,e){return this.x=t.x+e.x,this.y=t.y+e.y,this.z=t.z+e.z,this}addScaledVector(t,e){return this.x+=t.x*e,this.y+=t.y*e,this.z+=t.z*e,this}sub(t){return this.x-=t.x,this.y-=t.y,this.z-=t.z,this}subScalar(t){return this.x-=t,this.y-=t,this.z-=t,this}subVectors(t,e){return this.x=t.x-e.x,this.y=t.y-e.y,this.z=t.z-e.z,this}multiply(t){return this.x*=t.x,this.y*=t.y,this.z*=t.z,this}multiplyScalar(t){return this.x*=t,this.y*=t,this.z*=t,this}multiplyVectors(t,e){return this.x=t.x*e.x,this.y=t.y*e.y,this.z=t.z*e.z,this}applyEuler(t){return this.applyQuaternion(bo.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(bo.setFromAxisAngle(t,e))}applyMatrix3(t){const e=this.x,n=this.y,s=this.z,r=t.elements;return this.x=r[0]*e+r[3]*n+r[6]*s,this.y=r[1]*e+r[4]*n+r[7]*s,this.z=r[2]*e+r[5]*n+r[8]*s,this}applyNormalMatrix(t){return this.applyMatrix3(t).normalize()}applyMatrix4(t){const e=this.x,n=this.y,s=this.z,r=t.elements,a=1/(r[3]*e+r[7]*n+r[11]*s+r[15]);return this.x=(r[0]*e+r[4]*n+r[8]*s+r[12])*a,this.y=(r[1]*e+r[5]*n+r[9]*s+r[13])*a,this.z=(r[2]*e+r[6]*n+r[10]*s+r[14])*a,this}applyQuaternion(t){const e=this.x,n=this.y,s=this.z,r=t.x,a=t.y,o=t.z,l=t.w,c=2*(a*s-o*n),h=2*(o*e-r*s),d=2*(r*n-a*e);return this.x=e+l*c+a*d-o*h,this.y=n+l*h+o*c-r*d,this.z=s+l*d+r*h-a*c,this}project(t){return this.applyMatrix4(t.matrixWorldInverse).applyMatrix4(t.projectionMatrix)}unproject(t){return this.applyMatrix4(t.projectionMatrixInverse).applyMatrix4(t.matrixWorld)}transformDirection(t){const e=this.x,n=this.y,s=this.z,r=t.elements;return this.x=r[0]*e+r[4]*n+r[8]*s,this.y=r[1]*e+r[5]*n+r[9]*s,this.z=r[2]*e+r[6]*n+r[10]*s,this.normalize()}divide(t){return this.x/=t.x,this.y/=t.y,this.z/=t.z,this}divideScalar(t){return this.multiplyScalar(1/t)}min(t){return this.x=Math.min(this.x,t.x),this.y=Math.min(this.y,t.y),this.z=Math.min(this.z,t.z),this}max(t){return this.x=Math.max(this.x,t.x),this.y=Math.max(this.y,t.y),this.z=Math.max(this.z,t.z),this}clamp(t,e){return this.x=Xt(this.x,t.x,e.x),this.y=Xt(this.y,t.y,e.y),this.z=Xt(this.z,t.z,e.z),this}clampScalar(t,e){return this.x=Xt(this.x,t,e),this.y=Xt(this.y,t,e),this.z=Xt(this.z,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(Xt(n,t,e))}floor(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this.z=Math.floor(this.z),this}ceil(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this.z=Math.ceil(this.z),this}round(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this.z=Math.round(this.z),this}roundToZero(){return this.x=Math.trunc(this.x),this.y=Math.trunc(this.y),this.z=Math.trunc(this.z),this}negate(){return this.x=-this.x,this.y=-this.y,this.z=-this.z,this}dot(t){return this.x*t.x+this.y*t.y+this.z*t.z}lengthSq(){return this.x*this.x+this.y*this.y+this.z*this.z}length(){return Math.sqrt(this.x*this.x+this.y*this.y+this.z*this.z)}manhattanLength(){return Math.abs(this.x)+Math.abs(this.y)+Math.abs(this.z)}normalize(){return this.divideScalar(this.length()||1)}setLength(t){return this.normalize().multiplyScalar(t)}lerp(t,e){return this.x+=(t.x-this.x)*e,this.y+=(t.y-this.y)*e,this.z+=(t.z-this.z)*e,this}lerpVectors(t,e,n){return this.x=t.x+(e.x-t.x)*n,this.y=t.y+(e.y-t.y)*n,this.z=t.z+(e.z-t.z)*n,this}cross(t){return this.crossVectors(this,t)}crossVectors(t,e){const n=t.x,s=t.y,r=t.z,a=e.x,o=e.y,l=e.z;return this.x=s*l-r*o,this.y=r*a-n*l,this.z=n*o-s*a,this}projectOnVector(t){const e=t.lengthSq();if(e===0)return this.set(0,0,0);const n=t.dot(this)/e;return this.copy(t).multiplyScalar(n)}projectOnPlane(t){return Rr.copy(this).projectOnVector(t),this.sub(Rr)}reflect(t){return this.sub(Rr.copy(t).multiplyScalar(2*this.dot(t)))}angleTo(t){const e=Math.sqrt(this.lengthSq()*t.lengthSq());if(e===0)return Math.PI/2;const n=this.dot(t)/e;return Math.acos(Xt(n,-1,1))}distanceTo(t){return Math.sqrt(this.distanceToSquared(t))}distanceToSquared(t){const e=this.x-t.x,n=this.y-t.y,s=this.z-t.z;return e*e+n*n+s*s}manhattanDistanceTo(t){return Math.abs(this.x-t.x)+Math.abs(this.y-t.y)+Math.abs(this.z-t.z)}setFromSpherical(t){return this.setFromSphericalCoords(t.radius,t.phi,t.theta)}setFromSphericalCoords(t,e,n){const s=Math.sin(e)*t;return this.x=s*Math.sin(n),this.y=Math.cos(e)*t,this.z=s*Math.cos(n),this}setFromCylindrical(t){return this.setFromCylindricalCoords(t.radius,t.theta,t.y)}setFromCylindricalCoords(t,e,n){return this.x=t*Math.sin(e),this.y=n,this.z=t*Math.cos(e),this}setFromMatrixPosition(t){const e=t.elements;return this.x=e[12],this.y=e[13],this.z=e[14],this}setFromMatrixScale(t){const e=this.setFromMatrixColumn(t,0).length(),n=this.setFromMatrixColumn(t,1).length(),s=this.setFromMatrixColumn(t,2).length();return this.x=e,this.y=n,this.z=s,this}setFromMatrixColumn(t,e){return this.fromArray(t.elements,e*4)}setFromMatrix3Column(t,e){return this.fromArray(t.elements,e*3)}setFromEuler(t){return this.x=t._x,this.y=t._y,this.z=t._z,this}setFromColor(t){return this.x=t.r,this.y=t.g,this.z=t.b,this}equals(t){return t.x===this.x&&t.y===this.y&&t.z===this.z}fromArray(t,e=0){return this.x=t[e],this.y=t[e+1],this.z=t[e+2],this}toArray(t=[],e=0){return t[e]=this.x,t[e+1]=this.y,t[e+2]=this.z,t}fromBufferAttribute(t,e){return this.x=t.getX(e),this.y=t.getY(e),this.z=t.getZ(e),this}random(){return this.x=Math.random(),this.y=Math.random(),this.z=Math.random(),this}randomDirection(){const t=Math.random()*Math.PI*2,e=Math.random()*2-1,n=Math.sqrt(1-e*e);return this.x=n*Math.cos(t),this.y=e,this.z=n*Math.sin(t),this}*[Symbol.iterator](){yield this.x,yield this.y,yield this.z}}const Rr=new C,bo=new si;class ai{constructor(t=new C(1/0,1/0,1/0),e=new C(-1/0,-1/0,-1/0)){this.isBox3=!0,this.min=t,this.max=e}set(t,e){return this.min.copy(t),this.max.copy(e),this}setFromArray(t){this.makeEmpty();for(let e=0,n=t.length;e=this.min.x&&t.x<=this.max.x&&t.y>=this.min.y&&t.y<=this.max.y&&t.z>=this.min.z&&t.z<=this.max.z}containsBox(t){return this.min.x<=t.min.x&&t.max.x<=this.max.x&&this.min.y<=t.min.y&&t.max.y<=this.max.y&&this.min.z<=t.min.z&&t.max.z<=this.max.z}getParameter(t,e){return e.set((t.x-this.min.x)/(this.max.x-this.min.x),(t.y-this.min.y)/(this.max.y-this.min.y),(t.z-this.min.z)/(this.max.z-this.min.z))}intersectsBox(t){return t.max.x>=this.min.x&&t.min.x<=this.max.x&&t.max.y>=this.min.y&&t.min.y<=this.max.y&&t.max.z>=this.min.z&&t.min.z<=this.max.z}intersectsSphere(t){return this.clampPoint(t.center,sn),sn.distanceToSquared(t.center)<=t.radius*t.radius}intersectsPlane(t){let e,n;return t.normal.x>0?(e=t.normal.x*this.min.x,n=t.normal.x*this.max.x):(e=t.normal.x*this.max.x,n=t.normal.x*this.min.x),t.normal.y>0?(e+=t.normal.y*this.min.y,n+=t.normal.y*this.max.y):(e+=t.normal.y*this.max.y,n+=t.normal.y*this.min.y),t.normal.z>0?(e+=t.normal.z*this.min.z,n+=t.normal.z*this.max.z):(e+=t.normal.z*this.max.z,n+=t.normal.z*this.min.z),e<=-t.constant&&n>=-t.constant}intersectsTriangle(t){if(this.isEmpty())return!1;this.getCenter(Ki),Ts.subVectors(this.max,Ki),ui.subVectors(t.a,Ki),di.subVectors(t.b,Ki),fi.subVectors(t.c,Ki),Cn.subVectors(di,ui),Pn.subVectors(fi,di),Gn.subVectors(ui,fi);let e=[0,-Cn.z,Cn.y,0,-Pn.z,Pn.y,0,-Gn.z,Gn.y,Cn.z,0,-Cn.x,Pn.z,0,-Pn.x,Gn.z,0,-Gn.x,-Cn.y,Cn.x,0,-Pn.y,Pn.x,0,-Gn.y,Gn.x,0];return!Cr(e,ui,di,fi,Ts)||(e=[1,0,0,0,1,0,0,0,1],!Cr(e,ui,di,fi,Ts))?!1:(ws.crossVectors(Cn,Pn),e=[ws.x,ws.y,ws.z],Cr(e,ui,di,fi,Ts))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,sn).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=this.getSize(sn).length()*.5),t}intersect(t){return this.min.max(t.min),this.max.min(t.max),this.isEmpty()&&this.makeEmpty(),this}union(t){return this.min.min(t.min),this.max.max(t.max),this}applyMatrix4(t){return this.isEmpty()?this:(_n[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),_n[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),_n[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),_n[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),_n[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),_n[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),_n[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),_n[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(_n),this)}translate(t){return this.min.add(t),this.max.add(t),this}equals(t){return t.min.equals(this.min)&&t.max.equals(this.max)}}const _n=[new C,new C,new C,new C,new C,new C,new C,new C],sn=new C,bs=new ai,ui=new C,di=new C,fi=new C,Cn=new C,Pn=new C,Gn=new C,Ki=new C,Ts=new C,ws=new C,Wn=new C;function Cr(i,t,e,n,s){for(let r=0,a=i.length-3;r<=a;r+=3){Wn.fromArray(i,r);const o=s.x*Math.abs(Wn.x)+s.y*Math.abs(Wn.y)+s.z*Math.abs(Wn.z),l=t.dot(Wn),c=e.dot(Wn),h=n.dot(Wn);if(Math.max(-Math.max(l,c,h),Math.min(l,c,h))>o)return!1}return!0}const zh=new ai,$i=new C,Pr=new C;class oi{constructor(t=new C,e=-1){this.isSphere=!0,this.center=t,this.radius=e}set(t,e){return this.center.copy(t),this.radius=e,this}setFromPoints(t,e){const n=this.center;e!==void 0?n.copy(e):zh.setFromPoints(t).getCenter(n);let s=0;for(let r=0,a=t.length;rthis.radius*this.radius&&(e.sub(this.center).normalize(),e.multiplyScalar(this.radius).add(this.center)),e}getBoundingBox(t){return this.isEmpty()?(t.makeEmpty(),t):(t.set(this.center,this.center),t.expandByScalar(this.radius),t)}applyMatrix4(t){return this.center.applyMatrix4(t),this.radius=this.radius*t.getMaxScaleOnAxis(),this}translate(t){return this.center.add(t),this}expandByPoint(t){if(this.isEmpty())return this.center.copy(t),this.radius=0,this;$i.subVectors(t,this.center);const e=$i.lengthSq();if(e>this.radius*this.radius){const n=Math.sqrt(e),s=(n-this.radius)*.5;this.center.addScaledVector($i,s/n),this.radius+=s}return this}union(t){return t.isEmpty()?this:this.isEmpty()?(this.copy(t),this):(this.center.equals(t.center)===!0?this.radius=Math.max(this.radius,t.radius):(Pr.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint($i.copy(t.center).add(Pr)),this.expandByPoint($i.copy(t.center).sub(Pr))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return new this.constructor().copy(this)}}const vn=new C,Dr=new C,As=new C,Dn=new C,Lr=new C,Rs=new C,Ur=new C;class ms{constructor(t=new C,e=new C(0,0,-1)){this.origin=t,this.direction=e}set(t,e){return this.origin.copy(t),this.direction.copy(e),this}copy(t){return this.origin.copy(t.origin),this.direction.copy(t.direction),this}at(t,e){return e.copy(this.origin).addScaledVector(this.direction,t)}lookAt(t){return this.direction.copy(t).sub(this.origin).normalize(),this}recast(t){return this.origin.copy(this.at(t,vn)),this}closestPointToPoint(t,e){e.subVectors(t,this.origin);const n=e.dot(this.direction);return n<0?e.copy(this.origin):e.copy(this.origin).addScaledVector(this.direction,n)}distanceToPoint(t){return Math.sqrt(this.distanceSqToPoint(t))}distanceSqToPoint(t){const e=vn.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(vn.copy(this.origin).addScaledVector(this.direction,e),vn.distanceToSquared(t))}distanceSqToSegment(t,e,n,s){Dr.copy(t).add(e).multiplyScalar(.5),As.copy(e).sub(t).normalize(),Dn.copy(this.origin).sub(Dr);const r=t.distanceTo(e)*.5,a=-this.direction.dot(As),o=Dn.dot(this.direction),l=-Dn.dot(As),c=Dn.lengthSq(),h=Math.abs(1-a*a);let d,p,u,g;if(h>0)if(d=a*l-o,p=a*o-l,g=r*h,d>=0)if(p>=-g)if(p<=g){const v=1/h;d*=v,p*=v,u=d*(d+a*p+2*o)+p*(a*d+p+2*l)+c}else p=r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;else p=-r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;else p<=-g?(d=Math.max(0,-(-a*r+o)),p=d>0?-r:Math.min(Math.max(-r,-l),r),u=-d*d+p*(p+2*l)+c):p<=g?(d=0,p=Math.min(Math.max(-r,-l),r),u=p*(p+2*l)+c):(d=Math.max(0,-(a*r+o)),p=d>0?r:Math.min(Math.max(-r,-l),r),u=-d*d+p*(p+2*l)+c);else p=a>0?-r:r,d=Math.max(0,-(a*p+o)),u=-d*d+p*(p+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,d),s&&s.copy(Dr).addScaledVector(As,p),u}intersectSphere(t,e){vn.subVectors(t.center,this.origin);const n=vn.dot(this.direction),s=vn.dot(vn)-n*n,r=t.radius*t.radius;if(s>r)return null;const a=Math.sqrt(r-s),o=n-a,l=n+a;return l<0?null:o<0?this.at(l,e):this.at(o,e)}intersectsSphere(t){return this.distanceSqToPoint(t.center)<=t.radius*t.radius}distanceToPlane(t){const e=t.normal.dot(this.direction);if(e===0)return t.distanceToPoint(this.origin)===0?0:null;const n=-(this.origin.dot(t.normal)+t.constant)/e;return n>=0?n:null}intersectPlane(t,e){const n=this.distanceToPlane(t);return n===null?null:this.at(n,e)}intersectsPlane(t){const e=t.distanceToPoint(this.origin);return e===0||t.normal.dot(this.direction)*e<0}intersectBox(t,e){let n,s,r,a,o,l;const c=1/this.direction.x,h=1/this.direction.y,d=1/this.direction.z,p=this.origin;return c>=0?(n=(t.min.x-p.x)*c,s=(t.max.x-p.x)*c):(n=(t.max.x-p.x)*c,s=(t.min.x-p.x)*c),h>=0?(r=(t.min.y-p.y)*h,a=(t.max.y-p.y)*h):(r=(t.max.y-p.y)*h,a=(t.min.y-p.y)*h),n>a||r>s||((r>n||isNaN(n))&&(n=r),(a=0?(o=(t.min.z-p.z)*d,l=(t.max.z-p.z)*d):(o=(t.max.z-p.z)*d,l=(t.min.z-p.z)*d),n>l||o>s)||((o>n||n!==n)&&(n=o),(l=0?n:s,e)}intersectsBox(t){return this.intersectBox(t,vn)!==null}intersectTriangle(t,e,n,s,r){Lr.subVectors(e,t),Rs.subVectors(n,t),Ur.crossVectors(Lr,Rs);let a=this.direction.dot(Ur),o;if(a>0){if(s)return null;o=1}else if(a<0)o=-1,a=-a;else return null;Dn.subVectors(this.origin,t);const l=o*this.direction.dot(Rs.crossVectors(Dn,Rs));if(l<0)return null;const c=o*this.direction.dot(Lr.cross(Dn));if(c<0||l+c>a)return null;const h=-o*Dn.dot(Ur);return h<0?null:this.at(h/a,r)}applyMatrix4(t){return this.origin.applyMatrix4(t),this.direction.transformDirection(t),this}equals(t){return t.origin.equals(this.origin)&&t.direction.equals(this.direction)}clone(){return new this.constructor().copy(this)}}class ie{constructor(t,e,n,s,r,a,o,l,c,h,d,p,u,g,v,m){ie.prototype.isMatrix4=!0,this.elements=[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1],t!==void 0&&this.set(t,e,n,s,r,a,o,l,c,h,d,p,u,g,v,m)}set(t,e,n,s,r,a,o,l,c,h,d,p,u,g,v,m){const f=this.elements;return f[0]=t,f[4]=e,f[8]=n,f[12]=s,f[1]=r,f[5]=a,f[9]=o,f[13]=l,f[2]=c,f[6]=h,f[10]=d,f[14]=p,f[3]=u,f[7]=g,f[11]=v,f[15]=m,this}identity(){return this.set(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1),this}clone(){return new ie().fromArray(this.elements)}copy(t){const e=this.elements,n=t.elements;return e[0]=n[0],e[1]=n[1],e[2]=n[2],e[3]=n[3],e[4]=n[4],e[5]=n[5],e[6]=n[6],e[7]=n[7],e[8]=n[8],e[9]=n[9],e[10]=n[10],e[11]=n[11],e[12]=n[12],e[13]=n[13],e[14]=n[14],e[15]=n[15],this}copyPosition(t){const e=this.elements,n=t.elements;return e[12]=n[12],e[13]=n[13],e[14]=n[14],this}setFromMatrix3(t){const e=t.elements;return this.set(e[0],e[3],e[6],0,e[1],e[4],e[7],0,e[2],e[5],e[8],0,0,0,0,1),this}extractBasis(t,e,n){return t.setFromMatrixColumn(this,0),e.setFromMatrixColumn(this,1),n.setFromMatrixColumn(this,2),this}makeBasis(t,e,n){return this.set(t.x,e.x,n.x,0,t.y,e.y,n.y,0,t.z,e.z,n.z,0,0,0,0,1),this}extractRotation(t){const e=this.elements,n=t.elements,s=1/pi.setFromMatrixColumn(t,0).length(),r=1/pi.setFromMatrixColumn(t,1).length(),a=1/pi.setFromMatrixColumn(t,2).length();return e[0]=n[0]*s,e[1]=n[1]*s,e[2]=n[2]*s,e[3]=0,e[4]=n[4]*r,e[5]=n[5]*r,e[6]=n[6]*r,e[7]=0,e[8]=n[8]*a,e[9]=n[9]*a,e[10]=n[10]*a,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromEuler(t){const e=this.elements,n=t.x,s=t.y,r=t.z,a=Math.cos(n),o=Math.sin(n),l=Math.cos(s),c=Math.sin(s),h=Math.cos(r),d=Math.sin(r);if(t.order==="XYZ"){const p=a*h,u=a*d,g=o*h,v=o*d;e[0]=l*h,e[4]=-l*d,e[8]=c,e[1]=u+g*c,e[5]=p-v*c,e[9]=-o*l,e[2]=v-p*c,e[6]=g+u*c,e[10]=a*l}else if(t.order==="YXZ"){const p=l*h,u=l*d,g=c*h,v=c*d;e[0]=p+v*o,e[4]=g*o-u,e[8]=a*c,e[1]=a*d,e[5]=a*h,e[9]=-o,e[2]=u*o-g,e[6]=v+p*o,e[10]=a*l}else if(t.order==="ZXY"){const p=l*h,u=l*d,g=c*h,v=c*d;e[0]=p-v*o,e[4]=-a*d,e[8]=g+u*o,e[1]=u+g*o,e[5]=a*h,e[9]=v-p*o,e[2]=-a*c,e[6]=o,e[10]=a*l}else if(t.order==="ZYX"){const p=a*h,u=a*d,g=o*h,v=o*d;e[0]=l*h,e[4]=g*c-u,e[8]=p*c+v,e[1]=l*d,e[5]=v*c+p,e[9]=u*c-g,e[2]=-c,e[6]=o*l,e[10]=a*l}else if(t.order==="YZX"){const p=a*l,u=a*c,g=o*l,v=o*c;e[0]=l*h,e[4]=v-p*d,e[8]=g*d+u,e[1]=d,e[5]=a*h,e[9]=-o*h,e[2]=-c*h,e[6]=u*d+g,e[10]=p-v*d}else if(t.order==="XZY"){const p=a*l,u=a*c,g=o*l,v=o*c;e[0]=l*h,e[4]=-d,e[8]=c*h,e[1]=p*d+v,e[5]=a*h,e[9]=u*d-g,e[2]=g*d-u,e[6]=o*h,e[10]=v*d+p}return e[3]=0,e[7]=0,e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1,this}makeRotationFromQuaternion(t){return this.compose(kh,t,Hh)}lookAt(t,e,n){const s=this.elements;return Ye.subVectors(t,e),Ye.lengthSq()===0&&(Ye.z=1),Ye.normalize(),Ln.crossVectors(n,Ye),Ln.lengthSq()===0&&(Math.abs(n.z)===1?Ye.x+=1e-4:Ye.z+=1e-4,Ye.normalize(),Ln.crossVectors(n,Ye)),Ln.normalize(),Cs.crossVectors(Ye,Ln),s[0]=Ln.x,s[4]=Cs.x,s[8]=Ye.x,s[1]=Ln.y,s[5]=Cs.y,s[9]=Ye.y,s[2]=Ln.z,s[6]=Cs.z,s[10]=Ye.z,this}multiply(t){return this.multiplyMatrices(this,t)}premultiply(t){return this.multiplyMatrices(t,this)}multiplyMatrices(t,e){const n=t.elements,s=e.elements,r=this.elements,a=n[0],o=n[4],l=n[8],c=n[12],h=n[1],d=n[5],p=n[9],u=n[13],g=n[2],v=n[6],m=n[10],f=n[14],T=n[3],b=n[7],S=n[11],D=n[15],w=s[0],R=s[4],U=s[8],y=s[12],M=s[1],P=s[5],W=s[9],z=s[13],G=s[2],$=s[6],X=s[10],Q=s[14],k=s[3],it=s[7],ft=s[11],St=s[15];return r[0]=a*w+o*M+l*G+c*k,r[4]=a*R+o*P+l*$+c*it,r[8]=a*U+o*W+l*X+c*ft,r[12]=a*y+o*z+l*Q+c*St,r[1]=h*w+d*M+p*G+u*k,r[5]=h*R+d*P+p*$+u*it,r[9]=h*U+d*W+p*X+u*ft,r[13]=h*y+d*z+p*Q+u*St,r[2]=g*w+v*M+m*G+f*k,r[6]=g*R+v*P+m*$+f*it,r[10]=g*U+v*W+m*X+f*ft,r[14]=g*y+v*z+m*Q+f*St,r[3]=T*w+b*M+S*G+D*k,r[7]=T*R+b*P+S*$+D*it,r[11]=T*U+b*W+S*X+D*ft,r[15]=T*y+b*z+S*Q+D*St,this}multiplyScalar(t){const e=this.elements;return e[0]*=t,e[4]*=t,e[8]*=t,e[12]*=t,e[1]*=t,e[5]*=t,e[9]*=t,e[13]*=t,e[2]*=t,e[6]*=t,e[10]*=t,e[14]*=t,e[3]*=t,e[7]*=t,e[11]*=t,e[15]*=t,this}determinant(){const t=this.elements,e=t[0],n=t[4],s=t[8],r=t[12],a=t[1],o=t[5],l=t[9],c=t[13],h=t[2],d=t[6],p=t[10],u=t[14],g=t[3],v=t[7],m=t[11],f=t[15];return g*(+r*l*d-s*c*d-r*o*p+n*c*p+s*o*u-n*l*u)+v*(+e*l*u-e*c*p+r*a*p-s*a*u+s*c*h-r*l*h)+m*(+e*c*d-e*o*u-r*a*d+n*a*u+r*o*h-n*c*h)+f*(-s*o*h-e*l*d+e*o*p+s*a*d-n*a*p+n*l*h)}transpose(){const t=this.elements;let e;return e=t[1],t[1]=t[4],t[4]=e,e=t[2],t[2]=t[8],t[8]=e,e=t[6],t[6]=t[9],t[9]=e,e=t[3],t[3]=t[12],t[12]=e,e=t[7],t[7]=t[13],t[13]=e,e=t[11],t[11]=t[14],t[14]=e,this}setPosition(t,e,n){const s=this.elements;return t.isVector3?(s[12]=t.x,s[13]=t.y,s[14]=t.z):(s[12]=t,s[13]=e,s[14]=n),this}invert(){const t=this.elements,e=t[0],n=t[1],s=t[2],r=t[3],a=t[4],o=t[5],l=t[6],c=t[7],h=t[8],d=t[9],p=t[10],u=t[11],g=t[12],v=t[13],m=t[14],f=t[15],T=d*m*c-v*p*c+v*l*u-o*m*u-d*l*f+o*p*f,b=g*p*c-h*m*c-g*l*u+a*m*u+h*l*f-a*p*f,S=h*v*c-g*d*c+g*o*u-a*v*u-h*o*f+a*d*f,D=g*d*l-h*v*l-g*o*p+a*v*p+h*o*m-a*d*m,w=e*T+n*b+s*S+r*D;if(w===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const R=1/w;return t[0]=T*R,t[1]=(v*p*r-d*m*r-v*s*u+n*m*u+d*s*f-n*p*f)*R,t[2]=(o*m*r-v*l*r+v*s*c-n*m*c-o*s*f+n*l*f)*R,t[3]=(d*l*r-o*p*r-d*s*c+n*p*c+o*s*u-n*l*u)*R,t[4]=b*R,t[5]=(h*m*r-g*p*r+g*s*u-e*m*u-h*s*f+e*p*f)*R,t[6]=(g*l*r-a*m*r-g*s*c+e*m*c+a*s*f-e*l*f)*R,t[7]=(a*p*r-h*l*r+h*s*c-e*p*c-a*s*u+e*l*u)*R,t[8]=S*R,t[9]=(g*d*r-h*v*r-g*n*u+e*v*u+h*n*f-e*d*f)*R,t[10]=(a*v*r-g*o*r+g*n*c-e*v*c-a*n*f+e*o*f)*R,t[11]=(h*o*r-a*d*r-h*n*c+e*d*c+a*n*u-e*o*u)*R,t[12]=D*R,t[13]=(h*v*s-g*d*s+g*n*p-e*v*p-h*n*m+e*d*m)*R,t[14]=(g*o*s-a*v*s-g*n*l+e*v*l+a*n*m-e*o*m)*R,t[15]=(a*d*s-h*o*s+h*n*l-e*d*l-a*n*p+e*o*p)*R,this}scale(t){const e=this.elements,n=t.x,s=t.y,r=t.z;return e[0]*=n,e[4]*=s,e[8]*=r,e[1]*=n,e[5]*=s,e[9]*=r,e[2]*=n,e[6]*=s,e[10]*=r,e[3]*=n,e[7]*=s,e[11]*=r,this}getMaxScaleOnAxis(){const t=this.elements,e=t[0]*t[0]+t[1]*t[1]+t[2]*t[2],n=t[4]*t[4]+t[5]*t[5]+t[6]*t[6],s=t[8]*t[8]+t[9]*t[9]+t[10]*t[10];return Math.sqrt(Math.max(e,n,s))}makeTranslation(t,e,n){return t.isVector3?this.set(1,0,0,t.x,0,1,0,t.y,0,0,1,t.z,0,0,0,1):this.set(1,0,0,t,0,1,0,e,0,0,1,n,0,0,0,1),this}makeRotationX(t){const e=Math.cos(t),n=Math.sin(t);return this.set(1,0,0,0,0,e,-n,0,0,n,e,0,0,0,0,1),this}makeRotationY(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,0,n,0,0,1,0,0,-n,0,e,0,0,0,0,1),this}makeRotationZ(t){const e=Math.cos(t),n=Math.sin(t);return this.set(e,-n,0,0,n,e,0,0,0,0,1,0,0,0,0,1),this}makeRotationAxis(t,e){const n=Math.cos(e),s=Math.sin(e),r=1-n,a=t.x,o=t.y,l=t.z,c=r*a,h=r*o;return this.set(c*a+n,c*o-s*l,c*l+s*o,0,c*o+s*l,h*o+n,h*l-s*a,0,c*l-s*o,h*l+s*a,r*l*l+n,0,0,0,0,1),this}makeScale(t,e,n){return this.set(t,0,0,0,0,e,0,0,0,0,n,0,0,0,0,1),this}makeShear(t,e,n,s,r,a){return this.set(1,n,r,0,t,1,a,0,e,s,1,0,0,0,0,1),this}compose(t,e,n){const s=this.elements,r=e._x,a=e._y,o=e._z,l=e._w,c=r+r,h=a+a,d=o+o,p=r*c,u=r*h,g=r*d,v=a*h,m=a*d,f=o*d,T=l*c,b=l*h,S=l*d,D=n.x,w=n.y,R=n.z;return s[0]=(1-(v+f))*D,s[1]=(u+S)*D,s[2]=(g-b)*D,s[3]=0,s[4]=(u-S)*w,s[5]=(1-(p+f))*w,s[6]=(m+T)*w,s[7]=0,s[8]=(g+b)*R,s[9]=(m-T)*R,s[10]=(1-(p+v))*R,s[11]=0,s[12]=t.x,s[13]=t.y,s[14]=t.z,s[15]=1,this}decompose(t,e,n){const s=this.elements;let r=pi.set(s[0],s[1],s[2]).length();const a=pi.set(s[4],s[5],s[6]).length(),o=pi.set(s[8],s[9],s[10]).length();this.determinant()<0&&(r=-r),t.x=s[12],t.y=s[13],t.z=s[14],rn.copy(this);const c=1/r,h=1/a,d=1/o;return rn.elements[0]*=c,rn.elements[1]*=c,rn.elements[2]*=c,rn.elements[4]*=h,rn.elements[5]*=h,rn.elements[6]*=h,rn.elements[8]*=d,rn.elements[9]*=d,rn.elements[10]*=d,e.setFromRotationMatrix(rn),n.x=r,n.y=a,n.z=o,this}makePerspective(t,e,n,s,r,a,o=bn){const l=this.elements,c=2*r/(e-t),h=2*r/(n-s),d=(e+t)/(e-t),p=(n+s)/(n-s);let u,g;if(o===bn)u=-(a+r)/(a-r),g=-2*a*r/(a-r);else if(o===ur)u=-a/(a-r),g=-a*r/(a-r);else throw new Error("THREE.Matrix4.makePerspective(): Invalid coordinate system: "+o);return l[0]=c,l[4]=0,l[8]=d,l[12]=0,l[1]=0,l[5]=h,l[9]=p,l[13]=0,l[2]=0,l[6]=0,l[10]=u,l[14]=g,l[3]=0,l[7]=0,l[11]=-1,l[15]=0,this}makeOrthographic(t,e,n,s,r,a,o=bn){const l=this.elements,c=1/(e-t),h=1/(n-s),d=1/(a-r),p=(e+t)*c,u=(n+s)*h;let g,v;if(o===bn)g=(a+r)*d,v=-2*d;else if(o===ur)g=r*d,v=-1*d;else throw new Error("THREE.Matrix4.makeOrthographic(): Invalid coordinate system: "+o);return l[0]=2*c,l[4]=0,l[8]=0,l[12]=-p,l[1]=0,l[5]=2*h,l[9]=0,l[13]=-u,l[2]=0,l[6]=0,l[10]=v,l[14]=-g,l[3]=0,l[7]=0,l[11]=0,l[15]=1,this}equals(t){const e=this.elements,n=t.elements;for(let s=0;s<16;s++)if(e[s]!==n[s])return!1;return!0}fromArray(t,e=0){for(let n=0;n<16;n++)this.elements[n]=t[n+e];return this}toArray(t=[],e=0){const n=this.elements;return t[e]=n[0],t[e+1]=n[1],t[e+2]=n[2],t[e+3]=n[3],t[e+4]=n[4],t[e+5]=n[5],t[e+6]=n[6],t[e+7]=n[7],t[e+8]=n[8],t[e+9]=n[9],t[e+10]=n[10],t[e+11]=n[11],t[e+12]=n[12],t[e+13]=n[13],t[e+14]=n[14],t[e+15]=n[15],t}}const pi=new C,rn=new ie,kh=new C(0,0,0),Hh=new C(1,1,1),Ln=new C,Cs=new C,Ye=new C,To=new ie,wo=new si;class gn{constructor(t=0,e=0,n=0,s=gn.DEFAULT_ORDER){this.isEuler=!0,this._x=t,this._y=e,this._z=n,this._order=s}get x(){return this._x}set x(t){this._x=t,this._onChangeCallback()}get y(){return this._y}set y(t){this._y=t,this._onChangeCallback()}get z(){return this._z}set z(t){this._z=t,this._onChangeCallback()}get order(){return this._order}set order(t){this._order=t,this._onChangeCallback()}set(t,e,n,s=this._order){return this._x=t,this._y=e,this._z=n,this._order=s,this._onChangeCallback(),this}clone(){return new this.constructor(this._x,this._y,this._z,this._order)}copy(t){return this._x=t._x,this._y=t._y,this._z=t._z,this._order=t._order,this._onChangeCallback(),this}setFromRotationMatrix(t,e=this._order,n=!0){const s=t.elements,r=s[0],a=s[4],o=s[8],l=s[1],c=s[5],h=s[9],d=s[2],p=s[6],u=s[10];switch(e){case"XYZ":this._y=Math.asin(Xt(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-h,u),this._z=Math.atan2(-a,r)):(this._x=Math.atan2(p,c),this._z=0);break;case"YXZ":this._x=Math.asin(-Xt(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(o,u),this._z=Math.atan2(l,c)):(this._y=Math.atan2(-d,r),this._z=0);break;case"ZXY":this._x=Math.asin(Xt(p,-1,1)),Math.abs(p)<.9999999?(this._y=Math.atan2(-d,u),this._z=Math.atan2(-a,c)):(this._y=0,this._z=Math.atan2(l,r));break;case"ZYX":this._y=Math.asin(-Xt(d,-1,1)),Math.abs(d)<.9999999?(this._x=Math.atan2(p,u),this._z=Math.atan2(l,r)):(this._x=0,this._z=Math.atan2(-a,c));break;case"YZX":this._z=Math.asin(Xt(l,-1,1)),Math.abs(l)<.9999999?(this._x=Math.atan2(-h,c),this._y=Math.atan2(-d,r)):(this._x=0,this._y=Math.atan2(o,u));break;case"XZY":this._z=Math.asin(-Xt(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(p,c),this._y=Math.atan2(o,r)):(this._x=Math.atan2(-h,u),this._y=0);break;default:console.warn("THREE.Euler: .setFromRotationMatrix() encountered an unknown order: "+e)}return this._order=e,n===!0&&this._onChangeCallback(),this}setFromQuaternion(t,e,n){return To.makeRotationFromQuaternion(t),this.setFromRotationMatrix(To,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return wo.setFromEuler(this),this.setFromQuaternion(wo,t)}equals(t){return t._x===this._x&&t._y===this._y&&t._z===this._z&&t._order===this._order}fromArray(t){return this._x=t[0],this._y=t[1],this._z=t[2],t[3]!==void 0&&(this._order=t[3]),this._onChangeCallback(),this}toArray(t=[],e=0){return t[e]=this._x,t[e+1]=this._y,t[e+2]=this._z,t[e+3]=this._order,t}_onChange(t){return this._onChangeCallback=t,this}_onChangeCallback(){}*[Symbol.iterator](){yield this._x,yield this._y,yield this._z,yield this._order}}gn.DEFAULT_ORDER="XYZ";class ao{constructor(){this.mask=1}set(t){this.mask=(1<>>0}enable(t){this.mask|=1<1){for(let e=0;e1){for(let n=0;n0&&(s.userData=this.userData),s.layers=this.layers.mask,s.matrix=this.matrix.toArray(),s.up=this.up.toArray(),this.matrixAutoUpdate===!1&&(s.matrixAutoUpdate=!1),this.isInstancedMesh&&(s.type="InstancedMesh",s.count=this.count,s.instanceMatrix=this.instanceMatrix.toJSON(),this.instanceColor!==null&&(s.instanceColor=this.instanceColor.toJSON())),this.isBatchedMesh&&(s.type="BatchedMesh",s.perObjectFrustumCulled=this.perObjectFrustumCulled,s.sortObjects=this.sortObjects,s.drawRanges=this._drawRanges,s.reservedRanges=this._reservedRanges,s.visibility=this._visibility,s.active=this._active,s.bounds=this._bounds.map(o=>({boxInitialized:o.boxInitialized,boxMin:o.box.min.toArray(),boxMax:o.box.max.toArray(),sphereInitialized:o.sphereInitialized,sphereRadius:o.sphere.radius,sphereCenter:o.sphere.center.toArray()})),s.maxInstanceCount=this._maxInstanceCount,s.maxVertexCount=this._maxVertexCount,s.maxIndexCount=this._maxIndexCount,s.geometryInitialized=this._geometryInitialized,s.geometryCount=this._geometryCount,s.matricesTexture=this._matricesTexture.toJSON(t),this._colorsTexture!==null&&(s.colorsTexture=this._colorsTexture.toJSON(t)),this.boundingSphere!==null&&(s.boundingSphere={center:s.boundingSphere.center.toArray(),radius:s.boundingSphere.radius}),this.boundingBox!==null&&(s.boundingBox={min:s.boundingBox.min.toArray(),max:s.boundingBox.max.toArray()}));function r(o,l){return o[l.uuid]===void 0&&(o[l.uuid]=l.toJSON(t)),l.uuid}if(this.isScene)this.background&&(this.background.isColor?s.background=this.background.toJSON():this.background.isTexture&&(s.background=this.background.toJSON(t).uuid)),this.environment&&this.environment.isTexture&&this.environment.isRenderTargetTexture!==!0&&(s.environment=this.environment.toJSON(t).uuid);else if(this.isMesh||this.isLine||this.isPoints){s.geometry=r(t.geometries,this.geometry);const o=this.geometry.parameters;if(o!==void 0&&o.shapes!==void 0){const l=o.shapes;if(Array.isArray(l))for(let c=0,h=l.length;c0){s.children=[];for(let o=0;o0){s.animations=[];for(let o=0;o0&&(n.geometries=o),l.length>0&&(n.materials=l),c.length>0&&(n.textures=c),h.length>0&&(n.images=h),d.length>0&&(n.shapes=d),p.length>0&&(n.skeletons=p),u.length>0&&(n.animations=u),g.length>0&&(n.nodes=g)}return n.object=s,n;function a(o){const l=[];for(const c in o){const h=o[c];delete h.metadata,l.push(h)}return l}}clone(t){return new this.constructor().copy(this,t)}copy(t,e=!0){if(this.name=t.name,this.up.copy(t.up),this.position.copy(t.position),this.rotation.order=t.rotation.order,this.quaternion.copy(t.quaternion),this.scale.copy(t.scale),this.matrix.copy(t.matrix),this.matrixWorld.copy(t.matrixWorld),this.matrixAutoUpdate=t.matrixAutoUpdate,this.matrixWorldAutoUpdate=t.matrixWorldAutoUpdate,this.matrixWorldNeedsUpdate=t.matrixWorldNeedsUpdate,this.layers.mask=t.layers.mask,this.visible=t.visible,this.castShadow=t.castShadow,this.receiveShadow=t.receiveShadow,this.frustumCulled=t.frustumCulled,this.renderOrder=t.renderOrder,this.animations=t.animations.slice(),this.userData=JSON.parse(JSON.stringify(t.userData)),e===!0)for(let n=0;n0?s.multiplyScalar(1/Math.sqrt(r)):s.set(0,0,0)}static getBarycoord(t,e,n,s,r){an.subVectors(s,e),Mn.subVectors(n,e),Nr.subVectors(t,e);const a=an.dot(an),o=an.dot(Mn),l=an.dot(Nr),c=Mn.dot(Mn),h=Mn.dot(Nr),d=a*c-o*o;if(d===0)return r.set(0,0,0),null;const p=1/d,u=(c*l-o*h)*p,g=(a*h-o*l)*p;return r.set(1-u-g,g,u)}static containsPoint(t,e,n,s){return this.getBarycoord(t,e,n,s,Sn)===null?!1:Sn.x>=0&&Sn.y>=0&&Sn.x+Sn.y<=1}static getInterpolation(t,e,n,s,r,a,o,l){return this.getBarycoord(t,e,n,s,Sn)===null?(l.x=0,l.y=0,"z"in l&&(l.z=0),"w"in l&&(l.w=0),null):(l.setScalar(0),l.addScaledVector(r,Sn.x),l.addScaledVector(a,Sn.y),l.addScaledVector(o,Sn.z),l)}static getInterpolatedAttribute(t,e,n,s,r,a){return zr.setScalar(0),kr.setScalar(0),Hr.setScalar(0),zr.fromBufferAttribute(t,e),kr.fromBufferAttribute(t,n),Hr.fromBufferAttribute(t,s),a.setScalar(0),a.addScaledVector(zr,r.x),a.addScaledVector(kr,r.y),a.addScaledVector(Hr,r.z),a}static isFrontFacing(t,e,n,s){return an.subVectors(n,e),Mn.subVectors(t,e),an.cross(Mn).dot(s)<0}set(t,e,n){return this.a.copy(t),this.b.copy(e),this.c.copy(n),this}setFromPointsAndIndices(t,e,n,s){return this.a.copy(t[e]),this.b.copy(t[n]),this.c.copy(t[s]),this}setFromAttributeAndIndices(t,e,n,s){return this.a.fromBufferAttribute(t,e),this.b.fromBufferAttribute(t,n),this.c.fromBufferAttribute(t,s),this}clone(){return new this.constructor().copy(this)}copy(t){return this.a.copy(t.a),this.b.copy(t.b),this.c.copy(t.c),this}getArea(){return an.subVectors(this.c,this.b),Mn.subVectors(this.a,this.b),an.cross(Mn).length()*.5}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return tn.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return tn.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,s,r){return tn.getInterpolation(t,this.a,this.b,this.c,e,n,s,r)}containsPoint(t){return tn.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return tn.isFrontFacing(this.a,this.b,this.c,t)}intersectsBox(t){return t.intersectsTriangle(this)}closestPointToPoint(t,e){const n=this.a,s=this.b,r=this.c;let a,o;_i.subVectors(s,n),vi.subVectors(r,n),Fr.subVectors(t,n);const l=_i.dot(Fr),c=vi.dot(Fr);if(l<=0&&c<=0)return e.copy(n);Or.subVectors(t,s);const h=_i.dot(Or),d=vi.dot(Or);if(h>=0&&d<=h)return e.copy(s);const p=l*d-h*c;if(p<=0&&l>=0&&h<=0)return a=l/(l-h),e.copy(n).addScaledVector(_i,a);Br.subVectors(t,r);const u=_i.dot(Br),g=vi.dot(Br);if(g>=0&&u<=g)return e.copy(r);const v=u*c-l*g;if(v<=0&&c>=0&&g<=0)return o=c/(c-g),e.copy(n).addScaledVector(vi,o);const m=h*g-u*d;if(m<=0&&d-h>=0&&u-g>=0)return Lo.subVectors(r,s),o=(d-h)/(d-h+(u-g)),e.copy(s).addScaledVector(Lo,o);const f=1/(m+v+p);return a=v*f,o=p*f,e.copy(n).addScaledVector(_i,a).addScaledVector(vi,o)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}const sc={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074},Un={h:0,s:0,l:0},Ds={h:0,s:0,l:0};function Vr(i,t,e){return e<0&&(e+=1),e>1&&(e-=1),e<1/6?i+(t-i)*6*e:e<1/2?t:e<2/3?i+(t-i)*6*(2/3-e):i}class rt{constructor(t,e,n){return this.isColor=!0,this.r=1,this.g=1,this.b=1,this.set(t,e,n)}set(t,e,n){if(e===void 0&&n===void 0){const s=t;s&&s.isColor?this.copy(s):typeof s=="number"?this.setHex(s):typeof s=="string"&&this.setStyle(s)}else this.setRGB(t,e,n);return this}setScalar(t){return this.r=t,this.g=t,this.b=t,this}setHex(t,e=Qe){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(t&255)/255,Qt.toWorkingColorSpace(this,e),this}setRGB(t,e,n,s=Qt.workingColorSpace){return this.r=t,this.g=e,this.b=n,Qt.toWorkingColorSpace(this,s),this}setHSL(t,e,n,s=Qt.workingColorSpace){if(t=Ah(t,1),e=Xt(e,0,1),n=Xt(n,0,1),e===0)this.r=this.g=this.b=n;else{const r=n<=.5?n*(1+e):n+e-n*e,a=2*n-r;this.r=Vr(a,r,t+1/3),this.g=Vr(a,r,t),this.b=Vr(a,r,t-1/3)}return Qt.toWorkingColorSpace(this,s),this}setStyle(t,e=Qe){function n(r){r!==void 0&&parseFloat(r)<1&&console.warn("THREE.Color: Alpha component of "+t+" will be ignored.")}let s;if(s=/^(\w+)\(([^\)]*)\)/.exec(t)){let r;const a=s[1],o=s[2];switch(a){case"rgb":case"rgba":if(r=/^\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(255,parseInt(r[1],10))/255,Math.min(255,parseInt(r[2],10))/255,Math.min(255,parseInt(r[3],10))/255,e);if(r=/^\s*(\d+)\%\s*,\s*(\d+)\%\s*,\s*(\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setRGB(Math.min(100,parseInt(r[1],10))/100,Math.min(100,parseInt(r[2],10))/100,Math.min(100,parseInt(r[3],10))/100,e);break;case"hsl":case"hsla":if(r=/^\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\%\s*,\s*(\d*\.?\d+)\%\s*(?:,\s*(\d*\.?\d+)\s*)?$/.exec(o))return n(r[4]),this.setHSL(parseFloat(r[1])/360,parseFloat(r[2])/100,parseFloat(r[3])/100,e);break;default:console.warn("THREE.Color: Unknown color model "+t)}}else if(s=/^\#([A-Fa-f\d]+)$/.exec(t)){const r=s[1],a=r.length;if(a===3)return this.setRGB(parseInt(r.charAt(0),16)/15,parseInt(r.charAt(1),16)/15,parseInt(r.charAt(2),16)/15,e);if(a===6)return this.setHex(parseInt(r,16),e);console.warn("THREE.Color: Invalid hex color "+t)}else if(t&&t.length>0)return this.setColorName(t,e);return this}setColorName(t,e=Qe){const n=sc[t.toLowerCase()];return n!==void 0?this.setHex(n,e):console.warn("THREE.Color: Unknown color "+t),this}clone(){return new this.constructor(this.r,this.g,this.b)}copy(t){return this.r=t.r,this.g=t.g,this.b=t.b,this}copySRGBToLinear(t){return this.r=An(t.r),this.g=An(t.g),this.b=An(t.b),this}copyLinearToSRGB(t){return this.r=Ni(t.r),this.g=Ni(t.g),this.b=Ni(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=Qe){return Qt.fromWorkingColorSpace(Ce.copy(this),t),Math.round(Xt(Ce.r*255,0,255))*65536+Math.round(Xt(Ce.g*255,0,255))*256+Math.round(Xt(Ce.b*255,0,255))}getHexString(t=Qe){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Qt.workingColorSpace){Qt.fromWorkingColorSpace(Ce.copy(this),e);const n=Ce.r,s=Ce.g,r=Ce.b,a=Math.max(n,s,r),o=Math.min(n,s,r);let l,c;const h=(o+a)/2;if(o===a)l=0,c=0;else{const d=a-o;switch(c=h<=.5?d/(a+o):d/(2-a-o),a){case n:l=(s-r)/d+(s0!=t>0&&this.version++,this._alphaTest=t}onBeforeRender(){}onBeforeCompile(){}customProgramCacheKey(){return this.onBeforeCompile.toString()}setValues(t){if(t!==void 0)for(const e in t){const n=t[e];if(n===void 0){console.warn(`THREE.Material: parameter '${e}' has value of undefined.`);continue}const s=this[e];if(s===void 0){console.warn(`THREE.Material: '${e}' is not a property of THREE.${this.type}.`);continue}s&&s.isColor?s.set(n):s&&s.isVector3&&n&&n.isVector3?s.copy(n):this[e]=n}}toJSON(t){const e=t===void 0||typeof t=="string";e&&(t={textures:{},images:{}});const n={metadata:{version:4.6,type:"Material",generator:"Material.toJSON"}};n.uuid=this.uuid,n.type=this.type,this.name!==""&&(n.name=this.name),this.color&&this.color.isColor&&(n.color=this.color.getHex()),this.roughness!==void 0&&(n.roughness=this.roughness),this.metalness!==void 0&&(n.metalness=this.metalness),this.sheen!==void 0&&(n.sheen=this.sheen),this.sheenColor&&this.sheenColor.isColor&&(n.sheenColor=this.sheenColor.getHex()),this.sheenRoughness!==void 0&&(n.sheenRoughness=this.sheenRoughness),this.emissive&&this.emissive.isColor&&(n.emissive=this.emissive.getHex()),this.emissiveIntensity!==void 0&&this.emissiveIntensity!==1&&(n.emissiveIntensity=this.emissiveIntensity),this.specular&&this.specular.isColor&&(n.specular=this.specular.getHex()),this.specularIntensity!==void 0&&(n.specularIntensity=this.specularIntensity),this.specularColor&&this.specularColor.isColor&&(n.specularColor=this.specularColor.getHex()),this.shininess!==void 0&&(n.shininess=this.shininess),this.clearcoat!==void 0&&(n.clearcoat=this.clearcoat),this.clearcoatRoughness!==void 0&&(n.clearcoatRoughness=this.clearcoatRoughness),this.clearcoatMap&&this.clearcoatMap.isTexture&&(n.clearcoatMap=this.clearcoatMap.toJSON(t).uuid),this.clearcoatRoughnessMap&&this.clearcoatRoughnessMap.isTexture&&(n.clearcoatRoughnessMap=this.clearcoatRoughnessMap.toJSON(t).uuid),this.clearcoatNormalMap&&this.clearcoatNormalMap.isTexture&&(n.clearcoatNormalMap=this.clearcoatNormalMap.toJSON(t).uuid,n.clearcoatNormalScale=this.clearcoatNormalScale.toArray()),this.dispersion!==void 0&&(n.dispersion=this.dispersion),this.iridescence!==void 0&&(n.iridescence=this.iridescence),this.iridescenceIOR!==void 0&&(n.iridescenceIOR=this.iridescenceIOR),this.iridescenceThicknessRange!==void 0&&(n.iridescenceThicknessRange=this.iridescenceThicknessRange),this.iridescenceMap&&this.iridescenceMap.isTexture&&(n.iridescenceMap=this.iridescenceMap.toJSON(t).uuid),this.iridescenceThicknessMap&&this.iridescenceThicknessMap.isTexture&&(n.iridescenceThicknessMap=this.iridescenceThicknessMap.toJSON(t).uuid),this.anisotropy!==void 0&&(n.anisotropy=this.anisotropy),this.anisotropyRotation!==void 0&&(n.anisotropyRotation=this.anisotropyRotation),this.anisotropyMap&&this.anisotropyMap.isTexture&&(n.anisotropyMap=this.anisotropyMap.toJSON(t).uuid),this.map&&this.map.isTexture&&(n.map=this.map.toJSON(t).uuid),this.matcap&&this.matcap.isTexture&&(n.matcap=this.matcap.toJSON(t).uuid),this.alphaMap&&this.alphaMap.isTexture&&(n.alphaMap=this.alphaMap.toJSON(t).uuid),this.lightMap&&this.lightMap.isTexture&&(n.lightMap=this.lightMap.toJSON(t).uuid,n.lightMapIntensity=this.lightMapIntensity),this.aoMap&&this.aoMap.isTexture&&(n.aoMap=this.aoMap.toJSON(t).uuid,n.aoMapIntensity=this.aoMapIntensity),this.bumpMap&&this.bumpMap.isTexture&&(n.bumpMap=this.bumpMap.toJSON(t).uuid,n.bumpScale=this.bumpScale),this.normalMap&&this.normalMap.isTexture&&(n.normalMap=this.normalMap.toJSON(t).uuid,n.normalMapType=this.normalMapType,n.normalScale=this.normalScale.toArray()),this.displacementMap&&this.displacementMap.isTexture&&(n.displacementMap=this.displacementMap.toJSON(t).uuid,n.displacementScale=this.displacementScale,n.displacementBias=this.displacementBias),this.roughnessMap&&this.roughnessMap.isTexture&&(n.roughnessMap=this.roughnessMap.toJSON(t).uuid),this.metalnessMap&&this.metalnessMap.isTexture&&(n.metalnessMap=this.metalnessMap.toJSON(t).uuid),this.emissiveMap&&this.emissiveMap.isTexture&&(n.emissiveMap=this.emissiveMap.toJSON(t).uuid),this.specularMap&&this.specularMap.isTexture&&(n.specularMap=this.specularMap.toJSON(t).uuid),this.specularIntensityMap&&this.specularIntensityMap.isTexture&&(n.specularIntensityMap=this.specularIntensityMap.toJSON(t).uuid),this.specularColorMap&&this.specularColorMap.isTexture&&(n.specularColorMap=this.specularColorMap.toJSON(t).uuid),this.envMap&&this.envMap.isTexture&&(n.envMap=this.envMap.toJSON(t).uuid,this.combine!==void 0&&(n.combine=this.combine)),this.envMapRotation!==void 0&&(n.envMapRotation=this.envMapRotation.toArray()),this.envMapIntensity!==void 0&&(n.envMapIntensity=this.envMapIntensity),this.reflectivity!==void 0&&(n.reflectivity=this.reflectivity),this.refractionRatio!==void 0&&(n.refractionRatio=this.refractionRatio),this.gradientMap&&this.gradientMap.isTexture&&(n.gradientMap=this.gradientMap.toJSON(t).uuid),this.transmission!==void 0&&(n.transmission=this.transmission),this.transmissionMap&&this.transmissionMap.isTexture&&(n.transmissionMap=this.transmissionMap.toJSON(t).uuid),this.thickness!==void 0&&(n.thickness=this.thickness),this.thicknessMap&&this.thicknessMap.isTexture&&(n.thicknessMap=this.thicknessMap.toJSON(t).uuid),this.attenuationDistance!==void 0&&this.attenuationDistance!==1/0&&(n.attenuationDistance=this.attenuationDistance),this.attenuationColor!==void 0&&(n.attenuationColor=this.attenuationColor.getHex()),this.size!==void 0&&(n.size=this.size),this.shadowSide!==null&&(n.shadowSide=this.shadowSide),this.sizeAttenuation!==void 0&&(n.sizeAttenuation=this.sizeAttenuation),this.blending!==Ui&&(n.blending=this.blending),this.side!==kn&&(n.side=this.side),this.vertexColors===!0&&(n.vertexColors=!0),this.opacity<1&&(n.opacity=this.opacity),this.transparent===!0&&(n.transparent=!0),this.blendSrc!==ra&&(n.blendSrc=this.blendSrc),this.blendDst!==aa&&(n.blendDst=this.blendDst),this.blendEquation!==$n&&(n.blendEquation=this.blendEquation),this.blendSrcAlpha!==null&&(n.blendSrcAlpha=this.blendSrcAlpha),this.blendDstAlpha!==null&&(n.blendDstAlpha=this.blendDstAlpha),this.blendEquationAlpha!==null&&(n.blendEquationAlpha=this.blendEquationAlpha),this.blendColor&&this.blendColor.isColor&&(n.blendColor=this.blendColor.getHex()),this.blendAlpha!==0&&(n.blendAlpha=this.blendAlpha),this.depthFunc!==Oi&&(n.depthFunc=this.depthFunc),this.depthTest===!1&&(n.depthTest=this.depthTest),this.depthWrite===!1&&(n.depthWrite=this.depthWrite),this.colorWrite===!1&&(n.colorWrite=this.colorWrite),this.stencilWriteMask!==255&&(n.stencilWriteMask=this.stencilWriteMask),this.stencilFunc!==xo&&(n.stencilFunc=this.stencilFunc),this.stencilRef!==0&&(n.stencilRef=this.stencilRef),this.stencilFuncMask!==255&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==ci&&(n.stencilFail=this.stencilFail),this.stencilZFail!==ci&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==ci&&(n.stencilZPass=this.stencilZPass),this.stencilWrite===!0&&(n.stencilWrite=this.stencilWrite),this.rotation!==void 0&&this.rotation!==0&&(n.rotation=this.rotation),this.polygonOffset===!0&&(n.polygonOffset=!0),this.polygonOffsetFactor!==0&&(n.polygonOffsetFactor=this.polygonOffsetFactor),this.polygonOffsetUnits!==0&&(n.polygonOffsetUnits=this.polygonOffsetUnits),this.linewidth!==void 0&&this.linewidth!==1&&(n.linewidth=this.linewidth),this.dashSize!==void 0&&(n.dashSize=this.dashSize),this.gapSize!==void 0&&(n.gapSize=this.gapSize),this.scale!==void 0&&(n.scale=this.scale),this.dithering===!0&&(n.dithering=!0),this.alphaTest>0&&(n.alphaTest=this.alphaTest),this.alphaHash===!0&&(n.alphaHash=!0),this.alphaToCoverage===!0&&(n.alphaToCoverage=!0),this.premultipliedAlpha===!0&&(n.premultipliedAlpha=!0),this.forceSinglePass===!0&&(n.forceSinglePass=!0),this.wireframe===!0&&(n.wireframe=!0),this.wireframeLinewidth>1&&(n.wireframeLinewidth=this.wireframeLinewidth),this.wireframeLinecap!=="round"&&(n.wireframeLinecap=this.wireframeLinecap),this.wireframeLinejoin!=="round"&&(n.wireframeLinejoin=this.wireframeLinejoin),this.flatShading===!0&&(n.flatShading=!0),this.visible===!1&&(n.visible=!1),this.toneMapped===!1&&(n.toneMapped=!1),this.fog===!1&&(n.fog=!1),Object.keys(this.userData).length>0&&(n.userData=this.userData);function s(r){const a=[];for(const o in r){const l=r[o];delete l.metadata,a.push(l)}return a}if(e){const r=s(t.textures),a=s(t.images);r.length>0&&(n.textures=r),a.length>0&&(n.images=a)}return n}clone(){return new this.constructor().copy(this)}copy(t){this.name=t.name,this.blending=t.blending,this.side=t.side,this.vertexColors=t.vertexColors,this.opacity=t.opacity,this.transparent=t.transparent,this.blendSrc=t.blendSrc,this.blendDst=t.blendDst,this.blendEquation=t.blendEquation,this.blendSrcAlpha=t.blendSrcAlpha,this.blendDstAlpha=t.blendDstAlpha,this.blendEquationAlpha=t.blendEquationAlpha,this.blendColor.copy(t.blendColor),this.blendAlpha=t.blendAlpha,this.depthFunc=t.depthFunc,this.depthTest=t.depthTest,this.depthWrite=t.depthWrite,this.stencilWriteMask=t.stencilWriteMask,this.stencilFunc=t.stencilFunc,this.stencilRef=t.stencilRef,this.stencilFuncMask=t.stencilFuncMask,this.stencilFail=t.stencilFail,this.stencilZFail=t.stencilZFail,this.stencilZPass=t.stencilZPass,this.stencilWrite=t.stencilWrite;const e=t.clippingPlanes;let n=null;if(e!==null){const s=e.length;n=new Array(s);for(let r=0;r!==s;++r)n[r]=e[r].clone()}return this.clippingPlanes=n,this.clipIntersection=t.clipIntersection,this.clipShadows=t.clipShadows,this.shadowSide=t.shadowSide,this.colorWrite=t.colorWrite,this.precision=t.precision,this.polygonOffset=t.polygonOffset,this.polygonOffsetFactor=t.polygonOffsetFactor,this.polygonOffsetUnits=t.polygonOffsetUnits,this.dithering=t.dithering,this.alphaTest=t.alphaTest,this.alphaHash=t.alphaHash,this.alphaToCoverage=t.alphaToCoverage,this.premultipliedAlpha=t.premultipliedAlpha,this.forceSinglePass=t.forceSinglePass,this.visible=t.visible,this.toneMapped=t.toneMapped,this.userData=JSON.parse(JSON.stringify(t.userData)),this}dispose(){this.dispatchEvent({type:"dispose"})}set needsUpdate(t){t===!0&&this.version++}onBuild(){console.warn("Material: onBuild() has been removed.")}}class ds extends Hn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new rt(16777215),this.map=null,this.lightMap=null,this.lightMapIntensity=1,this.aoMap=null,this.aoMapIntensity=1,this.specularMap=null,this.alphaMap=null,this.envMap=null,this.envMapRotation=new gn,this.combine=Hl,this.reflectivity=1,this.refractionRatio=.98,this.wireframe=!1,this.wireframeLinewidth=1,this.wireframeLinecap="round",this.wireframeLinejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.lightMap=t.lightMap,this.lightMapIntensity=t.lightMapIntensity,this.aoMap=t.aoMap,this.aoMapIntensity=t.aoMapIntensity,this.specularMap=t.specularMap,this.alphaMap=t.alphaMap,this.envMap=t.envMap,this.envMapRotation.copy(t.envMapRotation),this.combine=t.combine,this.reflectivity=t.reflectivity,this.refractionRatio=t.refractionRatio,this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.wireframeLinecap=t.wireframeLinecap,this.wireframeLinejoin=t.wireframeLinejoin,this.fog=t.fog,this}}const ve=new C,Ls=new Mt;class ue{constructor(t,e,n=!1){if(Array.isArray(t))throw new TypeError("THREE.BufferAttribute: array should be a Typed Array.");this.isBufferAttribute=!0,this.name="",this.array=t,this.itemSize=e,this.count=t!==void 0?t.length/e:0,this.normalized=n,this.usage=Wa,this.updateRanges=[],this.gpuType=mn,this.version=0}onUploadCallback(){}set needsUpdate(t){t===!0&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.name=t.name,this.array=new t.array.constructor(t.array),this.itemSize=t.itemSize,this.count=t.count,this.normalized=t.normalized,this.usage=t.usage,this.gpuType=t.gpuType,this}copyAt(t,e,n){t*=this.itemSize,n*=e.itemSize;for(let s=0,r=this.itemSize;se.count&&console.warn("THREE.BufferGeometry: Buffer size too small for points data. Use .dispose() and create a new geometry."),e.needsUpdate=!0}return this}computeBoundingBox(){this.boundingBox===null&&(this.boundingBox=new ai);const t=this.attributes.position,e=this.morphAttributes.position;if(t&&t.isGLBufferAttribute){console.error("THREE.BufferGeometry.computeBoundingBox(): GLBufferAttribute requires a manual bounding box.",this),this.boundingBox.set(new C(-1/0,-1/0,-1/0),new C(1/0,1/0,1/0));return}if(t!==void 0){if(this.boundingBox.setFromBufferAttribute(t),e)for(let n=0,s=e.length;n0&&(t.userData=this.userData),this.parameters!==void 0){const l=this.parameters;for(const c in l)l[c]!==void 0&&(t[c]=l[c]);return t}t.data={attributes:{}};const e=this.index;e!==null&&(t.data.index={type:e.array.constructor.name,array:Array.prototype.slice.call(e.array)});const n=this.attributes;for(const l in n){const c=n[l];t.data.attributes[l]=c.toJSON(t.data)}const s={};let r=!1;for(const l in this.morphAttributes){const c=this.morphAttributes[l],h=[];for(let d=0,p=c.length;d0&&(s[l]=h,r=!0)}r&&(t.data.morphAttributes=s,t.data.morphTargetsRelative=this.morphTargetsRelative);const a=this.groups;a.length>0&&(t.data.groups=JSON.parse(JSON.stringify(a)));const o=this.boundingSphere;return o!==null&&(t.data.boundingSphere={center:o.center.toArray(),radius:o.radius}),t}clone(){return new this.constructor().copy(this)}copy(t){this.index=null,this.attributes={},this.morphAttributes={},this.groups=[],this.boundingBox=null,this.boundingSphere=null;const e={};this.name=t.name;const n=t.index;n!==null&&this.setIndex(n.clone(e));const s=t.attributes;for(const c in s){const h=s[c];this.setAttribute(c,h.clone(e))}const r=t.morphAttributes;for(const c in r){const h=[],d=r[c];for(let p=0,u=d.length;p0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;r(t.far-t.near)**2))&&(Uo.copy(r).invert(),Xn.copy(t.ray).applyMatrix4(Uo),!(n.boundingBox!==null&&Xn.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(t,e,Xn)))}_computeIntersections(t,e,n){let s;const r=this.geometry,a=this.material,o=r.index,l=r.attributes.position,c=r.attributes.uv,h=r.attributes.uv1,d=r.attributes.normal,p=r.groups,u=r.drawRange;if(o!==null)if(Array.isArray(a))for(let g=0,v=p.length;ge.far?null:{distance:c,point:Bs.clone(),object:i}}function zs(i,t,e,n,s,r,a,o,l,c){i.getVertexPosition(o,Is),i.getVertexPosition(l,Ns),i.getVertexPosition(c,Fs);const h=jh(i,t,e,n,Is,Ns,Fs,No);if(h){const d=new C;tn.getBarycoord(No,Is,Ns,Fs,d),s&&(h.uv=tn.getInterpolatedAttribute(s,o,l,c,d,new Mt)),r&&(h.uv1=tn.getInterpolatedAttribute(r,o,l,c,d,new Mt)),a&&(h.normal=tn.getInterpolatedAttribute(a,o,l,c,d,new C),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));const p={a:o,b:l,c,normal:new C,materialIndex:0};tn.getNormal(Is,Ns,Fs,p.normal),h.face=p,h.barycoord=d}return h}class gs extends ge{constructor(t=1,e=1,n=1,s=1,r=1,a=1){super(),this.type="BoxGeometry",this.parameters={width:t,height:e,depth:n,widthSegments:s,heightSegments:r,depthSegments:a};const o=this;s=Math.floor(s),r=Math.floor(r),a=Math.floor(a);const l=[],c=[],h=[],d=[];let p=0,u=0;g("z","y","x",-1,-1,n,e,t,a,r,0),g("z","y","x",1,-1,n,e,-t,a,r,1),g("x","z","y",1,1,t,n,e,s,a,2),g("x","z","y",1,-1,t,n,-e,s,a,3),g("x","y","z",1,-1,t,e,n,s,r,4),g("x","y","z",-1,-1,t,e,-n,s,r,5),this.setIndex(l),this.setAttribute("position",new Le(c,3)),this.setAttribute("normal",new Le(h,3)),this.setAttribute("uv",new Le(d,2));function g(v,m,f,T,b,S,D,w,R,U,y){const M=S/R,P=D/U,W=S/2,z=D/2,G=w/2,$=R+1,X=U+1;let Q=0,k=0;const it=new C;for(let ft=0;ft0?1:-1,h.push(it.x,it.y,it.z),d.push(Ft/R),d.push(1-ft/U),Q+=1}}for(let ft=0;ft0&&(e.defines=this.defines),e.vertexShader=this.vertexShader,e.fragmentShader=this.fragmentShader,e.lights=this.lights,e.clipping=this.clipping;const n={};for(const s in this.extensions)this.extensions[s]===!0&&(n[s]=!0);return Object.keys(n).length>0&&(e.extensions=n),e}}class lc extends Ae{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new ie,this.projectionMatrix=new ie,this.projectionMatrixInverse=new ie,this.coordinateSystem=bn}copy(t,e){return super.copy(t,e),this.matrixWorldInverse.copy(t.matrixWorldInverse),this.projectionMatrix.copy(t.projectionMatrix),this.projectionMatrixInverse.copy(t.projectionMatrixInverse),this.coordinateSystem=t.coordinateSystem,this}getWorldDirection(t){return super.getWorldDirection(t).negate()}updateMatrixWorld(t){super.updateMatrixWorld(t),this.matrixWorldInverse.copy(this.matrixWorld).invert()}updateWorldMatrix(t,e){super.updateWorldMatrix(t,e),this.matrixWorldInverse.copy(this.matrixWorld).invert()}clone(){return new this.constructor().copy(this)}}const In=new C,Fo=new Mt,Oo=new Mt;class je extends lc{constructor(t=50,e=1,n=.1,s=2e3){super(),this.isPerspectiveCamera=!0,this.type="PerspectiveCamera",this.fov=t,this.zoom=1,this.near=n,this.far=s,this.focus=10,this.aspect=e,this.view=null,this.filmGauge=35,this.filmOffset=0,this.updateProjectionMatrix()}copy(t,e){return super.copy(t,e),this.fov=t.fov,this.zoom=t.zoom,this.near=t.near,this.far=t.far,this.focus=t.focus,this.aspect=t.aspect,this.view=t.view===null?null:Object.assign({},t.view),this.filmGauge=t.filmGauge,this.filmOffset=t.filmOffset,this}setFocalLength(t){const e=.5*this.getFilmHeight()/t;this.fov=Xa*2*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(rr*.5*this.fov);return .5*this.getFilmHeight()/t}getEffectiveFOV(){return Xa*2*Math.atan(Math.tan(rr*.5*this.fov)/this.zoom)}getFilmWidth(){return this.filmGauge*Math.min(this.aspect,1)}getFilmHeight(){return this.filmGauge/Math.max(this.aspect,1)}getViewBounds(t,e,n){In.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(In.x,In.y).multiplyScalar(-t/In.z),In.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(In.x,In.y).multiplyScalar(-t/In.z)}getViewSize(t,e){return this.getViewBounds(t,Fo,Oo),e.subVectors(Oo,Fo)}setViewOffset(t,e,n,s,r,a){this.aspect=t/e,this.view===null&&(this.view={enabled:!0,fullWidth:1,fullHeight:1,offsetX:0,offsetY:0,width:1,height:1}),this.view.enabled=!0,this.view.fullWidth=t,this.view.fullHeight=e,this.view.offsetX=n,this.view.offsetY=s,this.view.width=r,this.view.height=a,this.updateProjectionMatrix()}clearViewOffset(){this.view!==null&&(this.view.enabled=!1),this.updateProjectionMatrix()}updateProjectionMatrix(){const t=this.near;let e=t*Math.tan(rr*.5*this.fov)/this.zoom,n=2*e,s=this.aspect*n,r=-.5*s;const a=this.view;if(this.view!==null&&this.view.enabled){const l=a.fullWidth,c=a.fullHeight;r+=a.offsetX*s/l,e-=a.offsetY*n/c,s*=a.width/l,n*=a.height/c}const o=this.filmOffset;o!==0&&(r+=t*o/this.getFilmWidth()),this.projectionMatrix.makePerspective(r,r+s,e,e-n,t,this.far,this.coordinateSystem),this.projectionMatrixInverse.copy(this.projectionMatrix).invert()}toJSON(t){const e=super.toJSON(t);return e.object.fov=this.fov,e.object.zoom=this.zoom,e.object.near=this.near,e.object.far=this.far,e.object.focus=this.focus,e.object.aspect=this.aspect,this.view!==null&&(e.object.view=Object.assign({},this.view)),e.object.filmGauge=this.filmGauge,e.object.filmOffset=this.filmOffset,e}}const Mi=-90,Si=1;class Jh extends Ae{constructor(t,e,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const s=new je(Mi,Si,t,e);s.layers=this.layers,this.add(s);const r=new je(Mi,Si,t,e);r.layers=this.layers,this.add(r);const a=new je(Mi,Si,t,e);a.layers=this.layers,this.add(a);const o=new je(Mi,Si,t,e);o.layers=this.layers,this.add(o);const l=new je(Mi,Si,t,e);l.layers=this.layers,this.add(l);const c=new je(Mi,Si,t,e);c.layers=this.layers,this.add(c)}updateCoordinateSystem(){const t=this.coordinateSystem,e=this.children.concat(),[n,s,r,a,o,l]=e;for(const c of e)this.remove(c);if(t===bn)n.up.set(0,1,0),n.lookAt(1,0,0),s.up.set(0,1,0),s.lookAt(-1,0,0),r.up.set(0,0,-1),r.lookAt(0,1,0),a.up.set(0,0,1),a.lookAt(0,-1,0),o.up.set(0,1,0),o.lookAt(0,0,1),l.up.set(0,1,0),l.lookAt(0,0,-1);else if(t===ur)n.up.set(0,-1,0),n.lookAt(-1,0,0),s.up.set(0,-1,0),s.lookAt(1,0,0),r.up.set(0,0,1),r.lookAt(0,1,0),a.up.set(0,0,-1),a.lookAt(0,-1,0),o.up.set(0,-1,0),o.lookAt(0,0,1),l.up.set(0,-1,0),l.lookAt(0,0,-1);else throw new Error("THREE.CubeCamera.updateCoordinateSystem(): Invalid coordinate system: "+t);for(const c of e)this.add(c),c.updateMatrixWorld()}update(t,e){this.parent===null&&this.updateMatrixWorld();const{renderTarget:n,activeMipmapLevel:s}=this;this.coordinateSystem!==t.coordinateSystem&&(this.coordinateSystem=t.coordinateSystem,this.updateCoordinateSystem());const[r,a,o,l,c,h]=this.children,d=t.getRenderTarget(),p=t.getActiveCubeFace(),u=t.getActiveMipmapLevel(),g=t.xr.enabled;t.xr.enabled=!1;const v=n.texture.generateMipmaps;n.texture.generateMipmaps=!1,t.setRenderTarget(n,0,s),t.render(e,r),t.setRenderTarget(n,1,s),t.render(e,a),t.setRenderTarget(n,2,s),t.render(e,o),t.setRenderTarget(n,3,s),t.render(e,l),t.setRenderTarget(n,4,s),t.render(e,c),n.texture.generateMipmaps=v,t.setRenderTarget(n,5,s),t.render(e,h),t.setRenderTarget(d,p,u),t.xr.enabled=g,n.texture.needsPMREMUpdate=!0}}class cc extends be{constructor(t,e,n,s,r,a,o,l,c,h){t=t!==void 0?t:[],e=e!==void 0?e:Bi,super(t,e,n,s,r,a,o,l,c,h),this.isCubeTexture=!0,this.flipY=!1}get images(){return this.image}set images(t){this.image=t}}class Qh extends cn{constructor(t=1,e={}){super(t,t,e),this.isWebGLCubeRenderTarget=!0;const n={width:t,height:t,depth:1},s=[n,n,n,n,n,n];this.texture=new cc(s,e.mapping,e.wrapS,e.wrapT,e.magFilter,e.minFilter,e.format,e.type,e.anisotropy,e.colorSpace),this.texture.isRenderTargetTexture=!0,this.texture.generateMipmaps=e.generateMipmaps!==void 0?e.generateMipmaps:!1,this.texture.minFilter=e.minFilter!==void 0?e.minFilter:pn}fromEquirectangularTexture(t,e){this.texture.type=e.type,this.texture.colorSpace=e.colorSpace,this.texture.generateMipmaps=e.generateMipmaps,this.texture.minFilter=e.minFilter,this.texture.magFilter=e.magFilter;const n={uniforms:{tEquirect:{value:null}},vertexShader:` - - varying vec3 vWorldDirection; - - vec3 transformDirection( in vec3 dir, in mat4 matrix ) { - - return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); - - } - - void main() { - - vWorldDirection = transformDirection( position, modelMatrix ); - - #include - #include - - } - `,fragmentShader:` - - uniform sampler2D tEquirect; - - varying vec3 vWorldDirection; - - #include - - void main() { - - vec3 direction = normalize( vWorldDirection ); - - vec2 sampleUV = equirectUv( direction ); - - gl_FragColor = texture2D( tEquirect, sampleUV ); - - } - `},s=new gs(5,5,5),r=new ze({name:"CubemapFromEquirect",uniforms:Gi(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:We,blending:Tn});r.uniforms.tEquirect.value=e;const a=new Me(s,r),o=e.minFilter;return e.minFilter===ti&&(e.minFilter=pn),new Jh(1,10,this).update(t,a),e.minFilter=o,a.geometry.dispose(),a.material.dispose(),this}clear(t,e,n,s){const r=t.getRenderTarget();for(let a=0;a<6;a++)t.setRenderTarget(this,a),t.clear(e,n,s);t.setRenderTarget(r)}}class xr{constructor(t,e=25e-5){this.isFogExp2=!0,this.name="",this.color=new rt(t),this.density=e}clone(){return new xr(this.color,this.density)}toJSON(){return{type:"FogExp2",name:this.name,color:this.color.getHex(),density:this.density}}}class tu extends Ae{constructor(){super(),this.isScene=!0,this.type="Scene",this.background=null,this.environment=null,this.fog=null,this.backgroundBlurriness=0,this.backgroundIntensity=1,this.backgroundRotation=new gn,this.environmentIntensity=1,this.environmentRotation=new gn,this.overrideMaterial=null,typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}copy(t,e){return super.copy(t,e),t.background!==null&&(this.background=t.background.clone()),t.environment!==null&&(this.environment=t.environment.clone()),t.fog!==null&&(this.fog=t.fog.clone()),this.backgroundBlurriness=t.backgroundBlurriness,this.backgroundIntensity=t.backgroundIntensity,this.backgroundRotation.copy(t.backgroundRotation),this.environmentIntensity=t.environmentIntensity,this.environmentRotation.copy(t.environmentRotation),t.overrideMaterial!==null&&(this.overrideMaterial=t.overrideMaterial.clone()),this.matrixAutoUpdate=t.matrixAutoUpdate,this}toJSON(t){const e=super.toJSON(t);return this.fog!==null&&(e.object.fog=this.fog.toJSON()),this.backgroundBlurriness>0&&(e.object.backgroundBlurriness=this.backgroundBlurriness),this.backgroundIntensity!==1&&(e.object.backgroundIntensity=this.backgroundIntensity),e.object.backgroundRotation=this.backgroundRotation.toArray(),this.environmentIntensity!==1&&(e.object.environmentIntensity=this.environmentIntensity),e.object.environmentRotation=this.environmentRotation.toArray(),e}}class eu{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=t!==void 0?t.length/e:0,this.usage=Wa,this.updateRanges=[],this.version=0,this.uuid=zn()}onUploadCallback(){}set needsUpdate(t){t===!0&&this.version++}setUsage(t){return this.usage=t,this}addUpdateRange(t,e){this.updateRanges.push({start:t,count:e})}clearUpdateRanges(){this.updateRanges.length=0}copy(t){return this.array=new t.array.constructor(t.array),this.count=t.count,this.stride=t.stride,this.usage=t.usage,this}copyAt(t,e,n){t*=this.stride,n*=e.stride;for(let s=0,r=this.stride;st.far||e.push({distance:l,point:ts.clone(),uv:tn.getInterpolation(ts,ks,ns,Hs,Bo,Xr,zo,new Mt),face:null,object:this})}copy(t,e){return super.copy(t,e),t.center!==void 0&&this.center.copy(t.center),this.material=t.material,this}}function Gs(i,t,e,n,s,r){Ti.subVectors(i,e).addScalar(.5).multiply(n),s!==void 0?(es.x=r*Ti.x-s*Ti.y,es.y=s*Ti.x+r*Ti.y):es.copy(Ti),i.copy(t),i.x+=es.x,i.y+=es.y,i.applyMatrix4(hc)}class nu extends be{constructor(t=null,e=1,n=1,s,r,a,o,l,c=Ze,h=Ze,d,p){super(null,a,o,l,c,h,s,r,d,p),this.isDataTexture=!0,this.image={data:t,width:e,height:n},this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}}class ko extends ue{constructor(t,e,n,s=1){super(t,e,n),this.isInstancedBufferAttribute=!0,this.meshPerAttribute=s}copy(t){return super.copy(t),this.meshPerAttribute=t.meshPerAttribute,this}toJSON(){const t=super.toJSON();return t.meshPerAttribute=this.meshPerAttribute,t.isInstancedBufferAttribute=!0,t}}const wi=new ie,Ho=new ie,Ws=[],Vo=new ai,iu=new ie,is=new Me,ss=new oi;class su extends Me{constructor(t,e,n){super(t,e),this.isInstancedMesh=!0,this.instanceMatrix=new ko(new Float32Array(n*16),16),this.instanceColor=null,this.morphTexture=null,this.count=n,this.boundingBox=null,this.boundingSphere=null;for(let s=0;s1?null:e.copy(t.start).addScaledVector(n,r)}intersectsLine(t){const e=this.distanceToPoint(t.start),n=this.distanceToPoint(t.end);return e<0&&n>0||n<0&&e>0}intersectsBox(t){return t.intersectsPlane(this)}intersectsSphere(t){return t.intersectsPlane(this)}coplanarPoint(t){return t.copy(this.normal).multiplyScalar(-this.constant)}applyMatrix4(t,e){const n=e||au.getNormalMatrix(t),s=this.coplanarPoint(Yr).applyMatrix4(t),r=this.normal.applyMatrix3(n).normalize();return this.constant=-s.dot(r),this}translate(t){return this.constant-=t.dot(this.normal),this}equals(t){return t.normal.equals(this.normal)&&t.constant===this.constant}clone(){return new this.constructor().copy(this)}}const Yn=new oi,Xs=new C;class oo{constructor(t=new Fn,e=new Fn,n=new Fn,s=new Fn,r=new Fn,a=new Fn){this.planes=[t,e,n,s,r,a]}set(t,e,n,s,r,a){const o=this.planes;return o[0].copy(t),o[1].copy(e),o[2].copy(n),o[3].copy(s),o[4].copy(r),o[5].copy(a),this}copy(t){const e=this.planes;for(let n=0;n<6;n++)e[n].copy(t.planes[n]);return this}setFromProjectionMatrix(t,e=bn){const n=this.planes,s=t.elements,r=s[0],a=s[1],o=s[2],l=s[3],c=s[4],h=s[5],d=s[6],p=s[7],u=s[8],g=s[9],v=s[10],m=s[11],f=s[12],T=s[13],b=s[14],S=s[15];if(n[0].setComponents(l-r,p-c,m-u,S-f).normalize(),n[1].setComponents(l+r,p+c,m+u,S+f).normalize(),n[2].setComponents(l+a,p+h,m+g,S+T).normalize(),n[3].setComponents(l-a,p-h,m-g,S-T).normalize(),n[4].setComponents(l-o,p-d,m-v,S-b).normalize(),e===bn)n[5].setComponents(l+o,p+d,m+v,S+b).normalize();else if(e===ur)n[5].setComponents(o,d,v,b).normalize();else throw new Error("THREE.Frustum.setFromProjectionMatrix(): Invalid coordinate system: "+e);return this}intersectsObject(t){if(t.boundingSphere!==void 0)t.boundingSphere===null&&t.computeBoundingSphere(),Yn.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;e.boundingSphere===null&&e.computeBoundingSphere(),Yn.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere(Yn)}intersectsSprite(t){return Yn.center.set(0,0,0),Yn.radius=.7071067811865476,Yn.applyMatrix4(t.matrixWorld),this.intersectsSphere(Yn)}intersectsSphere(t){const e=this.planes,n=t.center,s=-t.radius;for(let r=0;r<6;r++)if(e[r].distanceToPoint(n)0?t.max.x:t.min.x,Xs.y=s.normal.y>0?t.max.y:t.min.y,Xs.z=s.normal.z>0?t.max.z:t.min.z,s.distanceToPoint(Xs)<0)return!1}return!0}containsPoint(t){const e=this.planes;for(let n=0;n<6;n++)if(e[n].distanceToPoint(t)<0)return!1;return!0}clone(){return new this.constructor().copy(this)}}class mr extends Hn{constructor(t){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new rt(16777215),this.map=null,this.linewidth=1,this.linecap="round",this.linejoin="round",this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.linewidth=t.linewidth,this.linecap=t.linecap,this.linejoin=t.linejoin,this.fog=t.fog,this}}const gr=new C,_r=new C,Go=new ie,rs=new ms,Ys=new oi,qr=new C,Wo=new C;class Ya extends Ae{constructor(t=new ge,e=new mr){super(),this.isLine=!0,this.type="Line",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}computeLineDistances(){const t=this.geometry;if(t.index===null){const e=t.attributes.position,n=[0];for(let s=1,r=e.count;s0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;rn)return;qr.applyMatrix4(i.matrixWorld);const l=t.ray.origin.distanceTo(qr);if(!(lt.far))return{distance:l,point:Wo.clone().applyMatrix4(i.matrixWorld),index:s,face:null,faceIndex:null,barycoord:null,object:i}}class ni extends Hn{constructor(t){super(),this.isPointsMaterial=!0,this.type="PointsMaterial",this.color=new rt(16777215),this.map=null,this.alphaMap=null,this.size=1,this.sizeAttenuation=!0,this.fog=!0,this.setValues(t)}copy(t){return super.copy(t),this.color.copy(t.color),this.map=t.map,this.alphaMap=t.alphaMap,this.size=t.size,this.sizeAttenuation=t.sizeAttenuation,this.fog=t.fog,this}}const Xo=new ie,qa=new ms,js=new oi,Zs=new C;class Fi extends Ae{constructor(t=new ge,e=new ni){super(),this.isPoints=!0,this.type="Points",this.geometry=t,this.material=e,this.updateMorphTargets()}copy(t,e){return super.copy(t,e),this.material=Array.isArray(t.material)?t.material.slice():t.material,this.geometry=t.geometry,this}raycast(t,e){const n=this.geometry,s=this.matrixWorld,r=t.params.Points.threshold,a=n.drawRange;if(n.boundingSphere===null&&n.computeBoundingSphere(),js.copy(n.boundingSphere),js.applyMatrix4(s),js.radius+=r,t.ray.intersectsSphere(js)===!1)return;Xo.copy(s).invert(),qa.copy(t.ray).applyMatrix4(Xo);const o=r/((this.scale.x+this.scale.y+this.scale.z)/3),l=o*o,c=n.index,d=n.attributes.position;if(c!==null){const p=Math.max(0,a.start),u=Math.min(c.count,a.start+a.count);for(let g=p,v=u;g0){const s=e[n[0]];if(s!==void 0){this.morphTargetInfluences=[],this.morphTargetDictionary={};for(let r=0,a=s.length;rs.far)return;r.push({distance:c,distanceToRay:Math.sqrt(o),point:l,index:t,face:null,faceIndex:null,barycoord:null,object:a})}}class Pi extends Ae{constructor(){super(),this.isGroup=!0,this.type="Group"}}class uc extends be{constructor(t,e,n,s,r,a,o,l,c){super(t,e,n,s,r,a,o,l,c),this.isCanvasTexture=!0,this.needsUpdate=!0}}class dc extends be{constructor(t,e,n,s,r,a,o,l,c,h=Ii){if(h!==Ii&&h!==Hi)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");n===void 0&&h===Ii&&(n=ii),n===void 0&&h===Hi&&(n=ki),super(null,s,r,a,o,l,h,n,c),this.isDepthTexture=!0,this.image={width:t,height:e},this.magFilter=o!==void 0?o:Ze,this.minFilter=l!==void 0?l:Ze,this.flipY=!1,this.generateMipmaps=!1,this.compareFunction=null}copy(t){return super.copy(t),this.compareFunction=t.compareFunction,this}toJSON(t){const e=super.toJSON(t);return this.compareFunction!==null&&(e.compareFunction=this.compareFunction),e}}class _s extends ge{constructor(t=1,e=1,n=1,s=1){super(),this.type="PlaneGeometry",this.parameters={width:t,height:e,widthSegments:n,heightSegments:s};const r=t/2,a=e/2,o=Math.floor(n),l=Math.floor(s),c=o+1,h=l+1,d=t/o,p=e/l,u=[],g=[],v=[],m=[];for(let f=0;f0)&&u.push(b,S,w),(f!==n-1||lu.start-g.start);let p=0;for(let u=1;u 0 - vec4 plane; - #ifdef ALPHA_TO_COVERAGE - float distanceToPlane, distanceGradient; - float clipOpacity = 1.0; - #pragma unroll_loop_start - for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; - distanceGradient = fwidth( distanceToPlane ) / 2.0; - clipOpacity *= smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); - if ( clipOpacity == 0.0 ) discard; - } - #pragma unroll_loop_end - #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES - float unionClipOpacity = 1.0; - #pragma unroll_loop_start - for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - distanceToPlane = - dot( vClipPosition, plane.xyz ) + plane.w; - distanceGradient = fwidth( distanceToPlane ) / 2.0; - unionClipOpacity *= 1.0 - smoothstep( - distanceGradient, distanceGradient, distanceToPlane ); - } - #pragma unroll_loop_end - clipOpacity *= 1.0 - unionClipOpacity; - #endif - diffuseColor.a *= clipOpacity; - if ( diffuseColor.a == 0.0 ) discard; - #else - #pragma unroll_loop_start - for ( int i = 0; i < UNION_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - if ( dot( vClipPosition, plane.xyz ) > plane.w ) discard; - } - #pragma unroll_loop_end - #if UNION_CLIPPING_PLANES < NUM_CLIPPING_PLANES - bool clipped = true; - #pragma unroll_loop_start - for ( int i = UNION_CLIPPING_PLANES; i < NUM_CLIPPING_PLANES; i ++ ) { - plane = clippingPlanes[ i ]; - clipped = ( dot( vClipPosition, plane.xyz ) > plane.w ) && clipped; - } - #pragma unroll_loop_end - if ( clipped ) discard; - #endif - #endif -#endif`,Nu=`#if NUM_CLIPPING_PLANES > 0 - varying vec3 vClipPosition; - uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; -#endif`,Fu=`#if NUM_CLIPPING_PLANES > 0 - varying vec3 vClipPosition; -#endif`,Ou=`#if NUM_CLIPPING_PLANES > 0 - vClipPosition = - mvPosition.xyz; -#endif`,Bu=`#if defined( USE_COLOR_ALPHA ) - diffuseColor *= vColor; -#elif defined( USE_COLOR ) - diffuseColor.rgb *= vColor; -#endif`,zu=`#if defined( USE_COLOR_ALPHA ) - varying vec4 vColor; -#elif defined( USE_COLOR ) - varying vec3 vColor; -#endif`,ku=`#if defined( USE_COLOR_ALPHA ) - varying vec4 vColor; -#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) - varying vec3 vColor; -#endif`,Hu=`#if defined( USE_COLOR_ALPHA ) - vColor = vec4( 1.0 ); -#elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) - vColor = vec3( 1.0 ); -#endif -#ifdef USE_COLOR - vColor *= color; -#endif -#ifdef USE_INSTANCING_COLOR - vColor.xyz *= instanceColor.xyz; -#endif -#ifdef USE_BATCHING_COLOR - vec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) ); - vColor.xyz *= batchingColor.xyz; -#endif`,Vu=`#define PI 3.141592653589793 -#define PI2 6.283185307179586 -#define PI_HALF 1.5707963267948966 -#define RECIPROCAL_PI 0.3183098861837907 -#define RECIPROCAL_PI2 0.15915494309189535 -#define EPSILON 1e-6 -#ifndef saturate -#define saturate( a ) clamp( a, 0.0, 1.0 ) -#endif -#define whiteComplement( a ) ( 1.0 - saturate( a ) ) -float pow2( const in float x ) { return x*x; } -vec3 pow2( const in vec3 x ) { return x*x; } -float pow3( const in float x ) { return x*x*x; } -float pow4( const in float x ) { float x2 = x*x; return x2*x2; } -float max3( const in vec3 v ) { return max( max( v.x, v.y ), v.z ); } -float average( const in vec3 v ) { return dot( v, vec3( 0.3333333 ) ); } -highp float rand( const in vec2 uv ) { - const highp float a = 12.9898, b = 78.233, c = 43758.5453; - highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI ); - return fract( sin( sn ) * c ); -} -#ifdef HIGH_PRECISION - float precisionSafeLength( vec3 v ) { return length( v ); } -#else - float precisionSafeLength( vec3 v ) { - float maxComponent = max3( abs( v ) ); - return length( v / maxComponent ) * maxComponent; - } -#endif -struct IncidentLight { - vec3 color; - vec3 direction; - bool visible; -}; -struct ReflectedLight { - vec3 directDiffuse; - vec3 directSpecular; - vec3 indirectDiffuse; - vec3 indirectSpecular; -}; -#ifdef USE_ALPHAHASH - varying vec3 vPosition; -#endif -vec3 transformDirection( in vec3 dir, in mat4 matrix ) { - return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz ); -} -vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) { - return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz ); -} -mat3 transposeMat3( const in mat3 m ) { - mat3 tmp; - tmp[ 0 ] = vec3( m[ 0 ].x, m[ 1 ].x, m[ 2 ].x ); - tmp[ 1 ] = vec3( m[ 0 ].y, m[ 1 ].y, m[ 2 ].y ); - tmp[ 2 ] = vec3( m[ 0 ].z, m[ 1 ].z, m[ 2 ].z ); - return tmp; -} -bool isPerspectiveMatrix( mat4 m ) { - return m[ 2 ][ 3 ] == - 1.0; -} -vec2 equirectUv( in vec3 dir ) { - float u = atan( dir.z, dir.x ) * RECIPROCAL_PI2 + 0.5; - float v = asin( clamp( dir.y, - 1.0, 1.0 ) ) * RECIPROCAL_PI + 0.5; - return vec2( u, v ); -} -vec3 BRDF_Lambert( const in vec3 diffuseColor ) { - return RECIPROCAL_PI * diffuseColor; -} -vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { - float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); - return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); -} -float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { - float fresnel = exp2( ( - 5.55473 * dotVH - 6.98316 ) * dotVH ); - return f0 * ( 1.0 - fresnel ) + ( f90 * fresnel ); -} // validated`,Gu=`#ifdef ENVMAP_TYPE_CUBE_UV - #define cubeUV_minMipLevel 4.0 - #define cubeUV_minTileSize 16.0 - float getFace( vec3 direction ) { - vec3 absDirection = abs( direction ); - float face = - 1.0; - if ( absDirection.x > absDirection.z ) { - if ( absDirection.x > absDirection.y ) - face = direction.x > 0.0 ? 0.0 : 3.0; - else - face = direction.y > 0.0 ? 1.0 : 4.0; - } else { - if ( absDirection.z > absDirection.y ) - face = direction.z > 0.0 ? 2.0 : 5.0; - else - face = direction.y > 0.0 ? 1.0 : 4.0; - } - return face; - } - vec2 getUV( vec3 direction, float face ) { - vec2 uv; - if ( face == 0.0 ) { - uv = vec2( direction.z, direction.y ) / abs( direction.x ); - } else if ( face == 1.0 ) { - uv = vec2( - direction.x, - direction.z ) / abs( direction.y ); - } else if ( face == 2.0 ) { - uv = vec2( - direction.x, direction.y ) / abs( direction.z ); - } else if ( face == 3.0 ) { - uv = vec2( - direction.z, direction.y ) / abs( direction.x ); - } else if ( face == 4.0 ) { - uv = vec2( - direction.x, direction.z ) / abs( direction.y ); - } else { - uv = vec2( direction.x, direction.y ) / abs( direction.z ); - } - return 0.5 * ( uv + 1.0 ); - } - vec3 bilinearCubeUV( sampler2D envMap, vec3 direction, float mipInt ) { - float face = getFace( direction ); - float filterInt = max( cubeUV_minMipLevel - mipInt, 0.0 ); - mipInt = max( mipInt, cubeUV_minMipLevel ); - float faceSize = exp2( mipInt ); - highp vec2 uv = getUV( direction, face ) * ( faceSize - 2.0 ) + 1.0; - if ( face > 2.0 ) { - uv.y += faceSize; - face -= 3.0; - } - uv.x += face * faceSize; - uv.x += filterInt * 3.0 * cubeUV_minTileSize; - uv.y += 4.0 * ( exp2( CUBEUV_MAX_MIP ) - faceSize ); - uv.x *= CUBEUV_TEXEL_WIDTH; - uv.y *= CUBEUV_TEXEL_HEIGHT; - #ifdef texture2DGradEXT - return texture2DGradEXT( envMap, uv, vec2( 0.0 ), vec2( 0.0 ) ).rgb; - #else - return texture2D( envMap, uv ).rgb; - #endif - } - #define cubeUV_r0 1.0 - #define cubeUV_m0 - 2.0 - #define cubeUV_r1 0.8 - #define cubeUV_m1 - 1.0 - #define cubeUV_r4 0.4 - #define cubeUV_m4 2.0 - #define cubeUV_r5 0.305 - #define cubeUV_m5 3.0 - #define cubeUV_r6 0.21 - #define cubeUV_m6 4.0 - float roughnessToMip( float roughness ) { - float mip = 0.0; - if ( roughness >= cubeUV_r1 ) { - mip = ( cubeUV_r0 - roughness ) * ( cubeUV_m1 - cubeUV_m0 ) / ( cubeUV_r0 - cubeUV_r1 ) + cubeUV_m0; - } else if ( roughness >= cubeUV_r4 ) { - mip = ( cubeUV_r1 - roughness ) * ( cubeUV_m4 - cubeUV_m1 ) / ( cubeUV_r1 - cubeUV_r4 ) + cubeUV_m1; - } else if ( roughness >= cubeUV_r5 ) { - mip = ( cubeUV_r4 - roughness ) * ( cubeUV_m5 - cubeUV_m4 ) / ( cubeUV_r4 - cubeUV_r5 ) + cubeUV_m4; - } else if ( roughness >= cubeUV_r6 ) { - mip = ( cubeUV_r5 - roughness ) * ( cubeUV_m6 - cubeUV_m5 ) / ( cubeUV_r5 - cubeUV_r6 ) + cubeUV_m5; - } else { - mip = - 2.0 * log2( 1.16 * roughness ); } - return mip; - } - vec4 textureCubeUV( sampler2D envMap, vec3 sampleDir, float roughness ) { - float mip = clamp( roughnessToMip( roughness ), cubeUV_m0, CUBEUV_MAX_MIP ); - float mipF = fract( mip ); - float mipInt = floor( mip ); - vec3 color0 = bilinearCubeUV( envMap, sampleDir, mipInt ); - if ( mipF == 0.0 ) { - return vec4( color0, 1.0 ); - } else { - vec3 color1 = bilinearCubeUV( envMap, sampleDir, mipInt + 1.0 ); - return vec4( mix( color0, color1, mipF ), 1.0 ); - } - } -#endif`,Wu=`vec3 transformedNormal = objectNormal; -#ifdef USE_TANGENT - vec3 transformedTangent = objectTangent; -#endif -#ifdef USE_BATCHING - mat3 bm = mat3( batchingMatrix ); - transformedNormal /= vec3( dot( bm[ 0 ], bm[ 0 ] ), dot( bm[ 1 ], bm[ 1 ] ), dot( bm[ 2 ], bm[ 2 ] ) ); - transformedNormal = bm * transformedNormal; - #ifdef USE_TANGENT - transformedTangent = bm * transformedTangent; - #endif -#endif -#ifdef USE_INSTANCING - mat3 im = mat3( instanceMatrix ); - transformedNormal /= vec3( dot( im[ 0 ], im[ 0 ] ), dot( im[ 1 ], im[ 1 ] ), dot( im[ 2 ], im[ 2 ] ) ); - transformedNormal = im * transformedNormal; - #ifdef USE_TANGENT - transformedTangent = im * transformedTangent; - #endif -#endif -transformedNormal = normalMatrix * transformedNormal; -#ifdef FLIP_SIDED - transformedNormal = - transformedNormal; -#endif -#ifdef USE_TANGENT - transformedTangent = ( modelViewMatrix * vec4( transformedTangent, 0.0 ) ).xyz; - #ifdef FLIP_SIDED - transformedTangent = - transformedTangent; - #endif -#endif`,Xu=`#ifdef USE_DISPLACEMENTMAP - uniform sampler2D displacementMap; - uniform float displacementScale; - uniform float displacementBias; -#endif`,Yu=`#ifdef USE_DISPLACEMENTMAP - transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); -#endif`,qu=`#ifdef USE_EMISSIVEMAP - vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); - #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE - emissiveColor = sRGBTransferEOTF( emissiveColor ); - #endif - totalEmissiveRadiance *= emissiveColor.rgb; -#endif`,ju=`#ifdef USE_EMISSIVEMAP - uniform sampler2D emissiveMap; -#endif`,Zu="gl_FragColor = linearToOutputTexel( gl_FragColor );",Ku=`vec4 LinearTransferOETF( in vec4 value ) { - return value; -} -vec4 sRGBTransferEOTF( in vec4 value ) { - return vec4( mix( pow( value.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), value.rgb * 0.0773993808, vec3( lessThanEqual( value.rgb, vec3( 0.04045 ) ) ) ), value.a ); -} -vec4 sRGBTransferOETF( in vec4 value ) { - return vec4( mix( pow( value.rgb, vec3( 0.41666 ) ) * 1.055 - vec3( 0.055 ), value.rgb * 12.92, vec3( lessThanEqual( value.rgb, vec3( 0.0031308 ) ) ) ), value.a ); -}`,$u=`#ifdef USE_ENVMAP - #ifdef ENV_WORLDPOS - vec3 cameraToFrag; - if ( isOrthographic ) { - cameraToFrag = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); - } else { - cameraToFrag = normalize( vWorldPosition - cameraPosition ); - } - vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); - #ifdef ENVMAP_MODE_REFLECTION - vec3 reflectVec = reflect( cameraToFrag, worldNormal ); - #else - vec3 reflectVec = refract( cameraToFrag, worldNormal, refractionRatio ); - #endif - #else - vec3 reflectVec = vReflect; - #endif - #ifdef ENVMAP_TYPE_CUBE - vec4 envColor = textureCube( envMap, envMapRotation * vec3( flipEnvMap * reflectVec.x, reflectVec.yz ) ); - #else - vec4 envColor = vec4( 0.0 ); - #endif - #ifdef ENVMAP_BLENDING_MULTIPLY - outgoingLight = mix( outgoingLight, outgoingLight * envColor.xyz, specularStrength * reflectivity ); - #elif defined( ENVMAP_BLENDING_MIX ) - outgoingLight = mix( outgoingLight, envColor.xyz, specularStrength * reflectivity ); - #elif defined( ENVMAP_BLENDING_ADD ) - outgoingLight += envColor.xyz * specularStrength * reflectivity; - #endif -#endif`,Ju=`#ifdef USE_ENVMAP - uniform float envMapIntensity; - uniform float flipEnvMap; - uniform mat3 envMapRotation; - #ifdef ENVMAP_TYPE_CUBE - uniform samplerCube envMap; - #else - uniform sampler2D envMap; - #endif - -#endif`,Qu=`#ifdef USE_ENVMAP - uniform float reflectivity; - #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) - #define ENV_WORLDPOS - #endif - #ifdef ENV_WORLDPOS - varying vec3 vWorldPosition; - uniform float refractionRatio; - #else - varying vec3 vReflect; - #endif -#endif`,td=`#ifdef USE_ENVMAP - #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) - #define ENV_WORLDPOS - #endif - #ifdef ENV_WORLDPOS - - varying vec3 vWorldPosition; - #else - varying vec3 vReflect; - uniform float refractionRatio; - #endif -#endif`,ed=`#ifdef USE_ENVMAP - #ifdef ENV_WORLDPOS - vWorldPosition = worldPosition.xyz; - #else - vec3 cameraToVertex; - if ( isOrthographic ) { - cameraToVertex = normalize( vec3( - viewMatrix[ 0 ][ 2 ], - viewMatrix[ 1 ][ 2 ], - viewMatrix[ 2 ][ 2 ] ) ); - } else { - cameraToVertex = normalize( worldPosition.xyz - cameraPosition ); - } - vec3 worldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); - #ifdef ENVMAP_MODE_REFLECTION - vReflect = reflect( cameraToVertex, worldNormal ); - #else - vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); - #endif - #endif -#endif`,nd=`#ifdef USE_FOG - vFogDepth = - mvPosition.z; -#endif`,id=`#ifdef USE_FOG - varying float vFogDepth; -#endif`,sd=`#ifdef USE_FOG - #ifdef FOG_EXP2 - float fogFactor = 1.0 - exp( - fogDensity * fogDensity * vFogDepth * vFogDepth ); - #else - float fogFactor = smoothstep( fogNear, fogFar, vFogDepth ); - #endif - gl_FragColor.rgb = mix( gl_FragColor.rgb, fogColor, fogFactor ); -#endif`,rd=`#ifdef USE_FOG - uniform vec3 fogColor; - varying float vFogDepth; - #ifdef FOG_EXP2 - uniform float fogDensity; - #else - uniform float fogNear; - uniform float fogFar; - #endif -#endif`,ad=`#ifdef USE_GRADIENTMAP - uniform sampler2D gradientMap; -#endif -vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { - float dotNL = dot( normal, lightDirection ); - vec2 coord = vec2( dotNL * 0.5 + 0.5, 0.0 ); - #ifdef USE_GRADIENTMAP - return vec3( texture2D( gradientMap, coord ).r ); - #else - vec2 fw = fwidth( coord ) * 0.5; - return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( 0.7 - fw.x, 0.7 + fw.x, coord.x ) ); - #endif -}`,od=`#ifdef USE_LIGHTMAP - uniform sampler2D lightMap; - uniform float lightMapIntensity; -#endif`,ld=`LambertMaterial material; -material.diffuseColor = diffuseColor.rgb; -material.specularStrength = specularStrength;`,cd=`varying vec3 vViewPosition; -struct LambertMaterial { - vec3 diffuseColor; - float specularStrength; -}; -void RE_Direct_Lambert( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { - float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); - vec3 irradiance = dotNL * directLight.color; - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in LambertMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -#define RE_Direct RE_Direct_Lambert -#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,hd=`uniform bool receiveShadow; -uniform vec3 ambientLightColor; -#if defined( USE_LIGHT_PROBES ) - uniform vec3 lightProbe[ 9 ]; -#endif -vec3 shGetIrradianceAt( in vec3 normal, in vec3 shCoefficients[ 9 ] ) { - float x = normal.x, y = normal.y, z = normal.z; - vec3 result = shCoefficients[ 0 ] * 0.886227; - result += shCoefficients[ 1 ] * 2.0 * 0.511664 * y; - result += shCoefficients[ 2 ] * 2.0 * 0.511664 * z; - result += shCoefficients[ 3 ] * 2.0 * 0.511664 * x; - result += shCoefficients[ 4 ] * 2.0 * 0.429043 * x * y; - result += shCoefficients[ 5 ] * 2.0 * 0.429043 * y * z; - result += shCoefficients[ 6 ] * ( 0.743125 * z * z - 0.247708 ); - result += shCoefficients[ 7 ] * 2.0 * 0.429043 * x * z; - result += shCoefficients[ 8 ] * 0.429043 * ( x * x - y * y ); - return result; -} -vec3 getLightProbeIrradiance( const in vec3 lightProbe[ 9 ], const in vec3 normal ) { - vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); - vec3 irradiance = shGetIrradianceAt( worldNormal, lightProbe ); - return irradiance; -} -vec3 getAmbientLightIrradiance( const in vec3 ambientLightColor ) { - vec3 irradiance = ambientLightColor; - return irradiance; -} -float getDistanceAttenuation( const in float lightDistance, const in float cutoffDistance, const in float decayExponent ) { - float distanceFalloff = 1.0 / max( pow( lightDistance, decayExponent ), 0.01 ); - if ( cutoffDistance > 0.0 ) { - distanceFalloff *= pow2( saturate( 1.0 - pow4( lightDistance / cutoffDistance ) ) ); - } - return distanceFalloff; -} -float getSpotAttenuation( const in float coneCosine, const in float penumbraCosine, const in float angleCosine ) { - return smoothstep( coneCosine, penumbraCosine, angleCosine ); -} -#if NUM_DIR_LIGHTS > 0 - struct DirectionalLight { - vec3 direction; - vec3 color; - }; - uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ]; - void getDirectionalLightInfo( const in DirectionalLight directionalLight, out IncidentLight light ) { - light.color = directionalLight.color; - light.direction = directionalLight.direction; - light.visible = true; - } -#endif -#if NUM_POINT_LIGHTS > 0 - struct PointLight { - vec3 position; - vec3 color; - float distance; - float decay; - }; - uniform PointLight pointLights[ NUM_POINT_LIGHTS ]; - void getPointLightInfo( const in PointLight pointLight, const in vec3 geometryPosition, out IncidentLight light ) { - vec3 lVector = pointLight.position - geometryPosition; - light.direction = normalize( lVector ); - float lightDistance = length( lVector ); - light.color = pointLight.color; - light.color *= getDistanceAttenuation( lightDistance, pointLight.distance, pointLight.decay ); - light.visible = ( light.color != vec3( 0.0 ) ); - } -#endif -#if NUM_SPOT_LIGHTS > 0 - struct SpotLight { - vec3 position; - vec3 direction; - vec3 color; - float distance; - float decay; - float coneCos; - float penumbraCos; - }; - uniform SpotLight spotLights[ NUM_SPOT_LIGHTS ]; - void getSpotLightInfo( const in SpotLight spotLight, const in vec3 geometryPosition, out IncidentLight light ) { - vec3 lVector = spotLight.position - geometryPosition; - light.direction = normalize( lVector ); - float angleCos = dot( light.direction, spotLight.direction ); - float spotAttenuation = getSpotAttenuation( spotLight.coneCos, spotLight.penumbraCos, angleCos ); - if ( spotAttenuation > 0.0 ) { - float lightDistance = length( lVector ); - light.color = spotLight.color * spotAttenuation; - light.color *= getDistanceAttenuation( lightDistance, spotLight.distance, spotLight.decay ); - light.visible = ( light.color != vec3( 0.0 ) ); - } else { - light.color = vec3( 0.0 ); - light.visible = false; - } - } -#endif -#if NUM_RECT_AREA_LIGHTS > 0 - struct RectAreaLight { - vec3 color; - vec3 position; - vec3 halfWidth; - vec3 halfHeight; - }; - uniform sampler2D ltc_1; uniform sampler2D ltc_2; - uniform RectAreaLight rectAreaLights[ NUM_RECT_AREA_LIGHTS ]; -#endif -#if NUM_HEMI_LIGHTS > 0 - struct HemisphereLight { - vec3 direction; - vec3 skyColor; - vec3 groundColor; - }; - uniform HemisphereLight hemisphereLights[ NUM_HEMI_LIGHTS ]; - vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) { - float dotNL = dot( normal, hemiLight.direction ); - float hemiDiffuseWeight = 0.5 * dotNL + 0.5; - vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); - return irradiance; - } -#endif`,ud=`#ifdef USE_ENVMAP - vec3 getIBLIrradiance( const in vec3 normal ) { - #ifdef ENVMAP_TYPE_CUBE_UV - vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); - vec4 envMapColor = textureCubeUV( envMap, envMapRotation * worldNormal, 1.0 ); - return PI * envMapColor.rgb * envMapIntensity; - #else - return vec3( 0.0 ); - #endif - } - vec3 getIBLRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness ) { - #ifdef ENVMAP_TYPE_CUBE_UV - vec3 reflectVec = reflect( - viewDir, normal ); - reflectVec = normalize( mix( reflectVec, normal, roughness * roughness) ); - reflectVec = inverseTransformDirection( reflectVec, viewMatrix ); - vec4 envMapColor = textureCubeUV( envMap, envMapRotation * reflectVec, roughness ); - return envMapColor.rgb * envMapIntensity; - #else - return vec3( 0.0 ); - #endif - } - #ifdef USE_ANISOTROPY - vec3 getIBLAnisotropyRadiance( const in vec3 viewDir, const in vec3 normal, const in float roughness, const in vec3 bitangent, const in float anisotropy ) { - #ifdef ENVMAP_TYPE_CUBE_UV - vec3 bentNormal = cross( bitangent, viewDir ); - bentNormal = normalize( cross( bentNormal, bitangent ) ); - bentNormal = normalize( mix( bentNormal, normal, pow2( pow2( 1.0 - anisotropy * ( 1.0 - roughness ) ) ) ) ); - return getIBLRadiance( viewDir, bentNormal, roughness ); - #else - return vec3( 0.0 ); - #endif - } - #endif -#endif`,dd=`ToonMaterial material; -material.diffuseColor = diffuseColor.rgb;`,fd=`varying vec3 vViewPosition; -struct ToonMaterial { - vec3 diffuseColor; -}; -void RE_Direct_Toon( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { - vec3 irradiance = getGradientIrradiance( geometryNormal, directLight.direction ) * directLight.color; - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in ToonMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -#define RE_Direct RE_Direct_Toon -#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,pd=`BlinnPhongMaterial material; -material.diffuseColor = diffuseColor.rgb; -material.specularColor = specular; -material.specularShininess = shininess; -material.specularStrength = specularStrength;`,md=`varying vec3 vViewPosition; -struct BlinnPhongMaterial { - vec3 diffuseColor; - vec3 specularColor; - float specularShininess; - float specularStrength; -}; -void RE_Direct_BlinnPhong( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { - float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); - vec3 irradiance = dotNL * directLight.color; - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); - reflectedLight.directSpecular += irradiance * BRDF_BlinnPhong( directLight.direction, geometryViewDir, geometryNormal, material.specularColor, material.specularShininess ) * material.specularStrength; -} -void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in BlinnPhongMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -#define RE_Direct RE_Direct_BlinnPhong -#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,gd=`PhysicalMaterial material; -material.diffuseColor = diffuseColor.rgb * ( 1.0 - metalnessFactor ); -vec3 dxy = max( abs( dFdx( nonPerturbedNormal ) ), abs( dFdy( nonPerturbedNormal ) ) ); -float geometryRoughness = max( max( dxy.x, dxy.y ), dxy.z ); -material.roughness = max( roughnessFactor, 0.0525 );material.roughness += geometryRoughness; -material.roughness = min( material.roughness, 1.0 ); -#ifdef IOR - material.ior = ior; - #ifdef USE_SPECULAR - float specularIntensityFactor = specularIntensity; - vec3 specularColorFactor = specularColor; - #ifdef USE_SPECULAR_COLORMAP - specularColorFactor *= texture2D( specularColorMap, vSpecularColorMapUv ).rgb; - #endif - #ifdef USE_SPECULAR_INTENSITYMAP - specularIntensityFactor *= texture2D( specularIntensityMap, vSpecularIntensityMapUv ).a; - #endif - material.specularF90 = mix( specularIntensityFactor, 1.0, metalnessFactor ); - #else - float specularIntensityFactor = 1.0; - vec3 specularColorFactor = vec3( 1.0 ); - material.specularF90 = 1.0; - #endif - material.specularColor = mix( min( pow2( ( material.ior - 1.0 ) / ( material.ior + 1.0 ) ) * specularColorFactor, vec3( 1.0 ) ) * specularIntensityFactor, diffuseColor.rgb, metalnessFactor ); -#else - material.specularColor = mix( vec3( 0.04 ), diffuseColor.rgb, metalnessFactor ); - material.specularF90 = 1.0; -#endif -#ifdef USE_CLEARCOAT - material.clearcoat = clearcoat; - material.clearcoatRoughness = clearcoatRoughness; - material.clearcoatF0 = vec3( 0.04 ); - material.clearcoatF90 = 1.0; - #ifdef USE_CLEARCOATMAP - material.clearcoat *= texture2D( clearcoatMap, vClearcoatMapUv ).x; - #endif - #ifdef USE_CLEARCOAT_ROUGHNESSMAP - material.clearcoatRoughness *= texture2D( clearcoatRoughnessMap, vClearcoatRoughnessMapUv ).y; - #endif - material.clearcoat = saturate( material.clearcoat ); material.clearcoatRoughness = max( material.clearcoatRoughness, 0.0525 ); - material.clearcoatRoughness += geometryRoughness; - material.clearcoatRoughness = min( material.clearcoatRoughness, 1.0 ); -#endif -#ifdef USE_DISPERSION - material.dispersion = dispersion; -#endif -#ifdef USE_IRIDESCENCE - material.iridescence = iridescence; - material.iridescenceIOR = iridescenceIOR; - #ifdef USE_IRIDESCENCEMAP - material.iridescence *= texture2D( iridescenceMap, vIridescenceMapUv ).r; - #endif - #ifdef USE_IRIDESCENCE_THICKNESSMAP - material.iridescenceThickness = (iridescenceThicknessMaximum - iridescenceThicknessMinimum) * texture2D( iridescenceThicknessMap, vIridescenceThicknessMapUv ).g + iridescenceThicknessMinimum; - #else - material.iridescenceThickness = iridescenceThicknessMaximum; - #endif -#endif -#ifdef USE_SHEEN - material.sheenColor = sheenColor; - #ifdef USE_SHEEN_COLORMAP - material.sheenColor *= texture2D( sheenColorMap, vSheenColorMapUv ).rgb; - #endif - material.sheenRoughness = clamp( sheenRoughness, 0.07, 1.0 ); - #ifdef USE_SHEEN_ROUGHNESSMAP - material.sheenRoughness *= texture2D( sheenRoughnessMap, vSheenRoughnessMapUv ).a; - #endif -#endif -#ifdef USE_ANISOTROPY - #ifdef USE_ANISOTROPYMAP - mat2 anisotropyMat = mat2( anisotropyVector.x, anisotropyVector.y, - anisotropyVector.y, anisotropyVector.x ); - vec3 anisotropyPolar = texture2D( anisotropyMap, vAnisotropyMapUv ).rgb; - vec2 anisotropyV = anisotropyMat * normalize( 2.0 * anisotropyPolar.rg - vec2( 1.0 ) ) * anisotropyPolar.b; - #else - vec2 anisotropyV = anisotropyVector; - #endif - material.anisotropy = length( anisotropyV ); - if( material.anisotropy == 0.0 ) { - anisotropyV = vec2( 1.0, 0.0 ); - } else { - anisotropyV /= material.anisotropy; - material.anisotropy = saturate( material.anisotropy ); - } - material.alphaT = mix( pow2( material.roughness ), 1.0, pow2( material.anisotropy ) ); - material.anisotropyT = tbn[ 0 ] * anisotropyV.x + tbn[ 1 ] * anisotropyV.y; - material.anisotropyB = tbn[ 1 ] * anisotropyV.x - tbn[ 0 ] * anisotropyV.y; -#endif`,_d=`struct PhysicalMaterial { - vec3 diffuseColor; - float roughness; - vec3 specularColor; - float specularF90; - float dispersion; - #ifdef USE_CLEARCOAT - float clearcoat; - float clearcoatRoughness; - vec3 clearcoatF0; - float clearcoatF90; - #endif - #ifdef USE_IRIDESCENCE - float iridescence; - float iridescenceIOR; - float iridescenceThickness; - vec3 iridescenceFresnel; - vec3 iridescenceF0; - #endif - #ifdef USE_SHEEN - vec3 sheenColor; - float sheenRoughness; - #endif - #ifdef IOR - float ior; - #endif - #ifdef USE_TRANSMISSION - float transmission; - float transmissionAlpha; - float thickness; - float attenuationDistance; - vec3 attenuationColor; - #endif - #ifdef USE_ANISOTROPY - float anisotropy; - float alphaT; - vec3 anisotropyT; - vec3 anisotropyB; - #endif -}; -vec3 clearcoatSpecularDirect = vec3( 0.0 ); -vec3 clearcoatSpecularIndirect = vec3( 0.0 ); -vec3 sheenSpecularDirect = vec3( 0.0 ); -vec3 sheenSpecularIndirect = vec3(0.0 ); -vec3 Schlick_to_F0( const in vec3 f, const in float f90, const in float dotVH ) { - float x = clamp( 1.0 - dotVH, 0.0, 1.0 ); - float x2 = x * x; - float x5 = clamp( x * x2 * x2, 0.0, 0.9999 ); - return ( f - vec3( f90 ) * x5 ) / ( 1.0 - x5 ); -} -float V_GGX_SmithCorrelated( const in float alpha, const in float dotNL, const in float dotNV ) { - float a2 = pow2( alpha ); - float gv = dotNL * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNV ) ); - float gl = dotNV * sqrt( a2 + ( 1.0 - a2 ) * pow2( dotNL ) ); - return 0.5 / max( gv + gl, EPSILON ); -} -float D_GGX( const in float alpha, const in float dotNH ) { - float a2 = pow2( alpha ); - float denom = pow2( dotNH ) * ( a2 - 1.0 ) + 1.0; - return RECIPROCAL_PI * a2 / pow2( denom ); -} -#ifdef USE_ANISOTROPY - float V_GGX_SmithCorrelated_Anisotropic( const in float alphaT, const in float alphaB, const in float dotTV, const in float dotBV, const in float dotTL, const in float dotBL, const in float dotNV, const in float dotNL ) { - float gv = dotNL * length( vec3( alphaT * dotTV, alphaB * dotBV, dotNV ) ); - float gl = dotNV * length( vec3( alphaT * dotTL, alphaB * dotBL, dotNL ) ); - float v = 0.5 / ( gv + gl ); - return saturate(v); - } - float D_GGX_Anisotropic( const in float alphaT, const in float alphaB, const in float dotNH, const in float dotTH, const in float dotBH ) { - float a2 = alphaT * alphaB; - highp vec3 v = vec3( alphaB * dotTH, alphaT * dotBH, a2 * dotNH ); - highp float v2 = dot( v, v ); - float w2 = a2 / v2; - return RECIPROCAL_PI * a2 * pow2 ( w2 ); - } -#endif -#ifdef USE_CLEARCOAT - vec3 BRDF_GGX_Clearcoat( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material) { - vec3 f0 = material.clearcoatF0; - float f90 = material.clearcoatF90; - float roughness = material.clearcoatRoughness; - float alpha = pow2( roughness ); - vec3 halfDir = normalize( lightDir + viewDir ); - float dotNL = saturate( dot( normal, lightDir ) ); - float dotNV = saturate( dot( normal, viewDir ) ); - float dotNH = saturate( dot( normal, halfDir ) ); - float dotVH = saturate( dot( viewDir, halfDir ) ); - vec3 F = F_Schlick( f0, f90, dotVH ); - float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); - float D = D_GGX( alpha, dotNH ); - return F * ( V * D ); - } -#endif -vec3 BRDF_GGX( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, const in PhysicalMaterial material ) { - vec3 f0 = material.specularColor; - float f90 = material.specularF90; - float roughness = material.roughness; - float alpha = pow2( roughness ); - vec3 halfDir = normalize( lightDir + viewDir ); - float dotNL = saturate( dot( normal, lightDir ) ); - float dotNV = saturate( dot( normal, viewDir ) ); - float dotNH = saturate( dot( normal, halfDir ) ); - float dotVH = saturate( dot( viewDir, halfDir ) ); - vec3 F = F_Schlick( f0, f90, dotVH ); - #ifdef USE_IRIDESCENCE - F = mix( F, material.iridescenceFresnel, material.iridescence ); - #endif - #ifdef USE_ANISOTROPY - float dotTL = dot( material.anisotropyT, lightDir ); - float dotTV = dot( material.anisotropyT, viewDir ); - float dotTH = dot( material.anisotropyT, halfDir ); - float dotBL = dot( material.anisotropyB, lightDir ); - float dotBV = dot( material.anisotropyB, viewDir ); - float dotBH = dot( material.anisotropyB, halfDir ); - float V = V_GGX_SmithCorrelated_Anisotropic( material.alphaT, alpha, dotTV, dotBV, dotTL, dotBL, dotNV, dotNL ); - float D = D_GGX_Anisotropic( material.alphaT, alpha, dotNH, dotTH, dotBH ); - #else - float V = V_GGX_SmithCorrelated( alpha, dotNL, dotNV ); - float D = D_GGX( alpha, dotNH ); - #endif - return F * ( V * D ); -} -vec2 LTC_Uv( const in vec3 N, const in vec3 V, const in float roughness ) { - const float LUT_SIZE = 64.0; - const float LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE; - const float LUT_BIAS = 0.5 / LUT_SIZE; - float dotNV = saturate( dot( N, V ) ); - vec2 uv = vec2( roughness, sqrt( 1.0 - dotNV ) ); - uv = uv * LUT_SCALE + LUT_BIAS; - return uv; -} -float LTC_ClippedSphereFormFactor( const in vec3 f ) { - float l = length( f ); - return max( ( l * l + f.z ) / ( l + 1.0 ), 0.0 ); -} -vec3 LTC_EdgeVectorFormFactor( const in vec3 v1, const in vec3 v2 ) { - float x = dot( v1, v2 ); - float y = abs( x ); - float a = 0.8543985 + ( 0.4965155 + 0.0145206 * y ) * y; - float b = 3.4175940 + ( 4.1616724 + y ) * y; - float v = a / b; - float theta_sintheta = ( x > 0.0 ) ? v : 0.5 * inversesqrt( max( 1.0 - x * x, 1e-7 ) ) - v; - return cross( v1, v2 ) * theta_sintheta; -} -vec3 LTC_Evaluate( const in vec3 N, const in vec3 V, const in vec3 P, const in mat3 mInv, const in vec3 rectCoords[ 4 ] ) { - vec3 v1 = rectCoords[ 1 ] - rectCoords[ 0 ]; - vec3 v2 = rectCoords[ 3 ] - rectCoords[ 0 ]; - vec3 lightNormal = cross( v1, v2 ); - if( dot( lightNormal, P - rectCoords[ 0 ] ) < 0.0 ) return vec3( 0.0 ); - vec3 T1, T2; - T1 = normalize( V - N * dot( V, N ) ); - T2 = - cross( N, T1 ); - mat3 mat = mInv * transposeMat3( mat3( T1, T2, N ) ); - vec3 coords[ 4 ]; - coords[ 0 ] = mat * ( rectCoords[ 0 ] - P ); - coords[ 1 ] = mat * ( rectCoords[ 1 ] - P ); - coords[ 2 ] = mat * ( rectCoords[ 2 ] - P ); - coords[ 3 ] = mat * ( rectCoords[ 3 ] - P ); - coords[ 0 ] = normalize( coords[ 0 ] ); - coords[ 1 ] = normalize( coords[ 1 ] ); - coords[ 2 ] = normalize( coords[ 2 ] ); - coords[ 3 ] = normalize( coords[ 3 ] ); - vec3 vectorFormFactor = vec3( 0.0 ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 0 ], coords[ 1 ] ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 1 ], coords[ 2 ] ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 2 ], coords[ 3 ] ); - vectorFormFactor += LTC_EdgeVectorFormFactor( coords[ 3 ], coords[ 0 ] ); - float result = LTC_ClippedSphereFormFactor( vectorFormFactor ); - return vec3( result ); -} -#if defined( USE_SHEEN ) -float D_Charlie( float roughness, float dotNH ) { - float alpha = pow2( roughness ); - float invAlpha = 1.0 / alpha; - float cos2h = dotNH * dotNH; - float sin2h = max( 1.0 - cos2h, 0.0078125 ); - return ( 2.0 + invAlpha ) * pow( sin2h, invAlpha * 0.5 ) / ( 2.0 * PI ); -} -float V_Neubelt( float dotNV, float dotNL ) { - return saturate( 1.0 / ( 4.0 * ( dotNL + dotNV - dotNL * dotNV ) ) ); -} -vec3 BRDF_Sheen( const in vec3 lightDir, const in vec3 viewDir, const in vec3 normal, vec3 sheenColor, const in float sheenRoughness ) { - vec3 halfDir = normalize( lightDir + viewDir ); - float dotNL = saturate( dot( normal, lightDir ) ); - float dotNV = saturate( dot( normal, viewDir ) ); - float dotNH = saturate( dot( normal, halfDir ) ); - float D = D_Charlie( sheenRoughness, dotNH ); - float V = V_Neubelt( dotNV, dotNL ); - return sheenColor * ( D * V ); -} -#endif -float IBLSheenBRDF( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { - float dotNV = saturate( dot( normal, viewDir ) ); - float r2 = roughness * roughness; - float a = roughness < 0.25 ? -339.2 * r2 + 161.4 * roughness - 25.9 : -8.48 * r2 + 14.3 * roughness - 9.95; - float b = roughness < 0.25 ? 44.0 * r2 - 23.7 * roughness + 3.26 : 1.97 * r2 - 3.27 * roughness + 0.72; - float DG = exp( a * dotNV + b ) + ( roughness < 0.25 ? 0.0 : 0.1 * ( roughness - 0.25 ) ); - return saturate( DG * RECIPROCAL_PI ); -} -vec2 DFGApprox( const in vec3 normal, const in vec3 viewDir, const in float roughness ) { - float dotNV = saturate( dot( normal, viewDir ) ); - const vec4 c0 = vec4( - 1, - 0.0275, - 0.572, 0.022 ); - const vec4 c1 = vec4( 1, 0.0425, 1.04, - 0.04 ); - vec4 r = roughness * c0 + c1; - float a004 = min( r.x * r.x, exp2( - 9.28 * dotNV ) ) * r.x + r.y; - vec2 fab = vec2( - 1.04, 1.04 ) * a004 + r.zw; - return fab; -} -vec3 EnvironmentBRDF( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness ) { - vec2 fab = DFGApprox( normal, viewDir, roughness ); - return specularColor * fab.x + specularF90 * fab.y; -} -#ifdef USE_IRIDESCENCE -void computeMultiscatteringIridescence( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float iridescence, const in vec3 iridescenceF0, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { -#else -void computeMultiscattering( const in vec3 normal, const in vec3 viewDir, const in vec3 specularColor, const in float specularF90, const in float roughness, inout vec3 singleScatter, inout vec3 multiScatter ) { -#endif - vec2 fab = DFGApprox( normal, viewDir, roughness ); - #ifdef USE_IRIDESCENCE - vec3 Fr = mix( specularColor, iridescenceF0, iridescence ); - #else - vec3 Fr = specularColor; - #endif - vec3 FssEss = Fr * fab.x + specularF90 * fab.y; - float Ess = fab.x + fab.y; - float Ems = 1.0 - Ess; - vec3 Favg = Fr + ( 1.0 - Fr ) * 0.047619; vec3 Fms = FssEss * Favg / ( 1.0 - Ems * Favg ); - singleScatter += FssEss; - multiScatter += Fms * Ems; -} -#if NUM_RECT_AREA_LIGHTS > 0 - void RE_Direct_RectArea_Physical( const in RectAreaLight rectAreaLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { - vec3 normal = geometryNormal; - vec3 viewDir = geometryViewDir; - vec3 position = geometryPosition; - vec3 lightPos = rectAreaLight.position; - vec3 halfWidth = rectAreaLight.halfWidth; - vec3 halfHeight = rectAreaLight.halfHeight; - vec3 lightColor = rectAreaLight.color; - float roughness = material.roughness; - vec3 rectCoords[ 4 ]; - rectCoords[ 0 ] = lightPos + halfWidth - halfHeight; rectCoords[ 1 ] = lightPos - halfWidth - halfHeight; - rectCoords[ 2 ] = lightPos - halfWidth + halfHeight; - rectCoords[ 3 ] = lightPos + halfWidth + halfHeight; - vec2 uv = LTC_Uv( normal, viewDir, roughness ); - vec4 t1 = texture2D( ltc_1, uv ); - vec4 t2 = texture2D( ltc_2, uv ); - mat3 mInv = mat3( - vec3( t1.x, 0, t1.y ), - vec3( 0, 1, 0 ), - vec3( t1.z, 0, t1.w ) - ); - vec3 fresnel = ( material.specularColor * t2.x + ( vec3( 1.0 ) - material.specularColor ) * t2.y ); - reflectedLight.directSpecular += lightColor * fresnel * LTC_Evaluate( normal, viewDir, position, mInv, rectCoords ); - reflectedLight.directDiffuse += lightColor * material.diffuseColor * LTC_Evaluate( normal, viewDir, position, mat3( 1.0 ), rectCoords ); - } -#endif -void RE_Direct_Physical( const in IncidentLight directLight, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { - float dotNL = saturate( dot( geometryNormal, directLight.direction ) ); - vec3 irradiance = dotNL * directLight.color; - #ifdef USE_CLEARCOAT - float dotNLcc = saturate( dot( geometryClearcoatNormal, directLight.direction ) ); - vec3 ccIrradiance = dotNLcc * directLight.color; - clearcoatSpecularDirect += ccIrradiance * BRDF_GGX_Clearcoat( directLight.direction, geometryViewDir, geometryClearcoatNormal, material ); - #endif - #ifdef USE_SHEEN - sheenSpecularDirect += irradiance * BRDF_Sheen( directLight.direction, geometryViewDir, geometryNormal, material.sheenColor, material.sheenRoughness ); - #endif - reflectedLight.directSpecular += irradiance * BRDF_GGX( directLight.direction, geometryViewDir, geometryNormal, material ); - reflectedLight.directDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectDiffuse_Physical( const in vec3 irradiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight ) { - reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); -} -void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradiance, const in vec3 clearcoatRadiance, const in vec3 geometryPosition, const in vec3 geometryNormal, const in vec3 geometryViewDir, const in vec3 geometryClearcoatNormal, const in PhysicalMaterial material, inout ReflectedLight reflectedLight) { - #ifdef USE_CLEARCOAT - clearcoatSpecularIndirect += clearcoatRadiance * EnvironmentBRDF( geometryClearcoatNormal, geometryViewDir, material.clearcoatF0, material.clearcoatF90, material.clearcoatRoughness ); - #endif - #ifdef USE_SHEEN - sheenSpecularIndirect += irradiance * material.sheenColor * IBLSheenBRDF( geometryNormal, geometryViewDir, material.sheenRoughness ); - #endif - vec3 singleScattering = vec3( 0.0 ); - vec3 multiScattering = vec3( 0.0 ); - vec3 cosineWeightedIrradiance = irradiance * RECIPROCAL_PI; - #ifdef USE_IRIDESCENCE - computeMultiscatteringIridescence( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.iridescence, material.iridescenceFresnel, material.roughness, singleScattering, multiScattering ); - #else - computeMultiscattering( geometryNormal, geometryViewDir, material.specularColor, material.specularF90, material.roughness, singleScattering, multiScattering ); - #endif - vec3 totalScattering = singleScattering + multiScattering; - vec3 diffuse = material.diffuseColor * ( 1.0 - max( max( totalScattering.r, totalScattering.g ), totalScattering.b ) ); - reflectedLight.indirectSpecular += radiance * singleScattering; - reflectedLight.indirectSpecular += multiScattering * cosineWeightedIrradiance; - reflectedLight.indirectDiffuse += diffuse * cosineWeightedIrradiance; -} -#define RE_Direct RE_Direct_Physical -#define RE_Direct_RectArea RE_Direct_RectArea_Physical -#define RE_IndirectDiffuse RE_IndirectDiffuse_Physical -#define RE_IndirectSpecular RE_IndirectSpecular_Physical -float computeSpecularOcclusion( const in float dotNV, const in float ambientOcclusion, const in float roughness ) { - return saturate( pow( dotNV + ambientOcclusion, exp2( - 16.0 * roughness - 1.0 ) ) - 1.0 + ambientOcclusion ); -}`,vd=` -vec3 geometryPosition = - vViewPosition; -vec3 geometryNormal = normal; -vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); -vec3 geometryClearcoatNormal = vec3( 0.0 ); -#ifdef USE_CLEARCOAT - geometryClearcoatNormal = clearcoatNormal; -#endif -#ifdef USE_IRIDESCENCE - float dotNVi = saturate( dot( normal, geometryViewDir ) ); - if ( material.iridescenceThickness == 0.0 ) { - material.iridescence = 0.0; - } else { - material.iridescence = saturate( material.iridescence ); - } - if ( material.iridescence > 0.0 ) { - material.iridescenceFresnel = evalIridescence( 1.0, material.iridescenceIOR, dotNVi, material.iridescenceThickness, material.specularColor ); - material.iridescenceF0 = Schlick_to_F0( material.iridescenceFresnel, 1.0, dotNVi ); - } -#endif -IncidentLight directLight; -#if ( NUM_POINT_LIGHTS > 0 ) && defined( RE_Direct ) - PointLight pointLight; - #if defined( USE_SHADOWMAP ) && NUM_POINT_LIGHT_SHADOWS > 0 - PointLightShadow pointLightShadow; - #endif - #pragma unroll_loop_start - for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) { - pointLight = pointLights[ i ]; - getPointLightInfo( pointLight, geometryPosition, directLight ); - #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_POINT_LIGHT_SHADOWS ) - pointLightShadow = pointLightShadows[ i ]; - directLight.color *= ( directLight.visible && receiveShadow ) ? getPointShadow( pointShadowMap[ i ], pointLightShadow.shadowMapSize, pointLightShadow.shadowIntensity, pointLightShadow.shadowBias, pointLightShadow.shadowRadius, vPointShadowCoord[ i ], pointLightShadow.shadowCameraNear, pointLightShadow.shadowCameraFar ) : 1.0; - #endif - RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if ( NUM_SPOT_LIGHTS > 0 ) && defined( RE_Direct ) - SpotLight spotLight; - vec4 spotColor; - vec3 spotLightCoord; - bool inSpotLightMap; - #if defined( USE_SHADOWMAP ) && NUM_SPOT_LIGHT_SHADOWS > 0 - SpotLightShadow spotLightShadow; - #endif - #pragma unroll_loop_start - for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) { - spotLight = spotLights[ i ]; - getSpotLightInfo( spotLight, geometryPosition, directLight ); - #if ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) - #define SPOT_LIGHT_MAP_INDEX UNROLLED_LOOP_INDEX - #elif ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) - #define SPOT_LIGHT_MAP_INDEX NUM_SPOT_LIGHT_MAPS - #else - #define SPOT_LIGHT_MAP_INDEX ( UNROLLED_LOOP_INDEX - NUM_SPOT_LIGHT_SHADOWS + NUM_SPOT_LIGHT_SHADOWS_WITH_MAPS ) - #endif - #if ( SPOT_LIGHT_MAP_INDEX < NUM_SPOT_LIGHT_MAPS ) - spotLightCoord = vSpotLightCoord[ i ].xyz / vSpotLightCoord[ i ].w; - inSpotLightMap = all( lessThan( abs( spotLightCoord * 2. - 1. ), vec3( 1.0 ) ) ); - spotColor = texture2D( spotLightMap[ SPOT_LIGHT_MAP_INDEX ], spotLightCoord.xy ); - directLight.color = inSpotLightMap ? directLight.color * spotColor.rgb : directLight.color; - #endif - #undef SPOT_LIGHT_MAP_INDEX - #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) - spotLightShadow = spotLightShadows[ i ]; - directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( spotShadowMap[ i ], spotLightShadow.shadowMapSize, spotLightShadow.shadowIntensity, spotLightShadow.shadowBias, spotLightShadow.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; - #endif - RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if ( NUM_DIR_LIGHTS > 0 ) && defined( RE_Direct ) - DirectionalLight directionalLight; - #if defined( USE_SHADOWMAP ) && NUM_DIR_LIGHT_SHADOWS > 0 - DirectionalLightShadow directionalLightShadow; - #endif - #pragma unroll_loop_start - for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) { - directionalLight = directionalLights[ i ]; - getDirectionalLightInfo( directionalLight, directLight ); - #if defined( USE_SHADOWMAP ) && ( UNROLLED_LOOP_INDEX < NUM_DIR_LIGHT_SHADOWS ) - directionalLightShadow = directionalLightShadows[ i ]; - directLight.color *= ( directLight.visible && receiveShadow ) ? getShadow( directionalShadowMap[ i ], directionalLightShadow.shadowMapSize, directionalLightShadow.shadowIntensity, directionalLightShadow.shadowBias, directionalLightShadow.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; - #endif - RE_Direct( directLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if ( NUM_RECT_AREA_LIGHTS > 0 ) && defined( RE_Direct_RectArea ) - RectAreaLight rectAreaLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_RECT_AREA_LIGHTS; i ++ ) { - rectAreaLight = rectAreaLights[ i ]; - RE_Direct_RectArea( rectAreaLight, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); - } - #pragma unroll_loop_end -#endif -#if defined( RE_IndirectDiffuse ) - vec3 iblIrradiance = vec3( 0.0 ); - vec3 irradiance = getAmbientLightIrradiance( ambientLightColor ); - #if defined( USE_LIGHT_PROBES ) - irradiance += getLightProbeIrradiance( lightProbe, geometryNormal ); - #endif - #if ( NUM_HEMI_LIGHTS > 0 ) - #pragma unroll_loop_start - for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) { - irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal ); - } - #pragma unroll_loop_end - #endif -#endif -#if defined( RE_IndirectSpecular ) - vec3 radiance = vec3( 0.0 ); - vec3 clearcoatRadiance = vec3( 0.0 ); -#endif`,xd=`#if defined( RE_IndirectDiffuse ) - #ifdef USE_LIGHTMAP - vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); - vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; - irradiance += lightMapIrradiance; - #endif - #if defined( USE_ENVMAP ) && defined( STANDARD ) && defined( ENVMAP_TYPE_CUBE_UV ) - iblIrradiance += getIBLIrradiance( geometryNormal ); - #endif -#endif -#if defined( USE_ENVMAP ) && defined( RE_IndirectSpecular ) - #ifdef USE_ANISOTROPY - radiance += getIBLAnisotropyRadiance( geometryViewDir, geometryNormal, material.roughness, material.anisotropyB, material.anisotropy ); - #else - radiance += getIBLRadiance( geometryViewDir, geometryNormal, material.roughness ); - #endif - #ifdef USE_CLEARCOAT - clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); - #endif -#endif`,Md=`#if defined( RE_IndirectDiffuse ) - RE_IndirectDiffuse( irradiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); -#endif -#if defined( RE_IndirectSpecular ) - RE_IndirectSpecular( radiance, iblIrradiance, clearcoatRadiance, geometryPosition, geometryNormal, geometryViewDir, geometryClearcoatNormal, material, reflectedLight ); -#endif`,Sd=`#if defined( USE_LOGDEPTHBUF ) - gl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; -#endif`,yd=`#if defined( USE_LOGDEPTHBUF ) - uniform float logDepthBufFC; - varying float vFragDepth; - varying float vIsPerspective; -#endif`,Ed=`#ifdef USE_LOGDEPTHBUF - varying float vFragDepth; - varying float vIsPerspective; -#endif`,bd=`#ifdef USE_LOGDEPTHBUF - vFragDepth = 1.0 + gl_Position.w; - vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); -#endif`,Td=`#ifdef USE_MAP - vec4 sampledDiffuseColor = texture2D( map, vMapUv ); - #ifdef DECODE_VIDEO_TEXTURE - sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); - #endif - diffuseColor *= sampledDiffuseColor; -#endif`,wd=`#ifdef USE_MAP - uniform sampler2D map; -#endif`,Ad=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) - #if defined( USE_POINTS_UV ) - vec2 uv = vUv; - #else - vec2 uv = ( uvTransform * vec3( gl_PointCoord.x, 1.0 - gl_PointCoord.y, 1 ) ).xy; - #endif -#endif -#ifdef USE_MAP - diffuseColor *= texture2D( map, uv ); -#endif -#ifdef USE_ALPHAMAP - diffuseColor.a *= texture2D( alphaMap, uv ).g; -#endif`,Rd=`#if defined( USE_POINTS_UV ) - varying vec2 vUv; -#else - #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) - uniform mat3 uvTransform; - #endif -#endif -#ifdef USE_MAP - uniform sampler2D map; -#endif -#ifdef USE_ALPHAMAP - uniform sampler2D alphaMap; -#endif`,Cd=`float metalnessFactor = metalness; -#ifdef USE_METALNESSMAP - vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); - metalnessFactor *= texelMetalness.b; -#endif`,Pd=`#ifdef USE_METALNESSMAP - uniform sampler2D metalnessMap; -#endif`,Dd=`#ifdef USE_INSTANCING_MORPH - float morphTargetInfluences[ MORPHTARGETS_COUNT ]; - float morphTargetBaseInfluence = texelFetch( morphTexture, ivec2( 0, gl_InstanceID ), 0 ).r; - for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { - morphTargetInfluences[i] = texelFetch( morphTexture, ivec2( i + 1, gl_InstanceID ), 0 ).r; - } -#endif`,Ld=`#if defined( USE_MORPHCOLORS ) - vColor *= morphTargetBaseInfluence; - for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { - #if defined( USE_COLOR_ALPHA ) - if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ) * morphTargetInfluences[ i ]; - #elif defined( USE_COLOR ) - if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; - #endif - } -#endif`,Ud=`#ifdef USE_MORPHNORMALS - objectNormal *= morphTargetBaseInfluence; - for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { - if ( morphTargetInfluences[ i ] != 0.0 ) objectNormal += getMorph( gl_VertexID, i, 1 ).xyz * morphTargetInfluences[ i ]; - } -#endif`,Id=`#ifdef USE_MORPHTARGETS - #ifndef USE_INSTANCING_MORPH - uniform float morphTargetBaseInfluence; - uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; - #endif - uniform sampler2DArray morphTargetsTexture; - uniform ivec2 morphTargetsTextureSize; - vec4 getMorph( const in int vertexIndex, const in int morphTargetIndex, const in int offset ) { - int texelIndex = vertexIndex * MORPHTARGETS_TEXTURE_STRIDE + offset; - int y = texelIndex / morphTargetsTextureSize.x; - int x = texelIndex - y * morphTargetsTextureSize.x; - ivec3 morphUV = ivec3( x, y, morphTargetIndex ); - return texelFetch( morphTargetsTexture, morphUV, 0 ); - } -#endif`,Nd=`#ifdef USE_MORPHTARGETS - transformed *= morphTargetBaseInfluence; - for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { - if ( morphTargetInfluences[ i ] != 0.0 ) transformed += getMorph( gl_VertexID, i, 0 ).xyz * morphTargetInfluences[ i ]; - } -#endif`,Fd=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; -#ifdef FLAT_SHADED - vec3 fdx = dFdx( vViewPosition ); - vec3 fdy = dFdy( vViewPosition ); - vec3 normal = normalize( cross( fdx, fdy ) ); -#else - vec3 normal = normalize( vNormal ); - #ifdef DOUBLE_SIDED - normal *= faceDirection; - #endif -#endif -#if defined( USE_NORMALMAP_TANGENTSPACE ) || defined( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) - #ifdef USE_TANGENT - mat3 tbn = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); - #else - mat3 tbn = getTangentFrame( - vViewPosition, normal, - #if defined( USE_NORMALMAP ) - vNormalMapUv - #elif defined( USE_CLEARCOAT_NORMALMAP ) - vClearcoatNormalMapUv - #else - vUv - #endif - ); - #endif - #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) - tbn[0] *= faceDirection; - tbn[1] *= faceDirection; - #endif -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - #ifdef USE_TANGENT - mat3 tbn2 = mat3( normalize( vTangent ), normalize( vBitangent ), normal ); - #else - mat3 tbn2 = getTangentFrame( - vViewPosition, normal, vClearcoatNormalMapUv ); - #endif - #if defined( DOUBLE_SIDED ) && ! defined( FLAT_SHADED ) - tbn2[0] *= faceDirection; - tbn2[1] *= faceDirection; - #endif -#endif -vec3 nonPerturbedNormal = normal;`,Od=`#ifdef USE_NORMALMAP_OBJECTSPACE - normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; - #ifdef FLIP_SIDED - normal = - normal; - #endif - #ifdef DOUBLE_SIDED - normal = normal * faceDirection; - #endif - normal = normalize( normalMatrix * normal ); -#elif defined( USE_NORMALMAP_TANGENTSPACE ) - vec3 mapN = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; - mapN.xy *= normalScale; - normal = normalize( tbn * mapN ); -#elif defined( USE_BUMPMAP ) - normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); -#endif`,Bd=`#ifndef FLAT_SHADED - varying vec3 vNormal; - #ifdef USE_TANGENT - varying vec3 vTangent; - varying vec3 vBitangent; - #endif -#endif`,zd=`#ifndef FLAT_SHADED - varying vec3 vNormal; - #ifdef USE_TANGENT - varying vec3 vTangent; - varying vec3 vBitangent; - #endif -#endif`,kd=`#ifndef FLAT_SHADED - vNormal = normalize( transformedNormal ); - #ifdef USE_TANGENT - vTangent = normalize( transformedTangent ); - vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); - #endif -#endif`,Hd=`#ifdef USE_NORMALMAP - uniform sampler2D normalMap; - uniform vec2 normalScale; -#endif -#ifdef USE_NORMALMAP_OBJECTSPACE - uniform mat3 normalMatrix; -#endif -#if ! defined ( USE_TANGENT ) && ( defined ( USE_NORMALMAP_TANGENTSPACE ) || defined ( USE_CLEARCOAT_NORMALMAP ) || defined( USE_ANISOTROPY ) ) - mat3 getTangentFrame( vec3 eye_pos, vec3 surf_norm, vec2 uv ) { - vec3 q0 = dFdx( eye_pos.xyz ); - vec3 q1 = dFdy( eye_pos.xyz ); - vec2 st0 = dFdx( uv.st ); - vec2 st1 = dFdy( uv.st ); - vec3 N = surf_norm; - vec3 q1perp = cross( q1, N ); - vec3 q0perp = cross( N, q0 ); - vec3 T = q1perp * st0.x + q0perp * st1.x; - vec3 B = q1perp * st0.y + q0perp * st1.y; - float det = max( dot( T, T ), dot( B, B ) ); - float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); - return mat3( T * scale, B * scale, N ); - } -#endif`,Vd=`#ifdef USE_CLEARCOAT - vec3 clearcoatNormal = nonPerturbedNormal; -#endif`,Gd=`#ifdef USE_CLEARCOAT_NORMALMAP - vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; - clearcoatMapN.xy *= clearcoatNormalScale; - clearcoatNormal = normalize( tbn2 * clearcoatMapN ); -#endif`,Wd=`#ifdef USE_CLEARCOATMAP - uniform sampler2D clearcoatMap; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - uniform sampler2D clearcoatNormalMap; - uniform vec2 clearcoatNormalScale; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - uniform sampler2D clearcoatRoughnessMap; -#endif`,Xd=`#ifdef USE_IRIDESCENCEMAP - uniform sampler2D iridescenceMap; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - uniform sampler2D iridescenceThicknessMap; -#endif`,Yd=`#ifdef OPAQUE -diffuseColor.a = 1.0; -#endif -#ifdef USE_TRANSMISSION -diffuseColor.a *= material.transmissionAlpha; -#endif -gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,qd=`vec3 packNormalToRGB( const in vec3 normal ) { - return normalize( normal ) * 0.5 + 0.5; -} -vec3 unpackRGBToNormal( const in vec3 rgb ) { - return 2.0 * rgb.xyz - 1.0; -} -const float PackUpscale = 256. / 255.;const float UnpackDownscale = 255. / 256.;const float ShiftRight8 = 1. / 256.; -const float Inv255 = 1. / 255.; -const vec4 PackFactors = vec4( 1.0, 256.0, 256.0 * 256.0, 256.0 * 256.0 * 256.0 ); -const vec2 UnpackFactors2 = vec2( UnpackDownscale, 1.0 / PackFactors.g ); -const vec3 UnpackFactors3 = vec3( UnpackDownscale / PackFactors.rg, 1.0 / PackFactors.b ); -const vec4 UnpackFactors4 = vec4( UnpackDownscale / PackFactors.rgb, 1.0 / PackFactors.a ); -vec4 packDepthToRGBA( const in float v ) { - if( v <= 0.0 ) - return vec4( 0., 0., 0., 0. ); - if( v >= 1.0 ) - return vec4( 1., 1., 1., 1. ); - float vuf; - float af = modf( v * PackFactors.a, vuf ); - float bf = modf( vuf * ShiftRight8, vuf ); - float gf = modf( vuf * ShiftRight8, vuf ); - return vec4( vuf * Inv255, gf * PackUpscale, bf * PackUpscale, af ); -} -vec3 packDepthToRGB( const in float v ) { - if( v <= 0.0 ) - return vec3( 0., 0., 0. ); - if( v >= 1.0 ) - return vec3( 1., 1., 1. ); - float vuf; - float bf = modf( v * PackFactors.b, vuf ); - float gf = modf( vuf * ShiftRight8, vuf ); - return vec3( vuf * Inv255, gf * PackUpscale, bf ); -} -vec2 packDepthToRG( const in float v ) { - if( v <= 0.0 ) - return vec2( 0., 0. ); - if( v >= 1.0 ) - return vec2( 1., 1. ); - float vuf; - float gf = modf( v * 256., vuf ); - return vec2( vuf * Inv255, gf ); -} -float unpackRGBAToDepth( const in vec4 v ) { - return dot( v, UnpackFactors4 ); -} -float unpackRGBToDepth( const in vec3 v ) { - return dot( v, UnpackFactors3 ); -} -float unpackRGToDepth( const in vec2 v ) { - return v.r * UnpackFactors2.r + v.g * UnpackFactors2.g; -} -vec4 pack2HalfToRGBA( const in vec2 v ) { - vec4 r = vec4( v.x, fract( v.x * 255.0 ), v.y, fract( v.y * 255.0 ) ); - return vec4( r.x - r.y / 255.0, r.y, r.z - r.w / 255.0, r.w ); -} -vec2 unpackRGBATo2Half( const in vec4 v ) { - return vec2( v.x + ( v.y / 255.0 ), v.z + ( v.w / 255.0 ) ); -} -float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { - return ( viewZ + near ) / ( near - far ); -} -float orthographicDepthToViewZ( const in float depth, const in float near, const in float far ) { - return depth * ( near - far ) - near; -} -float viewZToPerspectiveDepth( const in float viewZ, const in float near, const in float far ) { - return ( ( near + viewZ ) * far ) / ( ( far - near ) * viewZ ); -} -float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { - return ( near * far ) / ( ( far - near ) * depth - far ); -}`,jd=`#ifdef PREMULTIPLIED_ALPHA - gl_FragColor.rgb *= gl_FragColor.a; -#endif`,Zd=`vec4 mvPosition = vec4( transformed, 1.0 ); -#ifdef USE_BATCHING - mvPosition = batchingMatrix * mvPosition; -#endif -#ifdef USE_INSTANCING - mvPosition = instanceMatrix * mvPosition; -#endif -mvPosition = modelViewMatrix * mvPosition; -gl_Position = projectionMatrix * mvPosition;`,Kd=`#ifdef DITHERING - gl_FragColor.rgb = dithering( gl_FragColor.rgb ); -#endif`,$d=`#ifdef DITHERING - vec3 dithering( vec3 color ) { - float grid_position = rand( gl_FragCoord.xy ); - vec3 dither_shift_RGB = vec3( 0.25 / 255.0, -0.25 / 255.0, 0.25 / 255.0 ); - dither_shift_RGB = mix( 2.0 * dither_shift_RGB, -2.0 * dither_shift_RGB, grid_position ); - return color + dither_shift_RGB; - } -#endif`,Jd=`float roughnessFactor = roughness; -#ifdef USE_ROUGHNESSMAP - vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); - roughnessFactor *= texelRoughness.g; -#endif`,Qd=`#ifdef USE_ROUGHNESSMAP - uniform sampler2D roughnessMap; -#endif`,tf=`#if NUM_SPOT_LIGHT_COORDS > 0 - varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; -#endif -#if NUM_SPOT_LIGHT_MAPS > 0 - uniform sampler2D spotLightMap[ NUM_SPOT_LIGHT_MAPS ]; -#endif -#ifdef USE_SHADOWMAP - #if NUM_DIR_LIGHT_SHADOWS > 0 - uniform sampler2D directionalShadowMap[ NUM_DIR_LIGHT_SHADOWS ]; - varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; - struct DirectionalLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; - #endif - #if NUM_SPOT_LIGHT_SHADOWS > 0 - uniform sampler2D spotShadowMap[ NUM_SPOT_LIGHT_SHADOWS ]; - struct SpotLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - uniform sampler2D pointShadowMap[ NUM_POINT_LIGHT_SHADOWS ]; - varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; - struct PointLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - float shadowCameraNear; - float shadowCameraFar; - }; - uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; - #endif - float texture2DCompare( sampler2D depths, vec2 uv, float compare ) { - return step( compare, unpackRGBAToDepth( texture2D( depths, uv ) ) ); - } - vec2 texture2DDistribution( sampler2D shadow, vec2 uv ) { - return unpackRGBATo2Half( texture2D( shadow, uv ) ); - } - float VSMShadow (sampler2D shadow, vec2 uv, float compare ){ - float occlusion = 1.0; - vec2 distribution = texture2DDistribution( shadow, uv ); - float hard_shadow = step( compare , distribution.x ); - if (hard_shadow != 1.0 ) { - float distance = compare - distribution.x ; - float variance = max( 0.00000, distribution.y * distribution.y ); - float softness_probability = variance / (variance + distance * distance ); softness_probability = clamp( ( softness_probability - 0.3 ) / ( 0.95 - 0.3 ), 0.0, 1.0 ); occlusion = clamp( max( hard_shadow, softness_probability ), 0.0, 1.0 ); - } - return occlusion; - } - float getShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord ) { - float shadow = 1.0; - shadowCoord.xyz /= shadowCoord.w; - shadowCoord.z += shadowBias; - bool inFrustum = shadowCoord.x >= 0.0 && shadowCoord.x <= 1.0 && shadowCoord.y >= 0.0 && shadowCoord.y <= 1.0; - bool frustumTest = inFrustum && shadowCoord.z <= 1.0; - if ( frustumTest ) { - #if defined( SHADOWMAP_TYPE_PCF ) - vec2 texelSize = vec2( 1.0 ) / shadowMapSize; - float dx0 = - texelSize.x * shadowRadius; - float dy0 = - texelSize.y * shadowRadius; - float dx1 = + texelSize.x * shadowRadius; - float dy1 = + texelSize.y * shadowRadius; - float dx2 = dx0 / 2.0; - float dy2 = dy0 / 2.0; - float dx3 = dx1 / 2.0; - float dy3 = dy1 / 2.0; - shadow = ( - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy2 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy2 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy2 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx2, dy3 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy3 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx3, dy3 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx0, dy1 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( 0.0, dy1 ), shadowCoord.z ) + - texture2DCompare( shadowMap, shadowCoord.xy + vec2( dx1, dy1 ), shadowCoord.z ) - ) * ( 1.0 / 17.0 ); - #elif defined( SHADOWMAP_TYPE_PCF_SOFT ) - vec2 texelSize = vec2( 1.0 ) / shadowMapSize; - float dx = texelSize.x; - float dy = texelSize.y; - vec2 uv = shadowCoord.xy; - vec2 f = fract( uv * shadowMapSize + 0.5 ); - uv -= f * texelSize; - shadow = ( - texture2DCompare( shadowMap, uv, shadowCoord.z ) + - texture2DCompare( shadowMap, uv + vec2( dx, 0.0 ), shadowCoord.z ) + - texture2DCompare( shadowMap, uv + vec2( 0.0, dy ), shadowCoord.z ) + - texture2DCompare( shadowMap, uv + texelSize, shadowCoord.z ) + - mix( texture2DCompare( shadowMap, uv + vec2( -dx, 0.0 ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 0.0 ), shadowCoord.z ), - f.x ) + - mix( texture2DCompare( shadowMap, uv + vec2( -dx, dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, dy ), shadowCoord.z ), - f.x ) + - mix( texture2DCompare( shadowMap, uv + vec2( 0.0, -dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 0.0, 2.0 * dy ), shadowCoord.z ), - f.y ) + - mix( texture2DCompare( shadowMap, uv + vec2( dx, -dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( dx, 2.0 * dy ), shadowCoord.z ), - f.y ) + - mix( mix( texture2DCompare( shadowMap, uv + vec2( -dx, -dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, -dy ), shadowCoord.z ), - f.x ), - mix( texture2DCompare( shadowMap, uv + vec2( -dx, 2.0 * dy ), shadowCoord.z ), - texture2DCompare( shadowMap, uv + vec2( 2.0 * dx, 2.0 * dy ), shadowCoord.z ), - f.x ), - f.y ) - ) * ( 1.0 / 9.0 ); - #elif defined( SHADOWMAP_TYPE_VSM ) - shadow = VSMShadow( shadowMap, shadowCoord.xy, shadowCoord.z ); - #else - shadow = texture2DCompare( shadowMap, shadowCoord.xy, shadowCoord.z ); - #endif - } - return mix( 1.0, shadow, shadowIntensity ); - } - vec2 cubeToUV( vec3 v, float texelSizeY ) { - vec3 absV = abs( v ); - float scaleToCube = 1.0 / max( absV.x, max( absV.y, absV.z ) ); - absV *= scaleToCube; - v *= scaleToCube * ( 1.0 - 2.0 * texelSizeY ); - vec2 planar = v.xy; - float almostATexel = 1.5 * texelSizeY; - float almostOne = 1.0 - almostATexel; - if ( absV.z >= almostOne ) { - if ( v.z > 0.0 ) - planar.x = 4.0 - v.x; - } else if ( absV.x >= almostOne ) { - float signX = sign( v.x ); - planar.x = v.z * signX + 2.0 * signX; - } else if ( absV.y >= almostOne ) { - float signY = sign( v.y ); - planar.x = v.x + 2.0 * signY + 2.0; - planar.y = v.z * signY - 2.0; - } - return vec2( 0.125, 0.25 ) * planar + vec2( 0.375, 0.75 ); - } - float getPointShadow( sampler2D shadowMap, vec2 shadowMapSize, float shadowIntensity, float shadowBias, float shadowRadius, vec4 shadowCoord, float shadowCameraNear, float shadowCameraFar ) { - float shadow = 1.0; - vec3 lightToPosition = shadowCoord.xyz; - - float lightToPositionLength = length( lightToPosition ); - if ( lightToPositionLength - shadowCameraFar <= 0.0 && lightToPositionLength - shadowCameraNear >= 0.0 ) { - float dp = ( lightToPositionLength - shadowCameraNear ) / ( shadowCameraFar - shadowCameraNear ); dp += shadowBias; - vec3 bd3D = normalize( lightToPosition ); - vec2 texelSize = vec2( 1.0 ) / ( shadowMapSize * vec2( 4.0, 2.0 ) ); - #if defined( SHADOWMAP_TYPE_PCF ) || defined( SHADOWMAP_TYPE_PCF_SOFT ) || defined( SHADOWMAP_TYPE_VSM ) - vec2 offset = vec2( - 1, 1 ) * shadowRadius * texelSize.y; - shadow = ( - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xyx, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yyx, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxy, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.xxx, texelSize.y ), dp ) + - texture2DCompare( shadowMap, cubeToUV( bd3D + offset.yxx, texelSize.y ), dp ) - ) * ( 1.0 / 9.0 ); - #else - shadow = texture2DCompare( shadowMap, cubeToUV( bd3D, texelSize.y ), dp ); - #endif - } - return mix( 1.0, shadow, shadowIntensity ); - } -#endif`,ef=`#if NUM_SPOT_LIGHT_COORDS > 0 - uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; - varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; -#endif -#ifdef USE_SHADOWMAP - #if NUM_DIR_LIGHT_SHADOWS > 0 - uniform mat4 directionalShadowMatrix[ NUM_DIR_LIGHT_SHADOWS ]; - varying vec4 vDirectionalShadowCoord[ NUM_DIR_LIGHT_SHADOWS ]; - struct DirectionalLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform DirectionalLightShadow directionalLightShadows[ NUM_DIR_LIGHT_SHADOWS ]; - #endif - #if NUM_SPOT_LIGHT_SHADOWS > 0 - struct SpotLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - }; - uniform SpotLightShadow spotLightShadows[ NUM_SPOT_LIGHT_SHADOWS ]; - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - uniform mat4 pointShadowMatrix[ NUM_POINT_LIGHT_SHADOWS ]; - varying vec4 vPointShadowCoord[ NUM_POINT_LIGHT_SHADOWS ]; - struct PointLightShadow { - float shadowIntensity; - float shadowBias; - float shadowNormalBias; - float shadowRadius; - vec2 shadowMapSize; - float shadowCameraNear; - float shadowCameraFar; - }; - uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; - #endif -#endif`,nf=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) - vec3 shadowWorldNormal = inverseTransformDirection( transformedNormal, viewMatrix ); - vec4 shadowWorldPosition; -#endif -#if defined( USE_SHADOWMAP ) - #if NUM_DIR_LIGHT_SHADOWS > 0 - #pragma unroll_loop_start - for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { - shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * directionalLightShadows[ i ].shadowNormalBias, 0 ); - vDirectionalShadowCoord[ i ] = directionalShadowMatrix[ i ] * shadowWorldPosition; - } - #pragma unroll_loop_end - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - #pragma unroll_loop_start - for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { - shadowWorldPosition = worldPosition + vec4( shadowWorldNormal * pointLightShadows[ i ].shadowNormalBias, 0 ); - vPointShadowCoord[ i ] = pointShadowMatrix[ i ] * shadowWorldPosition; - } - #pragma unroll_loop_end - #endif -#endif -#if NUM_SPOT_LIGHT_COORDS > 0 - #pragma unroll_loop_start - for ( int i = 0; i < NUM_SPOT_LIGHT_COORDS; i ++ ) { - shadowWorldPosition = worldPosition; - #if ( defined( USE_SHADOWMAP ) && UNROLLED_LOOP_INDEX < NUM_SPOT_LIGHT_SHADOWS ) - shadowWorldPosition.xyz += shadowWorldNormal * spotLightShadows[ i ].shadowNormalBias; - #endif - vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; - } - #pragma unroll_loop_end -#endif`,sf=`float getShadowMask() { - float shadow = 1.0; - #ifdef USE_SHADOWMAP - #if NUM_DIR_LIGHT_SHADOWS > 0 - DirectionalLightShadow directionalLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_DIR_LIGHT_SHADOWS; i ++ ) { - directionalLight = directionalLightShadows[ i ]; - shadow *= receiveShadow ? getShadow( directionalShadowMap[ i ], directionalLight.shadowMapSize, directionalLight.shadowIntensity, directionalLight.shadowBias, directionalLight.shadowRadius, vDirectionalShadowCoord[ i ] ) : 1.0; - } - #pragma unroll_loop_end - #endif - #if NUM_SPOT_LIGHT_SHADOWS > 0 - SpotLightShadow spotLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_SPOT_LIGHT_SHADOWS; i ++ ) { - spotLight = spotLightShadows[ i ]; - shadow *= receiveShadow ? getShadow( spotShadowMap[ i ], spotLight.shadowMapSize, spotLight.shadowIntensity, spotLight.shadowBias, spotLight.shadowRadius, vSpotLightCoord[ i ] ) : 1.0; - } - #pragma unroll_loop_end - #endif - #if NUM_POINT_LIGHT_SHADOWS > 0 - PointLightShadow pointLight; - #pragma unroll_loop_start - for ( int i = 0; i < NUM_POINT_LIGHT_SHADOWS; i ++ ) { - pointLight = pointLightShadows[ i ]; - shadow *= receiveShadow ? getPointShadow( pointShadowMap[ i ], pointLight.shadowMapSize, pointLight.shadowIntensity, pointLight.shadowBias, pointLight.shadowRadius, vPointShadowCoord[ i ], pointLight.shadowCameraNear, pointLight.shadowCameraFar ) : 1.0; - } - #pragma unroll_loop_end - #endif - #endif - return shadow; -}`,rf=`#ifdef USE_SKINNING - mat4 boneMatX = getBoneMatrix( skinIndex.x ); - mat4 boneMatY = getBoneMatrix( skinIndex.y ); - mat4 boneMatZ = getBoneMatrix( skinIndex.z ); - mat4 boneMatW = getBoneMatrix( skinIndex.w ); -#endif`,af=`#ifdef USE_SKINNING - uniform mat4 bindMatrix; - uniform mat4 bindMatrixInverse; - uniform highp sampler2D boneTexture; - mat4 getBoneMatrix( const in float i ) { - int size = textureSize( boneTexture, 0 ).x; - int j = int( i ) * 4; - int x = j % size; - int y = j / size; - vec4 v1 = texelFetch( boneTexture, ivec2( x, y ), 0 ); - vec4 v2 = texelFetch( boneTexture, ivec2( x + 1, y ), 0 ); - vec4 v3 = texelFetch( boneTexture, ivec2( x + 2, y ), 0 ); - vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 ); - return mat4( v1, v2, v3, v4 ); - } -#endif`,of=`#ifdef USE_SKINNING - vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); - vec4 skinned = vec4( 0.0 ); - skinned += boneMatX * skinVertex * skinWeight.x; - skinned += boneMatY * skinVertex * skinWeight.y; - skinned += boneMatZ * skinVertex * skinWeight.z; - skinned += boneMatW * skinVertex * skinWeight.w; - transformed = ( bindMatrixInverse * skinned ).xyz; -#endif`,lf=`#ifdef USE_SKINNING - mat4 skinMatrix = mat4( 0.0 ); - skinMatrix += skinWeight.x * boneMatX; - skinMatrix += skinWeight.y * boneMatY; - skinMatrix += skinWeight.z * boneMatZ; - skinMatrix += skinWeight.w * boneMatW; - skinMatrix = bindMatrixInverse * skinMatrix * bindMatrix; - objectNormal = vec4( skinMatrix * vec4( objectNormal, 0.0 ) ).xyz; - #ifdef USE_TANGENT - objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; - #endif -#endif`,cf=`float specularStrength; -#ifdef USE_SPECULARMAP - vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); - specularStrength = texelSpecular.r; -#else - specularStrength = 1.0; -#endif`,hf=`#ifdef USE_SPECULARMAP - uniform sampler2D specularMap; -#endif`,uf=`#if defined( TONE_MAPPING ) - gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); -#endif`,df=`#ifndef saturate -#define saturate( a ) clamp( a, 0.0, 1.0 ) -#endif -uniform float toneMappingExposure; -vec3 LinearToneMapping( vec3 color ) { - return saturate( toneMappingExposure * color ); -} -vec3 ReinhardToneMapping( vec3 color ) { - color *= toneMappingExposure; - return saturate( color / ( vec3( 1.0 ) + color ) ); -} -vec3 CineonToneMapping( vec3 color ) { - color *= toneMappingExposure; - color = max( vec3( 0.0 ), color - 0.004 ); - return pow( ( color * ( 6.2 * color + 0.5 ) ) / ( color * ( 6.2 * color + 1.7 ) + 0.06 ), vec3( 2.2 ) ); -} -vec3 RRTAndODTFit( vec3 v ) { - vec3 a = v * ( v + 0.0245786 ) - 0.000090537; - vec3 b = v * ( 0.983729 * v + 0.4329510 ) + 0.238081; - return a / b; -} -vec3 ACESFilmicToneMapping( vec3 color ) { - const mat3 ACESInputMat = mat3( - vec3( 0.59719, 0.07600, 0.02840 ), vec3( 0.35458, 0.90834, 0.13383 ), - vec3( 0.04823, 0.01566, 0.83777 ) - ); - const mat3 ACESOutputMat = mat3( - vec3( 1.60475, -0.10208, -0.00327 ), vec3( -0.53108, 1.10813, -0.07276 ), - vec3( -0.07367, -0.00605, 1.07602 ) - ); - color *= toneMappingExposure / 0.6; - color = ACESInputMat * color; - color = RRTAndODTFit( color ); - color = ACESOutputMat * color; - return saturate( color ); -} -const mat3 LINEAR_REC2020_TO_LINEAR_SRGB = mat3( - vec3( 1.6605, - 0.1246, - 0.0182 ), - vec3( - 0.5876, 1.1329, - 0.1006 ), - vec3( - 0.0728, - 0.0083, 1.1187 ) -); -const mat3 LINEAR_SRGB_TO_LINEAR_REC2020 = mat3( - vec3( 0.6274, 0.0691, 0.0164 ), - vec3( 0.3293, 0.9195, 0.0880 ), - vec3( 0.0433, 0.0113, 0.8956 ) -); -vec3 agxDefaultContrastApprox( vec3 x ) { - vec3 x2 = x * x; - vec3 x4 = x2 * x2; - return + 15.5 * x4 * x2 - - 40.14 * x4 * x - + 31.96 * x4 - - 6.868 * x2 * x - + 0.4298 * x2 - + 0.1191 * x - - 0.00232; -} -vec3 AgXToneMapping( vec3 color ) { - const mat3 AgXInsetMatrix = mat3( - vec3( 0.856627153315983, 0.137318972929847, 0.11189821299995 ), - vec3( 0.0951212405381588, 0.761241990602591, 0.0767994186031903 ), - vec3( 0.0482516061458583, 0.101439036467562, 0.811302368396859 ) - ); - const mat3 AgXOutsetMatrix = mat3( - vec3( 1.1271005818144368, - 0.1413297634984383, - 0.14132976349843826 ), - vec3( - 0.11060664309660323, 1.157823702216272, - 0.11060664309660294 ), - vec3( - 0.016493938717834573, - 0.016493938717834257, 1.2519364065950405 ) - ); - const float AgxMinEv = - 12.47393; const float AgxMaxEv = 4.026069; - color *= toneMappingExposure; - color = LINEAR_SRGB_TO_LINEAR_REC2020 * color; - color = AgXInsetMatrix * color; - color = max( color, 1e-10 ); color = log2( color ); - color = ( color - AgxMinEv ) / ( AgxMaxEv - AgxMinEv ); - color = clamp( color, 0.0, 1.0 ); - color = agxDefaultContrastApprox( color ); - color = AgXOutsetMatrix * color; - color = pow( max( vec3( 0.0 ), color ), vec3( 2.2 ) ); - color = LINEAR_REC2020_TO_LINEAR_SRGB * color; - color = clamp( color, 0.0, 1.0 ); - return color; -} -vec3 NeutralToneMapping( vec3 color ) { - const float StartCompression = 0.8 - 0.04; - const float Desaturation = 0.15; - color *= toneMappingExposure; - float x = min( color.r, min( color.g, color.b ) ); - float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; - color -= offset; - float peak = max( color.r, max( color.g, color.b ) ); - if ( peak < StartCompression ) return color; - float d = 1. - StartCompression; - float newPeak = 1. - d * d / ( peak + d - StartCompression ); - color *= newPeak / peak; - float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. ); - return mix( color, vec3( newPeak ), g ); -} -vec3 CustomToneMapping( vec3 color ) { return color; }`,ff=`#ifdef USE_TRANSMISSION - material.transmission = transmission; - material.transmissionAlpha = 1.0; - material.thickness = thickness; - material.attenuationDistance = attenuationDistance; - material.attenuationColor = attenuationColor; - #ifdef USE_TRANSMISSIONMAP - material.transmission *= texture2D( transmissionMap, vTransmissionMapUv ).r; - #endif - #ifdef USE_THICKNESSMAP - material.thickness *= texture2D( thicknessMap, vThicknessMapUv ).g; - #endif - vec3 pos = vWorldPosition; - vec3 v = normalize( cameraPosition - pos ); - vec3 n = inverseTransformDirection( normal, viewMatrix ); - vec4 transmitted = getIBLVolumeRefraction( - n, v, material.roughness, material.diffuseColor, material.specularColor, material.specularF90, - pos, modelMatrix, viewMatrix, projectionMatrix, material.dispersion, material.ior, material.thickness, - material.attenuationColor, material.attenuationDistance ); - material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); - totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); -#endif`,pf=`#ifdef USE_TRANSMISSION - uniform float transmission; - uniform float thickness; - uniform float attenuationDistance; - uniform vec3 attenuationColor; - #ifdef USE_TRANSMISSIONMAP - uniform sampler2D transmissionMap; - #endif - #ifdef USE_THICKNESSMAP - uniform sampler2D thicknessMap; - #endif - uniform vec2 transmissionSamplerSize; - uniform sampler2D transmissionSamplerMap; - uniform mat4 modelMatrix; - uniform mat4 projectionMatrix; - varying vec3 vWorldPosition; - float w0( float a ) { - return ( 1.0 / 6.0 ) * ( a * ( a * ( - a + 3.0 ) - 3.0 ) + 1.0 ); - } - float w1( float a ) { - return ( 1.0 / 6.0 ) * ( a * a * ( 3.0 * a - 6.0 ) + 4.0 ); - } - float w2( float a ){ - return ( 1.0 / 6.0 ) * ( a * ( a * ( - 3.0 * a + 3.0 ) + 3.0 ) + 1.0 ); - } - float w3( float a ) { - return ( 1.0 / 6.0 ) * ( a * a * a ); - } - float g0( float a ) { - return w0( a ) + w1( a ); - } - float g1( float a ) { - return w2( a ) + w3( a ); - } - float h0( float a ) { - return - 1.0 + w1( a ) / ( w0( a ) + w1( a ) ); - } - float h1( float a ) { - return 1.0 + w3( a ) / ( w2( a ) + w3( a ) ); - } - vec4 bicubic( sampler2D tex, vec2 uv, vec4 texelSize, float lod ) { - uv = uv * texelSize.zw + 0.5; - vec2 iuv = floor( uv ); - vec2 fuv = fract( uv ); - float g0x = g0( fuv.x ); - float g1x = g1( fuv.x ); - float h0x = h0( fuv.x ); - float h1x = h1( fuv.x ); - float h0y = h0( fuv.y ); - float h1y = h1( fuv.y ); - vec2 p0 = ( vec2( iuv.x + h0x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; - vec2 p1 = ( vec2( iuv.x + h1x, iuv.y + h0y ) - 0.5 ) * texelSize.xy; - vec2 p2 = ( vec2( iuv.x + h0x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; - vec2 p3 = ( vec2( iuv.x + h1x, iuv.y + h1y ) - 0.5 ) * texelSize.xy; - return g0( fuv.y ) * ( g0x * textureLod( tex, p0, lod ) + g1x * textureLod( tex, p1, lod ) ) + - g1( fuv.y ) * ( g0x * textureLod( tex, p2, lod ) + g1x * textureLod( tex, p3, lod ) ); - } - vec4 textureBicubic( sampler2D sampler, vec2 uv, float lod ) { - vec2 fLodSize = vec2( textureSize( sampler, int( lod ) ) ); - vec2 cLodSize = vec2( textureSize( sampler, int( lod + 1.0 ) ) ); - vec2 fLodSizeInv = 1.0 / fLodSize; - vec2 cLodSizeInv = 1.0 / cLodSize; - vec4 fSample = bicubic( sampler, uv, vec4( fLodSizeInv, fLodSize ), floor( lod ) ); - vec4 cSample = bicubic( sampler, uv, vec4( cLodSizeInv, cLodSize ), ceil( lod ) ); - return mix( fSample, cSample, fract( lod ) ); - } - vec3 getVolumeTransmissionRay( const in vec3 n, const in vec3 v, const in float thickness, const in float ior, const in mat4 modelMatrix ) { - vec3 refractionVector = refract( - v, normalize( n ), 1.0 / ior ); - vec3 modelScale; - modelScale.x = length( vec3( modelMatrix[ 0 ].xyz ) ); - modelScale.y = length( vec3( modelMatrix[ 1 ].xyz ) ); - modelScale.z = length( vec3( modelMatrix[ 2 ].xyz ) ); - return normalize( refractionVector ) * thickness * modelScale; - } - float applyIorToRoughness( const in float roughness, const in float ior ) { - return roughness * clamp( ior * 2.0 - 2.0, 0.0, 1.0 ); - } - vec4 getTransmissionSample( const in vec2 fragCoord, const in float roughness, const in float ior ) { - float lod = log2( transmissionSamplerSize.x ) * applyIorToRoughness( roughness, ior ); - return textureBicubic( transmissionSamplerMap, fragCoord.xy, lod ); - } - vec3 volumeAttenuation( const in float transmissionDistance, const in vec3 attenuationColor, const in float attenuationDistance ) { - if ( isinf( attenuationDistance ) ) { - return vec3( 1.0 ); - } else { - vec3 attenuationCoefficient = -log( attenuationColor ) / attenuationDistance; - vec3 transmittance = exp( - attenuationCoefficient * transmissionDistance ); return transmittance; - } - } - vec4 getIBLVolumeRefraction( const in vec3 n, const in vec3 v, const in float roughness, const in vec3 diffuseColor, - const in vec3 specularColor, const in float specularF90, const in vec3 position, const in mat4 modelMatrix, - const in mat4 viewMatrix, const in mat4 projMatrix, const in float dispersion, const in float ior, const in float thickness, - const in vec3 attenuationColor, const in float attenuationDistance ) { - vec4 transmittedLight; - vec3 transmittance; - #ifdef USE_DISPERSION - float halfSpread = ( ior - 1.0 ) * 0.025 * dispersion; - vec3 iors = vec3( ior - halfSpread, ior, ior + halfSpread ); - for ( int i = 0; i < 3; i ++ ) { - vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, iors[ i ], modelMatrix ); - vec3 refractedRayExit = position + transmissionRay; - vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); - vec2 refractionCoords = ndcPos.xy / ndcPos.w; - refractionCoords += 1.0; - refractionCoords /= 2.0; - vec4 transmissionSample = getTransmissionSample( refractionCoords, roughness, iors[ i ] ); - transmittedLight[ i ] = transmissionSample[ i ]; - transmittedLight.a += transmissionSample.a; - transmittance[ i ] = diffuseColor[ i ] * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance )[ i ]; - } - transmittedLight.a /= 3.0; - #else - vec3 transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix ); - vec3 refractedRayExit = position + transmissionRay; - vec4 ndcPos = projMatrix * viewMatrix * vec4( refractedRayExit, 1.0 ); - vec2 refractionCoords = ndcPos.xy / ndcPos.w; - refractionCoords += 1.0; - refractionCoords /= 2.0; - transmittedLight = getTransmissionSample( refractionCoords, roughness, ior ); - transmittance = diffuseColor * volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ); - #endif - vec3 attenuatedColor = transmittance * transmittedLight.rgb; - vec3 F = EnvironmentBRDF( n, v, specularColor, specularF90, roughness ); - float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; - return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); - } -#endif`,mf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) - varying vec2 vUv; -#endif -#ifdef USE_MAP - varying vec2 vMapUv; -#endif -#ifdef USE_ALPHAMAP - varying vec2 vAlphaMapUv; -#endif -#ifdef USE_LIGHTMAP - varying vec2 vLightMapUv; -#endif -#ifdef USE_AOMAP - varying vec2 vAoMapUv; -#endif -#ifdef USE_BUMPMAP - varying vec2 vBumpMapUv; -#endif -#ifdef USE_NORMALMAP - varying vec2 vNormalMapUv; -#endif -#ifdef USE_EMISSIVEMAP - varying vec2 vEmissiveMapUv; -#endif -#ifdef USE_METALNESSMAP - varying vec2 vMetalnessMapUv; -#endif -#ifdef USE_ROUGHNESSMAP - varying vec2 vRoughnessMapUv; -#endif -#ifdef USE_ANISOTROPYMAP - varying vec2 vAnisotropyMapUv; -#endif -#ifdef USE_CLEARCOATMAP - varying vec2 vClearcoatMapUv; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - varying vec2 vClearcoatNormalMapUv; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - varying vec2 vClearcoatRoughnessMapUv; -#endif -#ifdef USE_IRIDESCENCEMAP - varying vec2 vIridescenceMapUv; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - varying vec2 vIridescenceThicknessMapUv; -#endif -#ifdef USE_SHEEN_COLORMAP - varying vec2 vSheenColorMapUv; -#endif -#ifdef USE_SHEEN_ROUGHNESSMAP - varying vec2 vSheenRoughnessMapUv; -#endif -#ifdef USE_SPECULARMAP - varying vec2 vSpecularMapUv; -#endif -#ifdef USE_SPECULAR_COLORMAP - varying vec2 vSpecularColorMapUv; -#endif -#ifdef USE_SPECULAR_INTENSITYMAP - varying vec2 vSpecularIntensityMapUv; -#endif -#ifdef USE_TRANSMISSIONMAP - uniform mat3 transmissionMapTransform; - varying vec2 vTransmissionMapUv; -#endif -#ifdef USE_THICKNESSMAP - uniform mat3 thicknessMapTransform; - varying vec2 vThicknessMapUv; -#endif`,gf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) - varying vec2 vUv; -#endif -#ifdef USE_MAP - uniform mat3 mapTransform; - varying vec2 vMapUv; -#endif -#ifdef USE_ALPHAMAP - uniform mat3 alphaMapTransform; - varying vec2 vAlphaMapUv; -#endif -#ifdef USE_LIGHTMAP - uniform mat3 lightMapTransform; - varying vec2 vLightMapUv; -#endif -#ifdef USE_AOMAP - uniform mat3 aoMapTransform; - varying vec2 vAoMapUv; -#endif -#ifdef USE_BUMPMAP - uniform mat3 bumpMapTransform; - varying vec2 vBumpMapUv; -#endif -#ifdef USE_NORMALMAP - uniform mat3 normalMapTransform; - varying vec2 vNormalMapUv; -#endif -#ifdef USE_DISPLACEMENTMAP - uniform mat3 displacementMapTransform; - varying vec2 vDisplacementMapUv; -#endif -#ifdef USE_EMISSIVEMAP - uniform mat3 emissiveMapTransform; - varying vec2 vEmissiveMapUv; -#endif -#ifdef USE_METALNESSMAP - uniform mat3 metalnessMapTransform; - varying vec2 vMetalnessMapUv; -#endif -#ifdef USE_ROUGHNESSMAP - uniform mat3 roughnessMapTransform; - varying vec2 vRoughnessMapUv; -#endif -#ifdef USE_ANISOTROPYMAP - uniform mat3 anisotropyMapTransform; - varying vec2 vAnisotropyMapUv; -#endif -#ifdef USE_CLEARCOATMAP - uniform mat3 clearcoatMapTransform; - varying vec2 vClearcoatMapUv; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - uniform mat3 clearcoatNormalMapTransform; - varying vec2 vClearcoatNormalMapUv; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - uniform mat3 clearcoatRoughnessMapTransform; - varying vec2 vClearcoatRoughnessMapUv; -#endif -#ifdef USE_SHEEN_COLORMAP - uniform mat3 sheenColorMapTransform; - varying vec2 vSheenColorMapUv; -#endif -#ifdef USE_SHEEN_ROUGHNESSMAP - uniform mat3 sheenRoughnessMapTransform; - varying vec2 vSheenRoughnessMapUv; -#endif -#ifdef USE_IRIDESCENCEMAP - uniform mat3 iridescenceMapTransform; - varying vec2 vIridescenceMapUv; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - uniform mat3 iridescenceThicknessMapTransform; - varying vec2 vIridescenceThicknessMapUv; -#endif -#ifdef USE_SPECULARMAP - uniform mat3 specularMapTransform; - varying vec2 vSpecularMapUv; -#endif -#ifdef USE_SPECULAR_COLORMAP - uniform mat3 specularColorMapTransform; - varying vec2 vSpecularColorMapUv; -#endif -#ifdef USE_SPECULAR_INTENSITYMAP - uniform mat3 specularIntensityMapTransform; - varying vec2 vSpecularIntensityMapUv; -#endif -#ifdef USE_TRANSMISSIONMAP - uniform mat3 transmissionMapTransform; - varying vec2 vTransmissionMapUv; -#endif -#ifdef USE_THICKNESSMAP - uniform mat3 thicknessMapTransform; - varying vec2 vThicknessMapUv; -#endif`,_f=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) - vUv = vec3( uv, 1 ).xy; -#endif -#ifdef USE_MAP - vMapUv = ( mapTransform * vec3( MAP_UV, 1 ) ).xy; -#endif -#ifdef USE_ALPHAMAP - vAlphaMapUv = ( alphaMapTransform * vec3( ALPHAMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_LIGHTMAP - vLightMapUv = ( lightMapTransform * vec3( LIGHTMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_AOMAP - vAoMapUv = ( aoMapTransform * vec3( AOMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_BUMPMAP - vBumpMapUv = ( bumpMapTransform * vec3( BUMPMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_NORMALMAP - vNormalMapUv = ( normalMapTransform * vec3( NORMALMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_DISPLACEMENTMAP - vDisplacementMapUv = ( displacementMapTransform * vec3( DISPLACEMENTMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_EMISSIVEMAP - vEmissiveMapUv = ( emissiveMapTransform * vec3( EMISSIVEMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_METALNESSMAP - vMetalnessMapUv = ( metalnessMapTransform * vec3( METALNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_ROUGHNESSMAP - vRoughnessMapUv = ( roughnessMapTransform * vec3( ROUGHNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_ANISOTROPYMAP - vAnisotropyMapUv = ( anisotropyMapTransform * vec3( ANISOTROPYMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_CLEARCOATMAP - vClearcoatMapUv = ( clearcoatMapTransform * vec3( CLEARCOATMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_CLEARCOAT_NORMALMAP - vClearcoatNormalMapUv = ( clearcoatNormalMapTransform * vec3( CLEARCOAT_NORMALMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_CLEARCOAT_ROUGHNESSMAP - vClearcoatRoughnessMapUv = ( clearcoatRoughnessMapTransform * vec3( CLEARCOAT_ROUGHNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_IRIDESCENCEMAP - vIridescenceMapUv = ( iridescenceMapTransform * vec3( IRIDESCENCEMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_IRIDESCENCE_THICKNESSMAP - vIridescenceThicknessMapUv = ( iridescenceThicknessMapTransform * vec3( IRIDESCENCE_THICKNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SHEEN_COLORMAP - vSheenColorMapUv = ( sheenColorMapTransform * vec3( SHEEN_COLORMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SHEEN_ROUGHNESSMAP - vSheenRoughnessMapUv = ( sheenRoughnessMapTransform * vec3( SHEEN_ROUGHNESSMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SPECULARMAP - vSpecularMapUv = ( specularMapTransform * vec3( SPECULARMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SPECULAR_COLORMAP - vSpecularColorMapUv = ( specularColorMapTransform * vec3( SPECULAR_COLORMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_SPECULAR_INTENSITYMAP - vSpecularIntensityMapUv = ( specularIntensityMapTransform * vec3( SPECULAR_INTENSITYMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_TRANSMISSIONMAP - vTransmissionMapUv = ( transmissionMapTransform * vec3( TRANSMISSIONMAP_UV, 1 ) ).xy; -#endif -#ifdef USE_THICKNESSMAP - vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; -#endif`,vf=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 - vec4 worldPosition = vec4( transformed, 1.0 ); - #ifdef USE_BATCHING - worldPosition = batchingMatrix * worldPosition; - #endif - #ifdef USE_INSTANCING - worldPosition = instanceMatrix * worldPosition; - #endif - worldPosition = modelMatrix * worldPosition; -#endif`;const xf=`varying vec2 vUv; -uniform mat3 uvTransform; -void main() { - vUv = ( uvTransform * vec3( uv, 1 ) ).xy; - gl_Position = vec4( position.xy, 1.0, 1.0 ); -}`,Mf=`uniform sampler2D t2D; -uniform float backgroundIntensity; -varying vec2 vUv; -void main() { - vec4 texColor = texture2D( t2D, vUv ); - #ifdef DECODE_VIDEO_TEXTURE - texColor = vec4( mix( pow( texColor.rgb * 0.9478672986 + vec3( 0.0521327014 ), vec3( 2.4 ) ), texColor.rgb * 0.0773993808, vec3( lessThanEqual( texColor.rgb, vec3( 0.04045 ) ) ) ), texColor.w ); - #endif - texColor.rgb *= backgroundIntensity; - gl_FragColor = texColor; - #include - #include -}`,Sf=`varying vec3 vWorldDirection; -#include -void main() { - vWorldDirection = transformDirection( position, modelMatrix ); - #include - #include - gl_Position.z = gl_Position.w; -}`,yf=`#ifdef ENVMAP_TYPE_CUBE - uniform samplerCube envMap; -#elif defined( ENVMAP_TYPE_CUBE_UV ) - uniform sampler2D envMap; -#endif -uniform float flipEnvMap; -uniform float backgroundBlurriness; -uniform float backgroundIntensity; -uniform mat3 backgroundRotation; -varying vec3 vWorldDirection; -#include -void main() { - #ifdef ENVMAP_TYPE_CUBE - vec4 texColor = textureCube( envMap, backgroundRotation * vec3( flipEnvMap * vWorldDirection.x, vWorldDirection.yz ) ); - #elif defined( ENVMAP_TYPE_CUBE_UV ) - vec4 texColor = textureCubeUV( envMap, backgroundRotation * vWorldDirection, backgroundBlurriness ); - #else - vec4 texColor = vec4( 0.0, 0.0, 0.0, 1.0 ); - #endif - texColor.rgb *= backgroundIntensity; - gl_FragColor = texColor; - #include - #include -}`,Ef=`varying vec3 vWorldDirection; -#include -void main() { - vWorldDirection = transformDirection( position, modelMatrix ); - #include - #include - gl_Position.z = gl_Position.w; -}`,bf=`uniform samplerCube tCube; -uniform float tFlip; -uniform float opacity; -varying vec3 vWorldDirection; -void main() { - vec4 texColor = textureCube( tCube, vec3( tFlip * vWorldDirection.x, vWorldDirection.yz ) ); - gl_FragColor = texColor; - gl_FragColor.a *= opacity; - #include - #include -}`,Tf=`#include -#include -#include -#include -#include -#include -#include -#include -varying vec2 vHighPrecisionZW; -void main() { - #include - #include - #include - #include - #ifdef USE_DISPLACEMENTMAP - #include - #include - #include - #endif - #include - #include - #include - #include - #include - #include - #include - vHighPrecisionZW = gl_Position.zw; -}`,wf=`#if DEPTH_PACKING == 3200 - uniform float opacity; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -varying vec2 vHighPrecisionZW; -void main() { - vec4 diffuseColor = vec4( 1.0 ); - #include - #if DEPTH_PACKING == 3200 - diffuseColor.a = opacity; - #endif - #include - #include - #include - #include - #include - float fragCoordZ = 0.5 * vHighPrecisionZW[0] / vHighPrecisionZW[1] + 0.5; - #if DEPTH_PACKING == 3200 - gl_FragColor = vec4( vec3( 1.0 - fragCoordZ ), opacity ); - #elif DEPTH_PACKING == 3201 - gl_FragColor = packDepthToRGBA( fragCoordZ ); - #elif DEPTH_PACKING == 3202 - gl_FragColor = vec4( packDepthToRGB( fragCoordZ ), 1.0 ); - #elif DEPTH_PACKING == 3203 - gl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 ); - #endif -}`,Af=`#define DISTANCE -varying vec3 vWorldPosition; -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #ifdef USE_DISPLACEMENTMAP - #include - #include - #include - #endif - #include - #include - #include - #include - #include - #include - #include - vWorldPosition = worldPosition.xyz; -}`,Rf=`#define DISTANCE -uniform vec3 referencePosition; -uniform float nearDistance; -uniform float farDistance; -varying vec3 vWorldPosition; -#include -#include -#include -#include -#include -#include -#include -#include -void main () { - vec4 diffuseColor = vec4( 1.0 ); - #include - #include - #include - #include - #include - float dist = length( vWorldPosition - referencePosition ); - dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); - dist = saturate( dist ); - gl_FragColor = packDepthToRGBA( dist ); -}`,Cf=`varying vec3 vWorldDirection; -#include -void main() { - vWorldDirection = transformDirection( position, modelMatrix ); - #include - #include -}`,Pf=`uniform sampler2D tEquirect; -varying vec3 vWorldDirection; -#include -void main() { - vec3 direction = normalize( vWorldDirection ); - vec2 sampleUV = equirectUv( direction ); - gl_FragColor = texture2D( tEquirect, sampleUV ); - #include - #include -}`,Df=`uniform float scale; -attribute float lineDistance; -varying float vLineDistance; -#include -#include -#include -#include -#include -#include -#include -void main() { - vLineDistance = scale * lineDistance; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include -}`,Lf=`uniform vec3 diffuse; -uniform float opacity; -uniform float dashSize; -uniform float totalSize; -varying float vLineDistance; -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - if ( mod( vLineDistance, totalSize ) > dashSize ) { - discard; - } - vec3 outgoingLight = vec3( 0.0 ); - #include - #include - #include - outgoingLight = diffuseColor.rgb; - #include - #include - #include - #include - #include -}`,Uf=`#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #if defined ( USE_ENVMAP ) || defined ( USE_SKINNING ) - #include - #include - #include - #include - #include - #endif - #include - #include - #include - #include - #include - #include - #include - #include - #include -}`,If=`uniform vec3 diffuse; -uniform float opacity; -#ifndef FLAT_SHADED - varying vec3 vNormal; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - #include - #include - #include - #include - #include - #include - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - #ifdef USE_LIGHTMAP - vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); - reflectedLight.indirectDiffuse += lightMapTexel.rgb * lightMapIntensity * RECIPROCAL_PI; - #else - reflectedLight.indirectDiffuse += vec3( 1.0 ); - #endif - #include - reflectedLight.indirectDiffuse *= diffuseColor.rgb; - vec3 outgoingLight = reflectedLight.indirectDiffuse; - #include - #include - #include - #include - #include - #include - #include -}`,Nf=`#define LAMBERT -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include - #include -}`,Ff=`#define LAMBERT -uniform vec3 diffuse; -uniform vec3 emissive; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; - #include - #include - #include - #include - #include - #include - #include -}`,Of=`#define MATCAP -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; -}`,Bf=`#define MATCAP -uniform vec3 diffuse; -uniform float opacity; -uniform sampler2D matcap; -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 viewDir = normalize( vViewPosition ); - vec3 x = normalize( vec3( viewDir.z, 0.0, - viewDir.x ) ); - vec3 y = cross( viewDir, x ); - vec2 uv = vec2( dot( x, normal ), dot( y, normal ) ) * 0.495 + 0.5; - #ifdef USE_MATCAP - vec4 matcapColor = texture2D( matcap, uv ); - #else - vec4 matcapColor = vec4( vec3( mix( 0.2, 0.8, uv.y ) ), 1.0 ); - #endif - vec3 outgoingLight = diffuseColor.rgb * matcapColor.rgb; - #include - #include - #include - #include - #include - #include -}`,zf=`#define NORMAL -#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) - varying vec3 vViewPosition; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include -#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) - vViewPosition = - mvPosition.xyz; -#endif -}`,kf=`#define NORMAL -uniform float opacity; -#if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) - varying vec3 vViewPosition; -#endif -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( 0.0, 0.0, 0.0, opacity ); - #include - #include - #include - #include - gl_FragColor = vec4( packNormalToRGB( normal ), diffuseColor.a ); - #ifdef OPAQUE - gl_FragColor.a = 1.0; - #endif -}`,Hf=`#define PHONG -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include - #include -}`,Vf=`#define PHONG -uniform vec3 diffuse; -uniform vec3 emissive; -uniform vec3 specular; -uniform float shininess; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance; - #include - #include - #include - #include - #include - #include - #include -}`,Gf=`#define STANDARD -varying vec3 vViewPosition; -#ifdef USE_TRANSMISSION - varying vec3 vWorldPosition; -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include -#ifdef USE_TRANSMISSION - vWorldPosition = worldPosition.xyz; -#endif -}`,Wf=`#define STANDARD -#ifdef PHYSICAL - #define IOR - #define USE_SPECULAR -#endif -uniform vec3 diffuse; -uniform vec3 emissive; -uniform float roughness; -uniform float metalness; -uniform float opacity; -#ifdef IOR - uniform float ior; -#endif -#ifdef USE_SPECULAR - uniform float specularIntensity; - uniform vec3 specularColor; - #ifdef USE_SPECULAR_COLORMAP - uniform sampler2D specularColorMap; - #endif - #ifdef USE_SPECULAR_INTENSITYMAP - uniform sampler2D specularIntensityMap; - #endif -#endif -#ifdef USE_CLEARCOAT - uniform float clearcoat; - uniform float clearcoatRoughness; -#endif -#ifdef USE_DISPERSION - uniform float dispersion; -#endif -#ifdef USE_IRIDESCENCE - uniform float iridescence; - uniform float iridescenceIOR; - uniform float iridescenceThicknessMinimum; - uniform float iridescenceThicknessMaximum; -#endif -#ifdef USE_SHEEN - uniform vec3 sheenColor; - uniform float sheenRoughness; - #ifdef USE_SHEEN_COLORMAP - uniform sampler2D sheenColorMap; - #endif - #ifdef USE_SHEEN_ROUGHNESSMAP - uniform sampler2D sheenRoughnessMap; - #endif -#endif -#ifdef USE_ANISOTROPY - uniform vec2 anisotropyVector; - #ifdef USE_ANISOTROPYMAP - uniform sampler2D anisotropyMap; - #endif -#endif -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 totalDiffuse = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse; - vec3 totalSpecular = reflectedLight.directSpecular + reflectedLight.indirectSpecular; - #include - vec3 outgoingLight = totalDiffuse + totalSpecular + totalEmissiveRadiance; - #ifdef USE_SHEEN - float sheenEnergyComp = 1.0 - 0.157 * max3( material.sheenColor ); - outgoingLight = outgoingLight * sheenEnergyComp + sheenSpecularDirect + sheenSpecularIndirect; - #endif - #ifdef USE_CLEARCOAT - float dotNVcc = saturate( dot( geometryClearcoatNormal, geometryViewDir ) ); - vec3 Fcc = F_Schlick( material.clearcoatF0, material.clearcoatF90, dotNVcc ); - outgoingLight = outgoingLight * ( 1.0 - material.clearcoat * Fcc ) + ( clearcoatSpecularDirect + clearcoatSpecularIndirect ) * material.clearcoat; - #endif - #include - #include - #include - #include - #include - #include -}`,Xf=`#define TOON -varying vec3 vViewPosition; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vViewPosition = - mvPosition.xyz; - #include - #include - #include -}`,Yf=`#define TOON -uniform vec3 diffuse; -uniform vec3 emissive; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) ); - vec3 totalEmissiveRadiance = emissive; - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance; - #include - #include - #include - #include - #include - #include -}`,qf=`uniform float size; -uniform float scale; -#include -#include -#include -#include -#include -#include -#ifdef USE_POINTS_UV - varying vec2 vUv; - uniform mat3 uvTransform; -#endif -void main() { - #ifdef USE_POINTS_UV - vUv = ( uvTransform * vec3( uv, 1 ) ).xy; - #endif - #include - #include - #include - #include - #include - #include - gl_PointSize = size; - #ifdef USE_SIZEATTENUATION - bool isPerspective = isPerspectiveMatrix( projectionMatrix ); - if ( isPerspective ) gl_PointSize *= ( scale / - mvPosition.z ); - #endif - #include - #include - #include - #include -}`,jf=`uniform vec3 diffuse; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - vec3 outgoingLight = vec3( 0.0 ); - #include - #include - #include - #include - #include - outgoingLight = diffuseColor.rgb; - #include - #include - #include - #include - #include -}`,Zf=`#include -#include -#include -#include -#include -#include -#include -void main() { - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include - #include -}`,Kf=`uniform vec3 color; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - #include - gl_FragColor = vec4( color, opacity * ( 1.0 - getShadowMask() ) ); - #include - #include - #include -}`,$f=`uniform float rotation; -uniform vec2 center; -#include -#include -#include -#include -#include -void main() { - #include - vec4 mvPosition = modelViewMatrix[ 3 ]; - vec2 scale = vec2( length( modelMatrix[ 0 ].xyz ), length( modelMatrix[ 1 ].xyz ) ); - #ifndef USE_SIZEATTENUATION - bool isPerspective = isPerspectiveMatrix( projectionMatrix ); - if ( isPerspective ) scale *= - mvPosition.z; - #endif - vec2 alignedPosition = ( position.xy - ( center - vec2( 0.5 ) ) ) * scale; - vec2 rotatedPosition; - rotatedPosition.x = cos( rotation ) * alignedPosition.x - sin( rotation ) * alignedPosition.y; - rotatedPosition.y = sin( rotation ) * alignedPosition.x + cos( rotation ) * alignedPosition.y; - mvPosition.xy += rotatedPosition; - gl_Position = projectionMatrix * mvPosition; - #include - #include - #include -}`,Jf=`uniform vec3 diffuse; -uniform float opacity; -#include -#include -#include -#include -#include -#include -#include -#include -#include -void main() { - vec4 diffuseColor = vec4( diffuse, opacity ); - #include - vec3 outgoingLight = vec3( 0.0 ); - #include - #include - #include - #include - #include - outgoingLight = diffuseColor.rgb; - #include - #include - #include - #include -}`,kt={alphahash_fragment:xu,alphahash_pars_fragment:Mu,alphamap_fragment:Su,alphamap_pars_fragment:yu,alphatest_fragment:Eu,alphatest_pars_fragment:bu,aomap_fragment:Tu,aomap_pars_fragment:wu,batching_pars_vertex:Au,batching_vertex:Ru,begin_vertex:Cu,beginnormal_vertex:Pu,bsdfs:Du,iridescence_fragment:Lu,bumpmap_pars_fragment:Uu,clipping_planes_fragment:Iu,clipping_planes_pars_fragment:Nu,clipping_planes_pars_vertex:Fu,clipping_planes_vertex:Ou,color_fragment:Bu,color_pars_fragment:zu,color_pars_vertex:ku,color_vertex:Hu,common:Vu,cube_uv_reflection_fragment:Gu,defaultnormal_vertex:Wu,displacementmap_pars_vertex:Xu,displacementmap_vertex:Yu,emissivemap_fragment:qu,emissivemap_pars_fragment:ju,colorspace_fragment:Zu,colorspace_pars_fragment:Ku,envmap_fragment:$u,envmap_common_pars_fragment:Ju,envmap_pars_fragment:Qu,envmap_pars_vertex:td,envmap_physical_pars_fragment:ud,envmap_vertex:ed,fog_vertex:nd,fog_pars_vertex:id,fog_fragment:sd,fog_pars_fragment:rd,gradientmap_pars_fragment:ad,lightmap_pars_fragment:od,lights_lambert_fragment:ld,lights_lambert_pars_fragment:cd,lights_pars_begin:hd,lights_toon_fragment:dd,lights_toon_pars_fragment:fd,lights_phong_fragment:pd,lights_phong_pars_fragment:md,lights_physical_fragment:gd,lights_physical_pars_fragment:_d,lights_fragment_begin:vd,lights_fragment_maps:xd,lights_fragment_end:Md,logdepthbuf_fragment:Sd,logdepthbuf_pars_fragment:yd,logdepthbuf_pars_vertex:Ed,logdepthbuf_vertex:bd,map_fragment:Td,map_pars_fragment:wd,map_particle_fragment:Ad,map_particle_pars_fragment:Rd,metalnessmap_fragment:Cd,metalnessmap_pars_fragment:Pd,morphinstance_vertex:Dd,morphcolor_vertex:Ld,morphnormal_vertex:Ud,morphtarget_pars_vertex:Id,morphtarget_vertex:Nd,normal_fragment_begin:Fd,normal_fragment_maps:Od,normal_pars_fragment:Bd,normal_pars_vertex:zd,normal_vertex:kd,normalmap_pars_fragment:Hd,clearcoat_normal_fragment_begin:Vd,clearcoat_normal_fragment_maps:Gd,clearcoat_pars_fragment:Wd,iridescence_pars_fragment:Xd,opaque_fragment:Yd,packing:qd,premultiplied_alpha_fragment:jd,project_vertex:Zd,dithering_fragment:Kd,dithering_pars_fragment:$d,roughnessmap_fragment:Jd,roughnessmap_pars_fragment:Qd,shadowmap_pars_fragment:tf,shadowmap_pars_vertex:ef,shadowmap_vertex:nf,shadowmask_pars_fragment:sf,skinbase_vertex:rf,skinning_pars_vertex:af,skinning_vertex:of,skinnormal_vertex:lf,specularmap_fragment:cf,specularmap_pars_fragment:hf,tonemapping_fragment:uf,tonemapping_pars_fragment:df,transmission_fragment:ff,transmission_pars_fragment:pf,uv_pars_fragment:mf,uv_pars_vertex:gf,uv_vertex:_f,worldpos_vertex:vf,background_vert:xf,background_frag:Mf,backgroundCube_vert:Sf,backgroundCube_frag:yf,cube_vert:Ef,cube_frag:bf,depth_vert:Tf,depth_frag:wf,distanceRGBA_vert:Af,distanceRGBA_frag:Rf,equirect_vert:Cf,equirect_frag:Pf,linedashed_vert:Df,linedashed_frag:Lf,meshbasic_vert:Uf,meshbasic_frag:If,meshlambert_vert:Nf,meshlambert_frag:Ff,meshmatcap_vert:Of,meshmatcap_frag:Bf,meshnormal_vert:zf,meshnormal_frag:kf,meshphong_vert:Hf,meshphong_frag:Vf,meshphysical_vert:Gf,meshphysical_frag:Wf,meshtoon_vert:Xf,meshtoon_frag:Yf,points_vert:qf,points_frag:jf,shadow_vert:Zf,shadow_frag:Kf,sprite_vert:$f,sprite_frag:Jf},st={common:{diffuse:{value:new rt(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new Bt},alphaMap:{value:null},alphaMapTransform:{value:new Bt},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new Bt}},envmap:{envMap:{value:null},envMapRotation:{value:new Bt},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new Bt}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new Bt}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new Bt},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new Bt},normalScale:{value:new Mt(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new Bt},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new Bt}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new Bt}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new Bt}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new rt(16777215)}},lights:{ambientLightColor:{value:[]},lightProbe:{value:[]},directionalLights:{value:[],properties:{direction:{},color:{}}},directionalLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},directionalShadowMap:{value:[]},directionalShadowMatrix:{value:[]},spotLights:{value:[],properties:{color:{},position:{},direction:{},distance:{},coneCos:{},penumbraCos:{},decay:{}}},spotLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{}}},spotLightMap:{value:[]},spotShadowMap:{value:[]},spotLightMatrix:{value:[]},pointLights:{value:[],properties:{color:{},position:{},decay:{},distance:{}}},pointLightShadows:{value:[],properties:{shadowIntensity:1,shadowBias:{},shadowNormalBias:{},shadowRadius:{},shadowMapSize:{},shadowCameraNear:{},shadowCameraFar:{}}},pointShadowMap:{value:[]},pointShadowMatrix:{value:[]},hemisphereLights:{value:[],properties:{direction:{},skyColor:{},groundColor:{}}},rectAreaLights:{value:[],properties:{color:{},position:{},width:{},height:{}}},ltc_1:{value:null},ltc_2:{value:null}},points:{diffuse:{value:new rt(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new Bt},alphaTest:{value:0},uvTransform:{value:new Bt}},sprite:{diffuse:{value:new rt(16777215)},opacity:{value:1},center:{value:new Mt(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Bt},alphaMap:{value:null},alphaMapTransform:{value:new Bt},alphaTest:{value:0}}},un={basic:{uniforms:Oe([st.common,st.specularmap,st.envmap,st.aomap,st.lightmap,st.fog]),vertexShader:kt.meshbasic_vert,fragmentShader:kt.meshbasic_frag},lambert:{uniforms:Oe([st.common,st.specularmap,st.envmap,st.aomap,st.lightmap,st.emissivemap,st.bumpmap,st.normalmap,st.displacementmap,st.fog,st.lights,{emissive:{value:new rt(0)}}]),vertexShader:kt.meshlambert_vert,fragmentShader:kt.meshlambert_frag},phong:{uniforms:Oe([st.common,st.specularmap,st.envmap,st.aomap,st.lightmap,st.emissivemap,st.bumpmap,st.normalmap,st.displacementmap,st.fog,st.lights,{emissive:{value:new rt(0)},specular:{value:new rt(1118481)},shininess:{value:30}}]),vertexShader:kt.meshphong_vert,fragmentShader:kt.meshphong_frag},standard:{uniforms:Oe([st.common,st.envmap,st.aomap,st.lightmap,st.emissivemap,st.bumpmap,st.normalmap,st.displacementmap,st.roughnessmap,st.metalnessmap,st.fog,st.lights,{emissive:{value:new rt(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:kt.meshphysical_vert,fragmentShader:kt.meshphysical_frag},toon:{uniforms:Oe([st.common,st.aomap,st.lightmap,st.emissivemap,st.bumpmap,st.normalmap,st.displacementmap,st.gradientmap,st.fog,st.lights,{emissive:{value:new rt(0)}}]),vertexShader:kt.meshtoon_vert,fragmentShader:kt.meshtoon_frag},matcap:{uniforms:Oe([st.common,st.bumpmap,st.normalmap,st.displacementmap,st.fog,{matcap:{value:null}}]),vertexShader:kt.meshmatcap_vert,fragmentShader:kt.meshmatcap_frag},points:{uniforms:Oe([st.points,st.fog]),vertexShader:kt.points_vert,fragmentShader:kt.points_frag},dashed:{uniforms:Oe([st.common,st.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:kt.linedashed_vert,fragmentShader:kt.linedashed_frag},depth:{uniforms:Oe([st.common,st.displacementmap]),vertexShader:kt.depth_vert,fragmentShader:kt.depth_frag},normal:{uniforms:Oe([st.common,st.bumpmap,st.normalmap,st.displacementmap,{opacity:{value:1}}]),vertexShader:kt.meshnormal_vert,fragmentShader:kt.meshnormal_frag},sprite:{uniforms:Oe([st.sprite,st.fog]),vertexShader:kt.sprite_vert,fragmentShader:kt.sprite_frag},background:{uniforms:{uvTransform:{value:new Bt},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:kt.background_vert,fragmentShader:kt.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Bt}},vertexShader:kt.backgroundCube_vert,fragmentShader:kt.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:kt.cube_vert,fragmentShader:kt.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:kt.equirect_vert,fragmentShader:kt.equirect_frag},distanceRGBA:{uniforms:Oe([st.common,st.displacementmap,{referencePosition:{value:new C},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:kt.distanceRGBA_vert,fragmentShader:kt.distanceRGBA_frag},shadow:{uniforms:Oe([st.lights,st.fog,{color:{value:new rt(0)},opacity:{value:1}}]),vertexShader:kt.shadow_vert,fragmentShader:kt.shadow_frag}};un.physical={uniforms:Oe([un.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Bt},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Bt},clearcoatNormalScale:{value:new Mt(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new Bt},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new Bt},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new Bt},sheen:{value:0},sheenColor:{value:new rt(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new Bt},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new Bt},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new Bt},transmissionSamplerSize:{value:new Mt},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Bt},attenuationDistance:{value:0},attenuationColor:{value:new rt(0)},specularColor:{value:new rt(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Bt},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Bt},anisotropyVector:{value:new Mt},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Bt}}]),vertexShader:kt.meshphysical_vert,fragmentShader:kt.meshphysical_frag};const Ks={r:0,b:0,g:0},qn=new gn,Qf=new ie;function tp(i,t,e,n,s,r,a){const o=new rt(0);let l=r===!0?0:1,c,h,d=null,p=0,u=null;function g(b){let S=b.isScene===!0?b.background:null;return S&&S.isTexture&&(S=(b.backgroundBlurriness>0?e:t).get(S)),S}function v(b){let S=!1;const D=g(b);D===null?f(o,l):D&&D.isColor&&(f(D,1),S=!0);const w=i.xr.getEnvironmentBlendMode();w==="additive"?n.buffers.color.setClear(0,0,0,1,a):w==="alpha-blend"&&n.buffers.color.setClear(0,0,0,0,a),(i.autoClear||S)&&(n.buffers.depth.setTest(!0),n.buffers.depth.setMask(!0),n.buffers.color.setMask(!0),i.clear(i.autoClearColor,i.autoClearDepth,i.autoClearStencil))}function m(b,S){const D=g(S);D&&(D.isCubeTexture||D.mapping===vr)?(h===void 0&&(h=new Me(new gs(1,1,1),new ze({name:"BackgroundCubeMaterial",uniforms:Gi(un.backgroundCube.uniforms),vertexShader:un.backgroundCube.vertexShader,fragmentShader:un.backgroundCube.fragmentShader,side:We,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(w,R,U){this.matrixWorld.copyPosition(U.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),s.update(h)),qn.copy(S.backgroundRotation),qn.x*=-1,qn.y*=-1,qn.z*=-1,D.isCubeTexture&&D.isRenderTargetTexture===!1&&(qn.y*=-1,qn.z*=-1),h.material.uniforms.envMap.value=D,h.material.uniforms.flipEnvMap.value=D.isCubeTexture&&D.isRenderTargetTexture===!1?-1:1,h.material.uniforms.backgroundBlurriness.value=S.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=S.backgroundIntensity,h.material.uniforms.backgroundRotation.value.setFromMatrix4(Qf.makeRotationFromEuler(qn)),h.material.toneMapped=Qt.getTransfer(D.colorSpace)!==re,(d!==D||p!==D.version||u!==i.toneMapping)&&(h.material.needsUpdate=!0,d=D,p=D.version,u=i.toneMapping),h.layers.enableAll(),b.unshift(h,h.geometry,h.material,0,0,null)):D&&D.isTexture&&(c===void 0&&(c=new Me(new _s(2,2),new ze({name:"BackgroundMaterial",uniforms:Gi(un.background.uniforms),vertexShader:un.background.vertexShader,fragmentShader:un.background.fragmentShader,side:kn,depthTest:!1,depthWrite:!1,fog:!1})),c.geometry.deleteAttribute("normal"),Object.defineProperty(c.material,"map",{get:function(){return this.uniforms.t2D.value}}),s.update(c)),c.material.uniforms.t2D.value=D,c.material.uniforms.backgroundIntensity.value=S.backgroundIntensity,c.material.toneMapped=Qt.getTransfer(D.colorSpace)!==re,D.matrixAutoUpdate===!0&&D.updateMatrix(),c.material.uniforms.uvTransform.value.copy(D.matrix),(d!==D||p!==D.version||u!==i.toneMapping)&&(c.material.needsUpdate=!0,d=D,p=D.version,u=i.toneMapping),c.layers.enableAll(),b.unshift(c,c.geometry,c.material,0,0,null))}function f(b,S){b.getRGB(Ks,oc(i)),n.buffers.color.setClear(Ks.r,Ks.g,Ks.b,S,a)}function T(){h!==void 0&&(h.geometry.dispose(),h.material.dispose()),c!==void 0&&(c.geometry.dispose(),c.material.dispose())}return{getClearColor:function(){return o},setClearColor:function(b,S=1){o.set(b),l=S,f(o,l)},getClearAlpha:function(){return l},setClearAlpha:function(b){l=b,f(o,l)},render:v,addToRenderList:m,dispose:T}}function ep(i,t){const e=i.getParameter(i.MAX_VERTEX_ATTRIBS),n={},s=p(null);let r=s,a=!1;function o(M,P,W,z,G){let $=!1;const X=d(z,W,P);r!==X&&(r=X,c(r.object)),$=u(M,z,W,G),$&&g(M,z,W,G),G!==null&&t.update(G,i.ELEMENT_ARRAY_BUFFER),($||a)&&(a=!1,S(M,P,W,z),G!==null&&i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,t.get(G).buffer))}function l(){return i.createVertexArray()}function c(M){return i.bindVertexArray(M)}function h(M){return i.deleteVertexArray(M)}function d(M,P,W){const z=W.wireframe===!0;let G=n[M.id];G===void 0&&(G={},n[M.id]=G);let $=G[P.id];$===void 0&&($={},G[P.id]=$);let X=$[z];return X===void 0&&(X=p(l()),$[z]=X),X}function p(M){const P=[],W=[],z=[];for(let G=0;G=0){const ft=G[k];let St=$[k];if(St===void 0&&(k==="instanceMatrix"&&M.instanceMatrix&&(St=M.instanceMatrix),k==="instanceColor"&&M.instanceColor&&(St=M.instanceColor)),ft===void 0||ft.attribute!==St||St&&ft.data!==St.data)return!0;X++}return r.attributesNum!==X||r.index!==z}function g(M,P,W,z){const G={},$=P.attributes;let X=0;const Q=W.getAttributes();for(const k in Q)if(Q[k].location>=0){let ft=$[k];ft===void 0&&(k==="instanceMatrix"&&M.instanceMatrix&&(ft=M.instanceMatrix),k==="instanceColor"&&M.instanceColor&&(ft=M.instanceColor));const St={};St.attribute=ft,ft&&ft.data&&(St.data=ft.data),G[k]=St,X++}r.attributes=G,r.attributesNum=X,r.index=z}function v(){const M=r.newAttributes;for(let P=0,W=M.length;P=0){let it=G[Q];if(it===void 0&&(Q==="instanceMatrix"&&M.instanceMatrix&&(it=M.instanceMatrix),Q==="instanceColor"&&M.instanceColor&&(it=M.instanceColor)),it!==void 0){const ft=it.normalized,St=it.itemSize,Ft=t.get(it);if(Ft===void 0)continue;const Vt=Ft.buffer,Y=Ft.type,nt=Ft.bytesPerElement,_t=Y===i.INT||Y===i.UNSIGNED_INT||it.gpuType===Qa;if(it.isInterleavedBufferAttribute){const ot=it.data,Pt=ot.stride,Lt=it.offset;if(ot.isInstancedInterleavedBuffer){for(let Ht=0;Ht0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.HIGH_FLOAT).precision>0)return"highp";R="mediump"}return R==="mediump"&&i.getShaderPrecisionFormat(i.VERTEX_SHADER,i.MEDIUM_FLOAT).precision>0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.MEDIUM_FLOAT).precision>0?"mediump":"lowp"}let c=e.precision!==void 0?e.precision:"highp";const h=l(c);h!==c&&(console.warn("THREE.WebGLRenderer:",c,"not supported, using",h,"instead."),c=h);const d=e.logarithmicDepthBuffer===!0,p=e.reverseDepthBuffer===!0&&t.has("EXT_clip_control"),u=i.getParameter(i.MAX_TEXTURE_IMAGE_UNITS),g=i.getParameter(i.MAX_VERTEX_TEXTURE_IMAGE_UNITS),v=i.getParameter(i.MAX_TEXTURE_SIZE),m=i.getParameter(i.MAX_CUBE_MAP_TEXTURE_SIZE),f=i.getParameter(i.MAX_VERTEX_ATTRIBS),T=i.getParameter(i.MAX_VERTEX_UNIFORM_VECTORS),b=i.getParameter(i.MAX_VARYING_VECTORS),S=i.getParameter(i.MAX_FRAGMENT_UNIFORM_VECTORS),D=g>0,w=i.getParameter(i.MAX_SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:r,getMaxPrecision:l,textureFormatReadable:a,textureTypeReadable:o,precision:c,logarithmicDepthBuffer:d,reverseDepthBuffer:p,maxTextures:u,maxVertexTextures:g,maxTextureSize:v,maxCubemapSize:m,maxAttributes:f,maxVertexUniforms:T,maxVaryings:b,maxFragmentUniforms:S,vertexTextures:D,maxSamples:w}}function sp(i){const t=this;let e=null,n=0,s=!1,r=!1;const a=new Fn,o=new Bt,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(d,p){const u=d.length!==0||p||n!==0||s;return s=p,n=d.length,u},this.beginShadows=function(){r=!0,h(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(d,p){e=h(d,p,0)},this.setState=function(d,p,u){const g=d.clippingPlanes,v=d.clipIntersection,m=d.clipShadows,f=i.get(d);if(!s||g===null||g.length===0||r&&!m)r?h(null):c();else{const T=r?0:n,b=T*4;let S=f.clippingState||null;l.value=S,S=h(g,p,b,u);for(let D=0;D!==b;++D)S[D]=e[D];f.clippingState=S,this.numIntersection=v?this.numPlanes:0,this.numPlanes+=T}};function c(){l.value!==e&&(l.value=e,l.needsUpdate=n>0),t.numPlanes=n,t.numIntersection=0}function h(d,p,u,g){const v=d!==null?d.length:0;let m=null;if(v!==0){if(m=l.value,g!==!0||m===null){const f=u+v*4,T=p.matrixWorldInverse;o.getNormalMatrix(T),(m===null||m.length0){const c=new Qh(l.height);return c.fromEquirectangularTexture(i,a),t.set(a,c),a.addEventListener("dispose",s),e(c.texture,a.mapping)}else return null}}return a}function s(a){const o=a.target;o.removeEventListener("dispose",s);const l=t.get(o);l!==void 0&&(t.delete(o),l.dispose())}function r(){t=new WeakMap}return{get:n,dispose:r}}const Di=4,nl=[.125,.215,.35,.446,.526,.582],Jn=20,Kr=new pc,il=new rt;let $r=null,Jr=0,Qr=0,ta=!1;const Kn=(1+Math.sqrt(5))/2,Ai=1/Kn,sl=[new C(-Kn,Ai,0),new C(Kn,Ai,0),new C(-Ai,0,Kn),new C(Ai,0,Kn),new C(0,Kn,-Ai),new C(0,Kn,Ai),new C(-1,1,-1),new C(1,1,-1),new C(-1,1,1),new C(1,1,1)];class rl{constructor(t){this._renderer=t,this._pingPongRenderTarget=null,this._lodMax=0,this._cubeSize=0,this._lodPlanes=[],this._sizeLods=[],this._sigmas=[],this._blurMaterial=null,this._cubemapMaterial=null,this._equirectMaterial=null,this._compileMaterial(this._blurMaterial)}fromScene(t,e=0,n=.1,s=100){$r=this._renderer.getRenderTarget(),Jr=this._renderer.getActiveCubeFace(),Qr=this._renderer.getActiveMipmapLevel(),ta=this._renderer.xr.enabled,this._renderer.xr.enabled=!1,this._setSize(256);const r=this._allocateTargets();return r.depthBuffer=!0,this._sceneToCubeUV(t,n,s,r),e>0&&this._blur(r,0,0,e),this._applyPMREM(r),this._cleanup(r),r}fromEquirectangular(t,e=null){return this._fromTexture(t,e)}fromCubemap(t,e=null){return this._fromTexture(t,e)}compileCubemapShader(){this._cubemapMaterial===null&&(this._cubemapMaterial=ll(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=ol(),this._compileMaterial(this._equirectMaterial))}dispose(){this._dispose(),this._cubemapMaterial!==null&&this._cubemapMaterial.dispose(),this._equirectMaterial!==null&&this._equirectMaterial.dispose()}_setSize(t){this._lodMax=Math.floor(Math.log2(t)),this._cubeSize=Math.pow(2,this._lodMax)}_dispose(){this._blurMaterial!==null&&this._blurMaterial.dispose(),this._pingPongRenderTarget!==null&&this._pingPongRenderTarget.dispose();for(let t=0;t2?b:0,b,b),h.setRenderTarget(s),v&&h.render(g,o),h.render(t,o)}g.geometry.dispose(),g.material.dispose(),h.toneMapping=p,h.autoClear=d,t.background=m}_textureToCubeUV(t,e){const n=this._renderer,s=t.mapping===Bi||t.mapping===zi;s?(this._cubemapMaterial===null&&(this._cubemapMaterial=ll()),this._cubemapMaterial.uniforms.flipEnvMap.value=t.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=ol());const r=s?this._cubemapMaterial:this._equirectMaterial,a=new Me(this._lodPlanes[0],r),o=r.uniforms;o.envMap.value=t;const l=this._cubeSize;$s(e,0,0,3*l,2*l),n.setRenderTarget(e),n.render(a,Kr)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;const s=this._lodPlanes.length;for(let r=1;rJn&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to ${Jn}`);const f=[];let T=0;for(let R=0;Rb-Di?s-b+Di:0),w=4*(this._cubeSize-S);$s(e,D,w,3*S,2*S),l.setRenderTarget(e),l.render(d,Kr)}}function ap(i){const t=[],e=[],n=[];let s=i;const r=i-Di+1+nl.length;for(let a=0;ai-Di?l=nl[a-i+Di-1]:a===0&&(l=0),n.push(l);const c=1/(o-2),h=-c,d=1+c,p=[h,h,d,h,d,d,h,h,d,d,h,d],u=6,g=6,v=3,m=2,f=1,T=new Float32Array(v*g*u),b=new Float32Array(m*g*u),S=new Float32Array(f*g*u);for(let w=0;w2?0:-1,y=[R,U,0,R+2/3,U,0,R+2/3,U+1,0,R,U,0,R+2/3,U+1,0,R,U+1,0];T.set(y,v*g*w),b.set(p,m*g*w);const M=[w,w,w,w,w,w];S.set(M,f*g*w)}const D=new ge;D.setAttribute("position",new ue(T,v)),D.setAttribute("uv",new ue(b,m)),D.setAttribute("faceIndex",new ue(S,f)),t.push(D),s>Di&&s--}return{lodPlanes:t,sizeLods:e,sigmas:n}}function al(i,t,e){const n=new cn(i,t,e);return n.texture.mapping=vr,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function $s(i,t,e,n,s){i.viewport.set(t,e,n,s),i.scissor.set(t,e,n,s)}function op(i,t,e){const n=new Float32Array(Jn),s=new C(0,1,0);return new ze({name:"SphericalGaussianBlur",defines:{n:Jn,CUBEUV_TEXEL_WIDTH:1/t,CUBEUV_TEXEL_HEIGHT:1/e,CUBEUV_MAX_MIP:`${i}.0`},uniforms:{envMap:{value:null},samples:{value:1},weights:{value:n},latitudinal:{value:!1},dTheta:{value:0},mipInt:{value:0},poleAxis:{value:s}},vertexShader:co(),fragmentShader:` - - precision mediump float; - precision mediump int; - - varying vec3 vOutputDirection; - - uniform sampler2D envMap; - uniform int samples; - uniform float weights[ n ]; - uniform bool latitudinal; - uniform float dTheta; - uniform float mipInt; - uniform vec3 poleAxis; - - #define ENVMAP_TYPE_CUBE_UV - #include - - vec3 getSample( float theta, vec3 axis ) { - - float cosTheta = cos( theta ); - // Rodrigues' axis-angle rotation - vec3 sampleDirection = vOutputDirection * cosTheta - + cross( axis, vOutputDirection ) * sin( theta ) - + axis * dot( axis, vOutputDirection ) * ( 1.0 - cosTheta ); - - return bilinearCubeUV( envMap, sampleDirection, mipInt ); - - } - - void main() { - - vec3 axis = latitudinal ? poleAxis : cross( poleAxis, vOutputDirection ); - - if ( all( equal( axis, vec3( 0.0 ) ) ) ) { - - axis = vec3( vOutputDirection.z, 0.0, - vOutputDirection.x ); - - } - - axis = normalize( axis ); - - gl_FragColor = vec4( 0.0, 0.0, 0.0, 1.0 ); - gl_FragColor.rgb += weights[ 0 ] * getSample( 0.0, axis ); - - for ( int i = 1; i < n; i++ ) { - - if ( i >= samples ) { - - break; - - } - - float theta = dTheta * float( i ); - gl_FragColor.rgb += weights[ i ] * getSample( -1.0 * theta, axis ); - gl_FragColor.rgb += weights[ i ] * getSample( theta, axis ); - - } - - } - `,blending:Tn,depthTest:!1,depthWrite:!1})}function ol(){return new ze({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:co(),fragmentShader:` - - precision mediump float; - precision mediump int; - - varying vec3 vOutputDirection; - - uniform sampler2D envMap; - - #include - - void main() { - - vec3 outputDirection = normalize( vOutputDirection ); - vec2 uv = equirectUv( outputDirection ); - - gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); - - } - `,blending:Tn,depthTest:!1,depthWrite:!1})}function ll(){return new ze({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:co(),fragmentShader:` - - precision mediump float; - precision mediump int; - - uniform float flipEnvMap; - - varying vec3 vOutputDirection; - - uniform samplerCube envMap; - - void main() { - - gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); - - } - `,blending:Tn,depthTest:!1,depthWrite:!1})}function co(){return` - - precision mediump float; - precision mediump int; - - attribute float faceIndex; - - varying vec3 vOutputDirection; - - // RH coordinate system; PMREM face-indexing convention - vec3 getDirection( vec2 uv, float face ) { - - uv = 2.0 * uv - 1.0; - - vec3 direction = vec3( uv, 1.0 ); - - if ( face == 0.0 ) { - - direction = direction.zyx; // ( 1, v, u ) pos x - - } else if ( face == 1.0 ) { - - direction = direction.xzy; - direction.xz *= -1.0; // ( -u, 1, -v ) pos y - - } else if ( face == 2.0 ) { - - direction.x *= -1.0; // ( -u, v, 1 ) pos z - - } else if ( face == 3.0 ) { - - direction = direction.zyx; - direction.xz *= -1.0; // ( -1, v, -u ) neg x - - } else if ( face == 4.0 ) { - - direction = direction.xzy; - direction.xy *= -1.0; // ( -u, -1, v ) neg y - - } else if ( face == 5.0 ) { - - direction.z *= -1.0; // ( u, v, -1 ) neg z - - } - - return direction; - - } - - void main() { - - vOutputDirection = getDirection( uv, faceIndex ); - gl_Position = vec4( position, 1.0 ); - - } - `}function lp(i){let t=new WeakMap,e=null;function n(o){if(o&&o.isTexture){const l=o.mapping,c=l===pa||l===ma,h=l===Bi||l===zi;if(c||h){let d=t.get(o);const p=d!==void 0?d.texture.pmremVersion:0;if(o.isRenderTargetTexture&&o.pmremVersion!==p)return e===null&&(e=new rl(i)),d=c?e.fromEquirectangular(o,d):e.fromCubemap(o,d),d.texture.pmremVersion=o.pmremVersion,t.set(o,d),d.texture;if(d!==void 0)return d.texture;{const u=o.image;return c&&u&&u.height>0||h&&u&&s(u)?(e===null&&(e=new rl(i)),d=c?e.fromEquirectangular(o):e.fromCubemap(o),d.texture.pmremVersion=o.pmremVersion,t.set(o,d),o.addEventListener("dispose",r),d.texture):null}}}return o}function s(o){let l=0;const c=6;for(let h=0;ht.maxTextureSize&&(D=Math.ceil(S/t.maxTextureSize),S=t.maxTextureSize);const w=new Float32Array(S*D*4*d),R=new ic(w,S,D,d);R.type=mn,R.needsUpdate=!0;const U=b*4;for(let M=0;M0)return i;const s=t*e;let r=hl[s];if(r===void 0&&(r=new Float32Array(s),hl[s]=r),t!==0){n.toArray(r,0);for(let a=1,o=0;a!==t;++a)o+=e,i[a].toArray(r,o)}return r}function Se(i,t){if(i.length!==t.length)return!1;for(let e=0,n=i.length;e":" "} ${o}: ${e[a]}`)}return n.join(` -`)}const _l=new Bt;function lm(i){Qt._getMatrix(_l,Qt.workingColorSpace,i);const t=`mat3( ${_l.elements.map(e=>e.toFixed(4))} )`;switch(Qt.getTransfer(i)){case hr:return[t,"LinearTransferOETF"];case re:return[t,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",i),[t,"LinearTransferOETF"]}}function vl(i,t,e){const n=i.getShaderParameter(t,i.COMPILE_STATUS),s=i.getShaderInfoLog(t).trim();if(n&&s==="")return"";const r=/ERROR: 0:(\d+)/.exec(s);if(r){const a=parseInt(r[1]);return e.toUpperCase()+` - -`+s+` - -`+om(i.getShaderSource(t),a)}else return s}function cm(i,t){const e=lm(t);return[`vec4 ${i}( vec4 value ) {`,` return ${e[1]}( vec4( value.rgb * ${e[0]}, value.a ) );`,"}"].join(` -`)}function hm(i,t){let e;switch(t){case hh:e="Linear";break;case uh:e="Reinhard";break;case dh:e="Cineon";break;case Vl:e="ACESFilmic";break;case ph:e="AgX";break;case mh:e="Neutral";break;case fh:e="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",t),e="Linear"}return"vec3 "+i+"( vec3 color ) { return "+e+"ToneMapping( color ); }"}const Js=new C;function um(){Qt.getLuminanceCoefficients(Js);const i=Js.x.toFixed(4),t=Js.y.toFixed(4),e=Js.z.toFixed(4);return["float luminance( const in vec3 rgb ) {",` const vec3 weights = vec3( ${i}, ${t}, ${e} );`," return dot( weights, rgb );","}"].join(` -`)}function dm(i){return[i.extensionClipCullDistance?"#extension GL_ANGLE_clip_cull_distance : require":"",i.extensionMultiDraw?"#extension GL_ANGLE_multi_draw : require":""].filter(ls).join(` -`)}function fm(i){const t=[];for(const e in i){const n=i[e];n!==!1&&t.push("#define "+e+" "+n)}return t.join(` -`)}function pm(i,t){const e={},n=i.getProgramParameter(t,i.ACTIVE_ATTRIBUTES);for(let s=0;s/gm;function Za(i){return i.replace(mm,_m)}const gm=new Map;function _m(i,t){let e=kt[t];if(e===void 0){const n=gm.get(t);if(n!==void 0)e=kt[n],console.warn('THREE.WebGLRenderer: Shader chunk "%s" has been deprecated. Use "%s" instead.',t,n);else throw new Error("Can not resolve #include <"+t+">")}return Za(e)}const vm=/#pragma unroll_loop_start\s+for\s*\(\s*int\s+i\s*=\s*(\d+)\s*;\s*i\s*<\s*(\d+)\s*;\s*i\s*\+\+\s*\)\s*{([\s\S]+?)}\s+#pragma unroll_loop_end/g;function Sl(i){return i.replace(vm,xm)}function xm(i,t,e,n){let s="";for(let r=parseInt(t);r0&&(m+=` -`),f=["#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g].filter(ls).join(` -`),f.length>0&&(f+=` -`)):(m=[yl(e),"#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g,e.extensionClipCullDistance?"#define USE_CLIP_DISTANCE":"",e.batching?"#define USE_BATCHING":"",e.batchingColor?"#define USE_BATCHING_COLOR":"",e.instancing?"#define USE_INSTANCING":"",e.instancingColor?"#define USE_INSTANCING_COLOR":"",e.instancingMorph?"#define USE_INSTANCING_MORPH":"",e.useFog&&e.fog?"#define USE_FOG":"",e.useFog&&e.fogExp2?"#define FOG_EXP2":"",e.map?"#define USE_MAP":"",e.envMap?"#define USE_ENVMAP":"",e.envMap?"#define "+h:"",e.lightMap?"#define USE_LIGHTMAP":"",e.aoMap?"#define USE_AOMAP":"",e.bumpMap?"#define USE_BUMPMAP":"",e.normalMap?"#define USE_NORMALMAP":"",e.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",e.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",e.displacementMap?"#define USE_DISPLACEMENTMAP":"",e.emissiveMap?"#define USE_EMISSIVEMAP":"",e.anisotropy?"#define USE_ANISOTROPY":"",e.anisotropyMap?"#define USE_ANISOTROPYMAP":"",e.clearcoatMap?"#define USE_CLEARCOATMAP":"",e.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",e.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",e.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",e.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",e.specularMap?"#define USE_SPECULARMAP":"",e.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",e.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",e.roughnessMap?"#define USE_ROUGHNESSMAP":"",e.metalnessMap?"#define USE_METALNESSMAP":"",e.alphaMap?"#define USE_ALPHAMAP":"",e.alphaHash?"#define USE_ALPHAHASH":"",e.transmission?"#define USE_TRANSMISSION":"",e.transmissionMap?"#define USE_TRANSMISSIONMAP":"",e.thicknessMap?"#define USE_THICKNESSMAP":"",e.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",e.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",e.mapUv?"#define MAP_UV "+e.mapUv:"",e.alphaMapUv?"#define ALPHAMAP_UV "+e.alphaMapUv:"",e.lightMapUv?"#define LIGHTMAP_UV "+e.lightMapUv:"",e.aoMapUv?"#define AOMAP_UV "+e.aoMapUv:"",e.emissiveMapUv?"#define EMISSIVEMAP_UV "+e.emissiveMapUv:"",e.bumpMapUv?"#define BUMPMAP_UV "+e.bumpMapUv:"",e.normalMapUv?"#define NORMALMAP_UV "+e.normalMapUv:"",e.displacementMapUv?"#define DISPLACEMENTMAP_UV "+e.displacementMapUv:"",e.metalnessMapUv?"#define METALNESSMAP_UV "+e.metalnessMapUv:"",e.roughnessMapUv?"#define ROUGHNESSMAP_UV "+e.roughnessMapUv:"",e.anisotropyMapUv?"#define ANISOTROPYMAP_UV "+e.anisotropyMapUv:"",e.clearcoatMapUv?"#define CLEARCOATMAP_UV "+e.clearcoatMapUv:"",e.clearcoatNormalMapUv?"#define CLEARCOAT_NORMALMAP_UV "+e.clearcoatNormalMapUv:"",e.clearcoatRoughnessMapUv?"#define CLEARCOAT_ROUGHNESSMAP_UV "+e.clearcoatRoughnessMapUv:"",e.iridescenceMapUv?"#define IRIDESCENCEMAP_UV "+e.iridescenceMapUv:"",e.iridescenceThicknessMapUv?"#define IRIDESCENCE_THICKNESSMAP_UV "+e.iridescenceThicknessMapUv:"",e.sheenColorMapUv?"#define SHEEN_COLORMAP_UV "+e.sheenColorMapUv:"",e.sheenRoughnessMapUv?"#define SHEEN_ROUGHNESSMAP_UV "+e.sheenRoughnessMapUv:"",e.specularMapUv?"#define SPECULARMAP_UV "+e.specularMapUv:"",e.specularColorMapUv?"#define SPECULAR_COLORMAP_UV "+e.specularColorMapUv:"",e.specularIntensityMapUv?"#define SPECULAR_INTENSITYMAP_UV "+e.specularIntensityMapUv:"",e.transmissionMapUv?"#define TRANSMISSIONMAP_UV "+e.transmissionMapUv:"",e.thicknessMapUv?"#define THICKNESSMAP_UV "+e.thicknessMapUv:"",e.vertexTangents&&e.flatShading===!1?"#define USE_TANGENT":"",e.vertexColors?"#define USE_COLOR":"",e.vertexAlphas?"#define USE_COLOR_ALPHA":"",e.vertexUv1s?"#define USE_UV1":"",e.vertexUv2s?"#define USE_UV2":"",e.vertexUv3s?"#define USE_UV3":"",e.pointsUvs?"#define USE_POINTS_UV":"",e.flatShading?"#define FLAT_SHADED":"",e.skinning?"#define USE_SKINNING":"",e.morphTargets?"#define USE_MORPHTARGETS":"",e.morphNormals&&e.flatShading===!1?"#define USE_MORPHNORMALS":"",e.morphColors?"#define USE_MORPHCOLORS":"",e.morphTargetsCount>0?"#define MORPHTARGETS_TEXTURE_STRIDE "+e.morphTextureStride:"",e.morphTargetsCount>0?"#define MORPHTARGETS_COUNT "+e.morphTargetsCount:"",e.doubleSided?"#define DOUBLE_SIDED":"",e.flipSided?"#define FLIP_SIDED":"",e.shadowMapEnabled?"#define USE_SHADOWMAP":"",e.shadowMapEnabled?"#define "+l:"",e.sizeAttenuation?"#define USE_SIZEATTENUATION":"",e.numLightProbes>0?"#define USE_LIGHT_PROBES":"",e.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",e.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 modelMatrix;","uniform mat4 modelViewMatrix;","uniform mat4 projectionMatrix;","uniform mat4 viewMatrix;","uniform mat3 normalMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;","#ifdef USE_INSTANCING"," attribute mat4 instanceMatrix;","#endif","#ifdef USE_INSTANCING_COLOR"," attribute vec3 instanceColor;","#endif","#ifdef USE_INSTANCING_MORPH"," uniform sampler2D morphTexture;","#endif","attribute vec3 position;","attribute vec3 normal;","attribute vec2 uv;","#ifdef USE_UV1"," attribute vec2 uv1;","#endif","#ifdef USE_UV2"," attribute vec2 uv2;","#endif","#ifdef USE_UV3"," attribute vec2 uv3;","#endif","#ifdef USE_TANGENT"," attribute vec4 tangent;","#endif","#if defined( USE_COLOR_ALPHA )"," attribute vec4 color;","#elif defined( USE_COLOR )"," attribute vec3 color;","#endif","#ifdef USE_SKINNING"," attribute vec4 skinIndex;"," attribute vec4 skinWeight;","#endif",` -`].filter(ls).join(` -`),f=[yl(e),"#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g,e.useFog&&e.fog?"#define USE_FOG":"",e.useFog&&e.fogExp2?"#define FOG_EXP2":"",e.alphaToCoverage?"#define ALPHA_TO_COVERAGE":"",e.map?"#define USE_MAP":"",e.matcap?"#define USE_MATCAP":"",e.envMap?"#define USE_ENVMAP":"",e.envMap?"#define "+c:"",e.envMap?"#define "+h:"",e.envMap?"#define "+d:"",p?"#define CUBEUV_TEXEL_WIDTH "+p.texelWidth:"",p?"#define CUBEUV_TEXEL_HEIGHT "+p.texelHeight:"",p?"#define CUBEUV_MAX_MIP "+p.maxMip+".0":"",e.lightMap?"#define USE_LIGHTMAP":"",e.aoMap?"#define USE_AOMAP":"",e.bumpMap?"#define USE_BUMPMAP":"",e.normalMap?"#define USE_NORMALMAP":"",e.normalMapObjectSpace?"#define USE_NORMALMAP_OBJECTSPACE":"",e.normalMapTangentSpace?"#define USE_NORMALMAP_TANGENTSPACE":"",e.emissiveMap?"#define USE_EMISSIVEMAP":"",e.anisotropy?"#define USE_ANISOTROPY":"",e.anisotropyMap?"#define USE_ANISOTROPYMAP":"",e.clearcoat?"#define USE_CLEARCOAT":"",e.clearcoatMap?"#define USE_CLEARCOATMAP":"",e.clearcoatRoughnessMap?"#define USE_CLEARCOAT_ROUGHNESSMAP":"",e.clearcoatNormalMap?"#define USE_CLEARCOAT_NORMALMAP":"",e.dispersion?"#define USE_DISPERSION":"",e.iridescence?"#define USE_IRIDESCENCE":"",e.iridescenceMap?"#define USE_IRIDESCENCEMAP":"",e.iridescenceThicknessMap?"#define USE_IRIDESCENCE_THICKNESSMAP":"",e.specularMap?"#define USE_SPECULARMAP":"",e.specularColorMap?"#define USE_SPECULAR_COLORMAP":"",e.specularIntensityMap?"#define USE_SPECULAR_INTENSITYMAP":"",e.roughnessMap?"#define USE_ROUGHNESSMAP":"",e.metalnessMap?"#define USE_METALNESSMAP":"",e.alphaMap?"#define USE_ALPHAMAP":"",e.alphaTest?"#define USE_ALPHATEST":"",e.alphaHash?"#define USE_ALPHAHASH":"",e.sheen?"#define USE_SHEEN":"",e.sheenColorMap?"#define USE_SHEEN_COLORMAP":"",e.sheenRoughnessMap?"#define USE_SHEEN_ROUGHNESSMAP":"",e.transmission?"#define USE_TRANSMISSION":"",e.transmissionMap?"#define USE_TRANSMISSIONMAP":"",e.thicknessMap?"#define USE_THICKNESSMAP":"",e.vertexTangents&&e.flatShading===!1?"#define USE_TANGENT":"",e.vertexColors||e.instancingColor||e.batchingColor?"#define USE_COLOR":"",e.vertexAlphas?"#define USE_COLOR_ALPHA":"",e.vertexUv1s?"#define USE_UV1":"",e.vertexUv2s?"#define USE_UV2":"",e.vertexUv3s?"#define USE_UV3":"",e.pointsUvs?"#define USE_POINTS_UV":"",e.gradientMap?"#define USE_GRADIENTMAP":"",e.flatShading?"#define FLAT_SHADED":"",e.doubleSided?"#define DOUBLE_SIDED":"",e.flipSided?"#define FLIP_SIDED":"",e.shadowMapEnabled?"#define USE_SHADOWMAP":"",e.shadowMapEnabled?"#define "+l:"",e.premultipliedAlpha?"#define PREMULTIPLIED_ALPHA":"",e.numLightProbes>0?"#define USE_LIGHT_PROBES":"",e.decodeVideoTexture?"#define DECODE_VIDEO_TEXTURE":"",e.decodeVideoTextureEmissive?"#define DECODE_VIDEO_TEXTURE_EMISSIVE":"",e.logarithmicDepthBuffer?"#define USE_LOGDEPTHBUF":"",e.reverseDepthBuffer?"#define USE_REVERSEDEPTHBUF":"","uniform mat4 viewMatrix;","uniform vec3 cameraPosition;","uniform bool isOrthographic;",e.toneMapping!==Bn?"#define TONE_MAPPING":"",e.toneMapping!==Bn?kt.tonemapping_pars_fragment:"",e.toneMapping!==Bn?hm("toneMapping",e.toneMapping):"",e.dithering?"#define DITHERING":"",e.opaque?"#define OPAQUE":"",kt.colorspace_pars_fragment,cm("linearToOutputTexel",e.outputColorSpace),um(),e.useDepthPacking?"#define DEPTH_PACKING "+e.depthPacking:"",` -`].filter(ls).join(` -`)),a=Za(a),a=xl(a,e),a=Ml(a,e),o=Za(o),o=xl(o,e),o=Ml(o,e),a=Sl(a),o=Sl(o),e.isRawShaderMaterial!==!0&&(T=`#version 300 es -`,m=[u,"#define attribute in","#define varying out","#define texture2D texture"].join(` -`)+` -`+m,f=["#define varying in",e.glslVersion===Mo?"":"layout(location = 0) out highp vec4 pc_fragColor;",e.glslVersion===Mo?"":"#define gl_FragColor pc_fragColor","#define gl_FragDepthEXT gl_FragDepth","#define texture2D texture","#define textureCube texture","#define texture2DProj textureProj","#define texture2DLodEXT textureLod","#define texture2DProjLodEXT textureProjLod","#define textureCubeLodEXT textureLod","#define texture2DGradEXT textureGrad","#define texture2DProjGradEXT textureProjGrad","#define textureCubeGradEXT textureGrad"].join(` -`)+` -`+f);const b=T+m+a,S=T+f+o,D=gl(s,s.VERTEX_SHADER,b),w=gl(s,s.FRAGMENT_SHADER,S);s.attachShader(v,D),s.attachShader(v,w),e.index0AttributeName!==void 0?s.bindAttribLocation(v,0,e.index0AttributeName):e.morphTargets===!0&&s.bindAttribLocation(v,0,"position"),s.linkProgram(v);function R(P){if(i.debug.checkShaderErrors){const W=s.getProgramInfoLog(v).trim(),z=s.getShaderInfoLog(D).trim(),G=s.getShaderInfoLog(w).trim();let $=!0,X=!0;if(s.getProgramParameter(v,s.LINK_STATUS)===!1)if($=!1,typeof i.debug.onShaderError=="function")i.debug.onShaderError(s,v,D,w);else{const Q=vl(s,D,"vertex"),k=vl(s,w,"fragment");console.error("THREE.WebGLProgram: Shader Error "+s.getError()+" - VALIDATE_STATUS "+s.getProgramParameter(v,s.VALIDATE_STATUS)+` - -Material Name: `+P.name+` -Material Type: `+P.type+` - -Program Info Log: `+W+` -`+Q+` -`+k)}else W!==""?console.warn("THREE.WebGLProgram: Program Info Log:",W):(z===""||G==="")&&(X=!1);X&&(P.diagnostics={runnable:$,programLog:W,vertexShader:{log:z,prefix:m},fragmentShader:{log:G,prefix:f}})}s.deleteShader(D),s.deleteShader(w),U=new or(s,v),y=pm(s,v)}let U;this.getUniforms=function(){return U===void 0&&R(this),U};let y;this.getAttributes=function(){return y===void 0&&R(this),y};let M=e.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return M===!1&&(M=s.getProgramParameter(v,rm)),M},this.destroy=function(){n.releaseStatesOfProgram(this),s.deleteProgram(v),this.program=void 0},this.type=e.shaderType,this.name=e.shaderName,this.id=am++,this.cacheKey=t,this.usedTimes=1,this.program=v,this.vertexShader=D,this.fragmentShader=w,this}let wm=0;class Am{constructor(){this.shaderCache=new Map,this.materialCache=new Map}update(t){const e=t.vertexShader,n=t.fragmentShader,s=this._getShaderStage(e),r=this._getShaderStage(n),a=this._getShaderCacheForMaterial(t);return a.has(s)===!1&&(a.add(s),s.usedTimes++),a.has(r)===!1&&(a.add(r),r.usedTimes++),this}remove(t){const e=this.materialCache.get(t);for(const n of e)n.usedTimes--,n.usedTimes===0&&this.shaderCache.delete(n.code);return this.materialCache.delete(t),this}getVertexShaderID(t){return this._getShaderStage(t.vertexShader).id}getFragmentShaderID(t){return this._getShaderStage(t.fragmentShader).id}dispose(){this.shaderCache.clear(),this.materialCache.clear()}_getShaderCacheForMaterial(t){const e=this.materialCache;let n=e.get(t);return n===void 0&&(n=new Set,e.set(t,n)),n}_getShaderStage(t){const e=this.shaderCache;let n=e.get(t);return n===void 0&&(n=new Rm(t),e.set(t,n)),n}}class Rm{constructor(t){this.id=wm++,this.code=t,this.usedTimes=0}}function Cm(i,t,e,n,s,r,a){const o=new ao,l=new Am,c=new Set,h=[],d=s.logarithmicDepthBuffer,p=s.vertexTextures;let u=s.precision;const g={MeshDepthMaterial:"depth",MeshDistanceMaterial:"distanceRGBA",MeshNormalMaterial:"normal",MeshBasicMaterial:"basic",MeshLambertMaterial:"lambert",MeshPhongMaterial:"phong",MeshToonMaterial:"toon",MeshStandardMaterial:"physical",MeshPhysicalMaterial:"physical",MeshMatcapMaterial:"matcap",LineBasicMaterial:"basic",LineDashedMaterial:"dashed",PointsMaterial:"points",ShadowMaterial:"shadow",SpriteMaterial:"sprite"};function v(y){return c.add(y),y===0?"uv":`uv${y}`}function m(y,M,P,W,z){const G=W.fog,$=z.geometry,X=y.isMeshStandardMaterial?W.environment:null,Q=(y.isMeshStandardMaterial?e:t).get(y.envMap||X),k=Q&&Q.mapping===vr?Q.image.height:null,it=g[y.type];y.precision!==null&&(u=s.getMaxPrecision(y.precision),u!==y.precision&&console.warn("THREE.WebGLProgram.getParameters:",y.precision,"not supported, using",u,"instead."));const ft=$.morphAttributes.position||$.morphAttributes.normal||$.morphAttributes.color,St=ft!==void 0?ft.length:0;let Ft=0;$.morphAttributes.position!==void 0&&(Ft=1),$.morphAttributes.normal!==void 0&&(Ft=2),$.morphAttributes.color!==void 0&&(Ft=3);let Vt,Y,nt,_t;if(it){const Wt=un[it];Vt=Wt.vertexShader,Y=Wt.fragmentShader}else Vt=y.vertexShader,Y=y.fragmentShader,l.update(y),nt=l.getVertexShaderID(y),_t=l.getFragmentShaderID(y);const ot=i.getRenderTarget(),Pt=i.state.buffers.depth.getReversed(),Lt=z.isInstancedMesh===!0,Ht=z.isBatchedMesh===!0,he=!!y.map,Yt=!!y.matcap,fe=!!Q,A=!!y.aoMap,ke=!!y.lightMap,qt=!!y.bumpMap,jt=!!y.normalMap,J=!!y.displacementMap,Et=!!y.emissiveMap,ct=!!y.metalnessMap,E=!!y.roughnessMap,_=y.anisotropy>0,F=y.clearcoat>0,q=y.dispersion>0,K=y.iridescence>0,j=y.sheen>0,yt=y.transmission>0,lt=_&&!!y.anisotropyMap,pt=F&&!!y.clearcoatMap,Gt=F&&!!y.clearcoatNormalMap,et=F&&!!y.clearcoatRoughnessMap,mt=K&&!!y.iridescenceMap,Tt=K&&!!y.iridescenceThicknessMap,Ut=j&&!!y.sheenColorMap,gt=j&&!!y.sheenRoughnessMap,Zt=!!y.specularMap,Ot=!!y.specularColorMap,se=!!y.specularIntensityMap,L=yt&&!!y.transmissionMap,at=yt&&!!y.thicknessMap,H=!!y.gradientMap,Z=!!y.alphaMap,dt=y.alphaTest>0,ht=!!y.alphaHash,wt=!!y.extensions;let Kt=Bn;y.toneMapped&&(ot===null||ot.isXRRenderTarget===!0)&&(Kt=i.toneMapping);const ce={shaderID:it,shaderType:y.type,shaderName:y.name,vertexShader:Vt,fragmentShader:Y,defines:y.defines,customVertexShaderID:nt,customFragmentShaderID:_t,isRawShaderMaterial:y.isRawShaderMaterial===!0,glslVersion:y.glslVersion,precision:u,batching:Ht,batchingColor:Ht&&z._colorsTexture!==null,instancing:Lt,instancingColor:Lt&&z.instanceColor!==null,instancingMorph:Lt&&z.morphTexture!==null,supportsVertexTextures:p,outputColorSpace:ot===null?i.outputColorSpace:ot.isXRRenderTarget===!0?ot.texture.colorSpace:Vi,alphaToCoverage:!!y.alphaToCoverage,map:he,matcap:Yt,envMap:fe,envMapMode:fe&&Q.mapping,envMapCubeUVHeight:k,aoMap:A,lightMap:ke,bumpMap:qt,normalMap:jt,displacementMap:p&&J,emissiveMap:Et,normalMapObjectSpace:jt&&y.normalMapType===xh,normalMapTangentSpace:jt&&y.normalMapType===Ql,metalnessMap:ct,roughnessMap:E,anisotropy:_,anisotropyMap:lt,clearcoat:F,clearcoatMap:pt,clearcoatNormalMap:Gt,clearcoatRoughnessMap:et,dispersion:q,iridescence:K,iridescenceMap:mt,iridescenceThicknessMap:Tt,sheen:j,sheenColorMap:Ut,sheenRoughnessMap:gt,specularMap:Zt,specularColorMap:Ot,specularIntensityMap:se,transmission:yt,transmissionMap:L,thicknessMap:at,gradientMap:H,opaque:y.transparent===!1&&y.blending===Ui&&y.alphaToCoverage===!1,alphaMap:Z,alphaTest:dt,alphaHash:ht,combine:y.combine,mapUv:he&&v(y.map.channel),aoMapUv:A&&v(y.aoMap.channel),lightMapUv:ke&&v(y.lightMap.channel),bumpMapUv:qt&&v(y.bumpMap.channel),normalMapUv:jt&&v(y.normalMap.channel),displacementMapUv:J&&v(y.displacementMap.channel),emissiveMapUv:Et&&v(y.emissiveMap.channel),metalnessMapUv:ct&&v(y.metalnessMap.channel),roughnessMapUv:E&&v(y.roughnessMap.channel),anisotropyMapUv:lt&&v(y.anisotropyMap.channel),clearcoatMapUv:pt&&v(y.clearcoatMap.channel),clearcoatNormalMapUv:Gt&&v(y.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:et&&v(y.clearcoatRoughnessMap.channel),iridescenceMapUv:mt&&v(y.iridescenceMap.channel),iridescenceThicknessMapUv:Tt&&v(y.iridescenceThicknessMap.channel),sheenColorMapUv:Ut&&v(y.sheenColorMap.channel),sheenRoughnessMapUv:gt&&v(y.sheenRoughnessMap.channel),specularMapUv:Zt&&v(y.specularMap.channel),specularColorMapUv:Ot&&v(y.specularColorMap.channel),specularIntensityMapUv:se&&v(y.specularIntensityMap.channel),transmissionMapUv:L&&v(y.transmissionMap.channel),thicknessMapUv:at&&v(y.thicknessMap.channel),alphaMapUv:Z&&v(y.alphaMap.channel),vertexTangents:!!$.attributes.tangent&&(jt||_),vertexColors:y.vertexColors,vertexAlphas:y.vertexColors===!0&&!!$.attributes.color&&$.attributes.color.itemSize===4,pointsUvs:z.isPoints===!0&&!!$.attributes.uv&&(he||Z),fog:!!G,useFog:y.fog===!0,fogExp2:!!G&&G.isFogExp2,flatShading:y.flatShading===!0,sizeAttenuation:y.sizeAttenuation===!0,logarithmicDepthBuffer:d,reverseDepthBuffer:Pt,skinning:z.isSkinnedMesh===!0,morphTargets:$.morphAttributes.position!==void 0,morphNormals:$.morphAttributes.normal!==void 0,morphColors:$.morphAttributes.color!==void 0,morphTargetsCount:St,morphTextureStride:Ft,numDirLights:M.directional.length,numPointLights:M.point.length,numSpotLights:M.spot.length,numSpotLightMaps:M.spotLightMap.length,numRectAreaLights:M.rectArea.length,numHemiLights:M.hemi.length,numDirLightShadows:M.directionalShadowMap.length,numPointLightShadows:M.pointShadowMap.length,numSpotLightShadows:M.spotShadowMap.length,numSpotLightShadowsWithMaps:M.numSpotLightShadowsWithMaps,numLightProbes:M.numLightProbes,numClippingPlanes:a.numPlanes,numClipIntersection:a.numIntersection,dithering:y.dithering,shadowMapEnabled:i.shadowMap.enabled&&P.length>0,shadowMapType:i.shadowMap.type,toneMapping:Kt,decodeVideoTexture:he&&y.map.isVideoTexture===!0&&Qt.getTransfer(y.map.colorSpace)===re,decodeVideoTextureEmissive:Et&&y.emissiveMap.isVideoTexture===!0&&Qt.getTransfer(y.emissiveMap.colorSpace)===re,premultipliedAlpha:y.premultipliedAlpha,doubleSided:y.side===dn,flipSided:y.side===We,useDepthPacking:y.depthPacking>=0,depthPacking:y.depthPacking||0,index0AttributeName:y.index0AttributeName,extensionClipCullDistance:wt&&y.extensions.clipCullDistance===!0&&n.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(wt&&y.extensions.multiDraw===!0||Ht)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:y.customProgramCacheKey()};return ce.vertexUv1s=c.has(1),ce.vertexUv2s=c.has(2),ce.vertexUv3s=c.has(3),c.clear(),ce}function f(y){const M=[];if(y.shaderID?M.push(y.shaderID):(M.push(y.customVertexShaderID),M.push(y.customFragmentShaderID)),y.defines!==void 0)for(const P in y.defines)M.push(P),M.push(y.defines[P]);return y.isRawShaderMaterial===!1&&(T(M,y),b(M,y),M.push(i.outputColorSpace)),M.push(y.customProgramCacheKey),M.join()}function T(y,M){y.push(M.precision),y.push(M.outputColorSpace),y.push(M.envMapMode),y.push(M.envMapCubeUVHeight),y.push(M.mapUv),y.push(M.alphaMapUv),y.push(M.lightMapUv),y.push(M.aoMapUv),y.push(M.bumpMapUv),y.push(M.normalMapUv),y.push(M.displacementMapUv),y.push(M.emissiveMapUv),y.push(M.metalnessMapUv),y.push(M.roughnessMapUv),y.push(M.anisotropyMapUv),y.push(M.clearcoatMapUv),y.push(M.clearcoatNormalMapUv),y.push(M.clearcoatRoughnessMapUv),y.push(M.iridescenceMapUv),y.push(M.iridescenceThicknessMapUv),y.push(M.sheenColorMapUv),y.push(M.sheenRoughnessMapUv),y.push(M.specularMapUv),y.push(M.specularColorMapUv),y.push(M.specularIntensityMapUv),y.push(M.transmissionMapUv),y.push(M.thicknessMapUv),y.push(M.combine),y.push(M.fogExp2),y.push(M.sizeAttenuation),y.push(M.morphTargetsCount),y.push(M.morphAttributeCount),y.push(M.numDirLights),y.push(M.numPointLights),y.push(M.numSpotLights),y.push(M.numSpotLightMaps),y.push(M.numHemiLights),y.push(M.numRectAreaLights),y.push(M.numDirLightShadows),y.push(M.numPointLightShadows),y.push(M.numSpotLightShadows),y.push(M.numSpotLightShadowsWithMaps),y.push(M.numLightProbes),y.push(M.shadowMapType),y.push(M.toneMapping),y.push(M.numClippingPlanes),y.push(M.numClipIntersection),y.push(M.depthPacking)}function b(y,M){o.disableAll(),M.supportsVertexTextures&&o.enable(0),M.instancing&&o.enable(1),M.instancingColor&&o.enable(2),M.instancingMorph&&o.enable(3),M.matcap&&o.enable(4),M.envMap&&o.enable(5),M.normalMapObjectSpace&&o.enable(6),M.normalMapTangentSpace&&o.enable(7),M.clearcoat&&o.enable(8),M.iridescence&&o.enable(9),M.alphaTest&&o.enable(10),M.vertexColors&&o.enable(11),M.vertexAlphas&&o.enable(12),M.vertexUv1s&&o.enable(13),M.vertexUv2s&&o.enable(14),M.vertexUv3s&&o.enable(15),M.vertexTangents&&o.enable(16),M.anisotropy&&o.enable(17),M.alphaHash&&o.enable(18),M.batching&&o.enable(19),M.dispersion&&o.enable(20),M.batchingColor&&o.enable(21),y.push(o.mask),o.disableAll(),M.fog&&o.enable(0),M.useFog&&o.enable(1),M.flatShading&&o.enable(2),M.logarithmicDepthBuffer&&o.enable(3),M.reverseDepthBuffer&&o.enable(4),M.skinning&&o.enable(5),M.morphTargets&&o.enable(6),M.morphNormals&&o.enable(7),M.morphColors&&o.enable(8),M.premultipliedAlpha&&o.enable(9),M.shadowMapEnabled&&o.enable(10),M.doubleSided&&o.enable(11),M.flipSided&&o.enable(12),M.useDepthPacking&&o.enable(13),M.dithering&&o.enable(14),M.transmission&&o.enable(15),M.sheen&&o.enable(16),M.opaque&&o.enable(17),M.pointsUvs&&o.enable(18),M.decodeVideoTexture&&o.enable(19),M.decodeVideoTextureEmissive&&o.enable(20),M.alphaToCoverage&&o.enable(21),y.push(o.mask)}function S(y){const M=g[y.type];let P;if(M){const W=un[M];P=fr.clone(W.uniforms)}else P=y.uniforms;return P}function D(y,M){let P;for(let W=0,z=h.length;W0?n.push(f):u.transparent===!0?s.push(f):e.push(f)}function l(d,p,u,g,v,m){const f=a(d,p,u,g,v,m);u.transmission>0?n.unshift(f):u.transparent===!0?s.unshift(f):e.unshift(f)}function c(d,p){e.length>1&&e.sort(d||Dm),n.length>1&&n.sort(p||El),s.length>1&&s.sort(p||El)}function h(){for(let d=t,p=i.length;d=r.length?(a=new bl,r.push(a)):a=r[s],a}function e(){i=new WeakMap}return{get:t,dispose:e}}function Um(){const i={};return{get:function(t){if(i[t.id]!==void 0)return i[t.id];let e;switch(t.type){case"DirectionalLight":e={direction:new C,color:new rt};break;case"SpotLight":e={position:new C,direction:new C,color:new rt,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":e={position:new C,color:new rt,distance:0,decay:0};break;case"HemisphereLight":e={direction:new C,skyColor:new rt,groundColor:new rt};break;case"RectAreaLight":e={color:new rt,position:new C,halfWidth:new C,halfHeight:new C};break}return i[t.id]=e,e}}}function Im(){const i={};return{get:function(t){if(i[t.id]!==void 0)return i[t.id];let e;switch(t.type){case"DirectionalLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Mt};break;case"SpotLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Mt};break;case"PointLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new Mt,shadowCameraNear:1,shadowCameraFar:1e3};break}return i[t.id]=e,e}}}let Nm=0;function Fm(i,t){return(t.castShadow?2:0)-(i.castShadow?2:0)+(t.map?1:0)-(i.map?1:0)}function Om(i){const t=new Um,e=Im(),n={version:0,hash:{directionalLength:-1,pointLength:-1,spotLength:-1,rectAreaLength:-1,hemiLength:-1,numDirectionalShadows:-1,numPointShadows:-1,numSpotShadows:-1,numSpotMaps:-1,numLightProbes:-1},ambient:[0,0,0],probe:[],directional:[],directionalShadow:[],directionalShadowMap:[],directionalShadowMatrix:[],spot:[],spotLightMap:[],spotShadow:[],spotShadowMap:[],spotLightMatrix:[],rectArea:[],rectAreaLTC1:null,rectAreaLTC2:null,point:[],pointShadow:[],pointShadowMap:[],pointShadowMatrix:[],hemi:[],numSpotLightShadowsWithMaps:0,numLightProbes:0};for(let c=0;c<9;c++)n.probe.push(new C);const s=new C,r=new ie,a=new ie;function o(c){let h=0,d=0,p=0;for(let y=0;y<9;y++)n.probe[y].set(0,0,0);let u=0,g=0,v=0,m=0,f=0,T=0,b=0,S=0,D=0,w=0,R=0;c.sort(Fm);for(let y=0,M=c.length;y0&&(i.has("OES_texture_float_linear")===!0?(n.rectAreaLTC1=st.LTC_FLOAT_1,n.rectAreaLTC2=st.LTC_FLOAT_2):(n.rectAreaLTC1=st.LTC_HALF_1,n.rectAreaLTC2=st.LTC_HALF_2)),n.ambient[0]=h,n.ambient[1]=d,n.ambient[2]=p;const U=n.hash;(U.directionalLength!==u||U.pointLength!==g||U.spotLength!==v||U.rectAreaLength!==m||U.hemiLength!==f||U.numDirectionalShadows!==T||U.numPointShadows!==b||U.numSpotShadows!==S||U.numSpotMaps!==D||U.numLightProbes!==R)&&(n.directional.length=u,n.spot.length=v,n.rectArea.length=m,n.point.length=g,n.hemi.length=f,n.directionalShadow.length=T,n.directionalShadowMap.length=T,n.pointShadow.length=b,n.pointShadowMap.length=b,n.spotShadow.length=S,n.spotShadowMap.length=S,n.directionalShadowMatrix.length=T,n.pointShadowMatrix.length=b,n.spotLightMatrix.length=S+D-w,n.spotLightMap.length=D,n.numSpotLightShadowsWithMaps=w,n.numLightProbes=R,U.directionalLength=u,U.pointLength=g,U.spotLength=v,U.rectAreaLength=m,U.hemiLength=f,U.numDirectionalShadows=T,U.numPointShadows=b,U.numSpotShadows=S,U.numSpotMaps=D,U.numLightProbes=R,n.version=Nm++)}function l(c,h){let d=0,p=0,u=0,g=0,v=0;const m=h.matrixWorldInverse;for(let f=0,T=c.length;f=a.length?(o=new Tl(i),a.push(o)):o=a[r],o}function n(){t=new WeakMap}return{get:e,dispose:n}}const zm=`void main() { - gl_Position = vec4( position, 1.0 ); -}`,km=`uniform sampler2D shadow_pass; -uniform vec2 resolution; -uniform float radius; -#include -void main() { - const float samples = float( VSM_SAMPLES ); - float mean = 0.0; - float squared_mean = 0.0; - float uvStride = samples <= 1.0 ? 0.0 : 2.0 / ( samples - 1.0 ); - float uvStart = samples <= 1.0 ? 0.0 : - 1.0; - for ( float i = 0.0; i < samples; i ++ ) { - float uvOffset = uvStart + i * uvStride; - #ifdef HORIZONTAL_PASS - vec2 distribution = unpackRGBATo2Half( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( uvOffset, 0.0 ) * radius ) / resolution ) ); - mean += distribution.x; - squared_mean += distribution.y * distribution.y + distribution.x * distribution.x; - #else - float depth = unpackRGBAToDepth( texture2D( shadow_pass, ( gl_FragCoord.xy + vec2( 0.0, uvOffset ) * radius ) / resolution ) ); - mean += depth; - squared_mean += depth * depth; - #endif - } - mean = mean / samples; - squared_mean = squared_mean / samples; - float std_dev = sqrt( squared_mean - mean * mean ); - gl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) ); -}`;function Hm(i,t,e){let n=new oo;const s=new Mt,r=new Mt,a=new le,o=new lu({depthPacking:vh}),l=new cu,c={},h=e.maxTextureSize,d={[kn]:We,[We]:kn,[dn]:dn},p=new ze({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new Mt},radius:{value:4}},vertexShader:zm,fragmentShader:km}),u=p.clone();u.defines.HORIZONTAL_PASS=1;const g=new ge;g.setAttribute("position",new ue(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const v=new Me(g,p),m=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=kl;let f=this.type;this.render=function(w,R,U){if(m.enabled===!1||m.autoUpdate===!1&&m.needsUpdate===!1||w.length===0)return;const y=i.getRenderTarget(),M=i.getActiveCubeFace(),P=i.getActiveMipmapLevel(),W=i.state;W.setBlending(Tn),W.buffers.color.setClear(1,1,1,1),W.buffers.depth.setTest(!0),W.setScissorTest(!1);const z=f!==En&&this.type===En,G=f===En&&this.type!==En;for(let $=0,X=w.length;$h||s.y>h)&&(s.x>h&&(r.x=Math.floor(h/it.x),s.x=r.x*it.x,k.mapSize.x=r.x),s.y>h&&(r.y=Math.floor(h/it.y),s.y=r.y*it.y,k.mapSize.y=r.y)),k.map===null||z===!0||G===!0){const St=this.type!==En?{minFilter:Ze,magFilter:Ze}:{};k.map!==null&&k.map.dispose(),k.map=new cn(s.x,s.y,St),k.map.texture.name=Q.name+".shadowMap",k.camera.updateProjectionMatrix()}i.setRenderTarget(k.map),i.clear();const ft=k.getViewportCount();for(let St=0;St0||R.map&&R.alphaTest>0){const W=M.uuid,z=R.uuid;let G=c[W];G===void 0&&(G={},c[W]=G);let $=G[z];$===void 0&&($=M.clone(),G[z]=$,R.addEventListener("dispose",D)),M=$}if(M.visible=R.visible,M.wireframe=R.wireframe,y===En?M.side=R.shadowSide!==null?R.shadowSide:R.side:M.side=R.shadowSide!==null?R.shadowSide:d[R.side],M.alphaMap=R.alphaMap,M.alphaTest=R.alphaTest,M.map=R.map,M.clipShadows=R.clipShadows,M.clippingPlanes=R.clippingPlanes,M.clipIntersection=R.clipIntersection,M.displacementMap=R.displacementMap,M.displacementScale=R.displacementScale,M.displacementBias=R.displacementBias,M.wireframeLinewidth=R.wireframeLinewidth,M.linewidth=R.linewidth,U.isPointLight===!0&&M.isMeshDistanceMaterial===!0){const W=i.properties.get(M);W.light=U}return M}function S(w,R,U,y,M){if(w.visible===!1)return;if(w.layers.test(R.layers)&&(w.isMesh||w.isLine||w.isPoints)&&(w.castShadow||w.receiveShadow&&M===En)&&(!w.frustumCulled||n.intersectsObject(w))){w.modelViewMatrix.multiplyMatrices(U.matrixWorldInverse,w.matrixWorld);const z=t.update(w),G=w.material;if(Array.isArray(G)){const $=z.groups;for(let X=0,Q=$.length;X=1):k.indexOf("OpenGL ES")!==-1&&(Q=parseFloat(/^OpenGL ES (\d)/.exec(k)[1]),X=Q>=2);let it=null,ft={};const St=i.getParameter(i.SCISSOR_BOX),Ft=i.getParameter(i.VIEWPORT),Vt=new le().fromArray(St),Y=new le().fromArray(Ft);function nt(L,at,H,Z){const dt=new Uint8Array(4),ht=i.createTexture();i.bindTexture(L,ht),i.texParameteri(L,i.TEXTURE_MIN_FILTER,i.NEAREST),i.texParameteri(L,i.TEXTURE_MAG_FILTER,i.NEAREST);for(let wt=0;wt"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new Mt,h=new WeakMap;let d;const p=new WeakMap;let u=!1;try{u=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function g(E,_){return u?new OffscreenCanvas(E,_):dr("canvas")}function v(E,_,F){let q=1;const K=ct(E);if((K.width>F||K.height>F)&&(q=F/Math.max(K.width,K.height)),q<1)if(typeof HTMLImageElement<"u"&&E instanceof HTMLImageElement||typeof HTMLCanvasElement<"u"&&E instanceof HTMLCanvasElement||typeof ImageBitmap<"u"&&E instanceof ImageBitmap||typeof VideoFrame<"u"&&E instanceof VideoFrame){const j=Math.floor(q*K.width),yt=Math.floor(q*K.height);d===void 0&&(d=g(j,yt));const lt=_?g(j,yt):d;return lt.width=j,lt.height=yt,lt.getContext("2d").drawImage(E,0,0,j,yt),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+K.width+"x"+K.height+") to ("+j+"x"+yt+")."),lt}else return"data"in E&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+K.width+"x"+K.height+")."),E;return E}function m(E){return E.generateMipmaps}function f(E){i.generateMipmap(E)}function T(E){return E.isWebGLCubeRenderTarget?i.TEXTURE_CUBE_MAP:E.isWebGL3DRenderTarget?i.TEXTURE_3D:E.isWebGLArrayRenderTarget||E.isCompressedArrayTexture?i.TEXTURE_2D_ARRAY:i.TEXTURE_2D}function b(E,_,F,q,K=!1){if(E!==null){if(i[E]!==void 0)return i[E];console.warn("THREE.WebGLRenderer: Attempt to use non-existing WebGL internal format '"+E+"'")}let j=_;if(_===i.RED&&(F===i.FLOAT&&(j=i.R32F),F===i.HALF_FLOAT&&(j=i.R16F),F===i.UNSIGNED_BYTE&&(j=i.R8)),_===i.RED_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.R8UI),F===i.UNSIGNED_SHORT&&(j=i.R16UI),F===i.UNSIGNED_INT&&(j=i.R32UI),F===i.BYTE&&(j=i.R8I),F===i.SHORT&&(j=i.R16I),F===i.INT&&(j=i.R32I)),_===i.RG&&(F===i.FLOAT&&(j=i.RG32F),F===i.HALF_FLOAT&&(j=i.RG16F),F===i.UNSIGNED_BYTE&&(j=i.RG8)),_===i.RG_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RG8UI),F===i.UNSIGNED_SHORT&&(j=i.RG16UI),F===i.UNSIGNED_INT&&(j=i.RG32UI),F===i.BYTE&&(j=i.RG8I),F===i.SHORT&&(j=i.RG16I),F===i.INT&&(j=i.RG32I)),_===i.RGB_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RGB8UI),F===i.UNSIGNED_SHORT&&(j=i.RGB16UI),F===i.UNSIGNED_INT&&(j=i.RGB32UI),F===i.BYTE&&(j=i.RGB8I),F===i.SHORT&&(j=i.RGB16I),F===i.INT&&(j=i.RGB32I)),_===i.RGBA_INTEGER&&(F===i.UNSIGNED_BYTE&&(j=i.RGBA8UI),F===i.UNSIGNED_SHORT&&(j=i.RGBA16UI),F===i.UNSIGNED_INT&&(j=i.RGBA32UI),F===i.BYTE&&(j=i.RGBA8I),F===i.SHORT&&(j=i.RGBA16I),F===i.INT&&(j=i.RGBA32I)),_===i.RGB&&F===i.UNSIGNED_INT_5_9_9_9_REV&&(j=i.RGB9_E5),_===i.RGBA){const yt=K?hr:Qt.getTransfer(q);F===i.FLOAT&&(j=i.RGBA32F),F===i.HALF_FLOAT&&(j=i.RGBA16F),F===i.UNSIGNED_BYTE&&(j=yt===re?i.SRGB8_ALPHA8:i.RGBA8),F===i.UNSIGNED_SHORT_4_4_4_4&&(j=i.RGBA4),F===i.UNSIGNED_SHORT_5_5_5_1&&(j=i.RGB5_A1)}return(j===i.R16F||j===i.R32F||j===i.RG16F||j===i.RG32F||j===i.RGBA16F||j===i.RGBA32F)&&t.get("EXT_color_buffer_float"),j}function S(E,_){let F;return E?_===null||_===ii||_===ki?F=i.DEPTH24_STENCIL8:_===mn?F=i.DEPTH32F_STENCIL8:_===us&&(F=i.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):_===null||_===ii||_===ki?F=i.DEPTH_COMPONENT24:_===mn?F=i.DEPTH_COMPONENT32F:_===us&&(F=i.DEPTH_COMPONENT16),F}function D(E,_){return m(E)===!0||E.isFramebufferTexture&&E.minFilter!==Ze&&E.minFilter!==pn?Math.log2(Math.max(_.width,_.height))+1:E.mipmaps!==void 0&&E.mipmaps.length>0?E.mipmaps.length:E.isCompressedTexture&&Array.isArray(E.image)?_.mipmaps.length:1}function w(E){const _=E.target;_.removeEventListener("dispose",w),U(_),_.isVideoTexture&&h.delete(_)}function R(E){const _=E.target;_.removeEventListener("dispose",R),M(_)}function U(E){const _=n.get(E);if(_.__webglInit===void 0)return;const F=E.source,q=p.get(F);if(q){const K=q[_.__cacheKey];K.usedTimes--,K.usedTimes===0&&y(E),Object.keys(q).length===0&&p.delete(F)}n.remove(E)}function y(E){const _=n.get(E);i.deleteTexture(_.__webglTexture);const F=E.source,q=p.get(F);delete q[_.__cacheKey],a.memory.textures--}function M(E){const _=n.get(E);if(E.depthTexture&&(E.depthTexture.dispose(),n.remove(E.depthTexture)),E.isWebGLCubeRenderTarget)for(let q=0;q<6;q++){if(Array.isArray(_.__webglFramebuffer[q]))for(let K=0;K<_.__webglFramebuffer[q].length;K++)i.deleteFramebuffer(_.__webglFramebuffer[q][K]);else i.deleteFramebuffer(_.__webglFramebuffer[q]);_.__webglDepthbuffer&&i.deleteRenderbuffer(_.__webglDepthbuffer[q])}else{if(Array.isArray(_.__webglFramebuffer))for(let q=0;q<_.__webglFramebuffer.length;q++)i.deleteFramebuffer(_.__webglFramebuffer[q]);else i.deleteFramebuffer(_.__webglFramebuffer);if(_.__webglDepthbuffer&&i.deleteRenderbuffer(_.__webglDepthbuffer),_.__webglMultisampledFramebuffer&&i.deleteFramebuffer(_.__webglMultisampledFramebuffer),_.__webglColorRenderbuffer)for(let q=0;q<_.__webglColorRenderbuffer.length;q++)_.__webglColorRenderbuffer[q]&&i.deleteRenderbuffer(_.__webglColorRenderbuffer[q]);_.__webglDepthRenderbuffer&&i.deleteRenderbuffer(_.__webglDepthRenderbuffer)}const F=E.textures;for(let q=0,K=F.length;q=s.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+E+" texture units while this GPU supports only "+s.maxTextures),P+=1,E}function G(E){const _=[];return _.push(E.wrapS),_.push(E.wrapT),_.push(E.wrapR||0),_.push(E.magFilter),_.push(E.minFilter),_.push(E.anisotropy),_.push(E.internalFormat),_.push(E.format),_.push(E.type),_.push(E.generateMipmaps),_.push(E.premultiplyAlpha),_.push(E.flipY),_.push(E.unpackAlignment),_.push(E.colorSpace),_.join()}function $(E,_){const F=n.get(E);if(E.isVideoTexture&&J(E),E.isRenderTargetTexture===!1&&E.version>0&&F.__version!==E.version){const q=E.image;if(q===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(q.complete===!1)console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete");else{Y(F,E,_);return}}e.bindTexture(i.TEXTURE_2D,F.__webglTexture,i.TEXTURE0+_)}function X(E,_){const F=n.get(E);if(E.version>0&&F.__version!==E.version){Y(F,E,_);return}e.bindTexture(i.TEXTURE_2D_ARRAY,F.__webglTexture,i.TEXTURE0+_)}function Q(E,_){const F=n.get(E);if(E.version>0&&F.__version!==E.version){Y(F,E,_);return}e.bindTexture(i.TEXTURE_3D,F.__webglTexture,i.TEXTURE0+_)}function k(E,_){const F=n.get(E);if(E.version>0&&F.__version!==E.version){nt(F,E,_);return}e.bindTexture(i.TEXTURE_CUBE_MAP,F.__webglTexture,i.TEXTURE0+_)}const it={[ga]:i.REPEAT,[Qn]:i.CLAMP_TO_EDGE,[_a]:i.MIRRORED_REPEAT},ft={[Ze]:i.NEAREST,[gh]:i.NEAREST_MIPMAP_NEAREST,[Es]:i.NEAREST_MIPMAP_LINEAR,[pn]:i.LINEAR,[br]:i.LINEAR_MIPMAP_NEAREST,[ti]:i.LINEAR_MIPMAP_LINEAR},St={[Mh]:i.NEVER,[wh]:i.ALWAYS,[Sh]:i.LESS,[tc]:i.LEQUAL,[yh]:i.EQUAL,[Th]:i.GEQUAL,[Eh]:i.GREATER,[bh]:i.NOTEQUAL};function Ft(E,_){if(_.type===mn&&t.has("OES_texture_float_linear")===!1&&(_.magFilter===pn||_.magFilter===br||_.magFilter===Es||_.magFilter===ti||_.minFilter===pn||_.minFilter===br||_.minFilter===Es||_.minFilter===ti)&&console.warn("THREE.WebGLRenderer: Unable to use linear filtering with floating point textures. OES_texture_float_linear not supported on this device."),i.texParameteri(E,i.TEXTURE_WRAP_S,it[_.wrapS]),i.texParameteri(E,i.TEXTURE_WRAP_T,it[_.wrapT]),(E===i.TEXTURE_3D||E===i.TEXTURE_2D_ARRAY)&&i.texParameteri(E,i.TEXTURE_WRAP_R,it[_.wrapR]),i.texParameteri(E,i.TEXTURE_MAG_FILTER,ft[_.magFilter]),i.texParameteri(E,i.TEXTURE_MIN_FILTER,ft[_.minFilter]),_.compareFunction&&(i.texParameteri(E,i.TEXTURE_COMPARE_MODE,i.COMPARE_REF_TO_TEXTURE),i.texParameteri(E,i.TEXTURE_COMPARE_FUNC,St[_.compareFunction])),t.has("EXT_texture_filter_anisotropic")===!0){if(_.magFilter===Ze||_.minFilter!==Es&&_.minFilter!==ti||_.type===mn&&t.has("OES_texture_float_linear")===!1)return;if(_.anisotropy>1||n.get(_).__currentAnisotropy){const F=t.get("EXT_texture_filter_anisotropic");i.texParameterf(E,F.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(_.anisotropy,s.getMaxAnisotropy())),n.get(_).__currentAnisotropy=_.anisotropy}}}function Vt(E,_){let F=!1;E.__webglInit===void 0&&(E.__webglInit=!0,_.addEventListener("dispose",w));const q=_.source;let K=p.get(q);K===void 0&&(K={},p.set(q,K));const j=G(_);if(j!==E.__cacheKey){K[j]===void 0&&(K[j]={texture:i.createTexture(),usedTimes:0},a.memory.textures++,F=!0),K[j].usedTimes++;const yt=K[E.__cacheKey];yt!==void 0&&(K[E.__cacheKey].usedTimes--,yt.usedTimes===0&&y(_)),E.__cacheKey=j,E.__webglTexture=K[j].texture}return F}function Y(E,_,F){let q=i.TEXTURE_2D;(_.isDataArrayTexture||_.isCompressedArrayTexture)&&(q=i.TEXTURE_2D_ARRAY),_.isData3DTexture&&(q=i.TEXTURE_3D);const K=Vt(E,_),j=_.source;e.bindTexture(q,E.__webglTexture,i.TEXTURE0+F);const yt=n.get(j);if(j.version!==yt.__version||K===!0){e.activeTexture(i.TEXTURE0+F);const lt=Qt.getPrimaries(Qt.workingColorSpace),pt=_.colorSpace===On?null:Qt.getPrimaries(_.colorSpace),Gt=_.colorSpace===On||lt===pt?i.NONE:i.BROWSER_DEFAULT_WEBGL;i.pixelStorei(i.UNPACK_FLIP_Y_WEBGL,_.flipY),i.pixelStorei(i.UNPACK_PREMULTIPLY_ALPHA_WEBGL,_.premultiplyAlpha),i.pixelStorei(i.UNPACK_ALIGNMENT,_.unpackAlignment),i.pixelStorei(i.UNPACK_COLORSPACE_CONVERSION_WEBGL,Gt);let et=v(_.image,!1,s.maxTextureSize);et=Et(_,et);const mt=r.convert(_.format,_.colorSpace),Tt=r.convert(_.type);let Ut=b(_.internalFormat,mt,Tt,_.colorSpace,_.isVideoTexture);Ft(q,_);let gt;const Zt=_.mipmaps,Ot=_.isVideoTexture!==!0,se=yt.__version===void 0||K===!0,L=j.dataReady,at=D(_,et);if(_.isDepthTexture)Ut=S(_.format===Hi,_.type),se&&(Ot?e.texStorage2D(i.TEXTURE_2D,1,Ut,et.width,et.height):e.texImage2D(i.TEXTURE_2D,0,Ut,et.width,et.height,0,mt,Tt,null));else if(_.isDataTexture)if(Zt.length>0){Ot&&se&&e.texStorage2D(i.TEXTURE_2D,at,Ut,Zt[0].width,Zt[0].height);for(let H=0,Z=Zt.length;H0){const dt=el(gt.width,gt.height,_.format,_.type);for(const ht of _.layerUpdates){const wt=gt.data.subarray(ht*dt/gt.data.BYTES_PER_ELEMENT,(ht+1)*dt/gt.data.BYTES_PER_ELEMENT);e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,H,0,0,ht,gt.width,gt.height,1,mt,wt)}_.clearLayerUpdates()}else e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,H,0,0,0,gt.width,gt.height,et.depth,mt,gt.data)}else e.compressedTexImage3D(i.TEXTURE_2D_ARRAY,H,Ut,gt.width,gt.height,et.depth,0,gt.data,0,0);else console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else Ot?L&&e.texSubImage3D(i.TEXTURE_2D_ARRAY,H,0,0,0,gt.width,gt.height,et.depth,mt,Tt,gt.data):e.texImage3D(i.TEXTURE_2D_ARRAY,H,Ut,gt.width,gt.height,et.depth,0,mt,Tt,gt.data)}else{Ot&&se&&e.texStorage2D(i.TEXTURE_2D,at,Ut,Zt[0].width,Zt[0].height);for(let H=0,Z=Zt.length;H0){const H=el(et.width,et.height,_.format,_.type);for(const Z of _.layerUpdates){const dt=et.data.subarray(Z*H/et.data.BYTES_PER_ELEMENT,(Z+1)*H/et.data.BYTES_PER_ELEMENT);e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,Z,et.width,et.height,1,mt,Tt,dt)}_.clearLayerUpdates()}else e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,0,et.width,et.height,et.depth,mt,Tt,et.data)}else e.texImage3D(i.TEXTURE_2D_ARRAY,0,Ut,et.width,et.height,et.depth,0,mt,Tt,et.data);else if(_.isData3DTexture)Ot?(se&&e.texStorage3D(i.TEXTURE_3D,at,Ut,et.width,et.height,et.depth),L&&e.texSubImage3D(i.TEXTURE_3D,0,0,0,0,et.width,et.height,et.depth,mt,Tt,et.data)):e.texImage3D(i.TEXTURE_3D,0,Ut,et.width,et.height,et.depth,0,mt,Tt,et.data);else if(_.isFramebufferTexture){if(se)if(Ot)e.texStorage2D(i.TEXTURE_2D,at,Ut,et.width,et.height);else{let H=et.width,Z=et.height;for(let dt=0;dt>=1,Z>>=1}}else if(Zt.length>0){if(Ot&&se){const H=ct(Zt[0]);e.texStorage2D(i.TEXTURE_2D,at,Ut,H.width,H.height)}for(let H=0,Z=Zt.length;H0&&at++;const Z=ct(mt[0]);e.texStorage2D(i.TEXTURE_CUBE_MAP,at,Zt,Z.width,Z.height)}for(let Z=0;Z<6;Z++)if(et){Ot?L&&e.texSubImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+Z,0,0,0,mt[Z].width,mt[Z].height,Ut,gt,mt[Z].data):e.texImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+Z,0,Zt,mt[Z].width,mt[Z].height,0,Ut,gt,mt[Z].data);for(let dt=0;dt>j),Tt=Math.max(1,_.height>>j);K===i.TEXTURE_3D||K===i.TEXTURE_2D_ARRAY?e.texImage3D(K,j,pt,mt,Tt,_.depth,0,yt,lt,null):e.texImage2D(K,j,pt,mt,Tt,0,yt,lt,null)}e.bindFramebuffer(i.FRAMEBUFFER,E),jt(_)?o.framebufferTexture2DMultisampleEXT(i.FRAMEBUFFER,q,K,et.__webglTexture,0,qt(_)):(K===i.TEXTURE_2D||K>=i.TEXTURE_CUBE_MAP_POSITIVE_X&&K<=i.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&i.framebufferTexture2D(i.FRAMEBUFFER,q,K,et.__webglTexture,j),e.bindFramebuffer(i.FRAMEBUFFER,null)}function ot(E,_,F){if(i.bindRenderbuffer(i.RENDERBUFFER,E),_.depthBuffer){const q=_.depthTexture,K=q&&q.isDepthTexture?q.type:null,j=S(_.stencilBuffer,K),yt=_.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,lt=qt(_);jt(_)?o.renderbufferStorageMultisampleEXT(i.RENDERBUFFER,lt,j,_.width,_.height):F?i.renderbufferStorageMultisample(i.RENDERBUFFER,lt,j,_.width,_.height):i.renderbufferStorage(i.RENDERBUFFER,j,_.width,_.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,yt,i.RENDERBUFFER,E)}else{const q=_.textures;for(let K=0;K{delete _.__boundDepthTexture,delete _.__depthDisposeCallback,q.removeEventListener("dispose",K)};q.addEventListener("dispose",K),_.__depthDisposeCallback=K}_.__boundDepthTexture=q}if(E.depthTexture&&!_.__autoAllocateDepthBuffer){if(F)throw new Error("target.depthTexture not supported in Cube render targets");Pt(_.__webglFramebuffer,E)}else if(F){_.__webglDepthbuffer=[];for(let q=0;q<6;q++)if(e.bindFramebuffer(i.FRAMEBUFFER,_.__webglFramebuffer[q]),_.__webglDepthbuffer[q]===void 0)_.__webglDepthbuffer[q]=i.createRenderbuffer(),ot(_.__webglDepthbuffer[q],E,!1);else{const K=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,j=_.__webglDepthbuffer[q];i.bindRenderbuffer(i.RENDERBUFFER,j),i.framebufferRenderbuffer(i.FRAMEBUFFER,K,i.RENDERBUFFER,j)}}else if(e.bindFramebuffer(i.FRAMEBUFFER,_.__webglFramebuffer),_.__webglDepthbuffer===void 0)_.__webglDepthbuffer=i.createRenderbuffer(),ot(_.__webglDepthbuffer,E,!1);else{const q=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,K=_.__webglDepthbuffer;i.bindRenderbuffer(i.RENDERBUFFER,K),i.framebufferRenderbuffer(i.FRAMEBUFFER,q,i.RENDERBUFFER,K)}e.bindFramebuffer(i.FRAMEBUFFER,null)}function Ht(E,_,F){const q=n.get(E);_!==void 0&&_t(q.__webglFramebuffer,E,E.texture,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,0),F!==void 0&&Lt(E)}function he(E){const _=E.texture,F=n.get(E),q=n.get(_);E.addEventListener("dispose",R);const K=E.textures,j=E.isWebGLCubeRenderTarget===!0,yt=K.length>1;if(yt||(q.__webglTexture===void 0&&(q.__webglTexture=i.createTexture()),q.__version=_.version,a.memory.textures++),j){F.__webglFramebuffer=[];for(let lt=0;lt<6;lt++)if(_.mipmaps&&_.mipmaps.length>0){F.__webglFramebuffer[lt]=[];for(let pt=0;pt<_.mipmaps.length;pt++)F.__webglFramebuffer[lt][pt]=i.createFramebuffer()}else F.__webglFramebuffer[lt]=i.createFramebuffer()}else{if(_.mipmaps&&_.mipmaps.length>0){F.__webglFramebuffer=[];for(let lt=0;lt<_.mipmaps.length;lt++)F.__webglFramebuffer[lt]=i.createFramebuffer()}else F.__webglFramebuffer=i.createFramebuffer();if(yt)for(let lt=0,pt=K.length;lt0&&jt(E)===!1){F.__webglMultisampledFramebuffer=i.createFramebuffer(),F.__webglColorRenderbuffer=[],e.bindFramebuffer(i.FRAMEBUFFER,F.__webglMultisampledFramebuffer);for(let lt=0;lt0)for(let pt=0;pt<_.mipmaps.length;pt++)_t(F.__webglFramebuffer[lt][pt],E,_,i.COLOR_ATTACHMENT0,i.TEXTURE_CUBE_MAP_POSITIVE_X+lt,pt);else _t(F.__webglFramebuffer[lt],E,_,i.COLOR_ATTACHMENT0,i.TEXTURE_CUBE_MAP_POSITIVE_X+lt,0);m(_)&&f(i.TEXTURE_CUBE_MAP),e.unbindTexture()}else if(yt){for(let lt=0,pt=K.length;lt0)for(let pt=0;pt<_.mipmaps.length;pt++)_t(F.__webglFramebuffer[pt],E,_,i.COLOR_ATTACHMENT0,lt,pt);else _t(F.__webglFramebuffer,E,_,i.COLOR_ATTACHMENT0,lt,0);m(_)&&f(lt),e.unbindTexture()}E.depthBuffer&&Lt(E)}function Yt(E){const _=E.textures;for(let F=0,q=_.length;F0){if(jt(E)===!1){const _=E.textures,F=E.width,q=E.height;let K=i.COLOR_BUFFER_BIT;const j=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,yt=n.get(E),lt=_.length>1;if(lt)for(let pt=0;pt<_.length;pt++)e.bindFramebuffer(i.FRAMEBUFFER,yt.__webglMultisampledFramebuffer),i.framebufferRenderbuffer(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0+pt,i.RENDERBUFFER,null),e.bindFramebuffer(i.FRAMEBUFFER,yt.__webglFramebuffer),i.framebufferTexture2D(i.DRAW_FRAMEBUFFER,i.COLOR_ATTACHMENT0+pt,i.TEXTURE_2D,null,0);e.bindFramebuffer(i.READ_FRAMEBUFFER,yt.__webglMultisampledFramebuffer),e.bindFramebuffer(i.DRAW_FRAMEBUFFER,yt.__webglFramebuffer);for(let pt=0;pt<_.length;pt++){if(E.resolveDepthBuffer&&(E.depthBuffer&&(K|=i.DEPTH_BUFFER_BIT),E.stencilBuffer&&E.resolveStencilBuffer&&(K|=i.STENCIL_BUFFER_BIT)),lt){i.framebufferRenderbuffer(i.READ_FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.RENDERBUFFER,yt.__webglColorRenderbuffer[pt]);const Gt=n.get(_[pt]).__webglTexture;i.framebufferTexture2D(i.DRAW_FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,Gt,0)}i.blitFramebuffer(0,0,F,q,0,0,F,q,K,i.NEAREST),l===!0&&(fe.length=0,A.length=0,fe.push(i.COLOR_ATTACHMENT0+pt),E.depthBuffer&&E.resolveDepthBuffer===!1&&(fe.push(j),A.push(j),i.invalidateFramebuffer(i.DRAW_FRAMEBUFFER,A)),i.invalidateFramebuffer(i.READ_FRAMEBUFFER,fe))}if(e.bindFramebuffer(i.READ_FRAMEBUFFER,null),e.bindFramebuffer(i.DRAW_FRAMEBUFFER,null),lt)for(let pt=0;pt<_.length;pt++){e.bindFramebuffer(i.FRAMEBUFFER,yt.__webglMultisampledFramebuffer),i.framebufferRenderbuffer(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0+pt,i.RENDERBUFFER,yt.__webglColorRenderbuffer[pt]);const Gt=n.get(_[pt]).__webglTexture;e.bindFramebuffer(i.FRAMEBUFFER,yt.__webglFramebuffer),i.framebufferTexture2D(i.DRAW_FRAMEBUFFER,i.COLOR_ATTACHMENT0+pt,i.TEXTURE_2D,Gt,0)}e.bindFramebuffer(i.DRAW_FRAMEBUFFER,yt.__webglMultisampledFramebuffer)}else if(E.depthBuffer&&E.resolveDepthBuffer===!1&&l){const _=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT;i.invalidateFramebuffer(i.DRAW_FRAMEBUFFER,[_])}}}function qt(E){return Math.min(s.maxSamples,E.samples)}function jt(E){const _=n.get(E);return E.samples>0&&t.has("WEBGL_multisampled_render_to_texture")===!0&&_.__useRenderToTexture!==!1}function J(E){const _=a.render.frame;h.get(E)!==_&&(h.set(E,_),E.update())}function Et(E,_){const F=E.colorSpace,q=E.format,K=E.type;return E.isCompressedTexture===!0||E.isVideoTexture===!0||F!==Vi&&F!==On&&(Qt.getTransfer(F)===re?(q!==ln||K!==Rn)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",F)),_}function ct(E){return typeof HTMLImageElement<"u"&&E instanceof HTMLImageElement?(c.width=E.naturalWidth||E.width,c.height=E.naturalHeight||E.height):typeof VideoFrame<"u"&&E instanceof VideoFrame?(c.width=E.displayWidth,c.height=E.displayHeight):(c.width=E.width,c.height=E.height),c}this.allocateTextureUnit=z,this.resetTextureUnits=W,this.setTexture2D=$,this.setTexture2DArray=X,this.setTexture3D=Q,this.setTextureCube=k,this.rebindTextures=Ht,this.setupRenderTarget=he,this.updateRenderTargetMipmap=Yt,this.updateMultisampleRenderTarget=ke,this.setupDepthRenderbuffer=Lt,this.setupFrameBufferTexture=_t,this.useMultisampledRTT=jt}function Xm(i,t){function e(n,s=On){let r;const a=Qt.getTransfer(s);if(n===Rn)return i.UNSIGNED_BYTE;if(n===to)return i.UNSIGNED_SHORT_4_4_4_4;if(n===eo)return i.UNSIGNED_SHORT_5_5_5_1;if(n===Yl)return i.UNSIGNED_INT_5_9_9_9_REV;if(n===Wl)return i.BYTE;if(n===Xl)return i.SHORT;if(n===us)return i.UNSIGNED_SHORT;if(n===Qa)return i.INT;if(n===ii)return i.UNSIGNED_INT;if(n===mn)return i.FLOAT;if(n===wn)return i.HALF_FLOAT;if(n===ql)return i.ALPHA;if(n===jl)return i.RGB;if(n===ln)return i.RGBA;if(n===Zl)return i.LUMINANCE;if(n===Kl)return i.LUMINANCE_ALPHA;if(n===Ii)return i.DEPTH_COMPONENT;if(n===Hi)return i.DEPTH_STENCIL;if(n===no)return i.RED;if(n===io)return i.RED_INTEGER;if(n===$l)return i.RG;if(n===so)return i.RG_INTEGER;if(n===ro)return i.RGBA_INTEGER;if(n===tr||n===er||n===nr||n===ir)if(a===re)if(r=t.get("WEBGL_compressed_texture_s3tc_srgb"),r!==null){if(n===tr)return r.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===er)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===nr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===ir)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(r=t.get("WEBGL_compressed_texture_s3tc"),r!==null){if(n===tr)return r.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===er)return r.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===nr)return r.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===ir)return r.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===va||n===xa||n===Ma||n===Sa)if(r=t.get("WEBGL_compressed_texture_pvrtc"),r!==null){if(n===va)return r.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===xa)return r.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===Ma)return r.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===Sa)return r.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===ya||n===Ea||n===ba)if(r=t.get("WEBGL_compressed_texture_etc"),r!==null){if(n===ya||n===Ea)return a===re?r.COMPRESSED_SRGB8_ETC2:r.COMPRESSED_RGB8_ETC2;if(n===ba)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:r.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(n===Ta||n===wa||n===Aa||n===Ra||n===Ca||n===Pa||n===Da||n===La||n===Ua||n===Ia||n===Na||n===Fa||n===Oa||n===Ba)if(r=t.get("WEBGL_compressed_texture_astc"),r!==null){if(n===Ta)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:r.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===wa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:r.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Aa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:r.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===Ra)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:r.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===Ca)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:r.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===Pa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:r.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===Da)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:r.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===La)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:r.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===Ua)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:r.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===Ia)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:r.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===Na)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:r.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===Fa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:r.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===Oa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:r.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===Ba)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:r.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===sr||n===za||n===ka)if(r=t.get("EXT_texture_compression_bptc"),r!==null){if(n===sr)return a===re?r.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:r.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===za)return r.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===ka)return r.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===Jl||n===Ha||n===Va||n===Ga)if(r=t.get("EXT_texture_compression_rgtc"),r!==null){if(n===sr)return r.COMPRESSED_RED_RGTC1_EXT;if(n===Ha)return r.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===Va)return r.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===Ga)return r.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===ki?i.UNSIGNED_INT_24_8:i[n]!==void 0?i[n]:null}return{convert:e}}const Ym={type:"move"};class na{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return this._hand===null&&(this._hand=new Pi,this._hand.matrixAutoUpdate=!1,this._hand.visible=!1,this._hand.joints={},this._hand.inputState={pinching:!1}),this._hand}getTargetRaySpace(){return this._targetRay===null&&(this._targetRay=new Pi,this._targetRay.matrixAutoUpdate=!1,this._targetRay.visible=!1,this._targetRay.hasLinearVelocity=!1,this._targetRay.linearVelocity=new C,this._targetRay.hasAngularVelocity=!1,this._targetRay.angularVelocity=new C),this._targetRay}getGripSpace(){return this._grip===null&&(this._grip=new Pi,this._grip.matrixAutoUpdate=!1,this._grip.visible=!1,this._grip.hasLinearVelocity=!1,this._grip.linearVelocity=new C,this._grip.hasAngularVelocity=!1,this._grip.angularVelocity=new C),this._grip}dispatchEvent(t){return this._targetRay!==null&&this._targetRay.dispatchEvent(t),this._grip!==null&&this._grip.dispatchEvent(t),this._hand!==null&&this._hand.dispatchEvent(t),this}connect(t){if(t&&t.hand){const e=this._hand;if(e)for(const n of t.hand.values())this._getHandJoint(e,n)}return this.dispatchEvent({type:"connected",data:t}),this}disconnect(t){return this.dispatchEvent({type:"disconnected",data:t}),this._targetRay!==null&&(this._targetRay.visible=!1),this._grip!==null&&(this._grip.visible=!1),this._hand!==null&&(this._hand.visible=!1),this}update(t,e,n){let s=null,r=null,a=null;const o=this._targetRay,l=this._grip,c=this._hand;if(t&&e.session.visibilityState!=="visible-blurred"){if(c&&t.hand){a=!0;for(const v of t.hand.values()){const m=e.getJointPose(v,n),f=this._getHandJoint(c,v);m!==null&&(f.matrix.fromArray(m.transform.matrix),f.matrix.decompose(f.position,f.rotation,f.scale),f.matrixWorldNeedsUpdate=!0,f.jointRadius=m.radius),f.visible=m!==null}const h=c.joints["index-finger-tip"],d=c.joints["thumb-tip"],p=h.position.distanceTo(d.position),u=.02,g=.005;c.inputState.pinching&&p>u+g?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!c.inputState.pinching&&p<=u-g&&(c.inputState.pinching=!0,this.dispatchEvent({type:"pinchstart",handedness:t.handedness,target:this}))}else l!==null&&t.gripSpace&&(r=e.getPose(t.gripSpace,n),r!==null&&(l.matrix.fromArray(r.transform.matrix),l.matrix.decompose(l.position,l.rotation,l.scale),l.matrixWorldNeedsUpdate=!0,r.linearVelocity?(l.hasLinearVelocity=!0,l.linearVelocity.copy(r.linearVelocity)):l.hasLinearVelocity=!1,r.angularVelocity?(l.hasAngularVelocity=!0,l.angularVelocity.copy(r.angularVelocity)):l.hasAngularVelocity=!1));o!==null&&(s=e.getPose(t.targetRaySpace,n),s===null&&r!==null&&(s=r),s!==null&&(o.matrix.fromArray(s.transform.matrix),o.matrix.decompose(o.position,o.rotation,o.scale),o.matrixWorldNeedsUpdate=!0,s.linearVelocity?(o.hasLinearVelocity=!0,o.linearVelocity.copy(s.linearVelocity)):o.hasLinearVelocity=!1,s.angularVelocity?(o.hasAngularVelocity=!0,o.angularVelocity.copy(s.angularVelocity)):o.hasAngularVelocity=!1,this.dispatchEvent(Ym)))}return o!==null&&(o.visible=s!==null),l!==null&&(l.visible=r!==null),c!==null&&(c.visible=a!==null),this}_getHandJoint(t,e){if(t.joints[e.jointName]===void 0){const n=new Pi;n.matrixAutoUpdate=!1,n.visible=!1,t.joints[e.jointName]=n,t.add(n)}return t.joints[e.jointName]}}const qm=` -void main() { - - gl_Position = vec4( position, 1.0 ); - -}`,jm=` -uniform sampler2DArray depthColor; -uniform float depthWidth; -uniform float depthHeight; - -void main() { - - vec2 coord = vec2( gl_FragCoord.x / depthWidth, gl_FragCoord.y / depthHeight ); - - if ( coord.x >= 1.0 ) { - - gl_FragDepth = texture( depthColor, vec3( coord.x - 1.0, coord.y, 1 ) ).r; - - } else { - - gl_FragDepth = texture( depthColor, vec3( coord.x, coord.y, 0 ) ).r; - - } - -}`;class Zm{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(t,e,n){if(this.texture===null){const s=new be,r=t.properties.get(s);r.__webglTexture=e.texture,(e.depthNear!==n.depthNear||e.depthFar!==n.depthFar)&&(this.depthNear=e.depthNear,this.depthFar=e.depthFar),this.texture=s}}getMesh(t){if(this.texture!==null&&this.mesh===null){const e=t.cameras[0].viewport,n=new ze({vertexShader:qm,fragmentShader:jm,uniforms:{depthColor:{value:this.texture},depthWidth:{value:e.z},depthHeight:{value:e.w}}});this.mesh=new Me(new _s(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class Km extends ri{constructor(t,e){super();const n=this;let s=null,r=1,a=null,o="local-floor",l=1,c=null,h=null,d=null,p=null,u=null,g=null;const v=new Zm,m=e.getContextAttributes();let f=null,T=null;const b=[],S=[],D=new Mt;let w=null;const R=new je;R.viewport=new le;const U=new je;U.viewport=new le;const y=[R,U],M=new fu;let P=null,W=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(Y){let nt=b[Y];return nt===void 0&&(nt=new na,b[Y]=nt),nt.getTargetRaySpace()},this.getControllerGrip=function(Y){let nt=b[Y];return nt===void 0&&(nt=new na,b[Y]=nt),nt.getGripSpace()},this.getHand=function(Y){let nt=b[Y];return nt===void 0&&(nt=new na,b[Y]=nt),nt.getHandSpace()};function z(Y){const nt=S.indexOf(Y.inputSource);if(nt===-1)return;const _t=b[nt];_t!==void 0&&(_t.update(Y.inputSource,Y.frame,c||a),_t.dispatchEvent({type:Y.type,data:Y.inputSource}))}function G(){s.removeEventListener("select",z),s.removeEventListener("selectstart",z),s.removeEventListener("selectend",z),s.removeEventListener("squeeze",z),s.removeEventListener("squeezestart",z),s.removeEventListener("squeezeend",z),s.removeEventListener("end",G),s.removeEventListener("inputsourceschange",$);for(let Y=0;Y=0&&(S[ot]=null,b[ot].disconnect(_t))}for(let nt=0;nt=S.length){S.push(_t),ot=Lt;break}else if(S[Lt]===null){S[Lt]=_t,ot=Lt;break}if(ot===-1)break}const Pt=b[ot];Pt&&Pt.connect(_t)}}const X=new C,Q=new C;function k(Y,nt,_t){X.setFromMatrixPosition(nt.matrixWorld),Q.setFromMatrixPosition(_t.matrixWorld);const ot=X.distanceTo(Q),Pt=nt.projectionMatrix.elements,Lt=_t.projectionMatrix.elements,Ht=Pt[14]/(Pt[10]-1),he=Pt[14]/(Pt[10]+1),Yt=(Pt[9]+1)/Pt[5],fe=(Pt[9]-1)/Pt[5],A=(Pt[8]-1)/Pt[0],ke=(Lt[8]+1)/Lt[0],qt=Ht*A,jt=Ht*ke,J=ot/(-A+ke),Et=J*-A;if(nt.matrixWorld.decompose(Y.position,Y.quaternion,Y.scale),Y.translateX(Et),Y.translateZ(J),Y.matrixWorld.compose(Y.position,Y.quaternion,Y.scale),Y.matrixWorldInverse.copy(Y.matrixWorld).invert(),Pt[10]===-1)Y.projectionMatrix.copy(nt.projectionMatrix),Y.projectionMatrixInverse.copy(nt.projectionMatrixInverse);else{const ct=Ht+J,E=he+J,_=qt-Et,F=jt+(ot-Et),q=Yt*he/E*ct,K=fe*he/E*ct;Y.projectionMatrix.makePerspective(_,F,q,K,ct,E),Y.projectionMatrixInverse.copy(Y.projectionMatrix).invert()}}function it(Y,nt){nt===null?Y.matrixWorld.copy(Y.matrix):Y.matrixWorld.multiplyMatrices(nt.matrixWorld,Y.matrix),Y.matrixWorldInverse.copy(Y.matrixWorld).invert()}this.updateCamera=function(Y){if(s===null)return;let nt=Y.near,_t=Y.far;v.texture!==null&&(v.depthNear>0&&(nt=v.depthNear),v.depthFar>0&&(_t=v.depthFar)),M.near=U.near=R.near=nt,M.far=U.far=R.far=_t,(P!==M.near||W!==M.far)&&(s.updateRenderState({depthNear:M.near,depthFar:M.far}),P=M.near,W=M.far),R.layers.mask=Y.layers.mask|2,U.layers.mask=Y.layers.mask|4,M.layers.mask=R.layers.mask|U.layers.mask;const ot=Y.parent,Pt=M.cameras;it(M,ot);for(let Lt=0;Lt0&&(m.alphaTest.value=f.alphaTest);const T=t.get(f),b=T.envMap,S=T.envMapRotation;b&&(m.envMap.value=b,jn.copy(S),jn.x*=-1,jn.y*=-1,jn.z*=-1,b.isCubeTexture&&b.isRenderTargetTexture===!1&&(jn.y*=-1,jn.z*=-1),m.envMapRotation.value.setFromMatrix4($m.makeRotationFromEuler(jn)),m.flipEnvMap.value=b.isCubeTexture&&b.isRenderTargetTexture===!1?-1:1,m.reflectivity.value=f.reflectivity,m.ior.value=f.ior,m.refractionRatio.value=f.refractionRatio),f.lightMap&&(m.lightMap.value=f.lightMap,m.lightMapIntensity.value=f.lightMapIntensity,e(f.lightMap,m.lightMapTransform)),f.aoMap&&(m.aoMap.value=f.aoMap,m.aoMapIntensity.value=f.aoMapIntensity,e(f.aoMap,m.aoMapTransform))}function a(m,f){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,f.map&&(m.map.value=f.map,e(f.map,m.mapTransform))}function o(m,f){m.dashSize.value=f.dashSize,m.totalSize.value=f.dashSize+f.gapSize,m.scale.value=f.scale}function l(m,f,T,b){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,m.size.value=f.size*T,m.scale.value=b*.5,f.map&&(m.map.value=f.map,e(f.map,m.uvTransform)),f.alphaMap&&(m.alphaMap.value=f.alphaMap,e(f.alphaMap,m.alphaMapTransform)),f.alphaTest>0&&(m.alphaTest.value=f.alphaTest)}function c(m,f){m.diffuse.value.copy(f.color),m.opacity.value=f.opacity,m.rotation.value=f.rotation,f.map&&(m.map.value=f.map,e(f.map,m.mapTransform)),f.alphaMap&&(m.alphaMap.value=f.alphaMap,e(f.alphaMap,m.alphaMapTransform)),f.alphaTest>0&&(m.alphaTest.value=f.alphaTest)}function h(m,f){m.specular.value.copy(f.specular),m.shininess.value=Math.max(f.shininess,1e-4)}function d(m,f){f.gradientMap&&(m.gradientMap.value=f.gradientMap)}function p(m,f){m.metalness.value=f.metalness,f.metalnessMap&&(m.metalnessMap.value=f.metalnessMap,e(f.metalnessMap,m.metalnessMapTransform)),m.roughness.value=f.roughness,f.roughnessMap&&(m.roughnessMap.value=f.roughnessMap,e(f.roughnessMap,m.roughnessMapTransform)),f.envMap&&(m.envMapIntensity.value=f.envMapIntensity)}function u(m,f,T){m.ior.value=f.ior,f.sheen>0&&(m.sheenColor.value.copy(f.sheenColor).multiplyScalar(f.sheen),m.sheenRoughness.value=f.sheenRoughness,f.sheenColorMap&&(m.sheenColorMap.value=f.sheenColorMap,e(f.sheenColorMap,m.sheenColorMapTransform)),f.sheenRoughnessMap&&(m.sheenRoughnessMap.value=f.sheenRoughnessMap,e(f.sheenRoughnessMap,m.sheenRoughnessMapTransform))),f.clearcoat>0&&(m.clearcoat.value=f.clearcoat,m.clearcoatRoughness.value=f.clearcoatRoughness,f.clearcoatMap&&(m.clearcoatMap.value=f.clearcoatMap,e(f.clearcoatMap,m.clearcoatMapTransform)),f.clearcoatRoughnessMap&&(m.clearcoatRoughnessMap.value=f.clearcoatRoughnessMap,e(f.clearcoatRoughnessMap,m.clearcoatRoughnessMapTransform)),f.clearcoatNormalMap&&(m.clearcoatNormalMap.value=f.clearcoatNormalMap,e(f.clearcoatNormalMap,m.clearcoatNormalMapTransform),m.clearcoatNormalScale.value.copy(f.clearcoatNormalScale),f.side===We&&m.clearcoatNormalScale.value.negate())),f.dispersion>0&&(m.dispersion.value=f.dispersion),f.iridescence>0&&(m.iridescence.value=f.iridescence,m.iridescenceIOR.value=f.iridescenceIOR,m.iridescenceThicknessMinimum.value=f.iridescenceThicknessRange[0],m.iridescenceThicknessMaximum.value=f.iridescenceThicknessRange[1],f.iridescenceMap&&(m.iridescenceMap.value=f.iridescenceMap,e(f.iridescenceMap,m.iridescenceMapTransform)),f.iridescenceThicknessMap&&(m.iridescenceThicknessMap.value=f.iridescenceThicknessMap,e(f.iridescenceThicknessMap,m.iridescenceThicknessMapTransform))),f.transmission>0&&(m.transmission.value=f.transmission,m.transmissionSamplerMap.value=T.texture,m.transmissionSamplerSize.value.set(T.width,T.height),f.transmissionMap&&(m.transmissionMap.value=f.transmissionMap,e(f.transmissionMap,m.transmissionMapTransform)),m.thickness.value=f.thickness,f.thicknessMap&&(m.thicknessMap.value=f.thicknessMap,e(f.thicknessMap,m.thicknessMapTransform)),m.attenuationDistance.value=f.attenuationDistance,m.attenuationColor.value.copy(f.attenuationColor)),f.anisotropy>0&&(m.anisotropyVector.value.set(f.anisotropy*Math.cos(f.anisotropyRotation),f.anisotropy*Math.sin(f.anisotropyRotation)),f.anisotropyMap&&(m.anisotropyMap.value=f.anisotropyMap,e(f.anisotropyMap,m.anisotropyMapTransform))),m.specularIntensity.value=f.specularIntensity,m.specularColor.value.copy(f.specularColor),f.specularColorMap&&(m.specularColorMap.value=f.specularColorMap,e(f.specularColorMap,m.specularColorMapTransform)),f.specularIntensityMap&&(m.specularIntensityMap.value=f.specularIntensityMap,e(f.specularIntensityMap,m.specularIntensityMapTransform))}function g(m,f){f.matcap&&(m.matcap.value=f.matcap)}function v(m,f){const T=t.get(f).light;m.referencePosition.value.setFromMatrixPosition(T.matrixWorld),m.nearDistance.value=T.shadow.camera.near,m.farDistance.value=T.shadow.camera.far}return{refreshFogUniforms:n,refreshMaterialUniforms:s}}function Qm(i,t,e,n){let s={},r={},a=[];const o=i.getParameter(i.MAX_UNIFORM_BUFFER_BINDINGS);function l(T,b){const S=b.program;n.uniformBlockBinding(T,S)}function c(T,b){let S=s[T.id];S===void 0&&(g(T),S=h(T),s[T.id]=S,T.addEventListener("dispose",m));const D=b.program;n.updateUBOMapping(T,D);const w=t.render.frame;r[T.id]!==w&&(p(T),r[T.id]=w)}function h(T){const b=d();T.__bindingPointIndex=b;const S=i.createBuffer(),D=T.__size,w=T.usage;return i.bindBuffer(i.UNIFORM_BUFFER,S),i.bufferData(i.UNIFORM_BUFFER,D,w),i.bindBuffer(i.UNIFORM_BUFFER,null),i.bindBufferBase(i.UNIFORM_BUFFER,b,S),S}function d(){for(let T=0;T0&&(S+=D-w),T.__size=S,T.__cache={},this}function v(T){const b={boundary:0,storage:0};return typeof T=="number"||typeof T=="boolean"?(b.boundary=4,b.storage=4):T.isVector2?(b.boundary=8,b.storage=8):T.isVector3||T.isColor?(b.boundary=16,b.storage=12):T.isVector4?(b.boundary=16,b.storage=16):T.isMatrix3?(b.boundary=48,b.storage=48):T.isMatrix4?(b.boundary=64,b.storage=64):T.isTexture?console.warn("THREE.WebGLRenderer: Texture samplers can not be part of an uniforms group."):console.warn("THREE.WebGLRenderer: Unsupported uniform value type.",T),b}function m(T){const b=T.target;b.removeEventListener("dispose",m);const S=a.indexOf(b.__bindingPointIndex);a.splice(S,1),i.deleteBuffer(s[b.id]),delete s[b.id],delete r[b.id]}function f(){for(const T in s)i.deleteBuffer(s[T]);a=[],s={},r={}}return{bind:l,update:c,dispose:f}}class tg{constructor(t={}){const{canvas:e=Ch(),context:n=null,depth:s=!0,stencil:r=!1,alpha:a=!1,antialias:o=!1,premultipliedAlpha:l=!0,preserveDrawingBuffer:c=!1,powerPreference:h="default",failIfMajorPerformanceCaveat:d=!1,reverseDepthBuffer:p=!1}=t;this.isWebGLRenderer=!0;let u;if(n!==null){if(typeof WebGLRenderingContext<"u"&&n instanceof WebGLRenderingContext)throw new Error("THREE.WebGLRenderer: WebGL 1 is not supported since r163.");u=n.getContextAttributes().alpha}else u=a;const g=new Uint32Array(4),v=new Int32Array(4);let m=null,f=null;const T=[],b=[];this.domElement=e,this.debug={checkShaderErrors:!0,onShaderError:null},this.autoClear=!0,this.autoClearColor=!0,this.autoClearDepth=!0,this.autoClearStencil=!0,this.sortObjects=!0,this.clippingPlanes=[],this.localClippingEnabled=!1,this._outputColorSpace=Qe,this.toneMapping=Bn,this.toneMappingExposure=1;const S=this;let D=!1,w=0,R=0,U=null,y=-1,M=null;const P=new le,W=new le;let z=null;const G=new rt(0);let $=0,X=e.width,Q=e.height,k=1,it=null,ft=null;const St=new le(0,0,X,Q),Ft=new le(0,0,X,Q);let Vt=!1;const Y=new oo;let nt=!1,_t=!1;this.transmissionResolutionScale=1;const ot=new ie,Pt=new ie,Lt=new C,Ht=new le,he={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};let Yt=!1;function fe(){return U===null?k:1}let A=n;function ke(x,I){return e.getContext(x,I)}try{const x={alpha:!0,depth:s,stencil:r,antialias:o,premultipliedAlpha:l,preserveDrawingBuffer:c,powerPreference:h,failIfMajorPerformanceCaveat:d};if("setAttribute"in e&&e.setAttribute("data-engine",`three.js r${Ja}`),e.addEventListener("webglcontextlost",Z,!1),e.addEventListener("webglcontextrestored",dt,!1),e.addEventListener("webglcontextcreationerror",ht,!1),A===null){const I="webgl2";if(A=ke(I,x),A===null)throw ke(I)?new Error("Error creating WebGL context with your selected attributes."):new Error("Error creating WebGL context.")}}catch(x){throw console.error("THREE.WebGLRenderer: "+x.message),x}let qt,jt,J,Et,ct,E,_,F,q,K,j,yt,lt,pt,Gt,et,mt,Tt,Ut,gt,Zt,Ot,se,L;function at(){qt=new cp(A),qt.init(),Ot=new Xm(A,qt),jt=new ip(A,qt,t,Ot),J=new Gm(A,qt),jt.reverseDepthBuffer&&p&&J.buffers.depth.setReversed(!0),Et=new dp(A),ct=new Pm,E=new Wm(A,qt,J,ct,jt,Ot,Et),_=new rp(S),F=new lp(S),q=new vu(A),se=new ep(A,q),K=new hp(A,q,Et,se),j=new pp(A,K,q,Et),Ut=new fp(A,jt,E),et=new sp(ct),yt=new Cm(S,_,F,qt,jt,se,et),lt=new Jm(S,ct),pt=new Lm,Gt=new Bm(qt),Tt=new tp(S,_,F,J,j,u,l),mt=new Hm(S,j,jt),L=new Qm(A,Et,jt,J),gt=new np(A,qt,Et),Zt=new up(A,qt,Et),Et.programs=yt.programs,S.capabilities=jt,S.extensions=qt,S.properties=ct,S.renderLists=pt,S.shadowMap=mt,S.state=J,S.info=Et}at();const H=new Km(S,A);this.xr=H,this.getContext=function(){return A},this.getContextAttributes=function(){return A.getContextAttributes()},this.forceContextLoss=function(){const x=qt.get("WEBGL_lose_context");x&&x.loseContext()},this.forceContextRestore=function(){const x=qt.get("WEBGL_lose_context");x&&x.restoreContext()},this.getPixelRatio=function(){return k},this.setPixelRatio=function(x){x!==void 0&&(k=x,this.setSize(X,Q,!1))},this.getSize=function(x){return x.set(X,Q)},this.setSize=function(x,I,O=!0){if(H.isPresenting){console.warn("THREE.WebGLRenderer: Can't change size while VR device is presenting.");return}X=x,Q=I,e.width=Math.floor(x*k),e.height=Math.floor(I*k),O===!0&&(e.style.width=x+"px",e.style.height=I+"px"),this.setViewport(0,0,x,I)},this.getDrawingBufferSize=function(x){return x.set(X*k,Q*k).floor()},this.setDrawingBufferSize=function(x,I,O){X=x,Q=I,k=O,e.width=Math.floor(x*O),e.height=Math.floor(I*O),this.setViewport(0,0,x,I)},this.getCurrentViewport=function(x){return x.copy(P)},this.getViewport=function(x){return x.copy(St)},this.setViewport=function(x,I,O,B){x.isVector4?St.set(x.x,x.y,x.z,x.w):St.set(x,I,O,B),J.viewport(P.copy(St).multiplyScalar(k).round())},this.getScissor=function(x){return x.copy(Ft)},this.setScissor=function(x,I,O,B){x.isVector4?Ft.set(x.x,x.y,x.z,x.w):Ft.set(x,I,O,B),J.scissor(W.copy(Ft).multiplyScalar(k).round())},this.getScissorTest=function(){return Vt},this.setScissorTest=function(x){J.setScissorTest(Vt=x)},this.setOpaqueSort=function(x){it=x},this.setTransparentSort=function(x){ft=x},this.getClearColor=function(x){return x.copy(Tt.getClearColor())},this.setClearColor=function(){Tt.setClearColor.apply(Tt,arguments)},this.getClearAlpha=function(){return Tt.getClearAlpha()},this.setClearAlpha=function(){Tt.setClearAlpha.apply(Tt,arguments)},this.clear=function(x=!0,I=!0,O=!0){let B=0;if(x){let N=!1;if(U!==null){const tt=U.texture.format;N=tt===ro||tt===so||tt===io}if(N){const tt=U.texture.type,ut=tt===Rn||tt===ii||tt===us||tt===ki||tt===to||tt===eo,vt=Tt.getClearColor(),xt=Tt.getClearAlpha(),It=vt.r,Nt=vt.g,Rt=vt.b;ut?(g[0]=It,g[1]=Nt,g[2]=Rt,g[3]=xt,A.clearBufferuiv(A.COLOR,0,g)):(v[0]=It,v[1]=Nt,v[2]=Rt,v[3]=xt,A.clearBufferiv(A.COLOR,0,v))}else B|=A.COLOR_BUFFER_BIT}I&&(B|=A.DEPTH_BUFFER_BIT),O&&(B|=A.STENCIL_BUFFER_BIT,this.state.buffers.stencil.setMask(4294967295)),A.clear(B)},this.clearColor=function(){this.clear(!0,!1,!1)},this.clearDepth=function(){this.clear(!1,!0,!1)},this.clearStencil=function(){this.clear(!1,!1,!0)},this.dispose=function(){e.removeEventListener("webglcontextlost",Z,!1),e.removeEventListener("webglcontextrestored",dt,!1),e.removeEventListener("webglcontextcreationerror",ht,!1),Tt.dispose(),pt.dispose(),Gt.dispose(),ct.dispose(),_.dispose(),F.dispose(),j.dispose(),se.dispose(),L.dispose(),yt.dispose(),H.dispose(),H.removeEventListener("sessionstart",Yi),H.removeEventListener("sessionend",xs),hn.stop()};function Z(x){x.preventDefault(),console.log("THREE.WebGLRenderer: Context Lost."),D=!0}function dt(){console.log("THREE.WebGLRenderer: Context Restored."),D=!1;const x=Et.autoReset,I=mt.enabled,O=mt.autoUpdate,B=mt.needsUpdate,N=mt.type;at(),Et.autoReset=x,mt.enabled=I,mt.autoUpdate=O,mt.needsUpdate=B,mt.type=N}function ht(x){console.error("THREE.WebGLRenderer: A WebGL context could not be created. Reason: ",x.statusMessage)}function wt(x){const I=x.target;I.removeEventListener("dispose",wt),Kt(I)}function Kt(x){ce(x),ct.remove(x)}function ce(x){const I=ct.get(x).programs;I!==void 0&&(I.forEach(function(O){yt.releaseProgram(O)}),x.isShaderMaterial&&yt.releaseShaderCache(x))}this.renderBufferDirect=function(x,I,O,B,N,tt){I===null&&(I=he);const ut=N.isMesh&&N.matrixWorld.determinant()<0,vt=Ec(x,I,O,B,N);J.setMaterial(B,ut);let xt=O.index,It=1;if(B.wireframe===!0){if(xt=K.getWireframeAttribute(O),xt===void 0)return;It=2}const Nt=O.drawRange,Rt=O.attributes.position;let $t=Nt.start*It,ee=(Nt.start+Nt.count)*It;tt!==null&&($t=Math.max($t,tt.start*It),ee=Math.min(ee,(tt.start+tt.count)*It)),xt!==null?($t=Math.max($t,0),ee=Math.min(ee,xt.count)):Rt!=null&&($t=Math.max($t,0),ee=Math.min(ee,Rt.count));const _e=ee-$t;if(_e<0||_e===1/0)return;se.setup(N,B,vt,O,xt);let pe,Jt=gt;if(xt!==null&&(pe=q.get(xt),Jt=Zt,Jt.setIndex(pe)),N.isMesh)B.wireframe===!0?(J.setLineWidth(B.wireframeLinewidth*fe()),Jt.setMode(A.LINES)):Jt.setMode(A.TRIANGLES);else if(N.isLine){let Ct=B.linewidth;Ct===void 0&&(Ct=1),J.setLineWidth(Ct*fe()),N.isLineSegments?Jt.setMode(A.LINES):N.isLineLoop?Jt.setMode(A.LINE_LOOP):Jt.setMode(A.LINE_STRIP)}else N.isPoints?Jt.setMode(A.POINTS):N.isSprite&&Jt.setMode(A.TRIANGLES);if(N.isBatchedMesh)if(N._multiDrawInstances!==null)Jt.renderMultiDrawInstances(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount,N._multiDrawInstances);else if(qt.get("WEBGL_multi_draw"))Jt.renderMultiDraw(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount);else{const Ct=N._multiDrawStarts,Te=N._multiDrawCounts,ne=N._multiDrawCount,nn=xt?q.get(xt).bytesPerElement:1,li=ct.get(B).currentProgram.getUniforms();for(let Xe=0;Xe{function tt(){if(B.forEach(function(ut){ct.get(ut).currentProgram.isReady()&&B.delete(ut)}),B.size===0){N(x);return}setTimeout(tt,10)}qt.get("KHR_parallel_shader_compile")!==null?tt():setTimeout(tt,10)})};let Ie=null;function en(x){Ie&&Ie(x)}function Yi(){hn.stop()}function xs(){hn.start()}const hn=new mc;hn.setAnimationLoop(en),typeof self<"u"&&hn.setContext(self),this.setAnimationLoop=function(x){Ie=x,H.setAnimationLoop(x),x===null?hn.stop():hn.start()},H.addEventListener("sessionstart",Yi),H.addEventListener("sessionend",xs),this.render=function(x,I){if(I!==void 0&&I.isCamera!==!0){console.error("THREE.WebGLRenderer.render: camera is not an instance of THREE.Camera.");return}if(D===!0)return;if(x.matrixWorldAutoUpdate===!0&&x.updateMatrixWorld(),I.parent===null&&I.matrixWorldAutoUpdate===!0&&I.updateMatrixWorld(),H.enabled===!0&&H.isPresenting===!0&&(H.cameraAutoUpdate===!0&&H.updateCamera(I),I=H.getCamera()),x.isScene===!0&&x.onBeforeRender(S,x,I,U),f=Gt.get(x,b.length),f.init(I),b.push(f),Pt.multiplyMatrices(I.projectionMatrix,I.matrixWorldInverse),Y.setFromProjectionMatrix(Pt),_t=this.localClippingEnabled,nt=et.init(this.clippingPlanes,_t),m=pt.get(x,T.length),m.init(),T.push(m),H.enabled===!0&&H.isPresenting===!0){const tt=S.xr.getDepthSensingMesh();tt!==null&&qi(tt,I,-1/0,S.sortObjects)}qi(x,I,0,S.sortObjects),m.finish(),S.sortObjects===!0&&m.sort(it,ft),Yt=H.enabled===!1||H.isPresenting===!1||H.hasDepthSensing()===!1,Yt&&Tt.addToRenderList(m,x),this.info.render.frame++,nt===!0&&et.beginShadows();const O=f.state.shadowsArray;mt.render(O,x,I),nt===!0&&et.endShadows(),this.info.autoReset===!0&&this.info.reset();const B=m.opaque,N=m.transmissive;if(f.setupLights(),I.isArrayCamera){const tt=I.cameras;if(N.length>0)for(let ut=0,vt=tt.length;ut0&&uo(B,N,x,I),Yt&&Tt.render(x),Ms(m,x,I);U!==null&&R===0&&(E.updateMultisampleRenderTarget(U),E.updateRenderTargetMipmap(U)),x.isScene===!0&&x.onAfterRender(S,x,I),se.resetDefaultState(),y=-1,M=null,b.pop(),b.length>0?(f=b[b.length-1],nt===!0&&et.setGlobalState(S.clippingPlanes,f.state.camera)):f=null,T.pop(),T.length>0?m=T[T.length-1]:m=null};function qi(x,I,O,B){if(x.visible===!1)return;if(x.layers.test(I.layers)){if(x.isGroup)O=x.renderOrder;else if(x.isLOD)x.autoUpdate===!0&&x.update(I);else if(x.isLight)f.pushLight(x),x.castShadow&&f.pushShadow(x);else if(x.isSprite){if(!x.frustumCulled||Y.intersectsSprite(x)){B&&Ht.setFromMatrixPosition(x.matrixWorld).applyMatrix4(Pt);const ut=j.update(x),vt=x.material;vt.visible&&m.push(x,ut,vt,O,Ht.z,null)}}else if((x.isMesh||x.isLine||x.isPoints)&&(!x.frustumCulled||Y.intersectsObject(x))){const ut=j.update(x),vt=x.material;if(B&&(x.boundingSphere!==void 0?(x.boundingSphere===null&&x.computeBoundingSphere(),Ht.copy(x.boundingSphere.center)):(ut.boundingSphere===null&&ut.computeBoundingSphere(),Ht.copy(ut.boundingSphere.center)),Ht.applyMatrix4(x.matrixWorld).applyMatrix4(Pt)),Array.isArray(vt)){const xt=ut.groups;for(let It=0,Nt=xt.length;It0&&Ss(N,I,O),tt.length>0&&Ss(tt,I,O),ut.length>0&&Ss(ut,I,O),J.buffers.depth.setTest(!0),J.buffers.depth.setMask(!0),J.buffers.color.setMask(!0),J.setPolygonOffset(!1)}function uo(x,I,O,B){if((O.isScene===!0?O.overrideMaterial:null)!==null)return;f.state.transmissionRenderTarget[B.id]===void 0&&(f.state.transmissionRenderTarget[B.id]=new cn(1,1,{generateMipmaps:!0,type:qt.has("EXT_color_buffer_half_float")||qt.has("EXT_color_buffer_float")?wn:Rn,minFilter:ti,samples:4,stencilBuffer:r,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:Qt.workingColorSpace}));const tt=f.state.transmissionRenderTarget[B.id],ut=B.viewport||P;tt.setSize(ut.z*S.transmissionResolutionScale,ut.w*S.transmissionResolutionScale);const vt=S.getRenderTarget();S.setRenderTarget(tt),S.getClearColor(G),$=S.getClearAlpha(),$<1&&S.setClearColor(16777215,.5),S.clear(),Yt&&Tt.render(O);const xt=S.toneMapping;S.toneMapping=Bn;const It=B.viewport;if(B.viewport!==void 0&&(B.viewport=void 0),f.setupLightsView(B),nt===!0&&et.setGlobalState(S.clippingPlanes,B),Ss(x,O,B),E.updateMultisampleRenderTarget(tt),E.updateRenderTargetMipmap(tt),qt.has("WEBGL_multisampled_render_to_texture")===!1){let Nt=!1;for(let Rt=0,$t=I.length;Rt<$t;Rt++){const ee=I[Rt],_e=ee.object,pe=ee.geometry,Jt=ee.material,Ct=ee.group;if(Jt.side===dn&&_e.layers.test(B.layers)){const Te=Jt.side;Jt.side=We,Jt.needsUpdate=!0,fo(_e,O,B,pe,Jt,Ct),Jt.side=Te,Jt.needsUpdate=!0,Nt=!0}}Nt===!0&&(E.updateMultisampleRenderTarget(tt),E.updateRenderTargetMipmap(tt))}S.setRenderTarget(vt),S.setClearColor(G,$),It!==void 0&&(B.viewport=It),S.toneMapping=xt}function Ss(x,I,O){const B=I.isScene===!0?I.overrideMaterial:null;for(let N=0,tt=x.length;N0),Rt=!!O.morphAttributes.position,$t=!!O.morphAttributes.normal,ee=!!O.morphAttributes.color;let _e=Bn;B.toneMapped&&(U===null||U.isXRRenderTarget===!0)&&(_e=S.toneMapping);const pe=O.morphAttributes.position||O.morphAttributes.normal||O.morphAttributes.color,Jt=pe!==void 0?pe.length:0,Ct=ct.get(B),Te=f.state.lights;if(nt===!0&&(_t===!0||x!==M)){const Ne=x===M&&B.id===y;et.setState(B,x,Ne)}let ne=!1;B.version===Ct.__version?(Ct.needsLights&&Ct.lightsStateVersion!==Te.state.version||Ct.outputColorSpace!==vt||N.isBatchedMesh&&Ct.batching===!1||!N.isBatchedMesh&&Ct.batching===!0||N.isBatchedMesh&&Ct.batchingColor===!0&&N.colorTexture===null||N.isBatchedMesh&&Ct.batchingColor===!1&&N.colorTexture!==null||N.isInstancedMesh&&Ct.instancing===!1||!N.isInstancedMesh&&Ct.instancing===!0||N.isSkinnedMesh&&Ct.skinning===!1||!N.isSkinnedMesh&&Ct.skinning===!0||N.isInstancedMesh&&Ct.instancingColor===!0&&N.instanceColor===null||N.isInstancedMesh&&Ct.instancingColor===!1&&N.instanceColor!==null||N.isInstancedMesh&&Ct.instancingMorph===!0&&N.morphTexture===null||N.isInstancedMesh&&Ct.instancingMorph===!1&&N.morphTexture!==null||Ct.envMap!==xt||B.fog===!0&&Ct.fog!==tt||Ct.numClippingPlanes!==void 0&&(Ct.numClippingPlanes!==et.numPlanes||Ct.numIntersection!==et.numIntersection)||Ct.vertexAlphas!==It||Ct.vertexTangents!==Nt||Ct.morphTargets!==Rt||Ct.morphNormals!==$t||Ct.morphColors!==ee||Ct.toneMapping!==_e||Ct.morphTargetsCount!==Jt)&&(ne=!0):(ne=!0,Ct.__version=B.version);let nn=Ct.currentProgram;ne===!0&&(nn=ys(B,I,N));let li=!1,Xe=!1,ji=!1;const de=nn.getUniforms(),Ke=Ct.uniforms;if(J.useProgram(nn.program)&&(li=!0,Xe=!0,ji=!0),B.id!==y&&(y=B.id,Xe=!0),li||M!==x){J.buffers.depth.getReversed()?(ot.copy(x.projectionMatrix),Dh(ot),Lh(ot),de.setValue(A,"projectionMatrix",ot)):de.setValue(A,"projectionMatrix",x.projectionMatrix),de.setValue(A,"viewMatrix",x.matrixWorldInverse);const He=de.map.cameraPosition;He!==void 0&&He.setValue(A,Lt.setFromMatrixPosition(x.matrixWorld)),jt.logarithmicDepthBuffer&&de.setValue(A,"logDepthBufFC",2/(Math.log(x.far+1)/Math.LN2)),(B.isMeshPhongMaterial||B.isMeshToonMaterial||B.isMeshLambertMaterial||B.isMeshBasicMaterial||B.isMeshStandardMaterial||B.isShaderMaterial)&&de.setValue(A,"isOrthographic",x.isOrthographicCamera===!0),M!==x&&(M=x,Xe=!0,ji=!0)}if(N.isSkinnedMesh){de.setOptional(A,N,"bindMatrix"),de.setOptional(A,N,"bindMatrixInverse");const Ne=N.skeleton;Ne&&(Ne.boneTexture===null&&Ne.computeBoneTexture(),de.setValue(A,"boneTexture",Ne.boneTexture,E))}N.isBatchedMesh&&(de.setOptional(A,N,"batchingTexture"),de.setValue(A,"batchingTexture",N._matricesTexture,E),de.setOptional(A,N,"batchingIdTexture"),de.setValue(A,"batchingIdTexture",N._indirectTexture,E),de.setOptional(A,N,"batchingColorTexture"),N._colorsTexture!==null&&de.setValue(A,"batchingColorTexture",N._colorsTexture,E));const $e=O.morphAttributes;if(($e.position!==void 0||$e.normal!==void 0||$e.color!==void 0)&&Ut.update(N,O,nn),(Xe||Ct.receiveShadow!==N.receiveShadow)&&(Ct.receiveShadow=N.receiveShadow,de.setValue(A,"receiveShadow",N.receiveShadow)),B.isMeshGouraudMaterial&&B.envMap!==null&&(Ke.envMap.value=xt,Ke.flipEnvMap.value=xt.isCubeTexture&&xt.isRenderTargetTexture===!1?-1:1),B.isMeshStandardMaterial&&B.envMap===null&&I.environment!==null&&(Ke.envMapIntensity.value=I.environmentIntensity),Xe&&(de.setValue(A,"toneMappingExposure",S.toneMappingExposure),Ct.needsLights&&bc(Ke,ji),tt&&B.fog===!0&<.refreshFogUniforms(Ke,tt),lt.refreshMaterialUniforms(Ke,B,k,Q,f.state.transmissionRenderTarget[x.id]),or.upload(A,po(Ct),Ke,E)),B.isShaderMaterial&&B.uniformsNeedUpdate===!0&&(or.upload(A,po(Ct),Ke,E),B.uniformsNeedUpdate=!1),B.isSpriteMaterial&&de.setValue(A,"center",N.center),de.setValue(A,"modelViewMatrix",N.modelViewMatrix),de.setValue(A,"normalMatrix",N.normalMatrix),de.setValue(A,"modelMatrix",N.matrixWorld),B.isShaderMaterial||B.isRawShaderMaterial){const Ne=B.uniformsGroups;for(let He=0,yr=Ne.length;He0&&E.useMultisampledRTT(x)===!1?N=ct.get(x).__webglMultisampledFramebuffer:Array.isArray(Nt)?N=Nt[O]:N=Nt,P.copy(x.viewport),W.copy(x.scissor),z=x.scissorTest}else P.copy(St).multiplyScalar(k).floor(),W.copy(Ft).multiplyScalar(k).floor(),z=Vt;if(O!==0&&(N=wc),J.bindFramebuffer(A.FRAMEBUFFER,N)&&B&&J.drawBuffers(x,N),J.viewport(P),J.scissor(W),J.setScissorTest(z),tt){const xt=ct.get(x.texture);A.framebufferTexture2D(A.FRAMEBUFFER,A.COLOR_ATTACHMENT0,A.TEXTURE_CUBE_MAP_POSITIVE_X+I,xt.__webglTexture,O)}else if(ut){const xt=ct.get(x.texture),It=I;A.framebufferTextureLayer(A.FRAMEBUFFER,A.COLOR_ATTACHMENT0,xt.__webglTexture,O,It)}else if(x!==null&&O!==0){const xt=ct.get(x.texture);A.framebufferTexture2D(A.FRAMEBUFFER,A.COLOR_ATTACHMENT0,A.TEXTURE_2D,xt.__webglTexture,O)}y=-1},this.readRenderTargetPixels=function(x,I,O,B,N,tt,ut){if(!(x&&x.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let vt=ct.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&ut!==void 0&&(vt=vt[ut]),vt){J.bindFramebuffer(A.FRAMEBUFFER,vt);try{const xt=x.texture,It=xt.format,Nt=xt.type;if(!jt.textureFormatReadable(It)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!jt.textureTypeReadable(Nt)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}I>=0&&I<=x.width-B&&O>=0&&O<=x.height-N&&A.readPixels(I,O,B,N,Ot.convert(It),Ot.convert(Nt),tt)}finally{const xt=U!==null?ct.get(U).__webglFramebuffer:null;J.bindFramebuffer(A.FRAMEBUFFER,xt)}}},this.readRenderTargetPixelsAsync=async function(x,I,O,B,N,tt,ut){if(!(x&&x.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let vt=ct.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&ut!==void 0&&(vt=vt[ut]),vt){const xt=x.texture,It=xt.format,Nt=xt.type;if(!jt.textureFormatReadable(It))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!jt.textureTypeReadable(Nt))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");if(I>=0&&I<=x.width-B&&O>=0&&O<=x.height-N){J.bindFramebuffer(A.FRAMEBUFFER,vt);const Rt=A.createBuffer();A.bindBuffer(A.PIXEL_PACK_BUFFER,Rt),A.bufferData(A.PIXEL_PACK_BUFFER,tt.byteLength,A.STREAM_READ),A.readPixels(I,O,B,N,Ot.convert(It),Ot.convert(Nt),0);const $t=U!==null?ct.get(U).__webglFramebuffer:null;J.bindFramebuffer(A.FRAMEBUFFER,$t);const ee=A.fenceSync(A.SYNC_GPU_COMMANDS_COMPLETE,0);return A.flush(),await Ph(A,ee,4),A.bindBuffer(A.PIXEL_PACK_BUFFER,Rt),A.getBufferSubData(A.PIXEL_PACK_BUFFER,0,tt),A.deleteBuffer(Rt),A.deleteSync(ee),tt}else throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(x,I=null,O=0){x.isTexture!==!0&&(Ri("WebGLRenderer: copyFramebufferToTexture function signature has changed."),I=arguments[0]||null,x=arguments[1]);const B=Math.pow(2,-O),N=Math.floor(x.image.width*B),tt=Math.floor(x.image.height*B),ut=I!==null?I.x:0,vt=I!==null?I.y:0;E.setTexture2D(x,0),A.copyTexSubImage2D(A.TEXTURE_2D,O,0,0,ut,vt,N,tt),J.unbindTexture()};const Ac=A.createFramebuffer(),Rc=A.createFramebuffer();this.copyTextureToTexture=function(x,I,O=null,B=null,N=0,tt=null){x.isTexture!==!0&&(Ri("WebGLRenderer: copyTextureToTexture function signature has changed."),B=arguments[0]||null,x=arguments[1],I=arguments[2],tt=arguments[3]||0,O=null),tt===null&&(N!==0?(Ri("WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels."),tt=N,N=0):tt=0);let ut,vt,xt,It,Nt,Rt,$t,ee,_e;const pe=x.isCompressedTexture?x.mipmaps[tt]:x.image;if(O!==null)ut=O.max.x-O.min.x,vt=O.max.y-O.min.y,xt=O.isBox3?O.max.z-O.min.z:1,It=O.min.x,Nt=O.min.y,Rt=O.isBox3?O.min.z:0;else{const $e=Math.pow(2,-N);ut=Math.floor(pe.width*$e),vt=Math.floor(pe.height*$e),x.isDataArrayTexture?xt=pe.depth:x.isData3DTexture?xt=Math.floor(pe.depth*$e):xt=1,It=0,Nt=0,Rt=0}B!==null?($t=B.x,ee=B.y,_e=B.z):($t=0,ee=0,_e=0);const Jt=Ot.convert(I.format),Ct=Ot.convert(I.type);let Te;I.isData3DTexture?(E.setTexture3D(I,0),Te=A.TEXTURE_3D):I.isDataArrayTexture||I.isCompressedArrayTexture?(E.setTexture2DArray(I,0),Te=A.TEXTURE_2D_ARRAY):(E.setTexture2D(I,0),Te=A.TEXTURE_2D),A.pixelStorei(A.UNPACK_FLIP_Y_WEBGL,I.flipY),A.pixelStorei(A.UNPACK_PREMULTIPLY_ALPHA_WEBGL,I.premultiplyAlpha),A.pixelStorei(A.UNPACK_ALIGNMENT,I.unpackAlignment);const ne=A.getParameter(A.UNPACK_ROW_LENGTH),nn=A.getParameter(A.UNPACK_IMAGE_HEIGHT),li=A.getParameter(A.UNPACK_SKIP_PIXELS),Xe=A.getParameter(A.UNPACK_SKIP_ROWS),ji=A.getParameter(A.UNPACK_SKIP_IMAGES);A.pixelStorei(A.UNPACK_ROW_LENGTH,pe.width),A.pixelStorei(A.UNPACK_IMAGE_HEIGHT,pe.height),A.pixelStorei(A.UNPACK_SKIP_PIXELS,It),A.pixelStorei(A.UNPACK_SKIP_ROWS,Nt),A.pixelStorei(A.UNPACK_SKIP_IMAGES,Rt);const de=x.isDataArrayTexture||x.isData3DTexture,Ke=I.isDataArrayTexture||I.isData3DTexture;if(x.isDepthTexture){const $e=ct.get(x),Ne=ct.get(I),He=ct.get($e.__renderTarget),yr=ct.get(Ne.__renderTarget);J.bindFramebuffer(A.READ_FRAMEBUFFER,He.__webglFramebuffer),J.bindFramebuffer(A.DRAW_FRAMEBUFFER,yr.__webglFramebuffer);for(let Vn=0;VnMath.PI&&(n-=Ve),s<-Math.PI?s+=Ve:s>Math.PI&&(s-=Ve),n<=s?this._spherical.theta=Math.max(n,Math.min(s,this._spherical.theta)):this._spherical.theta=this._spherical.theta>(n+s)/2?Math.max(n,this._spherical.theta):Math.min(s,this._spherical.theta)),this._spherical.phi=Math.max(this.minPolarAngle,Math.min(this.maxPolarAngle,this._spherical.phi)),this._spherical.makeSafe(),this.enableDamping===!0?this.target.addScaledVector(this._panOffset,this.dampingFactor):this.target.add(this._panOffset),this.target.sub(this.cursor),this.target.clampLength(this.minTargetRadius,this.maxTargetRadius),this.target.add(this.cursor);let r=!1;if(this.zoomToCursor&&this._performCursorZoom||this.object.isOrthographicCamera)this._spherical.radius=this._clampDistance(this._spherical.radius);else{const a=this._spherical.radius;this._spherical.radius=this._clampDistance(this._spherical.radius*this._scale),r=a!=this._spherical.radius}if(xe.setFromSpherical(this._spherical),xe.applyQuaternion(this._quatInverse),e.copy(this.target).add(xe),this.object.lookAt(this.target),this.enableDamping===!0?(this._sphericalDelta.theta*=1-this.dampingFactor,this._sphericalDelta.phi*=1-this.dampingFactor,this._panOffset.multiplyScalar(1-this.dampingFactor)):(this._sphericalDelta.set(0,0,0),this._panOffset.set(0,0,0)),this.zoomToCursor&&this._performCursorZoom){let a=null;if(this.object.isPerspectiveCamera){const o=xe.length();a=this._clampDistance(o*this._scale);const l=o-a;this.object.position.addScaledVector(this._dollyDirection,l),this.object.updateMatrixWorld(),r=!!l}else if(this.object.isOrthographicCamera){const o=new C(this._mouse.x,this._mouse.y,0);o.unproject(this.object);const l=this.object.zoom;this.object.zoom=Math.max(this.minZoom,Math.min(this.maxZoom,this.object.zoom/this._scale)),this.object.updateProjectionMatrix(),r=l!==this.object.zoom;const c=new C(this._mouse.x,this._mouse.y,0);c.unproject(this.object),this.object.position.sub(c).add(o),this.object.updateMatrixWorld(),a=xe.length()}else console.warn("WARNING: OrbitControls.js encountered an unknown camera type - zoom to cursor disabled."),this.zoomToCursor=!1;a!==null&&(this.screenSpacePanning?this.target.set(0,0,-1).transformDirection(this.object.matrix).multiplyScalar(a).add(this.object.position):(Qs.origin.copy(this.object.position),Qs.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(Qs.direction))ia||8*(1-this._lastQuaternion.dot(this.object.quaternion))>ia||this._lastTargetPosition.distanceToSquared(this.target)>ia?(this.dispatchEvent(wl),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(t){return t!==null?Ve/60*this.autoRotateSpeed*t:Ve/60/60*this.autoRotateSpeed}_getZoomScale(t){const e=Math.abs(t*.01);return Math.pow(.95,this.zoomSpeed*e)}_rotateLeft(t){this._sphericalDelta.theta-=t}_rotateUp(t){this._sphericalDelta.phi-=t}_panLeft(t,e){xe.setFromMatrixColumn(e,0),xe.multiplyScalar(-t),this._panOffset.add(xe)}_panUp(t,e){this.screenSpacePanning===!0?xe.setFromMatrixColumn(e,1):(xe.setFromMatrixColumn(e,0),xe.crossVectors(this.object.up,xe)),xe.multiplyScalar(t),this._panOffset.add(xe)}_pan(t,e){const n=this.domElement;if(this.object.isPerspectiveCamera){const s=this.object.position;xe.copy(s).sub(this.target);let r=xe.length();r*=Math.tan(this.object.fov/2*Math.PI/180),this._panLeft(2*t*r/n.clientHeight,this.object.matrix),this._panUp(2*e*r/n.clientHeight,this.object.matrix)}else this.object.isOrthographicCamera?(this._panLeft(t*(this.object.right-this.object.left)/this.object.zoom/n.clientWidth,this.object.matrix),this._panUp(e*(this.object.top-this.object.bottom)/this.object.zoom/n.clientHeight,this.object.matrix)):(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - pan disabled."),this.enablePan=!1)}_dollyOut(t){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale/=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_dollyIn(t){this.object.isPerspectiveCamera||this.object.isOrthographicCamera?this._scale*=t:(console.warn("WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled."),this.enableZoom=!1)}_updateZoomParameters(t,e){if(!this.zoomToCursor)return;this._performCursorZoom=!0;const n=this.domElement.getBoundingClientRect(),s=t-n.left,r=e-n.top,a=n.width,o=n.height;this._mouse.x=s/a*2-1,this._mouse.y=-(r/o)*2+1,this._dollyDirection.set(this._mouse.x,this._mouse.y,1).unproject(this.object).sub(this.object.position).normalize()}_clampDistance(t){return Math.max(this.minDistance,Math.min(this.maxDistance,t))}_handleMouseDownRotate(t){this._rotateStart.set(t.clientX,t.clientY)}_handleMouseDownDolly(t){this._updateZoomParameters(t.clientX,t.clientX),this._dollyStart.set(t.clientX,t.clientY)}_handleMouseDownPan(t){this._panStart.set(t.clientX,t.clientY)}_handleMouseMoveRotate(t){this._rotateEnd.set(t.clientX,t.clientY),this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const e=this.domElement;this._rotateLeft(Ve*this._rotateDelta.x/e.clientHeight),this._rotateUp(Ve*this._rotateDelta.y/e.clientHeight),this._rotateStart.copy(this._rotateEnd),this.update()}_handleMouseMoveDolly(t){this._dollyEnd.set(t.clientX,t.clientY),this._dollyDelta.subVectors(this._dollyEnd,this._dollyStart),this._dollyDelta.y>0?this._dollyOut(this._getZoomScale(this._dollyDelta.y)):this._dollyDelta.y<0&&this._dollyIn(this._getZoomScale(this._dollyDelta.y)),this._dollyStart.copy(this._dollyEnd),this.update()}_handleMouseMovePan(t){this._panEnd.set(t.clientX,t.clientY),this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd),this.update()}_handleMouseWheel(t){this._updateZoomParameters(t.clientX,t.clientY),t.deltaY<0?this._dollyIn(this._getZoomScale(t.deltaY)):t.deltaY>0&&this._dollyOut(this._getZoomScale(t.deltaY)),this.update()}_handleKeyDown(t){let e=!1;switch(t.code){case this.keys.UP:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateUp(Ve*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,this.keyPanSpeed),e=!0;break;case this.keys.BOTTOM:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateUp(-Ve*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(0,-this.keyPanSpeed),e=!0;break;case this.keys.LEFT:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateLeft(Ve*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(this.keyPanSpeed,0),e=!0;break;case this.keys.RIGHT:t.ctrlKey||t.metaKey||t.shiftKey?this.enableRotate&&this._rotateLeft(-Ve*this.keyRotateSpeed/this.domElement.clientHeight):this.enablePan&&this._pan(-this.keyPanSpeed,0),e=!0;break}e&&(t.preventDefault(),this.update())}_handleTouchStartRotate(t){if(this._pointers.length===1)this._rotateStart.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._rotateStart.set(n,s)}}_handleTouchStartPan(t){if(this._pointers.length===1)this._panStart.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._panStart.set(n,s)}}_handleTouchStartDolly(t){const e=this._getSecondPointerPosition(t),n=t.pageX-e.x,s=t.pageY-e.y,r=Math.sqrt(n*n+s*s);this._dollyStart.set(0,r)}_handleTouchStartDollyPan(t){this.enableZoom&&this._handleTouchStartDolly(t),this.enablePan&&this._handleTouchStartPan(t)}_handleTouchStartDollyRotate(t){this.enableZoom&&this._handleTouchStartDolly(t),this.enableRotate&&this._handleTouchStartRotate(t)}_handleTouchMoveRotate(t){if(this._pointers.length==1)this._rotateEnd.set(t.pageX,t.pageY);else{const n=this._getSecondPointerPosition(t),s=.5*(t.pageX+n.x),r=.5*(t.pageY+n.y);this._rotateEnd.set(s,r)}this._rotateDelta.subVectors(this._rotateEnd,this._rotateStart).multiplyScalar(this.rotateSpeed);const e=this.domElement;this._rotateLeft(Ve*this._rotateDelta.x/e.clientHeight),this._rotateUp(Ve*this._rotateDelta.y/e.clientHeight),this._rotateStart.copy(this._rotateEnd)}_handleTouchMovePan(t){if(this._pointers.length===1)this._panEnd.set(t.pageX,t.pageY);else{const e=this._getSecondPointerPosition(t),n=.5*(t.pageX+e.x),s=.5*(t.pageY+e.y);this._panEnd.set(n,s)}this._panDelta.subVectors(this._panEnd,this._panStart).multiplyScalar(this.panSpeed),this._pan(this._panDelta.x,this._panDelta.y),this._panStart.copy(this._panEnd)}_handleTouchMoveDolly(t){const e=this._getSecondPointerPosition(t),n=t.pageX-e.x,s=t.pageY-e.y,r=Math.sqrt(n*n+s*s);this._dollyEnd.set(0,r),this._dollyDelta.set(0,Math.pow(this._dollyEnd.y/this._dollyStart.y,this.zoomSpeed)),this._dollyOut(this._dollyDelta.y),this._dollyStart.copy(this._dollyEnd);const a=(t.pageX+e.x)*.5,o=(t.pageY+e.y)*.5;this._updateZoomParameters(a,o)}_handleTouchMoveDollyPan(t){this.enableZoom&&this._handleTouchMoveDolly(t),this.enablePan&&this._handleTouchMovePan(t)}_handleTouchMoveDollyRotate(t){this.enableZoom&&this._handleTouchMoveDolly(t),this.enableRotate&&this._handleTouchMoveRotate(t)}_addPointer(t){this._pointers.push(t.pointerId)}_removePointer(t){delete this._pointerPositions[t.pointerId];for(let e=0;e - varying vec2 vUv; - uniform sampler2D colorTexture; - uniform vec2 invSize; - uniform vec2 direction; - uniform float gaussianCoefficients[KERNEL_RADIUS]; - - void main() { - float weightSum = gaussianCoefficients[0]; - vec3 diffuseSum = texture2D( colorTexture, vUv ).rgb * weightSum; - for( int i = 1; i < KERNEL_RADIUS; i ++ ) { - float x = float(i); - float w = gaussianCoefficients[i]; - vec2 uvOffset = direction * invSize * x; - vec3 sample1 = texture2D( colorTexture, vUv + uvOffset ).rgb; - vec3 sample2 = texture2D( colorTexture, vUv - uvOffset ).rgb; - diffuseSum += (sample1 + sample2) * w; - weightSum += 2.0 * w; - } - gl_FragColor = vec4(diffuseSum/weightSum, 1.0); - }`})}getCompositeMaterial(t){return new ze({defines:{NUM_MIPS:t},uniforms:{blurTexture1:{value:null},blurTexture2:{value:null},blurTexture3:{value:null},blurTexture4:{value:null},blurTexture5:{value:null},bloomStrength:{value:1},bloomFactors:{value:null},bloomTintColors:{value:null},bloomRadius:{value:0}},vertexShader:`varying vec2 vUv; - void main() { - vUv = uv; - gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 ); - }`,fragmentShader:`varying vec2 vUv; - uniform sampler2D blurTexture1; - uniform sampler2D blurTexture2; - uniform sampler2D blurTexture3; - uniform sampler2D blurTexture4; - uniform sampler2D blurTexture5; - uniform float bloomStrength; - uniform float bloomRadius; - uniform float bloomFactors[NUM_MIPS]; - uniform vec3 bloomTintColors[NUM_MIPS]; - - float lerpBloomFactor(const in float factor) { - float mirrorFactor = 1.2 - factor; - return mix(factor, mirrorFactor, bloomRadius); - } - - void main() { - gl_FragColor = bloomStrength * ( lerpBloomFactor(bloomFactors[0]) * vec4(bloomTintColors[0], 1.0) * texture2D(blurTexture1, vUv) + - lerpBloomFactor(bloomFactors[1]) * vec4(bloomTintColors[1], 1.0) * texture2D(blurTexture2, vUv) + - lerpBloomFactor(bloomFactors[2]) * vec4(bloomTintColors[2], 1.0) * texture2D(blurTexture3, vUv) + - lerpBloomFactor(bloomFactors[3]) * vec4(bloomTintColors[3], 1.0) * texture2D(blurTexture4, vUv) + - lerpBloomFactor(bloomFactors[4]) * vec4(bloomTintColors[4], 1.0) * texture2D(blurTexture5, vUv) ); - }`})}}Wi.BlurDirectionX=new Mt(1,0);Wi.BlurDirectionY=new Mt(0,1);function yg(){const t=new Float32Array(6e3),e=new Float32Array(2e3*3);for(let r=0;r<2e3;r++){const a=Math.random()*Math.PI*2,o=Math.acos(2*Math.random()-1),l=600+Math.random()*400;t[r*3]=l*Math.sin(o)*Math.cos(a),t[r*3+1]=l*Math.sin(o)*Math.sin(a),t[r*3+2]=l*Math.cos(o);const c=Math.random();e[r*3]=.55+c*.25,e[r*3+1]=.55+c*.15,e[r*3+2]=.75+c*.25}const n=new ge;n.setAttribute("position",new ue(t,3)),n.setAttribute("color",new ue(e,3));const s=new ni({size:1.6,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.6,depthWrite:!1,blending:Be});return new Fi(n,s)}function Eg(i){const t=new tu;t.background=new rt(328975),t.fog=new xr(657946,.0035);const e=new je(60,i.clientWidth/i.clientHeight,.1,2e3);e.position.set(0,30,80);const n=new tg({antialias:!0,alpha:!0,powerPreference:"high-performance"});n.setSize(i.clientWidth,i.clientHeight),n.setPixelRatio(Math.min(window.devicePixelRatio,2)),n.toneMapping=Vl,n.toneMappingExposure=1.25,i.appendChild(n.domElement);const s=new ng(e,n.domElement);s.enableDamping=!0,s.dampingFactor=.05,s.rotateSpeed=.5,s.zoomSpeed=.8,s.minDistance=10,s.maxDistance=500,s.autoRotate=!0,s.autoRotateSpeed=.3;const r=new xg(n);r.addPass(new Mg(t,e));const a=new Wi(new Mt(i.clientWidth,i.clientHeight),.55,.6,.2);r.addPass(a);const o=new du(2763354,.7);t.add(o);const l=new Ko(6514417,1.8,240);l.position.set(50,50,50),t.add(l);const c=new Ko(11032055,1.2,240);c.position.set(-50,-30,-50),t.add(c);const h=yg();t.add(h);const d=new mu;d.params.Points={threshold:2};const p=new Mt;return{scene:t,camera:e,renderer:n,controls:s,composer:r,bloomPass:a,raycaster:d,mouse:p,lights:{ambient:o,point1:l,point2:c},starfield:h}}function bg(i,t){const e=t.clientWidth,n=t.clientHeight;i.camera.aspect=e/n,i.camera.updateProjectionMatrix(),i.renderer.setSize(e,n),i.composer.setSize(e,n)}function Tg(i){i.scene.traverse(t=>{var e;(t instanceof Me||t instanceof su)&&((e=t.geometry)==null||e.dispose(),Array.isArray(t.material)?t.material.forEach(n=>n.dispose()):t.material&&t.material.dispose())}),i.renderer.dispose(),i.composer.dispose()}class wg{constructor(t){zt(this,"positions");zt(this,"velocities");zt(this,"running",!0);zt(this,"step",0);zt(this,"repulsionStrength",500);zt(this,"attractionStrength",.01);zt(this,"dampening",.9);zt(this,"baseMaxSteps",300);zt(this,"maxSteps",300);zt(this,"cooldownExtension",0);this.positions=t,this.velocities=new Map;for(const e of t.keys())this.velocities.set(e,new C)}addNode(t,e){this.positions.set(t,e.clone()),this.velocities.set(t,new C),this.cooldownExtension=100,this.maxSteps=Math.max(this.maxSteps,this.step+this.cooldownExtension),this.running=!0}removeNode(t){this.positions.delete(t),this.velocities.delete(t)}tick(t){if(!this.running)return;if(this.step>this.maxSteps){this.cooldownExtension>0&&(this.cooldownExtension=0,this.maxSteps=this.baseMaxSteps);return}this.step++;const e=Math.max(.001,1-this.step/this.maxSteps),n=Array.from(this.positions.keys());for(let s=0;s=.7?"active":i>=.4?"dormant":i>=.1?"silent":"unavailable"}const Ka={active:"#10b981",dormant:"#f59e0b",silent:"#8b5cf6",unavailable:"#6b7280"},Rg={active:"Easily retrievable (retention ≥ 70%)",dormant:"Retrievable with effort (40–70%)",silent:"Difficult, needs cues (10–40%)",unavailable:"Needs reinforcement (< 10%)"};function Cl(i,t){return t==="state"?Ka[Ag(i.retention)]:zl[i.type]||"#8B95A5"}let os=null;function Cg(){if(os)return os;const i=128,t=document.createElement("canvas");t.width=i,t.height=i;const e=t.getContext("2d");if(!e)return os=new be,os;const n=e.createRadialGradient(i/2,i/2,0,i/2,i/2,i/2);n.addColorStop(0,"rgba(255, 255, 255, 1.0)"),n.addColorStop(.25,"rgba(255, 255, 255, 0.7)"),n.addColorStop(.55,"rgba(255, 255, 255, 0.2)"),n.addColorStop(1,"rgba(255, 255, 255, 0.0)"),e.fillStyle=n,e.fillRect(0,0,i,i);const s=new uc(t);return s.needsUpdate=!0,os=s,s}function Pl(i){if(i===0||i===1)return i;const t=.3;return Math.pow(2,-10*i)*Math.sin((i-t/4)*(2*Math.PI)/t)+1}function Pg(i){return i*i*((1.70158+1)*i-1.70158)}class Dg{constructor(){zt(this,"group");zt(this,"meshMap",new Map);zt(this,"glowMap",new Map);zt(this,"positions",new Map);zt(this,"labelSprites",new Map);zt(this,"hoveredNode",null);zt(this,"selectedNode",null);zt(this,"colorMode","type");zt(this,"materializingNodes",[]);zt(this,"dissolvingNodes",[]);zt(this,"growingNodes",[]);this.group=new Pi}setColorMode(t){if(this.colorMode!==t){this.colorMode=t;for(const[e,n]of this.meshMap){const s=n.userData.retention??0,a={type:n.userData.type??"fact",retention:s},o=Cl(a,t),l=new rt(o),c=n.material;c.color.copy(l),c.emissive.copy(l);const h=this.glowMap.get(e);h&&h.material.color.copy(l)}}}createNodes(t){const e=(1+Math.sqrt(5))/2,n=t.length;for(let s=0;s0,o=new Mr(s,16,16),l=new ou({color:new rt(r),emissive:new rt(r),emissiveIntensity:a?0:.3+t.retention*.5,roughness:.3,metalness:.1,transparent:!0,opacity:a?.2:.3+t.retention*.7}),c=new Me(o,l);c.position.copy(e),c.scale.setScalar(n),c.userData={nodeId:t.id,type:t.type,retention:t.retention},this.meshMap.set(t.id,c),this.group.add(c);const h=new ar({map:Cg(),color:new rt(r),transparent:!0,opacity:n>0?a?.1:.3+t.retention*.35:0,blending:Be,depthWrite:!1}),d=new Vs(h);d.scale.set(s*6*n,s*6*n,1),d.position.copy(e),d.userData={isGlow:!0,nodeId:t.id},this.glowMap.set(t.id,d),this.group.add(d);const p=t.label||t.type,u=this.createTextSprite(p,"#94a3b8");return u.position.copy(e),u.position.y+=s*2+1.5,u.userData={isLabel:!0,nodeId:t.id,offset:s*2+1.5},this.group.add(u),this.labelSprites.set(t.id,u),{mesh:c,glow:d,label:u,size:s}}addNode(t,e){const n=(e==null?void 0:e.clone())??new C((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40);this.positions.set(t.id,n);const{mesh:s,glow:r,label:a}=this.createNodeMeshes(t,n,0);return s.scale.setScalar(.001),r.scale.set(.001,.001,1),r.material.opacity=0,a.material.opacity=0,this.materializingNodes.push({id:t.id,frame:0,totalFrames:30,mesh:s,glow:r,label:a,targetScale:.5+t.retention*2}),n}removeNode(t){const e=this.meshMap.get(t),n=this.glowMap.get(t),s=this.labelSprites.get(t);!e||!n||!s||(this.materializingNodes=this.materializingNodes.filter(r=>r.id!==t),this.dissolvingNodes.push({id:t,frame:0,totalFrames:60,mesh:e,glow:n,label:s,originalScale:e.scale.x}))}growNode(t,e){const n=this.meshMap.get(t);if(!n)return;const s=n.scale.x,r=.5+e*2;n.userData.retention=e,this.growingNodes.push({id:t,frame:0,totalFrames:30,startScale:s,targetScale:r})}createTextSprite(t,e){const n=document.createElement("canvas"),s=n.getContext("2d");if(!s){const f=new be;return new Vs(new ar({map:f,transparent:!0,opacity:0}))}n.width=512,n.height=64;const r=t.length>40?t.slice(0,37)+"...":t;s.clearRect(0,0,n.width,n.height),s.font='600 22px -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif';const o=s.measureText(r).width,c=Math.min(o+14*2,n.width-4),h=40,d=(n.width-c)/2,p=(n.height-h)/2,u=h/2;s.fillStyle="rgba(10, 16, 28, 0.82)",s.beginPath(),s.moveTo(d+u,p),s.lineTo(d+c-u,p),s.quadraticCurveTo(d+c,p,d+c,p+u),s.lineTo(d+c,p+h-u),s.quadraticCurveTo(d+c,p+h,d+c-u,p+h),s.lineTo(d+u,p+h),s.quadraticCurveTo(d,p+h,d,p+h-u),s.lineTo(d,p+u),s.quadraticCurveTo(d,p,d+u,p),s.closePath(),s.fill(),s.strokeStyle="rgba(148, 163, 184, 0.18)",s.lineWidth=1,s.stroke(),s.textAlign="center",s.textBaseline="middle",s.fillStyle=e,s.fillText(r,n.width/2,n.height/2+1);const g=new uc(n);g.needsUpdate=!0;const v=new ar({map:g,transparent:!0,opacity:0,depthTest:!1,sizeAttenuation:!0}),m=new Vs(v);return m.scale.set(9,1.2,1),m}updatePositions(){this.group.children.forEach(t=>{if(t.userData.nodeId){const e=this.positions.get(t.userData.nodeId);if(!e)return;t.userData.isGlow?t.position.copy(e):t.userData.isLabel?(t.position.copy(e),t.position.y+=t.userData.offset):t instanceof Me&&t.position.copy(e)}})}animate(t,e,n){var r,a;for(let o=this.materializingNodes.length-1;o>=0;o--){const l=this.materializingNodes[o];l.frame++;const c=Math.min(l.frame/l.totalFrames,1),h=Pl(c);if(l.mesh.scale.setScalar(Math.max(.001,h)),l.frame>=5){const d=Math.min((l.frame-5)/5,1),p=l.glow.material;p.opacity=d*.4;const u=l.targetScale*6*h;l.glow.scale.set(u,u,1)}if(l.frame>=40){const d=Math.min((l.frame-40)/20,1);l.label.material.opacity=d*.9}l.frame>=60&&this.materializingNodes.splice(o,1)}for(let o=this.dissolvingNodes.length-1;o>=0;o--){const l=this.dissolvingNodes[o];l.frame++;const c=Math.min(l.frame/l.totalFrames,1),h=1-Pg(c),d=Math.max(.001,l.originalScale*h);l.mesh.scale.setScalar(d);const p=d*6;l.glow.scale.set(p,p,1);const u=l.mesh.material;u.opacity*=.97,l.glow.material.opacity*=.95,l.label.material.opacity*=.93,l.frame>=l.totalFrames&&(this.group.remove(l.mesh),this.group.remove(l.glow),this.group.remove(l.label),l.mesh.geometry.dispose(),l.mesh.material.dispose(),(r=l.glow.material.map)==null||r.dispose(),l.glow.material.dispose(),(a=l.label.material.map)==null||a.dispose(),l.label.material.dispose(),this.meshMap.delete(l.id),this.glowMap.delete(l.id),this.labelSprites.delete(l.id),this.positions.delete(l.id),this.dissolvingNodes.splice(o,1))}for(let o=this.growingNodes.length-1;o>=0;o--){const l=this.growingNodes[o];l.frame++;const c=Math.min(l.frame/l.totalFrames,1),h=l.startScale+(l.targetScale-l.startScale)*Pl(c),d=this.meshMap.get(l.id);d&&d.scale.setScalar(h);const p=this.glowMap.get(l.id);if(p){const u=h*6;p.scale.set(u,u,1)}l.frame>=l.totalFrames&&this.growingNodes.splice(o,1)}const s=new Set([...this.materializingNodes.map(o=>o.id),...this.dissolvingNodes.map(o=>o.id),...this.growingNodes.map(o=>o.id)]);this.meshMap.forEach((o,l)=>{if(s.has(l))return;const c=e.find(p=>p.id===l);if(!c)return;const h=1+Math.sin(t*1.5+e.indexOf(c)*.5)*.15*c.retention;o.scale.setScalar(h);const d=o.material;if(l===this.hoveredNode)d.emissiveIntensity=1;else if(l===this.selectedNode)d.emissiveIntensity=.8;else{const u=.3+c.retention*.5+Math.sin(t*(.8+c.retention*.7))*.1*c.retention;d.emissiveIntensity=u}}),this.labelSprites.forEach((o,l)=>{if(s.has(l))return;const c=this.positions.get(l);if(!c)return;const h=n.position.distanceTo(c),d=o.material,p=l===this.hoveredNode||l===this.selectedNode?1:h<40?.9:h<80?.9*(1-(h-40)/40):0;d.opacity+=(p-d.opacity)*.1})}getMeshes(){return Array.from(this.meshMap.values())}dispose(){this.group.traverse(t=>{var e,n,s,r,a;t instanceof Me?((e=t.geometry)==null||e.dispose(),(n=t.material)==null||n.dispose()):t instanceof Vs&&((r=(s=t.material)==null?void 0:s.map)==null||r.dispose(),(a=t.material)==null||a.dispose())}),this.materializingNodes=[],this.dissolvingNodes=[],this.growingNodes=[]}}function Lg(i){return 1-Math.pow(1-i,3)}class Ug{constructor(){zt(this,"group");zt(this,"growingEdges",[]);zt(this,"dissolvingEdges",[]);this.group=new Pi}createEdges(t,e){for(const n of t){const s=e.get(n.source),r=e.get(n.target);if(!s||!r)continue;const a=[s,r],o=new ge().setFromPoints(a),l=new mr({color:9133302,transparent:!0,opacity:Math.min(.25+n.weight*.5,.8),blending:Be,depthWrite:!1}),c=new Ya(o,l);c.userData={source:n.source,target:n.target},this.group.add(c)}}addEdge(t,e){const n=e.get(t.source),s=e.get(t.target);if(!n||!s)return;const r=[n.clone(),n.clone()],a=new ge().setFromPoints(r),o=new mr({color:9133302,transparent:!0,opacity:0,blending:Be,depthWrite:!1}),l=new Ya(a,o);l.userData={source:t.source,target:t.target},this.group.add(l),this.growingEdges.push({line:l,source:t.source,target:t.target,frame:0,totalFrames:45})}removeEdgesForNode(t){const e=[];this.group.children.forEach(n=>{const s=n;(s.userData.source===t||s.userData.target===t)&&e.push(s)});for(const n of e)this.growingEdges=this.growingEdges.filter(s=>s.line!==n),this.dissolvingEdges.push({line:n,frame:0,totalFrames:40})}animateEdges(t){for(let e=this.growingEdges.length-1;e>=0;e--){const n=this.growingEdges[e];n.frame++;const s=Lg(Math.min(n.frame/n.totalFrames,1)),r=t.get(n.source),a=t.get(n.target);if(!r||!a)continue;const o=r.clone().lerp(a,s),l=n.line.geometry.attributes.position;l.setXYZ(0,r.x,r.y,r.z),l.setXYZ(1,o.x,o.y,o.z),l.needsUpdate=!0;const c=n.line.material;c.opacity=s*.65,n.frame>=n.totalFrames&&(c.opacity=.65,this.growingEdges.splice(e,1))}for(let e=this.dissolvingEdges.length-1;e>=0;e--){const n=this.dissolvingEdges[e];n.frame++;const s=n.frame/n.totalFrames,r=n.line.material;r.opacity=Math.max(0,.65*(1-s)),n.frame>=n.totalFrames&&(this.group.remove(n.line),n.line.geometry.dispose(),n.line.material.dispose(),this.dissolvingEdges.splice(e,1))}}updatePositions(t){this.group.children.forEach(e=>{const n=e;if(this.growingEdges.some(a=>a.line===n)||this.dissolvingEdges.some(a=>a.line===n))return;const s=t.get(n.userData.source),r=t.get(n.userData.target);if(s&&r){const a=n.geometry.attributes.position;a.setXYZ(0,s.x,s.y,s.z),a.setXYZ(1,r.x,r.y,r.z),a.needsUpdate=!0}})}dispose(){this.group.children.forEach(t=>{var n,s;const e=t;(n=e.geometry)==null||n.dispose(),(s=e.material)==null||s.dispose()}),this.growingEdges=[],this.dissolvingEdges=[]}}class Ig{constructor(t){zt(this,"starField");zt(this,"neuralParticles");this.starField=this.createStarField(),this.neuralParticles=this.createNeuralParticles(),t.add(this.starField),t.add(this.neuralParticles)}createStarField(){const e=new ge,n=new Float32Array(3e3*3),s=new Float32Array(3e3);for(let a=0;a<3e3;a++)n[a*3]=(Math.random()-.5)*1e3,n[a*3+1]=(Math.random()-.5)*1e3,n[a*3+2]=(Math.random()-.5)*1e3,s[a]=Math.random()*1.5;e.setAttribute("position",new ue(n,3)),e.setAttribute("size",new ue(s,1));const r=new ni({color:6514417,size:.5,transparent:!0,opacity:.4,sizeAttenuation:!0,blending:Be});return new Fi(e,r)}createNeuralParticles(){const e=new ge,n=new Float32Array(500*3),s=new Float32Array(500*3);for(let a=0;a<500;a++)n[a*3]=(Math.random()-.5)*100,n[a*3+1]=(Math.random()-.5)*100,n[a*3+2]=(Math.random()-.5)*100,s[a*3]=.4+Math.random()*.3,s[a*3+1]=.3+Math.random()*.2,s[a*3+2]=.8+Math.random()*.2;e.setAttribute("position",new ue(n,3)),e.setAttribute("color",new ue(s,3));const r=new ni({size:.3,vertexColors:!0,transparent:!0,opacity:.4,blending:Be,sizeAttenuation:!0});return new Fi(e,r)}animate(t){this.starField.rotation.y+=1e-4,this.starField.rotation.x+=5e-5;const e=this.neuralParticles.geometry.attributes.position;for(let n=0;n=0;s--){const r=this.pulseEffects[s];if(r.intensity-=r.decay,r.intensity<=0){this.pulseEffects.splice(s,1);continue}const a=t.get(r.nodeId);if(a){const o=a.material;o.emissive.lerp(r.color,r.intensity*.3),o.emissiveIntensity=Math.max(o.emissiveIntensity,r.intensity)}}for(let s=this.spawnBursts.length-1;s>=0;s--){const r=this.spawnBursts[s];if(r.age++,r.age>120){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),this.spawnBursts.splice(s,1);continue}const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity;for(let c=0;c=0;s--){const r=this.rainbowBursts[s];if(r.age++,r.age>r.maxAge){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),this.rainbowBursts.splice(s,1);continue}const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity;for(let p=0;p=0;s--){const r=this.rippleWaves[s];if(r.age++,r.radius+=r.speed,r.age>r.maxAge){this.rippleWaves.splice(s,1);continue}const a=r.radius,o=3;n.forEach((l,c)=>{if(r.pulsedNodes.has(c))return;const h=l.distanceTo(r.origin);h>=a-o&&h<=a+o&&(r.pulsedNodes.add(c),this.addPulse(c,.8,new rt(65489),.03))})}for(let s=this.implosions.length-1;s>=0;s--){const r=this.implosions[s];if(r.age++,r.age>r.maxAge+20){this.scene.remove(r.particles),r.particles.geometry.dispose(),r.particles.material.dispose(),r.flash&&(this.scene.remove(r.flash),r.flash.geometry.dispose(),r.flash.material.dispose()),this.implosions.splice(s,1);continue}if(r.age<=r.maxAge){const a=r.particles.geometry.attributes.position,o=r.particles.geometry.attributes.velocity,l=1+r.age*.02;for(let h=0;hr.maxAge){const a=(r.age-r.maxAge)/20;r.flash.material.opacity=Math.max(0,1-a),r.flash.scale.setScalar(1+a*3)}}for(let s=this.shockwaves.length-1;s>=0;s--){const r=this.shockwaves[s];if(r.age++,r.age>r.maxAge){this.scene.remove(r.mesh),r.mesh.geometry.dispose(),r.mesh.material.dispose(),this.shockwaves.splice(s,1);continue}const a=r.age/r.maxAge;r.mesh.scale.setScalar(1+a*20),r.mesh.material.opacity=.8*(1-a),r.mesh.lookAt(e.position)}for(let s=this.connectionFlashes.length-1;s>=0;s--){const r=this.connectionFlashes[s];if(r.intensity-=.015,r.intensity<=0){this.scene.remove(r.line),r.line.geometry.dispose(),r.line.material.dispose(),this.connectionFlashes.splice(s,1);continue}r.line.material.opacity=r.intensity}}dispose(){for(const t of this.spawnBursts)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose();for(const t of this.rainbowBursts)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose();for(const t of this.implosions)this.scene.remove(t.particles),t.particles.geometry.dispose(),t.particles.material.dispose(),t.flash&&(this.scene.remove(t.flash),t.flash.geometry.dispose(),t.flash.material.dispose());for(const t of this.shockwaves)this.scene.remove(t.mesh),t.mesh.geometry.dispose(),t.mesh.material.dispose();for(const t of this.connectionFlashes)this.scene.remove(t.line),t.line.geometry.dispose(),t.line.material.dispose();this.pulseEffects=[],this.spawnBursts=[],this.rainbowBursts=[],this.rippleWaves=[],this.implosions=[],this.shockwaves=[],this.connectionFlashes=[]}}const yn={bloomStrength:.8,rotateSpeed:.3,fogColor:328976,fogDensity:.008,nebulaIntensity:0,chromaticIntensity:.002,vignetteRadius:.9,breatheAmplitude:1},Nn={bloomStrength:1.8,rotateSpeed:.08,fogColor:656672,fogDensity:.006,nebulaIntensity:1,chromaticIntensity:.005,vignetteRadius:.7,breatheAmplitude:2};class Fg{constructor(){zt(this,"active",!1);zt(this,"transition",0);zt(this,"transitionSpeed",.008);zt(this,"current");zt(this,"auroraHue",0);this.current={...yn}}setActive(t){this.active=t}update(t,e,n,s,r){const a=this.active?1:0;this.transition+=(a-this.transition)*this.transitionSpeed*60*(1/60),this.transition=Math.max(0,Math.min(1,this.transition));const o=this.transition;this.current.bloomStrength=this.lerp(yn.bloomStrength,Nn.bloomStrength,o),this.current.rotateSpeed=this.lerp(yn.rotateSpeed,Nn.rotateSpeed,o),this.current.fogDensity=this.lerp(yn.fogDensity,Nn.fogDensity,o),this.current.nebulaIntensity=this.lerp(yn.nebulaIntensity,Nn.nebulaIntensity,o),this.current.chromaticIntensity=this.lerp(yn.chromaticIntensity,Nn.chromaticIntensity,o),this.current.vignetteRadius=this.lerp(yn.vignetteRadius,Nn.vignetteRadius,o),this.current.breatheAmplitude=this.lerp(yn.breatheAmplitude,Nn.breatheAmplitude,o),e.strength=this.current.bloomStrength,n.autoRotateSpeed=this.current.rotateSpeed;const l=new rt(yn.fogColor),c=new rt(Nn.fogColor),h=l.clone().lerp(c,o);if(t.fog=new xr(h,this.current.fogDensity),o>.01){this.auroraHue=r*.1%1;const d=new rt().setHSL(.75+this.auroraHue*.15,.8,.5),p=new rt().setHSL(.55+this.auroraHue*.2,.7,.4);s.point1.color.lerp(d,o*.3),s.point2.color.lerp(p,o*.3)}else s.point1.color.set(6514417),s.point2.color.set(11032055)}lerp(t,e,n){return t+(e-t)*n}}const Og=50,hs=[];function Bg(i,t,e){const n=i.tags??[],s=i.type??"";let r=null,a=0;for(const o of t){let l=0;o.type===s&&(l+=2);for(const c of o.tags)n.includes(c)&&(l+=1);l>a&&(a=l,r=o.id)}if(r&&a>0){const o=e.get(r);if(o)return new C(o.x+(Math.random()-.5)*10,o.y+(Math.random()-.5)*10,o.z+(Math.random()-.5)*10)}return new C((Math.random()-.5)*40,(Math.random()-.5)*40,(Math.random()-.5)*40)}function zg(i,t){if(hs.length<=Og)return;const e=hs.shift();i.edgeManager.removeEdgesForNode(e),i.nodeManager.removeNode(e),i.forceSim.removeNode(e),i.onMutation({type:"edgesRemoved",nodeId:e}),i.onMutation({type:"nodeRemoved",nodeId:e});const n=t.findIndex(s=>s.id===e);n!==-1&&t.splice(n,1)}function kg(i,t,e){var d,p;const{effects:n,nodeManager:s,edgeManager:r,forceSim:a,camera:o,onMutation:l}=t,c=s.positions,h=s.meshMap;switch(i.type){case"MemoryCreated":{const u=i.data;if(!u.id)break;const g={id:u.id,label:(u.content??"").slice(0,60),type:u.node_type??"fact",retention:Math.max(0,Math.min(1,u.retention??.9)),tags:u.tags??[],createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),isCenter:!1},v=Bg(g,e,c),m=s.addNode(g,v);a.addNode(u.id,m),hs.push(u.id),zg(t,e);const f=new rt(zl[g.type]||"#00ffd1");n.createRainbowBurst(v,f),n.createShockwave(v,f,o);const T=f.clone();T.offsetHSL(.15,0,0),setTimeout(()=>{n.createShockwave(v,T,o)},166),n.createRippleWave(v),l({type:"nodeAdded",node:g});break}case"ConnectionDiscovered":{const u=i.data;if(!u.source_id||!u.target_id)break;const g=c.get(u.source_id),v=c.get(u.target_id),m={source:u.source_id,target:u.target_id,weight:u.weight??.5,type:u.connection_type??"semantic"};r.addEdge(m,c),g&&v&&n.createConnectionFlash(g,v,new rt(54527)),u.source_id&&h.has(u.source_id)&&n.addPulse(u.source_id,1,new rt(54527),.02),u.target_id&&h.has(u.target_id)&&n.addPulse(u.target_id,1,new rt(54527),.02),l({type:"edgeAdded",edge:m});break}case"MemoryDeleted":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){const m=new rt(16729943);n.createImplosion(g,m)}r.removeEdgesForNode(u.id),s.removeNode(u.id),a.removeNode(u.id);const v=hs.indexOf(u.id);v!==-1&&hs.splice(v,1),l({type:"edgesRemoved",nodeId:u.id}),l({type:"nodeRemoved",nodeId:u.id});break}case"MemoryPromoted":{const u=i.data,g=u==null?void 0:u.id;if(!g)break;const v=u.new_retention??.95;if(h.has(g)){s.growNode(g,v),n.addPulse(g,1.2,new rt(65416),.01);const m=c.get(g);m&&(n.createShockwave(m,new rt(65416),o),n.createSpawnBurst(m,new rt(65416))),l({type:"nodeUpdated",nodeId:g,retention:v})}break}case"MemoryDemoted":{const u=i.data,g=u==null?void 0:u.id;if(!g)break;const v=u.new_retention??.3;h.has(g)&&(s.growNode(g,v),n.addPulse(g,.8,new rt(16729943),.03),l({type:"nodeUpdated",nodeId:g,retention:v}));break}case"MemoryUpdated":{const u=i.data,g=u==null?void 0:u.id;if(!g||!h.has(g))break;n.addPulse(g,.6,new rt(8490232),.02),u.retention!==void 0&&(s.growNode(g,u.retention),l({type:"nodeUpdated",nodeId:g,retention:u.retention}));break}case"SearchPerformed":{h.forEach((u,g)=>{n.addPulse(g,.6+Math.random()*.4,new rt(8490232),.02)});break}case"DreamStarted":{h.forEach((u,g)=>{n.addPulse(g,1,new rt(11032055),.005)});break}case"DreamProgress":{const u=(d=i.data)==null?void 0:d.memory_id;u&&h.has(u)&&n.addPulse(u,1.5,new rt(12616956),.01);break}case"DreamCompleted":{n.createSpawnBurst(new C(0,0,0),new rt(11032055)),n.createShockwave(new C(0,0,0),new rt(11032055),o);break}case"RetentionDecayed":{const u=(p=i.data)==null?void 0:p.id;u&&h.has(u)&&n.addPulse(u,.8,new rt(16729943),.03);break}case"ConsolidationCompleted":{h.forEach((u,g)=>{n.addPulse(g,.4+Math.random()*.3,new rt(16758784),.015)});break}case"ActivationSpread":{const u=i.data;if(u.source_id&&u.target_ids){const g=c.get(u.source_id);if(g)for(const v of u.target_ids){const m=c.get(v);m&&n.createConnectionFlash(g,m,new rt(1370310))}}break}case"MemorySuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){n.createImplosion(g,new rt(11032055));const v=Math.max(1,u.suppression_count??1),m=Math.min(.4+v*.15,1);n.addPulse(u.id,m,new rt(11032055),.04)}break}case"MemoryUnsuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);g&&h.has(u.id)&&(n.createRainbowBurst(g,new rt(65416)),n.addPulse(u.id,1,new rt(65416),.02));break}case"Rac1CascadeSwept":{const g=i.data.neighbors_affected??0;if(g===0)break;const v=Array.from(h.keys()),m=Math.min(g,v.length,12);for(let f=0;f');function $g(i,t){fs(t,!0);let e=cs(t,"events",19,()=>[]),n=cs(t,"isDreaming",3,!1),s=cs(t,"colorMode",3,"type");Dc(()=>{l==null||l.setColorMode(s())});let r,a,o,l,c,h,d,p,u,g,v,m=0,f=[];Ll(()=>{a=Eg(r),g=Gg(a.scene).material,v=jg(a.composer),h=new Ig(a.scene),l=new Dg,l.colorMode=s(),c=new Ug,d=new Ng(a.scene),u=new Fg;const y=l.createNodes(t.nodes);c.createEdges(t.edges,y),p=new wg(y),f=[...t.nodes],a.scene.add(c.group),a.scene.add(l.group),T(),window.addEventListener("resize",S),r.addEventListener("pointermove",D),r.addEventListener("click",w)}),Ul(()=>{cancelAnimationFrame(o),window.removeEventListener("resize",S),r==null||r.removeEventListener("pointermove",D),r==null||r.removeEventListener("click",w),d==null||d.dispose(),h==null||h.dispose(),l==null||l.dispose(),c==null||c.dispose(),a&&Tg(a)});function T(){o=requestAnimationFrame(T);const U=performance.now()*.001;p.tick(t.edges),l.updatePositions(),c.updatePositions(l.positions),c.animateEdges(l.positions),h.animate(U),l.animate(U,f,a.camera),u.setActive(n()),u.update(a.scene,a.bloomPass,a.controls,a.lights,U),Wg(g,U,u.current.nebulaIntensity,r.clientWidth,r.clientHeight),Zg(v,U,u.current.nebulaIntensity),b(),d.update(l.meshMap,a.camera,l.positions),a.controls.update(),a.composer.render()}function b(){if(!e()||e().length<=m)return;const U=e().slice(m);m=e().length;const y={effects:d,nodeManager:l,edgeManager:c,forceSim:p,camera:a.camera,onMutation:M=>{var P;M.type==="nodeAdded"?f=[...f,M.node]:M.type==="nodeRemoved"&&(f=f.filter(W=>W.id!==M.nodeId)),(P=t.onGraphMutation)==null||P.call(t,M)}};for(const M of U)kg(M,y,f)}function S(){!r||!a||bg(a,r)}function D(U){const y=r.getBoundingClientRect();a.mouse.x=(U.clientX-y.left)/y.width*2-1,a.mouse.y=-((U.clientY-y.top)/y.height)*2+1,a.raycaster.setFromCamera(a.mouse,a.camera);const M=a.raycaster.intersectObjects(l.getMeshes());M.length>0?(l.hoveredNode=M[0].object.userData.nodeId,r.style.cursor="pointer"):(l.hoveredNode=null,r.style.cursor="grab")}function w(){var U;if(l.hoveredNode){l.selectedNode=l.hoveredNode,(U=t.onSelect)==null||U.call(t,l.hoveredNode);const y=l.positions.get(l.hoveredNode);y&&a.controls.target.lerp(y.clone(),.5)}}var R=Kg();Bc(R,U=>r=U,()=>r),we(i,R),ps()}var Jg=Ue('
          '),Qg=Ue('
          ');function t_(i,t){fs(t,!0);let e=cs(t,"width",3,240),n=cs(t,"height",3,80);function s(m){return t.stability<=0?0:Math.exp(-m/t.stability)}let r=ei(()=>{const m=[],f=Math.max(t.stability*3,30),T=4,b=e()-T*2,S=n()-T*2;for(let D=0;D<=50;D++){const w=D/50*f,R=s(w),U=T+D/50*b,y=T+(1-R)*S;m.push(`${D===0?"M":"L"}${U.toFixed(1)},${y.toFixed(1)}`)}return m.join(" ")}),a=ei(()=>[{label:"Now",days:0,value:t.retention},{label:"1d",days:1,value:s(1)},{label:"7d",days:7,value:s(7)},{label:"30d",days:30,value:s(30)}]);function o(m){return m>.7?"#10b981":m>.4?"#f59e0b":"#ef4444"}var l=Qg(),c=At(l),h=At(c),d=Dt(h),p=Dt(d),u=Dt(p),g=Dt(u);Lc(),bt(c);var v=Dt(c,2);cr(v,21,()=>V(a),sa,(m,f)=>{var T=Jg(),b=At(T),S=At(b);bt(b);var D=Dt(b,2),w=At(D);bt(D),bt(T),on((R,U)=>{me(S,`${V(f).label??""}:`),$a(D,`color: ${R??""}`),me(w,`${U??""}%`)},[()=>o(V(f).value),()=>(V(f).value*100).toFixed(0)]),we(m,T)}),bt(v),bt(l),on(m=>{Pe(c,"width",e()),Pe(c,"height",n()),Pe(c,"viewBox",`0 0 ${e()??""} ${n()??""}`),Pe(h,"y1",4+(n()-8)*.5),Pe(h,"x2",e()-4),Pe(h,"y2",4+(n()-8)*.5),Pe(d,"y1",4+(n()-8)*.8),Pe(d,"x2",e()-4),Pe(d,"y2",4+(n()-8)*.8),Pe(p,"d",V(r)),Pe(u,"d",`${V(r)??""} L${e()-4},${n()-4} L4,${n()-4} Z`),Pe(g,"cy",4+(1-t.retention)*(n()-8)),Pe(g,"fill",m)},[()=>o(t.retention)]),we(i,l),ps()}function Dl(i,t,e){const n=e.getTime(),s=new Set,r=new Map,a=i.filter(l=>{const c=new Date(l.createdAt).getTime();if(c<=n){s.add(l.id);const h=n-c,d=1440*60*1e3,p=hs.has(l.source)&&s.has(l.target));return{visibleNodes:a,visibleEdges:o,nodeOpacities:r}}function e_(i){if(i.length===0){const n=new Date;return{oldest:n,newest:n}}let t=1/0,e=-1/0;for(const n of i){const s=new Date(n.createdAt).getTime();se&&(e=s)}return{oldest:new Date(t),newest:new Date(e)}}var n_=Ue(`
          `),i_=Ue('');function s_(i,t){fs(t,!0);let e=De(!1),n=De(!1),s=De(1),r=De(100),a,o=0,l=ei(()=>e_(t.nodes)),c=ei(()=>{const b=V(l).oldest.getTime(),D=V(l).newest.getTime()-b||1;return new Date(b+V(r)/100*D)});function h(b){return b.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function d(){te(e,!V(e)),t.onToggle(V(e)),V(e)&&(te(r,100),t.onDateChange(V(c)))}function p(){te(n,!V(n)),V(n)?(te(r,0),o=performance.now(),u()):cancelAnimationFrame(a)}function u(){V(n)&&(a=requestAnimationFrame(b=>{const S=(b-o)/1e3;o=b;const D=V(l).oldest.getTime(),R=(V(l).newest.getTime()-D)/(1440*60*1e3)||1,U=V(s)/R*100;if(te(r,Math.min(100,V(r)+U*S),!0),t.onDateChange(V(c)),V(r)>=100){te(n,!1);return}u()}))}function g(){t.onDateChange(V(c))}Ul(()=>{te(n,!1),cancelAnimationFrame(a)});var v=Ic(),m=Il(v);{var f=b=>{var S=n_(),D=At(S),w=At(D),R=At(w),U=At(R),y=At(U,!0);bt(U);var M=Dt(U,2),P=At(M);P.value=P.__value=1;var W=Dt(P);W.value=W.__value=7;var z=Dt(W);z.value=z.__value=30,bt(M),bt(R);var G=Dt(R,2),$=At(G,!0);bt(G);var X=Dt(G,2);bt(w);var Q=Dt(w,2);Fl(Q);var k=Dt(Q,2),it=At(k),ft=At(it,!0);bt(it);var St=Dt(it,2),Ft=At(St,!0);bt(St),bt(k),bt(D),bt(S),on((Vt,Y,nt)=>{me(y,V(n)?"⏸":"▶"),me($,Vt),me(ft,Y),me(Ft,nt)},[()=>h(V(c)),()=>h(V(l).oldest),()=>h(V(l).newest)]),Ge("click",U,p),Bl(M,()=>V(s),Vt=>te(s,Vt)),Ge("click",X,d),Ge("input",Q,g),Ol(Q,()=>V(r),Vt=>te(r,Vt)),we(b,S)},T=b=>{var S=i_();Ge("click",S,d),we(b,S)};Zn(m,b=>{V(e)?b(f):b(T,!1)})}we(i,v),ps()}Nl(["click","input"]);var r_=Ue('
          '),a_=Ue('
          FSRS accessibility
          ');function o_(i,t){fs(t,!1);const e=["active","dormant","silent","unavailable"];zc();var n=a_(),s=Dt(At(n),2);cr(s,1,()=>e,r=>r,(r,a)=>{var o=r_(),l=At(o),c=Dt(l,2),h=At(c,!0);bt(c);var d=Dt(c,2),p=At(d,!0);bt(d),bt(o),on(u=>{$a(l,`background: ${Ka[V(a)]??""}; box-shadow: 0 0 6px ${Ka[V(a)]??""}55;`),me(h,V(a)),me(p,u)},[()=>{var u;return((u=Rg[V(a)].match(/\(([^)]+)\)/))==null?void 0:u[1])??""}]),we(r,o)}),bt(n),we(i,n),ps()}var l_=Ue('

          Loading memory graph...

          '),c_=Ue('

          Your Mind Awaits

          '),h_=Ue(' · · ',1),u_=Ue('
          '),d_=Ue(' '),f_=Ue('
          '),p_=Ue("
          "),m_=Ue(`

          Memory Detail

          Retention Forecast
          ◬ Explore Connections
          `),g_=Ue(`
          `);function z_(i,t){fs(t,!0);const e=()=>Fc(kc,"$eventFeed",n),[n,s]=Nc();let r=De(null),a=De(null),o=De(!0),l=De(""),c=De(!1),h=De(""),d=De(150),p=De(!1),u=De(Uc(new Date)),g=De("type"),v=De(0),m=De(0),f=ei(()=>V(r)?V(p)?Dl(V(r).nodes,V(r).edges,V(u)).visibleNodes:V(r).nodes:[]),T=ei(()=>V(r)?V(p)?Dl(V(r).nodes,V(r).edges,V(u)).visibleEdges:V(r).edges:[]);function b(J){if(V(r))switch(J.type){case"nodeAdded":V(r).nodes=[...V(r).nodes,J.node],V(r).nodeCount=V(r).nodes.length,te(v,V(r).nodeCount,!0);break;case"nodeRemoved":V(r).nodes=V(r).nodes.filter(Et=>Et.id!==J.nodeId),V(r).nodeCount=V(r).nodes.length,te(v,V(r).nodeCount,!0);break;case"edgeAdded":V(r).edges=[...V(r).edges,J.edge],V(r).edgeCount=V(r).edges.length,te(m,V(r).edgeCount,!0);break;case"edgesRemoved":V(r).edges=V(r).edges.filter(Et=>Et.source!==J.nodeId&&Et.target!==J.nodeId),V(r).edgeCount=V(r).edges.length,te(m,V(r).edgeCount,!0);break;case"nodeUpdated":{const Et=V(r).nodes.find(ct=>ct.id===J.nodeId);Et&&(Et.retention=J.retention);break}}}Ll(()=>S());async function S(J,Et){var ct;te(o,!0),te(l,"");try{te(r,await Zi.graph({max_nodes:V(d),depth:3,query:J||void 0,center_id:Et||void 0}),!0),V(r)&&(te(v,V(r).nodeCount,!0),te(m,V(r).edgeCount,!0))}catch(E){const _=E instanceof Error?E.message:String(E),F=_.replace(/\/[\w./-]+\.(sqlite|rs|db|toml|lock)\b/g,"[path]").slice(0,200),q=(((ct=V(r))==null?void 0:ct.nodeCount)??0)===0&&/not found|404|empty|no memor/i.test(_);te(l,q?"No memories yet. Start using Vestige to populate your graph.":`Failed to load graph: ${F}`,!0)}finally{te(o,!1)}}async function D(){te(c,!0);try{await Zi.dream(),await S()}catch{}finally{te(c,!1)}}async function w(J){try{te(a,await Zi.memories.get(J),!0)}catch{te(a,null)}}function R(){V(h).trim()&&S(V(h))}var U=g_(),y=At(U);{var M=J=>{var Et=l_();we(J,Et)},P=J=>{var Et=c_(),ct=At(Et),E=Dt(At(ct),4),_=At(E,!0);bt(E),bt(ct),bt(Et),on(()=>me(_,V(l))),we(J,Et)},W=J=>{$g(J,{get nodes(){return V(f)},get edges(){return V(T)},get centerId(){return V(r).center_id},get events(){return e()},get isDreaming(){return V(c)},get colorMode(){return V(g)},onSelect:w,onGraphMutation:b})};Zn(y,J=>{V(o)?J(M):V(l)?J(P,1):V(r)&&J(W,2)})}var z=Dt(y,2),G=At(z),$=At(G);Fl($);var X=Dt($,2);bt(G);var Q=Dt(G,2),k=At(Q),it=At(k),ft=Dt(it,2);bt(k);var St=Dt(k,2),Ft=At(St);Ft.value=Ft.__value=50;var Vt=Dt(Ft);Vt.value=Vt.__value=100;var Y=Dt(Vt);Y.value=Y.__value=150;var nt=Dt(Y);nt.value=nt.__value=200,bt(St);var _t=Dt(St,2),ot=At(_t,!0);bt(_t);var Pt=Dt(_t,2);bt(Q),bt(z);var Lt=Dt(z,2),Ht=At(Lt);{var he=J=>{var Et=h_(),ct=Il(Et),E=At(ct);bt(ct);var _=Dt(ct,4),F=At(_);bt(_);var q=Dt(_,4),K=At(q);bt(q),on(()=>{me(E,`${V(v)??""} nodes`),me(F,`${V(m)??""} edges`),me(K,`depth ${V(r).depth??""}`)}),we(J,Et)};Zn(Ht,J=>{V(r)&&J(he)})}bt(Lt);var Yt=Dt(Lt,2);{var fe=J=>{var Et=u_(),ct=At(Et);o_(ct,{}),bt(Et),we(J,Et)};Zn(Yt,J=>{V(g)==="state"&&J(fe)})}var A=Dt(Yt,2);{var ke=J=>{s_(J,{get nodes(){return V(r).nodes},onDateChange:Et=>{te(u,Et,!0)},onToggle:Et=>{te(p,Et,!0)}})};Zn(A,J=>{V(r)&&J(ke)})}var qt=Dt(A,2);{var jt=J=>{var Et=m_(),ct=At(Et),E=Dt(At(ct),2);bt(ct);var _=Dt(ct,2),F=At(_),q=At(F),K=At(q,!0);bt(q);var j=Dt(q,2);cr(j,17,()=>V(a).tags,sa,(wt,Kt)=>{var ce=d_(),Wt=At(ce,!0);bt(ce),on(()=>me(Wt,V(Kt))),we(wt,ce)}),bt(F);var yt=Dt(F,2),lt=At(yt,!0);bt(yt);var pt=Dt(yt,2);cr(pt,21,()=>[{label:"Retention",value:V(a).retentionStrength},{label:"Storage",value:V(a).storageStrength},{label:"Retrieval",value:V(a).retrievalStrength}],sa,(wt,Kt)=>{var ce=f_(),Wt=At(ce),Ie=At(Wt),en=At(Ie,!0);bt(Ie);var Yi=Dt(Ie,2),xs=At(Yi);bt(Yi),bt(Wt);var hn=Dt(Wt,2),qi=At(hn);bt(hn),bt(ce),on(Ms=>{me(en,V(Kt).label),me(xs,`${Ms??""}%`),$a(qi,`width: ${V(Kt).value*100}%; background: ${V(Kt).value>.7?"#10b981":V(Kt).value>.4?"#f59e0b":"#ef4444"}`)},[()=>(V(Kt).value*100).toFixed(1)]),we(wt,ce)}),bt(pt);var Gt=Dt(pt,2),et=Dt(At(Gt),2);{let wt=ei(()=>V(a).storageStrength*30);t_(et,{get retention(){return V(a).retentionStrength},get stability(){return V(wt)}})}bt(Gt);var mt=Dt(Gt,2),Tt=At(mt),Ut=At(Tt);bt(Tt);var gt=Dt(Tt,2),Zt=At(gt);bt(gt);var Ot=Dt(gt,2);{var se=wt=>{var Kt=p_(),ce=At(Kt);bt(Kt),on(Wt=>me(ce,`Accessed: ${Wt??""}`),[()=>new Date(V(a).lastAccessedAt).toLocaleString()]),we(wt,Kt)};Zn(Ot,wt=>{V(a).lastAccessedAt&&wt(se)})}var L=Dt(Ot,2),at=At(L);bt(L),bt(mt);var H=Dt(mt,2),Z=At(H),dt=Dt(Z,2);bt(H);var ht=Dt(H,2);bt(_),bt(Et),on((wt,Kt)=>{me(K,V(a).nodeType),me(lt,V(a).content),me(Ut,`Created: ${wt??""}`),me(Zt,`Updated: ${Kt??""}`),me(at,`Reviews: ${V(a).reviewCount??0??""}`),Pe(ht,"href",`${Oc??""}/explore`)},[()=>new Date(V(a).createdAt).toLocaleString(),()=>new Date(V(a).updatedAt).toLocaleString()]),Ge("click",E,()=>te(a,null)),Ge("click",Z,()=>{V(a)&&Zi.memories.promote(V(a).id)}),Ge("click",dt,()=>{V(a)&&Zi.memories.demote(V(a).id)}),we(J,Et)};Zn(qt,J=>{V(a)&&J(jt)})}bt(U),on(()=>{Pe(it,"aria-checked",V(g)==="type"),Er(it,1,`px-3 py-1.5 rounded-lg transition ${V(g)==="type"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),Pe(ft,"aria-checked",V(g)==="state"),Er(ft,1,`px-3 py-1.5 rounded-lg transition ${V(g)==="state"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),_t.disabled=V(c),Er(_t,1,`px-4 py-2 rounded-xl bg-dream/20 border border-dream/40 text-dream-glow text-sm - hover:bg-dream/30 transition-all backdrop-blur-sm disabled:opacity-50 - ${V(c)?"glow-dream animate-pulse-glow":""}`),me(ot,V(c)?"◈ Dreaming...":"◈ Dream")}),Ge("keydown",$,J=>J.key==="Enter"&&R()),Ol($,()=>V(h),J=>te(h,J)),Ge("click",X,R),Ge("click",it,()=>te(g,"type")),Ge("click",ft,()=>te(g,"state")),Ge("change",St,()=>S()),Bl(St,()=>V(d),J=>te(d,J)),Ge("click",_t,D),Ge("click",Pt,()=>S()),we(i,U),ps(),s()}Nl(["keydown","click","change"]);export{z_ as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js.br b/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js.br deleted file mode 100644 index 24366ce..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js.gz b/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js.gz deleted file mode 100644 index 35d2038..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/6.QRT_dh4Q.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js b/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js new file mode 100644 index 0000000..bf4106d --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js @@ -0,0 +1,5 @@ +import"../chunks/Bzak7iHL.js";import{o as Ce,a as Re}from"../chunks/GG5zm9kr.js";import{p as _e,f as we,g as e,a as ke,u as I,d as r,r as n,e as o,t as S,h as w,s as K,c as xe,n as he}from"../chunks/CpWkWWOo.js";import{d as Te,s as g,a as z}from"../chunks/BlVfL1ME.js";import{i as G}from"../chunks/B4yTwGkE.js";import{e as oe,i as Ae}from"../chunks/CGEBXrjl.js";import{c as Le,a as v,f as m}from"../chunks/CHOnp4oo.js";import{s as se,r as Fe}from"../chunks/A7po6GxK.js";import{b as Ne}from"../chunks/sZcqyNBA.js";import{s as pe}from"../chunks/aVbAZ-t7.js";import{s as re}from"../chunks/Cx-f-Pzo.js";import{N as Ze}from"../chunks/CcUbQ_Wl.js";function De(s){return s>=.92?"near-identical":s>=.8?"strong":"weak"}function ge(s){const i=De(s);return i==="near-identical"?"var(--color-decay)":i==="strong"?"var(--color-warning)":"#fde047"}function Ee(s){const i=De(s);return i==="near-identical"?"Near-identical":i==="strong"?"Strong match":"Weak match"}function Pe(s){return s>.7?"#10b981":s>.4?"#f59e0b":"#ef4444"}function Be(s){if(!s||s.length===0)return null;let i=s[0],d=Number.isFinite(i.retention)?i.retention:-1/0;for(let u=1;ud&&(i=b,d=_)}return i}function Ie(s,i){return s.filter(d=>d.similarity>=i)}function be(s){return s.map(i=>i.id).slice().sort().join("|")}function Oe(s,i=80){if(!s)return"";const d=s.trim().replace(/\s+/g," ");return d.length<=i?d:d.slice(0,i)+"…"}function ye(s){if(!s||typeof s!="string")return"";const i=new Date(s);return Number.isNaN(i.getTime())?"":i.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function He(s,i=4){return Array.isArray(s)?s.slice(0,i):[]}var Ue=m('WINNER'),We=m(' '),je=m('
          '),Ke=m('

          '),ze=m('
          ');function Ge(s,i){_e(i,!0);let d=K(!1);const u=I(()=>Be(i.memories)),b=I(()=>e(u)?i.memories.filter(x=>x.id!==e(u).id).map(x=>x.id):[]);function _(){i.onMerge&&e(u)&&i.onMerge(e(u).id,e(b))}var A=Le(),V=we(A);{var le=x=>{var X=ze(),O=r(X),Y=r(O),h=r(Y),C=r(h),ee=r(C);n(C);var H=o(C,2),de=r(H,!0);n(H);var U=o(H,2),q=r(U);n(U),n(h);var R=o(h,2),W=r(R);n(R),n(Y);var j=o(Y,2),ce=r(j);n(j),n(O);var L=o(O,2);oe(L,21,()=>i.memories,k=>k.id,(k,c)=>{var D=Ke(),N=r(D),Z=o(N,2),t=r(Z),a=r(t),l=r(a,!0);n(a);var p=o(a,2);{var E=y=>{var T=Ue();v(y,T)};G(p,y=>{e(c).id===e(u).id&&y(E)})}var M=o(p,2);oe(M,17,()=>He(e(c).tags,4),Ae,(y,T)=>{var B=We(),me=r(B,!0);n(B),S(()=>g(me,e(T))),v(y,B)}),n(t);var f=o(t,2),P=r(f,!0);n(f);var ae=o(f,2);{var Q=y=>{var T=je(),B=r(T,!0);n(T),S(me=>g(B,me),[()=>ye(e(c).createdAt)]),v(y,T)},ve=I(()=>ye(e(c).createdAt));G(ae,y=>{e(ve)&&y(Q)})}n(Z);var ne=o(Z,2),$=r(ne),Me=r($);n($);var fe=o($,2),Se=r(fe);n(fe),n(ne),n(D),S((y,T,B)=>{pe(D,1,`group flex items-start gap-3 rounded-xl border border-synapse/5 bg-white/[0.02] p-3 transition-all duration-200 hover:border-synapse/20 hover:bg-white/[0.04] ${e(c).id===e(u).id?"ring-1 ring-recall/30":""}`),re(N,`background: ${(Ze[e(c).nodeType]||"#8B95A5")??""}`),se(N,"title",e(c).nodeType),g(l,e(c).nodeType),pe(f,1,`text-sm text-text leading-relaxed ${e(d)?"whitespace-pre-wrap":""}`),g(P,y),re(Me,`width: ${e(c).retention*100}%; background: ${T??""}`),g(Se,`${B??""}%`)},[()=>e(d)?e(c).content:Oe(e(c).content),()=>Pe(e(c).retention),()=>(e(c).retention*100).toFixed(0)]),v(k,D)}),n(L);var te=o(L,2),J=r(te),F=o(J,2),ue=r(F,!0);n(F);var ie=o(F,2);n(te),n(X),S((k,c,D,N,Z,t,a,l)=>{re(C,`color: ${k??""}`),g(ee,`${c??""}%`),g(de,D),g(q,`· ${i.memories.length??""} memories`),se(R,"aria-valuenow",N),re(W,`width: ${Z??""}%; background: ${t??""}; box-shadow: 0 0 12px ${a??""}66`),pe(j,1,`flex-shrink-0 rounded-full border px-3 py-1 text-xs font-medium ${i.suggestedAction==="merge"?"border-recall/40 bg-recall/10 text-recall":"border-dream-glow/40 bg-dream/10 text-dream-glow"}`),g(ce,`Suggested: ${i.suggestedAction==="merge"?"Merge":"Review"}`),se(J,"title",`Merge all into highest-retention memory (${l??""}%)`),se(F,"aria-expanded",e(d)),g(ue,e(d)?"Collapse":"Review")},[()=>ge(i.similarity),()=>(i.similarity*100).toFixed(1),()=>Ee(i.similarity),()=>Math.round(i.similarity*100),()=>(i.similarity*100).toFixed(1),()=>ge(i.similarity),()=>ge(i.similarity),()=>(e(u).retention*100).toFixed(0)]),z("click",J,_),z("click",F,()=>w(d,!e(d))),z("click",ie,function(...k){var c;(c=i.onDismiss)==null||c.apply(this,k)}),v(x,X)};G(V,x=>{i.memories.length>0&&e(u)&&x(le)})}v(s,A),ke()}Te(["click"]);var Ve=m(' Detecting…',1),Xe=m(' Error',1),Ye=m(' ',1),qe=m(`
          Couldn't detect duplicates
          `),Je=m('
          '),Qe=m('
          '),$e=m('
          ·
          No duplicates found above threshold.
          Memory is clean.
          '),et=m('
          '),tt=m('
          '),it=m('
          '),at=m(`

          Memory Hygiene — Duplicate Detection

          Cosine-similarity clustering over embeddings. Merges reinforce the winner's FSRS state; + losers inherit into the merged node. Dismissed clusters are hidden for this session only.

          `);function ft(s,i){_e(i,!0);let d=K(.8),u=K(xe([])),b=K(xe(new Set)),_=K(!0),A=K(null),V;async function le(t){return await new Promise(l=>setTimeout(l,450)),{clusters:Ie([{similarity:.96,suggestedAction:"merge",memories:[{id:"m-001",content:"BUG FIX: Harmony parser dropped `final` channel tokens when tool call followed. Root cause: 5-layer fallback missed the final channel marker when channel switched mid-stream. Solution: added final-channel detector before tool-call pop. Files: src/parser/harmony.rs",nodeType:"fact",tags:["bug-fix","benchmark-suite","parser"],retention:.91,createdAt:"2026-04-12T14:22:00Z"},{id:"m-002",content:"Fixed Harmony parser final-channel bug — 5-layer fallback was missing the final channel marker when a tool call followed. Added detector before tool pop.",nodeType:"fact",tags:["bug-fix","benchmark-suite"],retention:.64,createdAt:"2026-04-13T09:15:00Z"},{id:"m-003",content:"Harmony parser: final channel dropped on tool-call. Patched the fallback stack.",nodeType:"note",tags:["parser"],retention:.38,createdAt:"2026-04-14T11:02:00Z"}]},{similarity:.88,suggestedAction:"merge",memories:[{id:"m-004",content:"DECISION: Use vLLM prefix caching at 0.35 gpu_memory_utilization for benchmark suite submissions. Alternatives considered: sglang (slower cold start), TensorRT-LLM (deployment friction).",nodeType:"decision",tags:["vllm","benchmark-suite","inference"],retention:.84,createdAt:"2026-04-05T18:44:00Z"},{id:"m-005",content:"Chose vLLM with prefix caching (0.35 mem util) over sglang and TensorRT-LLM for benchmark suite inference.",nodeType:"decision",tags:["vllm","benchmark-suite"],retention:.72,createdAt:"2026-04-06T10:30:00Z"}]},{similarity:.83,suggestedAction:"review",memories:[{id:"m-006",content:"Release process prefers one change per benchmark submission — stacking changes destroyed signal in a prior run.",nodeType:"pattern",tags:["methodology","benchmark-suite"],retention:.88,createdAt:"2026-04-04T22:10:00Z"},{id:"m-007",content:"One-variable-at-a-time rule: never stack multiple changes per submission. Paper 2603.27844 proves +/-2 points is noise.",nodeType:"pattern",tags:["kaggle","methodology"],retention:.67,createdAt:"2026-04-08T16:20:00Z"},{id:"m-008",content:"Lesson: stacking many changes in one benchmark run hid the causal signal. Always isolate variables.",nodeType:"note",tags:["methodology"],retention:.42,createdAt:"2026-04-15T08:55:00Z"}]},{similarity:.78,suggestedAction:"review",memories:[{id:"m-009",content:"Dimensional Illusion performance: 7-minute flow poi set, LED config Parthenos overcook preset, tempo 128 BPM.",nodeType:"event",tags:["dimensional-illusion","poi","performance"],retention:.76,createdAt:"2026-03-28T19:45:00Z"},{id:"m-010",content:"Dimensional Illusion set: 7 min, Parthenos LED overcook, 128 BPM.",nodeType:"event",tags:["dimensional-illusion","poi"],retention:.51,createdAt:"2026-04-02T12:12:00Z"}]},{similarity:.76,suggestedAction:"review",memories:[{id:"m-011",content:"Vestige v2.0.7 shipped active forgetting via Anderson 2025 top-down inhibition + Davis Rac1 cascade. Suppress compounds, reversible 24h.",nodeType:"fact",tags:["vestige","release","active-forgetting"],retention:.93,createdAt:"2026-04-17T03:22:00Z"},{id:"m-012",content:"Active Forgetting feature: compounds on each suppress, 24h reversible labile window, violet implosion animation in graph view.",nodeType:"concept",tags:["vestige","active-forgetting"],retention:.81,createdAt:"2026-04-18T09:07:00Z"}]}],t)}}async function x(){w(_,!0),w(A,null);try{const t=await le(e(d));w(u,t.clusters,!0);const a=new Set(e(u).map(p=>be(p.memories))),l=new Set;for(const p of e(b))a.has(p)&&l.add(p);w(b,l,!0)}catch(t){w(A,t instanceof Error?t.message:"Failed to detect duplicates",!0),w(u,[],!0)}finally{w(_,!1)}}function X(){clearTimeout(V),V=setTimeout(x,250)}function O(t){const a=new Set(e(b));a.add(t),w(b,a,!0)}function Y(t,a,l){console.log("Merge cluster",t,{winnerId:a,loserIds:l}),O(t)}const h=I(()=>e(u).map(t=>({c:t,key:be(t.memories)})).filter(({key:t})=>!e(b).has(t))),C=I(()=>e(h).reduce((t,{c:a})=>t+a.memories.length,0)),ee=50,H=I(()=>e(h).length>ee),de=I(()=>e(H)?e(h).slice(0,ee):e(h));Ce(()=>x()),Re(()=>clearTimeout(V));var U=at(),q=o(r(U),2),R=r(q),W=o(r(R),2);Fe(W);var j=o(W,2),ce=r(j);n(j),n(R);var L=o(R,2),te=r(L);{var J=t=>{var a=Ve();he(2),v(t,a)},F=t=>{var a=Xe();he(2),v(t,a)},ue=t=>{var a=Ye(),l=o(we(a),2),p=r(l);n(l),S(()=>g(p,`${e(h).length??""} + ${e(h).length===1?"cluster":"clusters"}, + ${e(C)??""} potential duplicate${e(C)===1?"":"s"}`)),v(t,a)};G(te,t=>{e(_)?t(J):e(A)?t(F,1):t(ue,!1)})}n(L);var ie=o(L,2);n(q);var k=o(q,2);{var c=t=>{var a=qe(),l=o(r(a),2),p=r(l,!0);n(l);var E=o(l,2);n(a),S(()=>g(p,e(A))),z("click",E,x),v(t,a)},D=t=>{var a=Qe();oe(a,20,()=>Array(3),Ae,(l,p)=>{var E=Je();v(l,E)}),n(a),v(t,a)},N=t=>{var a=$e();v(t,a)},Z=t=>{var a=it(),l=r(a);{var p=M=>{var f=et(),P=r(f);n(f),S(()=>g(P,`Showing first 50 of ${e(h).length??""} clusters. Raise the + threshold to narrow results.`)),v(M,f)};G(l,M=>{e(H)&&M(p)})}var E=o(l,2);oe(E,17,()=>e(de),({c:M,key:f})=>f,(M,f)=>{let P=()=>e(f).c,ae=()=>e(f).key;var Q=tt(),ve=r(Q);Ge(ve,{get similarity(){return P().similarity},get memories(){return P().memories},get suggestedAction(){return P().suggestedAction},onDismiss:()=>O(ae()),onMerge:(ne,$)=>Y(ae(),ne,$)}),n(Q),v(M,Q)}),n(a),v(t,a)};G(k,t=>{e(A)?t(c):e(_)?t(D,1):e(h).length===0?t(N,2):t(Z,!1)})}n(U),S(t=>{g(ce,`${t??""}%`),ie.disabled=e(_)},[()=>(e(d)*100).toFixed(0)]),z("input",W,X),Ne(W,()=>e(d),t=>w(d,t)),z("click",ie,x),v(s,U),ke()}Te(["input","click"]);export{ft as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js.br b/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js.br new file mode 100644 index 0000000..30a09c9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js.gz b/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js.gz new file mode 100644 index 0000000..41058ca Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/7.2YrTacps.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js.br b/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js.br deleted file mode 100644 index 683a431..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js.gz b/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js.gz deleted file mode 100644 index 90cbe84..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/7.B1rI2ZuC.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js b/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js deleted file mode 100644 index 014a288..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js +++ /dev/null @@ -1,4 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Be}from"../chunks/DWVWfZUn.js";import{p as Qe,s as b,c as Ye,t as D,g as e,a as ze,d as t,e as o,h as x,r as a}from"../chunks/VE8Jor13.js";import{d as Ge,a as n,s as v}from"../chunks/DHnEMX8z.js";import{i as ue}from"../chunks/JkhlGLjU.js";import{e as ee,i as xe}from"../chunks/ByItJEsC.js";import{a as g,f}from"../chunks/7UNxJI5L.js";import{r as _e}from"../chunks/Cu3VmnGp.js";import{s as He}from"../chunks/BR2EHpd7.js";import{s as ge}from"../chunks/ussr1V5_.js";import{b as fe}from"../chunks/BRHZEveZ.js";import{b as Ie}from"../chunks/B5Pq2mnD.js";import{a as u}from"../chunks/DcQGRi49.js";import{N as Je}from"../chunks/BNytumrp.js";var Ke=f('
          '),Ue=f('
          '),Ve=f(' '),We=f('

          Promote Demote Suppress Delete
          '),Xe=f(''),Ze=f('
          '),et=f(`

          Memories

          Min retention:
          `);function _t(me,be){Qe(be,!0);let k=b(Ye([])),P=b(""),S=b(""),he="",h=b(0),A=b(!0),T=b(null),te;Be(()=>m());async function m(){x(A,!0);try{const i={};e(P)&&(i.q=e(P)),e(S)&&(i.node_type=e(S)),e(h)>0&&(i.min_retention=String(e(h)));const c=await u.memories.list(i);x(k,c.memories,!0)}catch{x(k,[],!0)}finally{x(A,!1)}}function ye(){clearTimeout(te),te=setTimeout(m,300)}function we(i){return i>.7?"#10b981":i>.4?"#f59e0b":"#ef4444"}var C=et(),F=o(C),ae=t(o(F),2),ke=o(ae);a(ae),a(F);var M=t(F,2),$=o(M);_e($);var y=t($,2),R=o(y);R.value=R.__value="";var N=t(R);N.value=N.__value="fact";var O=t(N);O.value=O.__value="concept";var j=t(O);j.value=j.__value="event";var L=t(j);L.value=L.__value="person";var q=t(L);q.value=q.__value="place";var B=t(q);B.value=B.__value="note";var Q=t(B);Q.value=Q.__value="pattern";var se=t(Q);se.value=se.__value="decision",a(y);var oe=t(y,2),E=t(o(oe),2);_e(E);var ie=t(E,2),Pe=o(ie);a(ie),a(oe),a(M);var Se=t(M,2);{var Te=i=>{var c=Ue();ee(c,20,()=>Array(8),xe,(w,s)=>{var _=Ke();g(w,_)}),a(c),g(i,c)},$e=i=>{var c=Ze();ee(c,21,()=>e(k),w=>w.id,(w,s)=>{var _=Xe(),Y=o(_),z=o(Y),G=o(z),re=o(G),H=t(re,2),Ee=o(H,!0);a(H);var De=t(H,2);ee(De,17,()=>e(s).tags.slice(0,3),xe,(p,d)=>{var l=Ve(),J=o(l,!0);a(l),D(()=>v(J,e(d))),g(p,l)}),a(G);var ne=t(G,2),Ae=o(ne,!0);a(ne),a(z);var pe=t(z,2),I=o(pe),Ce=o(I);a(I);var de=t(I,2),Fe=o(de);a(de),a(pe),a(Y);var Me=t(Y,2);{var Re=p=>{var d=We(),l=o(d),J=o(l,!0);a(l);var K=t(l,2),U=o(K),Ne=o(U);a(U);var V=t(U,2),Oe=o(V);a(V);var le=t(V,2),je=o(le);a(le),a(K);var ve=t(K,2),W=o(ve),X=t(W,2),Z=t(X,2),ce=t(Z,2);a(ve),a(d),D((r,Le,qe)=>{v(J,e(s).content),v(Ne,`Storage: ${r??""}%`),v(Oe,`Retrieval: ${Le??""}%`),v(je,`Created: ${qe??""}`)},[()=>(e(s).storageStrength*100).toFixed(1),()=>(e(s).retrievalStrength*100).toFixed(1),()=>new Date(e(s).createdAt).toLocaleDateString()]),n("click",W,r=>{r.stopPropagation(),u.memories.promote(e(s).id)}),n("keydown",W,r=>{r.key==="Enter"&&(r.stopPropagation(),u.memories.promote(e(s).id))}),n("click",X,r=>{r.stopPropagation(),u.memories.demote(e(s).id)}),n("keydown",X,r=>{r.key==="Enter"&&(r.stopPropagation(),u.memories.demote(e(s).id))}),n("click",Z,async r=>{r.stopPropagation(),await u.memories.suppress(e(s).id,"dashboard trigger")}),n("keydown",Z,async r=>{r.key==="Enter"&&(r.stopPropagation(),await u.memories.suppress(e(s).id,"dashboard trigger"))}),n("click",ce,async r=>{r.stopPropagation(),await u.memories.delete(e(s).id),m()}),n("keydown",ce,async r=>{r.key==="Enter"&&(r.stopPropagation(),await u.memories.delete(e(s).id),m())}),g(p,d)};ue(Me,p=>{var d;((d=e(T))==null?void 0:d.id)===e(s).id&&p(Re)})}a(_),D((p,d)=>{var l;He(_,1,`text-left p-4 glass-subtle rounded-xl hover:bg-white/[0.04] - transition-all duration-200 group - ${((l=e(T))==null?void 0:l.id)===e(s).id?"!border-synapse/40 glow-synapse":""}`),ge(re,`background: ${(Je[e(s).nodeType]||"#8B95A5")??""}`),v(Ee,e(s).nodeType),v(Ae,e(s).content),ge(Ce,`width: ${e(s).retentionStrength*100}%; background: ${p??""}`),v(Fe,`${d??""}%`)},[()=>we(e(s).retentionStrength),()=>(e(s).retentionStrength*100).toFixed(0)]),n("click",_,()=>{var p;return x(T,((p=e(T))==null?void 0:p.id)===e(s).id?null:e(s),!0)}),g(w,_)}),a(c),g(i,c)};ue(Se,i=>{e(A)?i(Te):i($e,!1)})}a(C),D(i=>{v(ke,`${e(k).length??""} results`),v(Pe,`${i??""}%`)},[()=>(e(h)*100).toFixed(0)]),n("input",$,ye),fe($,()=>e(P),i=>x(P,i)),n("change",y,m),Ie(y,()=>e(S),i=>x(S,i)),n("change",E,m),fe(E,()=>e(h),i=>x(h,i)),g(me,C),ze()}Ge(["input","change","click","keydown"]);export{_t as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js.br b/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js.br deleted file mode 100644 index 89e42a9..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js.gz b/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js.gz deleted file mode 100644 index 77dcb3b..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/8.BmBiit5q.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js b/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js similarity index 94% rename from apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js rename to apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js index 107913e..33dba0f 100644 --- a/apps/dashboard/build/_app/immutable/nodes/4.BEP4iikl.js +++ b/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js @@ -1,4 +1,4 @@ -import"../chunks/Bzak7iHL.js";import{p as ze,s as I,c as Ae,g as e,a as Pe,d as a,e as r,h as b,r as t,i as Qe,t as y,f as ge,u as se,j as qe}from"../chunks/VE8Jor13.js";import{d as Be,a as q,s as o}from"../chunks/DHnEMX8z.js";import{a as c,f as m,c as De}from"../chunks/7UNxJI5L.js";import{i as k}from"../chunks/JkhlGLjU.js";import{e as ie,i as ne}from"../chunks/ByItJEsC.js";import{r as ye}from"../chunks/Cu3VmnGp.js";import{s as oe}from"../chunks/BR2EHpd7.js";import{s as Ke}from"../chunks/ussr1V5_.js";import{b as de}from"../chunks/BRHZEveZ.js";import{a as X}from"../chunks/DcQGRi49.js";var Re=m(''),Ue=m('
          Source

          '),Ve=m('
          Target

          '),Ge=m(`
          Target Memory
          '),Ue=m('
          Source

          '),Ve=m('
          Target

          '),Ge=m(`
          Target Memory
          `,1),He=m('

          '),Je=m(' '),Le=m(" "),We=m(" "),Xe=m(" "),Ye=m(' '),Ze=m('

          '),et=m('

          '),tt=m('

          No connections found for this query.

          '),rt=m('
          '),at=m('
          '),st=m('
          '),it=m(`

          Explore Connections

          Source Memory

          Importance Scorer

          4-channel neuroscience scoring: novelty, arousal, reward, attention

          `);function ft(he,we){ze(we,!0);let V=I(""),G=I(""),F=I(null),C=I(null),B=I(Ae([])),$=I("associations"),O=I(!1),H=I(""),D=I(null);const le={associations:{icon:"◎",desc:"Spreading activation — find related memories via graph traversal"},chains:{icon:"⟿",desc:"Build reasoning path from source to target memory"},bridges:{icon:"⬡",desc:"Find connecting memories between two concepts"}};async function ve(){if(e(V).trim()){b(O,!0);try{const s=await X.search(e(V),1);s.results.length>0&&(b(F,s.results[0],!0),await Y())}catch{}finally{b(O,!1)}}}async function pe(){if(e(G).trim()){b(O,!0);try{const s=await X.search(e(G),1);s.results.length>0&&(b(C,s.results[0],!0),e(F)&&await Y())}catch{}finally{b(O,!1)}}}async function Y(){if(e(F)){b(O,!0);try{const s=(e($)==="chains"||e($)==="bridges")&&e(C)?e(C).id:void 0,i=await X.explore(e(F).id,e($),s);b(B,i.results||i.nodes||i.chain||i.bridges||[],!0)}catch{b(B,[],!0)}finally{b(O,!1)}}}async function ke(){e(H).trim()&&b(D,await X.importance(e(H)),!0)}function Se(s){b($,s,!0),e(F)&&Y()}var Z=it(),ee=a(r(Z),2);ie(ee,20,()=>["associations","chains","bridges"],ne,(s,i)=>{var d=Re(),_=r(d),h=r(_,!0);t(_);var f=a(_,2),p=r(f,!0);t(f);var n=a(f,2),g=r(n,!0);t(n),t(d),y(w=>{oe(d,1,`flex flex-col items-center gap-1 p-3 rounded-xl text-sm transition diff --git a/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js.br b/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js.br new file mode 100644 index 0000000..87e1326 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js.gz b/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js.gz new file mode 100644 index 0000000..7225004 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/8.CokrlgDp.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js b/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js deleted file mode 100644 index 7359846..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js +++ /dev/null @@ -1,2 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as ze}from"../chunks/DWVWfZUn.js";import{p as Je,t as w,a as We,d as r,e as t,g as s,s as S,h as m,r as e,O as b,f as Be,u as j}from"../chunks/VE8Jor13.js";import{d as He,a as X,s as p}from"../chunks/DHnEMX8z.js";import{i as u}from"../chunks/JkhlGLjU.js";import{e as Z,i as ee}from"../chunks/ByItJEsC.js";import{a as v,f as l,t as ue}from"../chunks/7UNxJI5L.js";import{s as ge}from"../chunks/BR2EHpd7.js";import{s as fe}from"../chunks/ussr1V5_.js";import{s as Le,a as te}from"../chunks/AcZBvMXu.js";import{a as O}from"../chunks/DcQGRi49.js";import{m as Ue,a as Ye,i as qe}from"../chunks/XIUN5r_Y.js";var Qe=l(' Running...',1),Xe=l('
          Processed
          '),Ze=l('
          Decayed
          '),et=l('
          Embedded
          '),tt=l('
          '),st=l(' Dreaming...',1),at=l('
          '),it=l('
          Insights Discovered:
          ',1),dt=l('
          Connections found:
          '),rt=l('
          Memories replayed:
          '),ot=l('
          '),nt=l('
          '),vt=l('
          '),lt=l('

          Retention Distribution

          '),ct=l('
          '),xt=l(`

          Settings & System

          Memories
          Avg Retention
          WebSocket
          v2.1
          Vestige

          Cognitive Operations

          FSRS-6 Consolidation
          Apply spaced-repetition decay, regenerate embeddings, run maintenance
          Memory Dream Cycle
          Replay memories, discover hidden connections, synthesize insights

          Keyboard Shortcuts

          About

          V
          Vestige v2.1 "Nuclear Dashboard"
          Your AI's long-term memory system
          29 cognitive modules
          FSRS-6 spaced repetition
          Nomic Embed v1.5 (256d)
          Jina Reranker v1 Turbo
          USearch HNSW (20x FAISS)
          Local-first, zero cloud
          Built with Rust + Axum + SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4
          `);function Ct(_e,be){Je(be,!0);const ye=()=>te(Ue,"$memoryCount",I),P=()=>te(Ye,"$avgRetention",I),se=()=>te(qe,"$isConnected",I),[I,he]=Le();let T=S(!1),A=S(!1),y=S(null),g=S(null),we=S(null),$=S(null),ae=S(!0),Se=S(null);ze(()=>{E()});async function E(){m(ae,!0);try{const[a,o,c]=await Promise.all([O.stats().catch(()=>null),O.health().catch(()=>null),O.retentionDistribution().catch(()=>null)]);m(we,a,!0),m(Se,o,!0),m($,c,!0)}finally{m(ae,!1)}}async function ke(){m(T,!0),m(y,null);try{m(y,await O.consolidate(),!0),await E()}catch{}finally{m(T,!1)}}async function Ce(){m(A,!0),m(g,null);try{m(g,await O.dream(),!0),await E()}catch{}finally{m(A,!1)}}var K=xt(),V=t(K),Re=r(t(V),2);e(V);var z=r(V,2),J=t(z),ie=t(J),$e=t(ie,!0);e(ie),b(2),e(J);var W=r(J,2),B=t(W),Ae=t(B);e(B),b(2),e(W);var de=r(W,2),re=t(de),oe=t(re),ne=r(oe,2),De=t(ne,!0);e(ne),e(re),b(2),e(de),b(2),e(z);var H=r(z,2),L=r(t(H),2),U=t(L),N=r(t(U),2),Ge=t(N);{var Me=a=>{var o=Qe();b(),v(a,o)},Fe=a=>{var o=ue("Consolidate");v(a,o)};u(Ge,a=>{s(T)?a(Me):a(Fe,!1)})}e(N),e(U);var je=r(U,2);{var Oe=a=>{var o=tt(),c=t(o),f=t(c);{var k=d=>{var i=Xe(),n=t(i),x=t(n,!0);e(n),b(2),e(i),w(()=>p(x,s(y).nodesProcessed)),v(d,i)};u(f,d=>{s(y).nodesProcessed!==void 0&&d(k)})}var _=r(f,2);{var h=d=>{var i=Ze(),n=t(i),x=t(n,!0);e(n),b(2),e(i),w(()=>p(x,s(y).decayApplied)),v(d,i)};u(_,d=>{s(y).decayApplied!==void 0&&d(h)})}var C=r(_,2);{var G=d=>{var i=et(),n=t(i),x=t(n,!0);e(n),b(2),e(i),w(()=>p(x,s(y).embeddingsGenerated)),v(d,i)};u(C,d=>{s(y).embeddingsGenerated!==void 0&&d(G)})}e(c),e(o),v(a,o)};u(je,a=>{s(y)&&a(Oe)})}e(L);var ve=r(L,2),Y=t(ve),D=r(t(Y),2),Te=t(D);{var Ee=a=>{var o=st();b(),v(a,o)},Ne=a=>{var o=ue("Dream");v(a,o)};u(Te,a=>{s(A)?a(Ee):a(Ne,!1)})}e(D),e(Y);var Pe=r(Y,2);{var Ie=a=>{var o=ot(),c=t(o);{var f=d=>{var i=it(),n=r(Be(i),2);Z(n,17,()=>s(g).insights,ee,(x,M)=>{var R=at(),F=t(R,!0);e(R),w(q=>p(F,q),[()=>typeof s(M)=="string"?s(M):JSON.stringify(s(M))]),v(x,R)}),v(d,i)},k=j(()=>s(g).insights&&Array.isArray(s(g).insights));u(c,d=>{s(k)&&d(f)})}var _=r(c,2);{var h=d=>{var i=dt(),n=r(t(i)),x=t(n,!0);e(n),e(i),w(()=>p(x,s(g).connections_found)),v(d,i)};u(_,d=>{s(g).connections_found!==void 0&&d(h)})}var C=r(_,2);{var G=d=>{var i=rt(),n=r(t(i)),x=t(n,!0);e(n),e(i),w(()=>p(x,s(g).memories_replayed)),v(d,i)};u(C,d=>{s(g).memories_replayed!==void 0&&d(G)})}e(o),v(a,o)};u(Pe,a=>{s(g)&&a(Ie)})}e(ve),e(H);var le=r(H,2);{var Ke=a=>{var o=lt(),c=r(t(o),2),f=t(c);{var k=h=>{var C=vt();Z(C,21,()=>s($).distribution,ee,(G,d,i)=>{const n=j(()=>Math.max(...s($).distribution.map(Q=>Q.count),1)),x=j(()=>s(d).count/s(n)*100),M=j(()=>i<2?"#ef4444":i<4?"#f59e0b":i<7?"#6366f1":"#10b981");var R=nt(),F=t(R),q=t(F,!0);e(F);var pe=r(F,2),Ve=r(pe,2);Ve.textContent=`${i*10}%`,e(R),w(Q=>{p(q,s(d).count),fe(pe,`height: ${Q??""}%; background: ${s(M)??""}; opacity: 0.7`)},[()=>Math.max(s(x),2)]),v(G,R)}),e(C),v(h,C)},_=j(()=>s($).distribution&&Array.isArray(s($).distribution));u(f,h=>{s(_)&&h(k)})}e(c),e(o),v(a,o)};u(le,a=>{s($)&&a(Ke)})}var ce=r(le,2),xe=r(t(ce),2),me=t(xe);Z(me,20,()=>[{key:"⌘ K",desc:"Command palette"},{key:"/",desc:"Focus search"},{key:"G",desc:"Go to Graph"},{key:"M",desc:"Go to Memories"},{key:"T",desc:"Go to Timeline"},{key:"F",desc:"Go to Feed"},{key:"E",desc:"Go to Explore"},{key:"S",desc:"Go to Stats"}],ee,(a,o)=>{var c=ct(),f=t(c),k=t(f,!0);e(f);var _=r(f,2),h=t(_,!0);e(_),e(c),w(()=>{p(k,o.key),p(h,o.desc)}),v(a,c)}),e(me),e(xe),e(ce),b(2),e(K),w(a=>{p($e,ye()),fe(B,`color: ${P()>.7?"#10b981":P()>.4?"#f59e0b":"#ef4444"}`),p(Ae,`${a??""}%`),ge(oe,1,`w-2.5 h-2.5 rounded-full ${se()?"bg-recall animate-pulse-glow":"bg-decay"}`),p(De,se()?"Online":"Offline"),N.disabled=s(T),D.disabled=s(A),ge(D,1,`px-4 py-2 bg-dream/20 border border-dream/40 text-dream-glow text-sm rounded-xl hover:bg-dream/30 transition disabled:opacity-50 flex items-center gap-2 - ${s(A)?"glow-dream animate-pulse-glow":""}`)},[()=>(P()*100).toFixed(1)]),X("click",Re,E),X("click",N,ke),X("click",D,Ce),v(_e,K),We(),he()}He(["click"]);export{Ct as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js.br b/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js.br deleted file mode 100644 index 7ac4c60..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js.gz b/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js.gz deleted file mode 100644 index c610e87..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/9.Ds9IBqJA.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js b/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js similarity index 50% rename from apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js rename to apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js index 359af7b..d3ddfe5 100644 --- a/apps/dashboard/build/_app/immutable/nodes/5.BgcStf4T.js +++ b/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js @@ -1,8 +1,8 @@ -import"../chunks/Bzak7iHL.js";import"../chunks/CrlWs-6R.js";import{p as ee,I as oe,g as e,h as M,e as o,d,r as s,f as ie,t as $,a as te,s as K,u as X,C as Z}from"../chunks/VE8Jor13.js";import{s as x,d as ne,a as de}from"../chunks/DHnEMX8z.js";import{a as l,f as m}from"../chunks/7UNxJI5L.js";import{i as w}from"../chunks/JkhlGLjU.js";import{e as re,i as ae}from"../chunks/ByItJEsC.js";import{s as C}from"../chunks/ussr1V5_.js";import{i as ce}from"../chunks/jyeIy8pa.js";import{s as le,a as me}from"../chunks/AcZBvMXu.js";import{w as ve,e as ue}from"../chunks/XIUN5r_Y.js";import{E as I}from"../chunks/BNytumrp.js";import{s as pe}from"../chunks/Cu3VmnGp.js";import{s as fe}from"../chunks/BR2EHpd7.js";import{p as Q}from"../chunks/ykT2B6d3.js";var xe=m(' '),_e=m('
          '),ge=m('
          ',1),he=m('
          '),ye=m('
          '),$e=m('
          Cognitive Search Pipeline
          ');function be(O,F){ee(F,!0);let S=Q(F,"resultCount",3,0),j=Q(F,"durationMs",3,0),q=Q(F,"active",3,!1);const p=[{name:"Overfetch",icon:"◎",color:"#818CF8",desc:"Pull 3x results from hybrid search"},{name:"Rerank",icon:"⟿",color:"#00A8FF",desc:"Re-score by relevance quality"},{name:"Temporal",icon:"◷",color:"#00D4FF",desc:"Recent memories get recency bonus"},{name:"Access",icon:"◇",color:"#00FFD1",desc:"FSRS-6 retention threshold filter"},{name:"Context",icon:"◬",color:"#FFB800",desc:"Encoding specificity matching"},{name:"Compete",icon:"⬡",color:"#FF3CAC",desc:"Retrieval-induced forgetting"},{name:"Activate",icon:"◈",color:"#9D00FF",desc:"Spreading activation cascade"}];let _=K(-1),g=K(!1),u=K(!1);oe(()=>{q()&&!e(g)&&P()});function P(){M(g,!0),M(_,-1),M(u,!1);const t=Math.max(1500,(j()||50)*2),a=t/(p.length+1);p.forEach((i,v)=>{setTimeout(()=>{M(_,v,!0)},a*(v+1))}),setTimeout(()=>{M(u,!0),M(g,!1)},t)}var D=$e(),b=o(D),L=d(o(b),2);{var V=t=>{var a=xe(),i=o(a);s(a),$(()=>x(i,`${S()??""} results in ${j()??""}ms`)),l(t,a)};w(L,t=>{e(u)&&t(V)})}s(b);var A=d(b,2);re(A,21,()=>p,ae,(t,a,i)=>{const v=X(()=>i<=e(_)),E=X(()=>i===e(_)&&e(g));var k=ge(),h=ie(k),y=o(h),J=o(y,!0);s(y);var R=d(y,2),T=o(R,!0);s(R),s(h);var U=d(h,2);{var W=B=>{var c=_e();$(()=>C(c,`background: ${i{i{fe(y,1,`w-8 h-8 rounded-full flex items-center justify-center text-xs transition-all duration-300 +import"../chunks/Bzak7iHL.js";import{i as oe}from"../chunks/BUoSzNdg.js";import{p as ee,aB as ie,g as e,h as A,d as o,e as d,r as s,f as ne,t as $,a as te,s as K,u as X,A as Z}from"../chunks/CpWkWWOo.js";import{s as x,d as de,a as ce}from"../chunks/BlVfL1ME.js";import{a as l,f as m}from"../chunks/CHOnp4oo.js";import{i as w}from"../chunks/B4yTwGkE.js";import{e as re,i as ae}from"../chunks/CGEBXrjl.js";import{s as C}from"../chunks/Cx-f-Pzo.js";import{s as le,a as me}from"../chunks/C6HuKgyx.js";import{w as ve,e as ue}from"../chunks/MAY1QfFZ.js";import{E as O}from"../chunks/CcUbQ_Wl.js";import{s as pe}from"../chunks/A7po6GxK.js";import{s as fe}from"../chunks/aVbAZ-t7.js";import{p as Q}from"../chunks/V6gjw5Ec.js";var xe=m(' '),_e=m('
          '),ge=m('
          ',1),he=m('
          '),ye=m('
          '),$e=m('
          Cognitive Search Pipeline
          ');function be(q,F){ee(F,!0);let S=Q(F,"resultCount",3,0),j=Q(F,"durationMs",3,0),I=Q(F,"active",3,!1);const p=[{name:"Overfetch",icon:"◎",color:"#818CF8",desc:"Pull 3x results from hybrid search"},{name:"Rerank",icon:"⟿",color:"#00A8FF",desc:"Re-score by relevance quality"},{name:"Temporal",icon:"◷",color:"#00D4FF",desc:"Recent memories get recency bonus"},{name:"Access",icon:"◇",color:"#00FFD1",desc:"FSRS-6 retention threshold filter"},{name:"Context",icon:"◬",color:"#FFB800",desc:"Encoding specificity matching"},{name:"Compete",icon:"⬡",color:"#FF3CAC",desc:"Retrieval-induced forgetting"},{name:"Activate",icon:"◈",color:"#9D00FF",desc:"Spreading activation cascade"}];let _=K(-1),g=K(!1),u=K(!1);ie(()=>{I()&&!e(g)&&M()});function M(){A(g,!0),A(_,-1),A(u,!1);const t=Math.max(1500,(j()||50)*2),a=t/(p.length+1);p.forEach((i,v)=>{setTimeout(()=>{A(_,v,!0)},a*(v+1))}),setTimeout(()=>{A(u,!0),A(g,!1)},t)}var D=$e(),b=o(D),L=d(o(b),2);{var V=t=>{var a=xe(),i=o(a);s(a),$(()=>x(i,`${S()??""} results in ${j()??""}ms`)),l(t,a)};w(L,t=>{e(u)&&t(V)})}s(b);var P=d(b,2);re(P,21,()=>p,ae,(t,a,i)=>{const v=X(()=>i<=e(_)),E=X(()=>i===e(_)&&e(g));var k=ge(),h=ne(k),y=o(h),J=o(y,!0);s(y);var R=d(y,2),T=o(R,!0);s(R),s(h);var U=d(h,2);{var W=B=>{var c=_e();$(()=>C(c,`background: ${i{i{fe(y,1,`w-8 h-8 rounded-full flex items-center justify-center text-xs transition-all duration-300 ${e(E)?"scale-125":""}`),C(y,`background: ${e(v)?e(a).color+"25":"rgba(255,255,255,0.03)"}; border: 1.5px solid ${(e(v)?e(a).color:"rgba(255,255,255,0.06)")??""}; color: ${(e(v)?e(a).color:"#4a4a7a")??""}; - box-shadow: ${e(E)?"0 0 12px "+e(a).color+"40":"none"}`),pe(y,"title",e(a).desc),x(J,e(a).icon),C(R,`color: ${(e(v)?e(a).color:"#4a4a7a")??""}`),x(T,e(a).name)}),l(t,k)}),s(A);var N=d(A,2),z=o(N);{var n=t=>{var a=he();$(i=>C(a,`width: ${i??""}%; + box-shadow: ${e(E)?"0 0 12px "+e(a).color+"40":"none"}`),pe(y,"title",e(a).desc),x(J,e(a).icon),C(R,`color: ${(e(v)?e(a).color:"#4a4a7a")??""}`),x(T,e(a).name)}),l(t,k)}),s(P);var N=d(P,2),z=o(N);{var n=t=>{var a=he();$(i=>C(a,`width: ${i??""}%; background: linear-gradient(90deg, #818CF8, #00FFD1, #9D00FF); - transition-duration: ${e(g)?"300ms":"500ms"}`),[()=>e(u)?"100":((e(_)+1)/p.length*100).toFixed(0)]),l(t,a)};w(z,t=>{(e(g)||e(u))&&t(n)})}s(N);var r=d(N,2);{var H=t=>{var a=ye(),i=d(o(a),2),v=o(i);s(i),s(a),$(()=>x(v,`Pipeline complete: ${S()??""} memories surfaced from ${p.length??""}-stage cognitive cascade`)),l(t,a)};w(r,t=>{e(u)&&t(H)})}s(D),l(O,D),te()}var we=m('

          Waiting for cognitive events...

          Events appear here in real-time as Vestige thinks.

          '),Ce=m(' '),Fe=m('
          '),Se=m(`

          `),De=m('
          '),ke=m('

          Live Feed

          ');function He(O,F){ee(F,!1);const S=()=>me(ue,"$eventFeed",j),[j,q]=le();function p(n){return new Date(n).toLocaleTimeString()}function _(n){return{MemoryCreated:"+",MemoryUpdated:"~",MemoryDeleted:"×",MemoryPromoted:"↑",MemoryDemoted:"↓",SearchPerformed:"◎",DreamStarted:"◈",DreamProgress:"◈",DreamCompleted:"◈",ConsolidationStarted:"◉",ConsolidationCompleted:"◉",RetentionDecayed:"↘",ConnectionDiscovered:"━",ActivationSpread:"◬",ImportanceScored:"◫",Heartbeat:"♡"}[n]||"·"}function g(n){const r=n.data;switch(n.type){case"MemoryCreated":return`New ${r.node_type}: "${String(r.content_preview).slice(0,60)}..."`;case"SearchPerformed":return`Searched "${r.query}" → ${r.result_count} results (${r.duration_ms}ms)`;case"DreamStarted":return`Dream started with ${r.memory_count} memories`;case"DreamCompleted":return`Dream complete: ${r.connections_found} connections, ${r.insights_generated} insights (${r.duration_ms}ms)`;case"ConsolidationStarted":return"Consolidation cycle started";case"ConsolidationCompleted":return`Consolidated ${r.nodes_processed} nodes, ${r.decay_applied} decayed (${r.duration_ms}ms)`;case"ConnectionDiscovered":return`Connection: ${String(r.connection_type)} (weight: ${Number(r.weight).toFixed(2)})`;case"ImportanceScored":return`Scored ${Number(r.composite_score).toFixed(2)}: "${String(r.content_preview).slice(0,50)}..."`;case"MemoryPromoted":return`Promoted → ${(Number(r.new_retention)*100).toFixed(0)}% retention`;case"MemoryDemoted":return`Demoted → ${(Number(r.new_retention)*100).toFixed(0)}% retention`;default:return JSON.stringify(r).slice(0,100)}}ce();var u=ke(),P=o(u),D=d(o(P),2),b=o(D),L=o(b);s(b);var V=d(b,2);s(D),s(P);var A=d(P,2);{var N=n=>{var r=we();l(n,r)},z=n=>{var r=De();re(r,5,S,ae,(H,t)=>{var a=Se(),i=o(a),v=o(i,!0);s(i);var E=d(i,2),k=o(E),h=o(k),y=o(h,!0);s(h);var J=d(h,2);{var R=c=>{var f=Ce(),Y=o(f,!0);s(f),$(G=>x(Y,G),[()=>p(String(e(t).data.timestamp))]),l(c,f)};w(J,c=>{e(t).data.timestamp&&c(R)})}s(k);var T=d(k,2),U=o(T,!0);s(T);var W=d(T,2);{var B=c=>{var f=Fe(),Y=o(f);{let G=Z(()=>Number(e(t).data.result_count)||0),se=Z(()=>Number(e(t).data.duration_ms)||0);be(Y,{get resultCount(){return e(G)},get durationMs(){return e(se)},active:!0})}s(f),l(c,f)};w(W,c=>{e(t).type==="SearchPerformed"&&c(B)})}s(E),s(a),$((c,f)=>{C(a,`border-left: 3px solid ${(I[e(t).type]||"#8B95A5")??""}`),C(i,`background: ${(I[e(t).type]||"#8B95A5")??""}15; color: ${(I[e(t).type]||"#8B95A5")??""}`),x(v,c),C(h,`color: ${(I[e(t).type]||"#8B95A5")??""}`),x(y,e(t).type),x(U,f)},[()=>_(e(t).type),()=>g(e(t))]),l(H,a)}),s(r),l(n,r)};w(A,n=>{S().length===0?n(N):n(z,!1)})}s(u),$(()=>x(L,`${S().length??""} events`)),de("click",V,()=>ve.clearEvents()),l(O,u),te(),q()}ne(["click"]);export{He as component}; + transition-duration: ${e(g)?"300ms":"500ms"}`),[()=>e(u)?"100":((e(_)+1)/p.length*100).toFixed(0)]),l(t,a)};w(z,t=>{(e(g)||e(u))&&t(n)})}s(N);var r=d(N,2);{var H=t=>{var a=ye(),i=d(o(a),2),v=o(i);s(i),s(a),$(()=>x(v,`Pipeline complete: ${S()??""} memories surfaced from ${p.length??""}-stage cognitive cascade`)),l(t,a)};w(r,t=>{e(u)&&t(H)})}s(D),l(q,D),te()}var we=m('

          Waiting for cognitive events...

          Events appear here in real-time as Vestige thinks.

          '),Ce=m(' '),Fe=m('
          '),Se=m(`

          `),De=m('
          '),ke=m('

          Live Feed

          ');function ze(q,F){ee(F,!1);const S=()=>me(ue,"$eventFeed",j),[j,I]=le();function p(n){return new Date(n).toLocaleTimeString()}function _(n){return{MemoryCreated:"+",MemoryUpdated:"~",MemoryDeleted:"×",MemoryPromoted:"↑",MemoryDemoted:"↓",SearchPerformed:"◎",DreamStarted:"◈",DreamProgress:"◈",DreamCompleted:"◈",ConsolidationStarted:"◉",ConsolidationCompleted:"◉",RetentionDecayed:"↘",ConnectionDiscovered:"━",ActivationSpread:"◬",ImportanceScored:"◫",Heartbeat:"♡"}[n]||"·"}function g(n){const r=n.data;switch(n.type){case"MemoryCreated":return`New ${r.node_type}: "${String(r.content_preview).slice(0,60)}..."`;case"SearchPerformed":return`Searched "${r.query}" → ${r.result_count} results (${r.duration_ms}ms)`;case"DreamStarted":return`Dream started with ${r.memory_count} memories`;case"DreamCompleted":return`Dream complete: ${r.connections_found} connections, ${r.insights_generated} insights (${r.duration_ms}ms)`;case"ConsolidationStarted":return"Consolidation cycle started";case"ConsolidationCompleted":return`Consolidated ${r.nodes_processed} nodes, ${r.decay_applied} decayed (${r.duration_ms}ms)`;case"ConnectionDiscovered":return`Connection: ${String(r.connection_type)} (weight: ${Number(r.weight).toFixed(2)})`;case"ImportanceScored":return`Scored ${Number(r.composite_score).toFixed(2)}: "${String(r.content_preview).slice(0,50)}..."`;case"MemoryPromoted":return`Promoted → ${(Number(r.new_retention)*100).toFixed(0)}% retention`;case"MemoryDemoted":return`Demoted → ${(Number(r.new_retention)*100).toFixed(0)}% retention`;default:return JSON.stringify(r).slice(0,100)}}oe();var u=ke(),M=o(u),D=d(o(M),2),b=o(D),L=o(b);s(b);var V=d(b,2);s(D),s(M);var P=d(M,2);{var N=n=>{var r=we();l(n,r)},z=n=>{var r=De();re(r,5,S,ae,(H,t)=>{var a=Se(),i=o(a),v=o(i,!0);s(i);var E=d(i,2),k=o(E),h=o(k),y=o(h,!0);s(h);var J=d(h,2);{var R=c=>{var f=Ce(),Y=o(f,!0);s(f),$(G=>x(Y,G),[()=>p(String(e(t).data.timestamp))]),l(c,f)};w(J,c=>{e(t).data.timestamp&&c(R)})}s(k);var T=d(k,2),U=o(T,!0);s(T);var W=d(T,2);{var B=c=>{var f=Fe(),Y=o(f);{let G=Z(()=>Number(e(t).data.result_count)||0),se=Z(()=>Number(e(t).data.duration_ms)||0);be(Y,{get resultCount(){return e(G)},get durationMs(){return e(se)},active:!0})}s(f),l(c,f)};w(W,c=>{e(t).type==="SearchPerformed"&&c(B)})}s(E),s(a),$((c,f)=>{C(a,`border-left: 3px solid ${(O[e(t).type]||"#8B95A5")??""}`),C(i,`background: ${(O[e(t).type]||"#8B95A5")??""}15; color: ${(O[e(t).type]||"#8B95A5")??""}`),x(v,c),C(h,`color: ${(O[e(t).type]||"#8B95A5")??""}`),x(y,e(t).type),x(U,f)},[()=>_(e(t).type),()=>g(e(t))]),l(H,a)}),s(r),l(n,r)};w(P,n=>{S().length===0?n(N):n(z,!1)})}s(u),$(()=>x(L,`${S().length??""} events`)),ce("click",V,()=>ve.clearEvents()),l(q,u),te(),I()}de(["click"]);export{ze as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js.br b/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js.br new file mode 100644 index 0000000..90505a6 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js.gz b/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js.gz new file mode 100644 index 0000000..433c6c1 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/9.Vu2AXN40.js.gz differ diff --git a/apps/dashboard/build/_app/version.json b/apps/dashboard/build/_app/version.json index 4b178dc..a0b8a69 100644 --- a/apps/dashboard/build/_app/version.json +++ b/apps/dashboard/build/_app/version.json @@ -1 +1 @@ -{"version":"1776653229849"} \ No newline at end of file +{"version":"2.1.22"} \ No newline at end of file diff --git a/apps/dashboard/build/_app/version.json.br b/apps/dashboard/build/_app/version.json.br index 7fb36d9..7ee1248 100644 Binary files a/apps/dashboard/build/_app/version.json.br and b/apps/dashboard/build/_app/version.json.br differ diff --git a/apps/dashboard/build/_app/version.json.gz b/apps/dashboard/build/_app/version.json.gz index e2667a6..9667dd0 100644 Binary files a/apps/dashboard/build/_app/version.json.gz and b/apps/dashboard/build/_app/version.json.gz differ diff --git a/apps/dashboard/build/favicon.svg.gz b/apps/dashboard/build/favicon.svg.gz index 61b0904..06ecd9b 100644 Binary files a/apps/dashboard/build/favicon.svg.gz and b/apps/dashboard/build/favicon.svg.gz differ diff --git a/apps/dashboard/build/index.html b/apps/dashboard/build/index.html index f56c28a..ce5c054 100644 --- a/apps/dashboard/build/index.html +++ b/apps/dashboard/build/index.html @@ -11,21 +11,21 @@ - - - - - - - - - + + + + + + + + + - - - - - + + + + + Vestige @@ -33,7 +33,7 @@
          + + + + + + + + + + + + + + + + + + + + + + + + + {#each ripples as r, i (i)} + + {/each} + + + {#each activeEdges as e, i (i)} + {@const pt = edgePoint(e)} + {#if pt} + + {/if} + {/each} + + + {#each activeNodes as n (n.id)} + {@const color = nodeColor(n.nodeType, n.isSource)} + {@const r = n.isSource + ? SOURCE_RADIUS * (0.7 + 0.3 * n.activation) + : NEIGHBOUR_RADIUS_BASE * (0.5 + 0.8 * n.activation)} + + + + + + + + {#if n.isSource && n.label} + + {n.label.length > 40 ? n.label.slice(0, 40) + '…' : n.label} + + {/if} + + {/each} + diff --git a/apps/dashboard/src/lib/components/AmbientAwarenessStrip.svelte b/apps/dashboard/src/lib/components/AmbientAwarenessStrip.svelte new file mode 100644 index 0000000..4b9453b --- /dev/null +++ b/apps/dashboard/src/lib/components/AmbientAwarenessStrip.svelte @@ -0,0 +1,312 @@ + + + +
          + +
          + + + + + {$memoryCount} + memories + · + + {retentionPct}% + + avg retention +
          + + + + +
          + {#if atRiskCount !== null && atRiskCount > 0} + {atRiskCount} + at risk + {:else if atRiskCount === 0} + 0 + at risk + {:else} + + at risk + {/if} +
          + + + + + + + + + + + + + + + {#if dreamState.isDreaming} + +
          + + + + + DREAMING... +
          + {/if} + + +
          + + + {#if suppressionFlash} + + {/if} +
          + + diff --git a/apps/dashboard/src/lib/components/ContradictionArcs.svelte b/apps/dashboard/src/lib/components/ContradictionArcs.svelte new file mode 100644 index 0000000..2969beb --- /dev/null +++ b/apps/dashboard/src/lib/components/ContradictionArcs.svelte @@ -0,0 +1,421 @@ + + +
          + + + { hoverNode = null; hoverArc = null; }} + onclick={handleBgClick} + > + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {#each layout.arcs as arc (arc.pairIndex)} + {@const op = pairOpacity(arc.pairIndex, focusedPairIndex)} + {@const isFocused = focusedPairIndex === arc.pairIndex} + + + + { e.stopPropagation(); handleArcClick(arc.pairIndex); }} + onmouseenter={() => (hoverArc = arc)} + onmouseleave={() => (hoverArc = null)} + aria-label="contradiction {arc.pairIndex + 1}: {arc.topic}" + role="button" + tabindex="0" + onkeydown={(e) => { if (e.key === 'Enter') handleArcClick(arc.pairIndex); }} + /> + + + {/each} + + + {#each layout.nodes as node, i (node.memoryId + '-' + node.side + '-' + i)} + {@const op = pairOpacity(node.pairIndex, focusedPairIndex)} + {@const isFocused = focusedPairIndex === node.pairIndex} + {@const r = nodeRadius(node.trust)} + {@const fill = nodeColor(node.type)} + + + + (hoverNode = node)} + onmouseleave={() => (hoverNode = null)} + onclick={(e) => { e.stopPropagation(); handleArcClick(node.pairIndex); }} + role="button" + tabindex="0" + aria-label="memory {truncate(node.preview, 40)}" + onkeydown={(e) => { if (e.key === 'Enter') handleArcClick(node.pairIndex); }} + /> + + {#if isFocused} + {truncate(node.preview, 40)} + {/if} + {/each} + + + + + SEVERITY + + strong (>0.7) + + moderate (0.5-0.7) + + mild (0.3-0.5) + + + + + {#if hoverNode} +
          +
          +
          + {hoverNode.type ?? 'memory'} + trust {(hoverNode.trust * 100).toFixed(0)}% +
          +
          {hoverNode.preview}
          + {#if hoverNode.created} +
          created {hoverNode.created}
          + {/if} + {#if hoverNode.tags && hoverNode.tags.length > 0} +
          + {hoverNode.tags.slice(0, 4).join(' · ')} +
          + {/if} +
          + {:else if hoverArc} +
          +
          +
          + {hoverArc.severity} conflict +
          +
          topic: {hoverArc.topic}
          +
          + similarity {(hoverArc.similarity * 100).toFixed(0)}% · {hoverArc.dateDiff}d apart +
          +
          + {/if} +
          + + diff --git a/apps/dashboard/src/lib/components/DreamInsightCard.svelte b/apps/dashboard/src/lib/components/DreamInsightCard.svelte new file mode 100644 index 0000000..6024015 --- /dev/null +++ b/apps/dashboard/src/lib/components/DreamInsightCard.svelte @@ -0,0 +1,211 @@ + + + +
          + +
          + + {insight.type ?? 'insight'} + + {#if isHighNovelty} + + novel + + {/if} +
          + + +

          + {insight.insight} +

          + + +
          +
          + Novelty + {novelty.toFixed(2)} +
          +
          +
          +
          +
          + + +
          + Confidence + + {formatConfidencePct(confidence)} + +
          + + + {#if firstSources.length > 0} +
          +
          + Sources + {#if extraCount > 0} + (+{extraCount}) + {/if} +
          +
          + {#each firstSources as id (id)} + + {shortMemoryId(id)} + + {/each} +
          +
          + {/if} +
          + + diff --git a/apps/dashboard/src/lib/components/DreamStageReplay.svelte b/apps/dashboard/src/lib/components/DreamStageReplay.svelte new file mode 100644 index 0000000..b407191 --- /dev/null +++ b/apps/dashboard/src/lib/components/DreamStageReplay.svelte @@ -0,0 +1,539 @@ + + + +
          + +
          +
          +
          + {current.num} +
          +
          +
          {current.name}
          +
          {current.desc}
          +
          +
          + +
          + + +
          + + {#if stageIdx === 5} +
          + Episodic + hippocampus +
          +
          + Semantic + cortex +
          +
          + {/if} + + + + + + {#each cards as card (card.id)} +
          +
          +
          +
          +
          +
          +
          + {/each} + + + {#if stageIdx === 1} + + {/if} +
          + + +
          + {#if stageIdx === 1} + Replaying {dreamResult?.memoriesReplayed ?? cardCount} memories + {:else if stageIdx === 2} + New connections found: {dreamResult?.stats?.newConnectionsFound ?? connectionCount} + {:else if stageIdx === 3} + Strengthened: {dreamResult?.stats?.memoriesStrengthened ?? strengthenedCount} + {:else if stageIdx === 4} + Compressed: {dreamResult?.stats?.memoriesCompressed ?? prunedCount} + {:else if stageIdx === 5} + Connections persisted: {dreamResult?.connectionsPersisted ?? 0} + Insights: {dreamResult?.stats?.insightsGenerated ?? 0} + {/if} +
          +
          + + diff --git a/apps/dashboard/src/lib/components/DuplicateCluster.svelte b/apps/dashboard/src/lib/components/DuplicateCluster.svelte new file mode 100644 index 0000000..f414ec5 --- /dev/null +++ b/apps/dashboard/src/lib/components/DuplicateCluster.svelte @@ -0,0 +1,192 @@ + + + +{#if memories.length > 0 && winner} +
          + +
          +
          +
          + + {(similarity * 100).toFixed(1)}% + + {similarityBandLabel(similarity)} + · {memories.length} memories +
          +
          +
          +
          +
          + + + + Suggested: {suggestedAction === 'merge' ? 'Merge' : 'Review'} + +
          + + +
          + {#each memories as memory (memory.id)} +
          + + + +
          + +
          + {memory.nodeType} + {#if memory.id === winner.id} + + WINNER + + {/if} + {#each safeTags(memory.tags, 4) as tag} + {tag} + {/each} +
          + + +

          + {expanded ? memory.content : previewContent(memory.content)} +

          + + + {#if formatDate(memory.createdAt)} +
          + {formatDate(memory.createdAt)} +
          + {/if} +
          + + +
          +
          +
          +
          + + {(memory.retention * 100).toFixed(0)}% + +
          +
          + {/each} +
          + + +
          + + + +
          +
          +{/if} diff --git a/apps/dashboard/src/lib/components/EvidenceCard.svelte b/apps/dashboard/src/lib/components/EvidenceCard.svelte new file mode 100644 index 0000000..e845666 --- /dev/null +++ b/apps/dashboard/src/lib/components/EvidenceCard.svelte @@ -0,0 +1,157 @@ + + +
          + +
          +
          + + {meta.icon}{meta.label} + + {#if nodeType} + + {nodeType} + + {/if} +
          + #{shortId} +
          + + +

          {preview}

          + + +
          +
          + Trust + {trustPct.toFixed(0)}% +
          +
          +
          +
          +
          + + +
          + {formatDate(date)} + FSRS · reps × retention +
          +
          + + diff --git a/apps/dashboard/src/lib/components/FSRSCalendar.svelte b/apps/dashboard/src/lib/components/FSRSCalendar.svelte new file mode 100644 index 0000000..053dc5e --- /dev/null +++ b/apps/dashboard/src/lib/components/FSRSCalendar.svelte @@ -0,0 +1,344 @@ + + +
          + +
          +
          + Avg retention of memories due — last 2 weeks → next 4 +
          + retention + today +
          +
          + +
          + + +
          + {#each DOW_LABELS as label} +
          {label}
          + {/each} +
          + + +
          + {#each cells as cell (cell.key)} + {@const colors = cellColor(cell)} + + {/each} +
          + + +
          + + + Overdue + + + + Due today + + + + Within 7 days + + + + Future (8+ days) + +
          + + + {#if selectedCell && selectedCell.memories.length > 0} +
          +
          +
          +

          {fullDate(selectedCell.date)}

          +

          + {selectedCell.memories.length} memor{selectedCell.memories.length === 1 ? 'y' : 'ies'} due + · avg retention {(selectedCell.avgRetention * 100).toFixed(0)}% +

          +
          + +
          +
          + {#each selectedCell.memories.slice(0, 100) as m (m.id)} +
          + +
          +

          {m.content}

          +
          + {m.nodeType} + {#if m.reviewCount !== undefined} + · {m.reviewCount} review{m.reviewCount === 1 ? '' : 's'} + {/if} + {#each m.tags.slice(0, 2) as tag} + {tag} + {/each} +
          +
          +
          +
          +
          +
          + {(m.retentionStrength * 100).toFixed(0)}% +
          +
          + {/each} + {#if selectedCell.memories.length > 100} +

          + +{selectedCell.memories.length - 100} more +

          + {/if} +
          +
          + {/if} +
          + + diff --git a/apps/dashboard/src/lib/components/Graph3D.svelte b/apps/dashboard/src/lib/components/Graph3D.svelte index a9960a2..8c0f769 100644 --- a/apps/dashboard/src/lib/components/Graph3D.svelte +++ b/apps/dashboard/src/lib/components/Graph3D.svelte @@ -11,6 +11,7 @@ import { mapEventToEffects, type GraphMutationContext, type GraphMutation } from '$lib/graph/events'; import { createNebulaBackground, updateNebula } from '$lib/graph/shaders/nebula.frag'; import { createPostProcessing, updatePostProcessing, type PostProcessingStack } from '$lib/graph/shaders/post-processing'; + import { graphState } from '$lib/stores/graph-state.svelte'; import type * as THREE from 'three'; interface Props { @@ -19,9 +20,9 @@ centerId: string; events?: VestigeEvent[]; isDreaming?: boolean; - /// v2.0.8: colour mode for node spheres. "type" tints by node type - /// (fact/concept/event/…); "state" tints by FSRS accessibility bucket - /// (active/dormant/silent/unavailable). Toggled live from the graph page. + /// Colour mode for node spheres. "type" tints by node type, "state" + /// tints by FSRS accessibility bucket, and "ahagraph" tints by + /// learning tags from AhaGraph. colorMode?: ColorMode; onSelect?: (nodeId: string) => void; onGraphMutation?: (mutation: GraphMutation) => void; @@ -59,8 +60,13 @@ let nebulaMaterial: THREE.ShaderMaterial; let postStack: PostProcessingStack; - // Event tracking - let processedEventCount = 0; + // Event tracking — we track the last-processed event by reference identity + // rather than by count, because the WebSocket store PREPENDS new events + // at index 0 and CAPS the array at MAX_EVENTS, so a numeric high-water + // mark would drift out of alignment (and did for ~3 versions — v2.3 + // demo uncovered this while trying to fire multiple MemoryCreated events + // in sequence). + let lastProcessedEvent: VestigeEvent | null = null; // Internal tracking: initial nodes + live-added nodes let allNodes: GraphNode[] = []; @@ -116,9 +122,23 @@ if (ctx) disposeScene(ctx); }); + // 120Hz Governor. All physics and effect counters are frame-based + // (orb.age++, forceSim.tick, materialization frames). On a ProMotion + // display the browser drives rAF at 120 FPS, which would double-speed + // every ritual. Clamping to ~60 FPS keeps the visual timing identical + // across displays without rewriting every counter to use delta time. + // The `- (dt % 16)` carry avoids long-term drift. + let govLastTime = 0; + function animate() { animationId = requestAnimationFrame(animate); - const time = performance.now() * 0.001; + const now = performance.now(); + if (govLastTime === 0) govLastTime = now; + const dt = now - govLastTime; + if (dt < 16) return; + govLastTime = now - (dt % 16); + + const time = now * 0.001; // Force simulation forceSim.tick(edges); @@ -132,7 +152,7 @@ // Animate particles.animate(time); - nodeManager.animate(time, allNodes, ctx.camera); + nodeManager.animate(time, allNodes, ctx.camera, graphState.brightness); // Dream mode dreamMode.setActive(isDreaming); @@ -157,10 +177,33 @@ } function processEvents() { - if (!events || events.length <= processedEventCount) return; + if (!events || events.length === 0) return; - const newEvents = events.slice(processedEventCount); - processedEventCount = events.length; + // Walk the feed from newest (index 0) backward until we hit the last + // event we already processed. Everything between is fresh. This is + // robust against both (a) prepend ordering and (b) the MAX_EVENTS cap + // dropping old entries off the tail. + const fresh: VestigeEvent[] = []; + for (const e of events) { + if (e === lastProcessedEvent) break; + fresh.push(e); + } + if (fresh.length === 0) return; + + // Event Horizon Guard. If the last-processed reference fell off the + // end of the capped array (burst of >MAX_EVENTS events in one tick), + // the walk above consumed the ENTIRE buffer — we'd try to animate + // 200 simultaneous births and melt the GPU. Detect the overflow and + // drop this batch on the floor; state is already current via + // lastProcessedEvent pointing forward. + if (fresh.length === events.length && events.length >= 200) { + // eslint-disable-next-line no-console + console.warn('[vestige] Event horizon overflow: dropping visuals for', fresh.length, 'events'); + lastProcessedEvent = events[0]; + return; + } + + lastProcessedEvent = events[0]; const mutationCtx: GraphMutationContext = { effects, @@ -180,8 +223,11 @@ }, }; - for (const event of newEvents) { - mapEventToEffects(event, mutationCtx, allNodes); + // Process oldest-first so cause precedes effect (e.g. MemoryCreated + // fires before a ConnectionDiscovered that references the new node). + // `fresh` is newest-first from the walk above, so iterate reversed. + for (let i = fresh.length - 1; i >= 0; i--) { + mapEventToEffects(fresh[i], mutationCtx, allNodes); } } diff --git a/apps/dashboard/src/lib/components/ImportanceRadar.svelte b/apps/dashboard/src/lib/components/ImportanceRadar.svelte new file mode 100644 index 0000000..92cf1b3 --- /dev/null +++ b/apps/dashboard/src/lib/components/ImportanceRadar.svelte @@ -0,0 +1,174 @@ + + + + + {#each RINGS as ring} + + {/each} + + + {#each AXIS_ORDER as axis} + {@const [x, y] = pointAt(1, axis.angle)} + + {/each} + + + + + + {#if size !== 'sm'} + {#each AXIS_ORDER as axis} + {@const [px, py] = pointAt(values[axis.key] * animProgress, axis.angle)} + + {/each} + {/if} + + + {#if showLabels} + {#each AXIS_ORDER as axis} + {@const pos = labelPos(axis.angle)} + + {(values[axis.key] * 100).toFixed(0)}% + + + {AXIS_LABELS[axis.key]} + + {/each} + {/if} + diff --git a/apps/dashboard/src/lib/components/InsightToast.svelte b/apps/dashboard/src/lib/components/InsightToast.svelte new file mode 100644 index 0000000..8a0a991 --- /dev/null +++ b/apps/dashboard/src/lib/components/InsightToast.svelte @@ -0,0 +1,254 @@ + + + +
          + {#each $toasts as t (t.id)} + + {/each} +
          + + diff --git a/apps/dashboard/src/lib/components/MemoryAuditTrail.svelte b/apps/dashboard/src/lib/components/MemoryAuditTrail.svelte new file mode 100644 index 0000000..4a4e8f3 --- /dev/null +++ b/apps/dashboard/src/lib/components/MemoryAuditTrail.svelte @@ -0,0 +1,185 @@ + + +
          + {#if loading} +
          + {#each Array(5) as _} +
          + {/each} +
          + {:else if errored} +

          Audit trail failed to load.

          + {:else if !memoryId} +

          No memory selected.

          + {:else if events.length === 0} +

          No audit events recorded yet.

          + {:else} +
            + {#each visibleEvents as ev, i (ev.timestamp + i)} + {@const m = META[ev.action]} + {@const delta = formatRetentionDelta(ev.old_value, ev.new_value)} +
          1. + + + + +
            +
            +
            + {m.label} + {#if ev.triggered_by} + {ev.triggered_by} + {/if} +
            + + {relativeTime(ev.timestamp)} + +
            + {#if delta} +
            + retention {delta} +
            + {/if} + {#if ev.reason} +
            {ev.reason}
            + {/if} +
            +
          2. + {/each} +
          + + {#if hiddenCount > 0} + + {/if} + {/if} +
          + + diff --git a/apps/dashboard/src/lib/components/PatternTransferHeatmap.svelte b/apps/dashboard/src/lib/components/PatternTransferHeatmap.svelte new file mode 100644 index 0000000..263661d --- /dev/null +++ b/apps/dashboard/src/lib/components/PatternTransferHeatmap.svelte @@ -0,0 +1,251 @@ + + + +
          + + + + +
          +
          + {mobileList.length} transfer pair{mobileList.length === 1 ? '' : 's'} · tap to filter +
          + {#if mobileList.length === 0} +
          + No cross-project transfers recorded yet. +
          + {:else} + {#each mobileList as row (row.from + '->' + row.to)} + + {/each} + {/if} +
          +
          diff --git a/apps/dashboard/src/lib/components/ReasoningChain.svelte b/apps/dashboard/src/lib/components/ReasoningChain.svelte new file mode 100644 index 0000000..61ea92d --- /dev/null +++ b/apps/dashboard/src/lib/components/ReasoningChain.svelte @@ -0,0 +1,259 @@ + + +
          + {#each STAGES as stage, i (stage.key)} +
          + + {#if i < STAGES.length - 1} +
          + {/if} + + +
          + {stage.icon} +
          + +
          +
          + 0{i + 1} + {stage.label} +
          +

          {hintFor(stage.key, stage.base)}

          +
          + + +
          + {/each} +
          + + diff --git a/apps/dashboard/src/lib/components/ThemeToggle.svelte b/apps/dashboard/src/lib/components/ThemeToggle.svelte new file mode 100644 index 0000000..25539a5 --- /dev/null +++ b/apps/dashboard/src/lib/components/ThemeToggle.svelte @@ -0,0 +1,175 @@ + + + + + + diff --git a/apps/dashboard/src/lib/components/VerdictBar.svelte b/apps/dashboard/src/lib/components/VerdictBar.svelte new file mode 100644 index 0000000..2131642 --- /dev/null +++ b/apps/dashboard/src/lib/components/VerdictBar.svelte @@ -0,0 +1,327 @@ + + +{#if visible} +
          + + + {#if expanded && receipt} +
          +
          +
          +
          Claim
          +

          {displayClaim?.text ?? receipt.draftPreview}

          +
          +
          +
          Verdict
          +

          {displayClaim?.decision ?? receipt.overall} · {displayClaim?.evidence_state ?? verdict}

          +
          +
          +
          Precedent
          +
            + {#each precedentText(displayClaim) as item} +
          • {item}
          • + {/each} +
          +
          +
          +
          Fix
          +

          {displayClaim?.fix || 'No change required.'}

          +
          +
          + +
          + Appeal + {#if appealClaim && receipt.verdictBar === 'VETO'} + + + + {:else if receipt.verdictBar === 'APPEALED'} +

          Appeal recorded.

          + {:else} +

          No appealable veto in this receipt.

          + {/if} +
          +
          + {/if} +
          +{/if} + + diff --git a/apps/dashboard/src/lib/components/__tests__/ActivationNetwork.test.ts b/apps/dashboard/src/lib/components/__tests__/ActivationNetwork.test.ts new file mode 100644 index 0000000..a1641c1 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/ActivationNetwork.test.ts @@ -0,0 +1,464 @@ +/** + * Unit tests for Spreading Activation helpers. + * + * Pure-logic coverage only — the SVG render layer is not exercised here + * (no jsdom). The six concerns we test are the ones that actually decide + * whether the burst looks right: + * + * 1. Per-tick decay math (Collins & Loftus 1975, 0.93/frame) + * 2. Compound decay after N ticks + * 3. Threshold filter (activation < 0.05 → invisible) + * 4. Concentric-ring placement around a source (8-per-ring, even angles) + * 5. Color mapping (source → synapse-glow, unknown type → fallback) + * 6. Staggered edge delay (rank ordering, ring-2 bonus) + * 7. Event-feed filter (only NEW ActivationSpread events since lastSeen) + * + * The test environment is Node (vitest `environment: 'node'`) — the same + * harness the graph + dream helper tests use. + */ +import { describe, it, expect } from 'vitest'; +import { + DECAY, + FALLBACK_COLOR, + MIN_VISIBLE, + RING_GAP, + RING_1_CAPACITY, + SOURCE_COLOR, + STAGGER_PER_RANK, + STAGGER_RING_2_BONUS, + activationColor, + applyDecay, + compoundDecay, + computeRing, + edgeStagger, + filterNewSpreadEvents, + initialActivation, + isVisible, + layoutNeighbours, + ringPositions, + ticksUntilInvisible, +} from '../activation-helpers'; +import { NODE_TYPE_COLORS, type VestigeEvent } from '$types'; + +// --------------------------------------------------------------------------- +// 1. Decay math — single tick +// --------------------------------------------------------------------------- + +describe('applyDecay (Collins & Loftus 1975, 0.93/frame)', () => { + it('multiplies activation by 0.93 per tick', () => { + expect(applyDecay(1)).toBeCloseTo(0.93, 10); + }); + + it('matches the documented constant', () => { + expect(DECAY).toBe(0.93); + }); + + it('returns 0 for zero / negative / non-finite input', () => { + expect(applyDecay(0)).toBe(0); + expect(applyDecay(-0.5)).toBe(0); + expect(applyDecay(Number.NaN)).toBe(0); + expect(applyDecay(Number.POSITIVE_INFINITY)).toBe(0); + }); + + it('preserves strict monotonic decrease', () => { + let a = 1; + let prev = a; + for (let i = 0; i < 50; i++) { + a = applyDecay(a); + if (a === 0) break; + expect(a).toBeLessThan(prev); + prev = a; + } + }); +}); + +// --------------------------------------------------------------------------- +// 2. Compound decay — N ticks +// --------------------------------------------------------------------------- + +describe('compoundDecay', () => { + it('0 ticks returns the input unchanged', () => { + expect(compoundDecay(0.8, 0)).toBe(0.8); + }); + + it('N ticks equals applyDecay called N times', () => { + let iterative = 1; + for (let i = 0; i < 10; i++) iterative = applyDecay(iterative); + expect(compoundDecay(1, 10)).toBeCloseTo(iterative, 10); + }); + + it('5 ticks from 1.0 lands in the 0.69..0.70 band', () => { + // 0.93^5 ≈ 0.6957 + const result = compoundDecay(1, 5); + expect(result).toBeGreaterThan(0.69); + expect(result).toBeLessThan(0.7); + }); + + it('treats negative tick counts as no-op', () => { + expect(compoundDecay(0.5, -3)).toBe(0.5); + }); +}); + +// --------------------------------------------------------------------------- +// 3. Threshold filter — fade/remove below MIN_VISIBLE +// --------------------------------------------------------------------------- + +describe('isVisible / MIN_VISIBLE threshold', () => { + it('MIN_VISIBLE is exactly 0.05', () => { + expect(MIN_VISIBLE).toBe(0.05); + }); + + it('returns true at exactly the threshold (inclusive floor)', () => { + expect(isVisible(0.05)).toBe(true); + }); + + it('returns false just below the threshold', () => { + expect(isVisible(0.0499)).toBe(false); + }); + + it('returns false for zero / negative / NaN', () => { + expect(isVisible(0)).toBe(false); + expect(isVisible(-0.1)).toBe(false); + expect(isVisible(Number.NaN)).toBe(false); + }); + + it('returns true for typical full-activation source', () => { + expect(isVisible(1)).toBe(true); + }); +}); + +describe('ticksUntilInvisible', () => { + it('returns 0 when input is already at/below MIN_VISIBLE', () => { + expect(ticksUntilInvisible(MIN_VISIBLE)).toBe(0); + expect(ticksUntilInvisible(0.03)).toBe(0); + expect(ticksUntilInvisible(0)).toBe(0); + }); + + it('produces a count that actually crosses the threshold', () => { + const n = ticksUntilInvisible(1); + expect(n).toBeGreaterThan(0); + // After n ticks we should be BELOW the threshold... + expect(compoundDecay(1, n)).toBeLessThan(MIN_VISIBLE); + // ...but one fewer tick should still be visible. + expect(compoundDecay(1, n - 1)).toBeGreaterThanOrEqual(MIN_VISIBLE); + }); + + it('takes ~42 ticks for a full-strength burst to fade to threshold', () => { + // log(0.05) / log(0.93) ≈ 41.27 → ceil → 42 + expect(ticksUntilInvisible(1)).toBe(42); + }); +}); + +// --------------------------------------------------------------------------- +// 4. Ring placement +// --------------------------------------------------------------------------- + +describe('computeRing', () => { + it('ranks 0..7 land on ring 1', () => { + for (let r = 0; r < RING_1_CAPACITY; r++) { + expect(computeRing(r)).toBe(1); + } + }); + + it('rank 8 and beyond land on ring 2', () => { + expect(computeRing(RING_1_CAPACITY)).toBe(2); + expect(computeRing(15)).toBe(2); + expect(computeRing(99)).toBe(2); + }); +}); + +describe('ringPositions (concentric circle layout)', () => { + it('returns an empty array for count 0', () => { + expect(ringPositions(0, 0, 0, 1)).toEqual([]); + }); + + it('places 4 nodes on ring 1 at radius RING_GAP, evenly spaced', () => { + const pts = ringPositions(0, 0, 4, 1, 0); + expect(pts).toHaveLength(4); + // First point at angle 0 → (RING_GAP, 0) + expect(pts[0].x).toBeCloseTo(RING_GAP, 6); + expect(pts[0].y).toBeCloseTo(0, 6); + // Every point sits on the circle of the correct radius. + for (const p of pts) { + const dist = Math.hypot(p.x, p.y); + expect(dist).toBeCloseTo(RING_GAP, 6); + } + }); + + it('places ring 2 at 2× RING_GAP from center', () => { + const pts = ringPositions(0, 0, 3, 2, 0); + for (const p of pts) { + expect(Math.hypot(p.x, p.y)).toBeCloseTo(RING_GAP * 2, 6); + } + }); + + it('honours the center (cx, cy)', () => { + const pts = ringPositions(500, 280, 2, 1, 0); + // With angleOffset=0 and 2 points, the two angles are 0 and π. + expect(pts[0].x).toBeCloseTo(500 + RING_GAP, 6); + expect(pts[0].y).toBeCloseTo(280, 6); + expect(pts[1].x).toBeCloseTo(500 - RING_GAP, 6); + expect(pts[1].y).toBeCloseTo(280, 6); + }); + + it('applies angleOffset to every point', () => { + const unrot = ringPositions(0, 0, 3, 1, 0); + const rot = ringPositions(0, 0, 3, 1, Math.PI / 2); + for (let i = 0; i < 3; i++) { + // Rotation preserves distance from center. + expect(Math.hypot(rot[i].x, rot[i].y)).toBeCloseTo( + Math.hypot(unrot[i].x, unrot[i].y), + 6, + ); + } + // And the first rotated point should now be near (0, RING_GAP) rather + // than (RING_GAP, 0). + expect(rot[0].x).toBeCloseTo(0, 6); + expect(rot[0].y).toBeCloseTo(RING_GAP, 6); + }); +}); + +describe('layoutNeighbours (spills overflow to ring 2)', () => { + it('returns one point per neighbour', () => { + expect(layoutNeighbours(0, 0, 15, 0)).toHaveLength(15); + expect(layoutNeighbours(0, 0, 3, 0)).toHaveLength(3); + expect(layoutNeighbours(0, 0, 0, 0)).toHaveLength(0); + }); + + it('first 8 neighbours are on ring 1 (radius RING_GAP)', () => { + const pts = layoutNeighbours(0, 0, 15, 0); + for (let i = 0; i < RING_1_CAPACITY; i++) { + expect(Math.hypot(pts[i].x, pts[i].y)).toBeCloseTo(RING_GAP, 6); + } + }); + + it('neighbour 9..N are on ring 2 (radius 2*RING_GAP)', () => { + const pts = layoutNeighbours(0, 0, 15, 0); + for (let i = RING_1_CAPACITY; i < 15; i++) { + expect(Math.hypot(pts[i].x, pts[i].y)).toBeCloseTo(RING_GAP * 2, 6); + } + }); +}); + +describe('initialActivation', () => { + it('rank 0 gets the highest activation', () => { + const a0 = initialActivation(0, 10); + const a1 = initialActivation(1, 10); + expect(a0).toBeGreaterThan(a1); + }); + + it('ring-2 ranks get a 0.75 ring penalty', () => { + // Rank 7 (last of ring 1) vs rank 8 (first of ring 2) — the jump in + // activation between them should include the 0.75 ring factor. + const ring1Last = initialActivation(7, 16); + const ring2First = initialActivation(8, 16); + expect(ring2First).toBeLessThan(ring1Last * 0.78); + }); + + it('returns values in (0, 1]', () => { + for (let i = 0; i < 20; i++) { + const a = initialActivation(i, 20); + expect(a).toBeGreaterThan(0); + expect(a).toBeLessThanOrEqual(1); + } + }); + + it('returns 0 for invalid inputs', () => { + expect(initialActivation(-1, 10)).toBe(0); + expect(initialActivation(0, 0)).toBe(0); + expect(initialActivation(Number.NaN, 10)).toBe(0); + }); +}); + +// --------------------------------------------------------------------------- +// 5. Color mapping +// --------------------------------------------------------------------------- + +describe('activationColor', () => { + it('source nodes always use SOURCE_COLOR (synapse-glow)', () => { + expect(activationColor('fact', true)).toBe(SOURCE_COLOR); + expect(activationColor('concept', true)).toBe(SOURCE_COLOR); + // Even if nodeType is garbage, source overrides. + expect(activationColor('garbage-type', true)).toBe(SOURCE_COLOR); + }); + + it('fact → NODE_TYPE_COLORS.fact (#00A8FF)', () => { + expect(activationColor('fact', false)).toBe(NODE_TYPE_COLORS.fact); + expect(activationColor('fact', false)).toBe('#00A8FF'); + }); + + it('every known node type resolves to its palette entry', () => { + for (const type of Object.keys(NODE_TYPE_COLORS)) { + expect(activationColor(type, false)).toBe(NODE_TYPE_COLORS[type]); + } + }); + + it('unknown node type falls back to FALLBACK_COLOR (soft steel)', () => { + expect(activationColor('not-a-real-type', false)).toBe(FALLBACK_COLOR); + expect(FALLBACK_COLOR).toBe('#8B95A5'); + }); + + it('null/undefined/empty nodeType also falls back', () => { + expect(activationColor(null, false)).toBe(FALLBACK_COLOR); + expect(activationColor(undefined, false)).toBe(FALLBACK_COLOR); + expect(activationColor('', false)).toBe(FALLBACK_COLOR); + }); +}); + +// --------------------------------------------------------------------------- +// 6. Staggered edge delay +// --------------------------------------------------------------------------- + +describe('edgeStagger', () => { + it('rank 0 has zero delay (first edge lights up immediately)', () => { + expect(edgeStagger(0)).toBe(0); + }); + + it('ring-1 edges are STAGGER_PER_RANK apart', () => { + expect(edgeStagger(1)).toBe(STAGGER_PER_RANK); + expect(edgeStagger(2)).toBe(STAGGER_PER_RANK * 2); + expect(edgeStagger(7)).toBe(STAGGER_PER_RANK * 7); + }); + + it('ring-2 edges add STAGGER_RING_2_BONUS on top of rank×stagger', () => { + expect(edgeStagger(8)).toBe(8 * STAGGER_PER_RANK + STAGGER_RING_2_BONUS); + expect(edgeStagger(12)).toBe(12 * STAGGER_PER_RANK + STAGGER_RING_2_BONUS); + }); + + it('monotonically non-decreasing', () => { + let prev = -1; + for (let i = 0; i < 20; i++) { + const s = edgeStagger(i); + expect(s).toBeGreaterThanOrEqual(prev); + prev = s; + } + }); + + it('produces 15 distinct delays for a typical 15-neighbour burst', () => { + const delays = Array.from({ length: 15 }, (_, i) => edgeStagger(i)); + expect(new Set(delays).size).toBe(15); + }); +}); + +// --------------------------------------------------------------------------- +// 7. Event-feed filter +// --------------------------------------------------------------------------- + +function spreadEvent( + source_id: string, + target_ids: string[], +): VestigeEvent { + return { type: 'ActivationSpread', data: { source_id, target_ids } }; +} + +describe('filterNewSpreadEvents', () => { + it('returns [] on empty feed', () => { + expect(filterNewSpreadEvents([], null)).toEqual([]); + }); + + it('returns all ActivationSpread payloads when lastSeen is null', () => { + const feed = [ + spreadEvent('a', ['b', 'c']), + spreadEvent('d', ['e']), + ]; + const out = filterNewSpreadEvents(feed, null); + expect(out).toHaveLength(2); + }); + + it('returns in oldest-first order (feed itself is newest-first)', () => { + const newest = spreadEvent('new', ['n1']); + const older = spreadEvent('old', ['o1']); + const out = filterNewSpreadEvents([newest, older], null); + expect(out[0].source_id).toBe('old'); + expect(out[1].source_id).toBe('new'); + }); + + it('stops at the lastSeen reference (object identity)', () => { + const oldest = spreadEvent('o', ['x']); + const middle = spreadEvent('m', ['y']); + const newest = spreadEvent('n', ['z']); + // Feed is prepended, so order is [newest, middle, oldest] + const feed = [newest, middle, oldest]; + const out = filterNewSpreadEvents(feed, middle); + // Only `newest` is fresh — middle and oldest were already processed. + expect(out).toHaveLength(1); + expect(out[0].source_id).toBe('n'); + }); + + it('returns [] if lastSeen is already the newest event', () => { + const e = spreadEvent('a', ['b']); + const out = filterNewSpreadEvents([e], e); + expect(out).toEqual([]); + }); + + it('ignores non-ActivationSpread events', () => { + const feed: VestigeEvent[] = [ + { type: 'MemoryCreated', data: { id: 'x' } }, + spreadEvent('a', ['b']), + { type: 'Heartbeat', data: {} }, + ]; + const out = filterNewSpreadEvents(feed, null); + expect(out).toHaveLength(1); + expect(out[0].source_id).toBe('a'); + }); + + it('skips malformed ActivationSpread events (missing / wrong-type fields)', () => { + const feed: VestigeEvent[] = [ + { type: 'ActivationSpread', data: {} }, // missing both + { type: 'ActivationSpread', data: { source_id: 'a' } }, // no targets + { type: 'ActivationSpread', data: { target_ids: ['b'] } }, // no source + { + type: 'ActivationSpread', + data: { source_id: 'a', target_ids: 'not-an-array' }, + }, + { + type: 'ActivationSpread', + data: { source_id: 'a', target_ids: [123, null, 'x'] }, + }, + ]; + const out = filterNewSpreadEvents(feed, null); + // Only the last one survives, with numeric/null targets filtered out. + expect(out).toHaveLength(1); + expect(out[0].source_id).toBe('a'); + expect(out[0].target_ids).toEqual(['x']); + }); + + it('preserves target array contents faithfully', () => { + const feed = [spreadEvent('src', ['t1', 't2', 't3'])]; + const out = filterNewSpreadEvents(feed, null); + expect(out[0].target_ids).toEqual(['t1', 't2', 't3']); + }); + + it('does not mutate its inputs', () => { + const feed = [spreadEvent('a', ['b', 'c'])]; + const snapshot = JSON.stringify(feed); + filterNewSpreadEvents(feed, null); + expect(JSON.stringify(feed)).toBe(snapshot); + }); +}); + +// --------------------------------------------------------------------------- +// Sanity: exported constants are the values the docstring promises +// --------------------------------------------------------------------------- + +describe('exported constants (contract pinning)', () => { + it('RING_1_CAPACITY is 8', () => { + expect(RING_1_CAPACITY).toBe(8); + }); + + it('STAGGER_PER_RANK is 4 frames', () => { + expect(STAGGER_PER_RANK).toBe(4); + }); + + it('STAGGER_RING_2_BONUS is 12 frames', () => { + expect(STAGGER_RING_2_BONUS).toBe(12); + }); + + it('RING_GAP is 140px', () => { + expect(RING_GAP).toBe(140); + }); + + it('SOURCE_COLOR is synapse-glow #818cf8', () => { + expect(SOURCE_COLOR).toBe('#818cf8'); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/AmbientAwarenessStrip.test.ts b/apps/dashboard/src/lib/components/__tests__/AmbientAwarenessStrip.test.ts new file mode 100644 index 0000000..e159e82 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/AmbientAwarenessStrip.test.ts @@ -0,0 +1,439 @@ +import { describe, it, expect } from 'vitest'; +import { + ACTIVITY_BUCKET_COUNT, + ACTIVITY_BUCKET_MS, + ACTIVITY_WINDOW_MS, + bucketizeActivity, + dreamInsightsCount, + findRecentDream, + formatAgo, + hasRecentSuppression, + isDreaming, + parseEventTimestamp, + type EventLike, +} from '../awareness-helpers'; + +// Fixed "now" — March 1 2026 12:00:00 UTC. All tests are clock-free. +const NOW = Date.parse('2026-03-01T12:00:00.000Z'); + +function mkEvent( + type: string, + data: Record = {}, +): EventLike { + return { type, data }; +} + +// ───────────────────────────────────────────────────────────────────────── +// parseEventTimestamp +// ───────────────────────────────────────────────────────────────────────── +describe('parseEventTimestamp', () => { + it('parses ISO-8601 string', () => { + const e = mkEvent('Foo', { timestamp: '2026-03-01T12:00:00.000Z' }); + expect(parseEventTimestamp(e)).toBe(NOW); + }); + + it('parses numeric ms (> 1e12)', () => { + const e = mkEvent('Foo', { timestamp: NOW }); + expect(parseEventTimestamp(e)).toBe(NOW); + }); + + it('parses numeric seconds (<= 1e12) by scaling x1000', () => { + const secs = Math.floor(NOW / 1000); + const e = mkEvent('Foo', { timestamp: secs }); + // Allow floating precision — must land in same second + const result = parseEventTimestamp(e); + expect(result).not.toBeNull(); + expect(Math.abs((result as number) - NOW)).toBeLessThan(1000); + }); + + it('falls back to `at` field', () => { + const e = mkEvent('Foo', { at: '2026-03-01T12:00:00.000Z' }); + expect(parseEventTimestamp(e)).toBe(NOW); + }); + + it('falls back to `occurred_at` field', () => { + const e = mkEvent('Foo', { occurred_at: '2026-03-01T12:00:00.000Z' }); + expect(parseEventTimestamp(e)).toBe(NOW); + }); + + it('prefers `timestamp` over `at` over `occurred_at`', () => { + const e = mkEvent('Foo', { + timestamp: '2026-03-01T12:00:00.000Z', + at: '2020-01-01T00:00:00.000Z', + occurred_at: '2019-01-01T00:00:00.000Z', + }); + expect(parseEventTimestamp(e)).toBe(NOW); + }); + + it('returns null for missing data', () => { + expect(parseEventTimestamp({ type: 'Foo' })).toBeNull(); + }); + + it('returns null for empty data object', () => { + expect(parseEventTimestamp(mkEvent('Foo', {}))).toBeNull(); + }); + + it('returns null for bad ISO string', () => { + expect(parseEventTimestamp(mkEvent('Foo', { timestamp: 'not-a-date' }))).toBeNull(); + }); + + it('returns null for non-finite number (NaN)', () => { + expect(parseEventTimestamp(mkEvent('Foo', { timestamp: Number.NaN }))).toBeNull(); + }); + + it('returns null for non-finite number (Infinity)', () => { + expect(parseEventTimestamp(mkEvent('Foo', { timestamp: Number.POSITIVE_INFINITY }))).toBeNull(); + }); + + it('returns null for null timestamp', () => { + expect(parseEventTimestamp(mkEvent('Foo', { timestamp: null as unknown as string }))).toBeNull(); + }); + + it('returns null for non-string non-number timestamp (object)', () => { + expect(parseEventTimestamp(mkEvent('Foo', { timestamp: {} as unknown as string }))).toBeNull(); + }); + + it('returns null for a boolean timestamp', () => { + expect(parseEventTimestamp(mkEvent('Foo', { timestamp: true as unknown as string }))).toBeNull(); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// bucketizeActivity +// ───────────────────────────────────────────────────────────────────────── +describe('bucketizeActivity', () => { + it('returns 10 buckets of 30s each covering a 5-min window', () => { + expect(ACTIVITY_BUCKET_COUNT).toBe(10); + expect(ACTIVITY_BUCKET_MS).toBe(30_000); + expect(ACTIVITY_WINDOW_MS).toBe(300_000); + const result = bucketizeActivity([], NOW); + expect(result).toHaveLength(10); + expect(result.every((b) => b.count === 0 && b.ratio === 0)).toBe(true); + }); + + it('assigns newest event to the last bucket (index 9)', () => { + const e = mkEvent('MemoryCreated', { timestamp: NOW - 100 }); + const result = bucketizeActivity([e], NOW); + expect(result[9].count).toBe(1); + expect(result[9].ratio).toBe(1); + for (let i = 0; i < 9; i++) expect(result[i].count).toBe(0); + }); + + it('assigns oldest-edge event to bucket 0', () => { + // Exactly 5 min ago → at start boundary → floor((0)/30s) = 0 + const e = mkEvent('MemoryCreated', { timestamp: NOW - ACTIVITY_WINDOW_MS + 1 }); + const result = bucketizeActivity([e], NOW); + expect(result[0].count).toBe(1); + }); + + it('drops events older than 5 min (clock skew / pre-history)', () => { + const e = mkEvent('MemoryCreated', { timestamp: NOW - ACTIVITY_WINDOW_MS - 1 }); + const result = bucketizeActivity([e], NOW); + expect(result.every((b) => b.count === 0)).toBe(true); + }); + + it('drops future events (negative clock skew)', () => { + const e = mkEvent('MemoryCreated', { timestamp: NOW + 5_000 }); + const result = bucketizeActivity([e], NOW); + expect(result.every((b) => b.count === 0)).toBe(true); + }); + + it('drops Heartbeat events as noise', () => { + const e = mkEvent('Heartbeat', { timestamp: NOW - 100 }); + const result = bucketizeActivity([e], NOW); + expect(result.every((b) => b.count === 0)).toBe(true); + }); + + it('drops events with unparseable timestamps', () => { + const e = mkEvent('MemoryCreated', { timestamp: 'garbage' }); + const result = bucketizeActivity([e], NOW); + expect(result.every((b) => b.count === 0)).toBe(true); + }); + + it('distributes events across buckets and computes correct ratios', () => { + const events = [ + // Bucket 9 (newest 30s): 3 events + mkEvent('MemoryCreated', { timestamp: NOW - 5_000 }), + mkEvent('MemoryCreated', { timestamp: NOW - 10_000 }), + mkEvent('MemoryCreated', { timestamp: NOW - 15_000 }), + // Bucket 8: 1 event (31s - 60s ago) + mkEvent('MemoryCreated', { timestamp: NOW - 35_000 }), + // Bucket 0 (oldest): 1 event (270s - 300s ago) + mkEvent('MemoryCreated', { timestamp: NOW - 290_000 }), + ]; + const result = bucketizeActivity(events, NOW); + expect(result[9].count).toBe(3); + expect(result[8].count).toBe(1); + expect(result[0].count).toBe(1); + expect(result[9].ratio).toBe(1); + expect(result[8].ratio).toBeCloseTo(1 / 3, 5); + expect(result[0].ratio).toBeCloseTo(1 / 3, 5); + }); + + it('handles events with numeric ms timestamp', () => { + const e = { type: 'MemoryCreated', data: { timestamp: NOW - 10_000 } }; + const result = bucketizeActivity([e], NOW); + expect(result[9].count).toBe(1); + }); + + it('works with a mixed real-world feed (200 events, some stale)', () => { + const events: EventLike[] = []; + for (let i = 0; i < 200; i++) { + const offset = i * 3_000; // one every 3s, oldest first + events.unshift(mkEvent('MemoryCreated', { timestamp: NOW - offset })); + } + // add 10 Heartbeats mid-stream + for (let i = 0; i < 10; i++) { + events.push(mkEvent('Heartbeat', { timestamp: NOW - i * 1_000 })); + } + const result = bucketizeActivity(events, NOW); + // 101 events fit in the [now-300s, now] window: offsets 0, 3s, 6s, …, 300s. + // Heartbeats excluded. Sum should be exactly 101. + const total = result.reduce((s, b) => s + b.count, 0); + expect(total).toBe(101); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// findRecentDream +// ───────────────────────────────────────────────────────────────────────── +describe('findRecentDream', () => { + it('returns null on empty feed', () => { + expect(findRecentDream([], NOW)).toBeNull(); + }); + + it('returns null when no DreamCompleted in feed', () => { + const feed = [ + mkEvent('MemoryCreated', { timestamp: NOW - 1000 }), + mkEvent('DreamStarted', { timestamp: NOW - 500 }), + ]; + expect(findRecentDream(feed, NOW)).toBeNull(); + }); + + it('returns the newest DreamCompleted within 24h', () => { + const fresh = mkEvent('DreamCompleted', { + timestamp: NOW - 60_000, + insights_generated: 7, + }); + const stale = mkEvent('DreamCompleted', { + timestamp: NOW - 2 * 24 * 60 * 60 * 1000, + }); + // Feed is newest-first + const result = findRecentDream([fresh, stale], NOW); + expect(result).toBe(fresh); + }); + + it('returns null when only DreamCompleted is older than 24h', () => { + const stale = mkEvent('DreamCompleted', { + timestamp: NOW - 25 * 60 * 60 * 1000, + }); + expect(findRecentDream([stale], NOW)).toBeNull(); + }); + + it('exactly 24h ago still counts (inclusive)', () => { + const edge = mkEvent('DreamCompleted', { + timestamp: NOW - 24 * 60 * 60 * 1000, + }); + expect(findRecentDream([edge], NOW)).toBe(edge); + }); + + it('stops at first DreamCompleted in newest-first feed', () => { + const newest = mkEvent('DreamCompleted', { timestamp: NOW - 1_000 }); + const older = mkEvent('DreamCompleted', { timestamp: NOW - 60_000 }); + expect(findRecentDream([newest, older], NOW)).toBe(newest); + }); + + it('falls back to nowMs for unparseable timestamps (treated as recent)', () => { + const e = mkEvent('DreamCompleted', { timestamp: 'bad' }); + expect(findRecentDream([e], NOW)).toBe(e); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// dreamInsightsCount +// ───────────────────────────────────────────────────────────────────────── +describe('dreamInsightsCount', () => { + it('returns null for null input', () => { + expect(dreamInsightsCount(null)).toBeNull(); + }); + + it('returns null when missing', () => { + expect(dreamInsightsCount(mkEvent('DreamCompleted', {}))).toBeNull(); + }); + + it('reads insights_generated (snake_case)', () => { + expect( + dreamInsightsCount(mkEvent('DreamCompleted', { insights_generated: 5 })), + ).toBe(5); + }); + + it('reads insightsGenerated (camelCase)', () => { + expect( + dreamInsightsCount(mkEvent('DreamCompleted', { insightsGenerated: 3 })), + ).toBe(3); + }); + + it('prefers snake_case when both present', () => { + expect( + dreamInsightsCount( + mkEvent('DreamCompleted', { insights_generated: 7, insightsGenerated: 99 }), + ), + ).toBe(7); + }); + + it('returns null for non-numeric value', () => { + expect( + dreamInsightsCount(mkEvent('DreamCompleted', { insights_generated: 'seven' as unknown as number })), + ).toBeNull(); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// isDreaming +// ───────────────────────────────────────────────────────────────────────── +describe('isDreaming', () => { + it('returns false for empty feed', () => { + expect(isDreaming([], NOW)).toBe(false); + }); + + it('returns false when no DreamStarted in feed', () => { + expect(isDreaming([mkEvent('MemoryCreated', { timestamp: NOW })], NOW)).toBe(false); + }); + + it('returns true for DreamStarted in last 5 min with no DreamCompleted', () => { + const feed = [mkEvent('DreamStarted', { timestamp: NOW - 60_000 })]; + expect(isDreaming(feed, NOW)).toBe(true); + }); + + it('returns false for DreamStarted older than 5 min with no DreamCompleted', () => { + const feed = [mkEvent('DreamStarted', { timestamp: NOW - 6 * 60 * 1000 })]; + expect(isDreaming(feed, NOW)).toBe(false); + }); + + it('returns false when DreamCompleted newer than DreamStarted', () => { + // Feed is newest-first: completed, then started + const feed = [ + mkEvent('DreamCompleted', { timestamp: NOW - 30_000 }), + mkEvent('DreamStarted', { timestamp: NOW - 60_000 }), + ]; + expect(isDreaming(feed, NOW)).toBe(false); + }); + + it('returns true when DreamCompleted is OLDER than DreamStarted (new cycle began)', () => { + // Newest-first: started is newer, and there's an older completed from a prior cycle + const feed = [ + mkEvent('DreamStarted', { timestamp: NOW - 30_000 }), + mkEvent('DreamCompleted', { timestamp: NOW - 10 * 60 * 1000 }), + ]; + expect(isDreaming(feed, NOW)).toBe(true); + }); + + it('boundary: DreamStarted exactly 5 min ago → still dreaming (>= check)', () => { + const feed = [mkEvent('DreamStarted', { timestamp: NOW - 5 * 60 * 1000 })]; + expect(isDreaming(feed, NOW)).toBe(true); + }); + + it('only considers FIRST DreamStarted / FIRST DreamCompleted (newest-first semantics)', () => { + const feed = [ + mkEvent('DreamStarted', { timestamp: NOW - 10_000 }), + mkEvent('DreamCompleted', { timestamp: NOW - 20_000 }), // older — prior cycle + mkEvent('DreamStarted', { timestamp: NOW - 30_000 }), // ignored + ]; + expect(isDreaming(feed, NOW)).toBe(true); + }); + + it('unparseable DreamStarted timestamp falls back to nowMs (counts as dreaming)', () => { + const feed = [mkEvent('DreamStarted', { timestamp: 'bad' })]; + expect(isDreaming(feed, NOW)).toBe(true); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// hasRecentSuppression +// ───────────────────────────────────────────────────────────────────────── +describe('hasRecentSuppression', () => { + it('returns false for empty feed', () => { + expect(hasRecentSuppression([], NOW)).toBe(false); + }); + + it('returns false when no MemorySuppressed in feed', () => { + const feed = [ + mkEvent('MemoryCreated', { timestamp: NOW }), + mkEvent('DreamStarted', { timestamp: NOW }), + ]; + expect(hasRecentSuppression(feed, NOW)).toBe(false); + }); + + it('returns true for MemorySuppressed within 10s', () => { + const feed = [mkEvent('MemorySuppressed', { timestamp: NOW - 5_000 })]; + expect(hasRecentSuppression(feed, NOW)).toBe(true); + }); + + it('returns false for MemorySuppressed older than 10s', () => { + const feed = [mkEvent('MemorySuppressed', { timestamp: NOW - 11_000 })]; + expect(hasRecentSuppression(feed, NOW)).toBe(false); + }); + + it('respects custom threshold', () => { + const feed = [mkEvent('MemorySuppressed', { timestamp: NOW - 8_000 })]; + expect(hasRecentSuppression(feed, NOW, 5_000)).toBe(false); + expect(hasRecentSuppression(feed, NOW, 10_000)).toBe(true); + }); + + it('stops at first MemorySuppressed (newest-first short-circuit)', () => { + const feed = [ + mkEvent('MemorySuppressed', { timestamp: NOW - 30_000 }), // first, outside window + mkEvent('MemorySuppressed', { timestamp: NOW - 1_000 }), // inside, but never checked + ]; + expect(hasRecentSuppression(feed, NOW)).toBe(false); + }); + + it('boundary: exactly at threshold counts (>= check)', () => { + const feed = [mkEvent('MemorySuppressed', { timestamp: NOW - 10_000 })]; + expect(hasRecentSuppression(feed, NOW, 10_000)).toBe(true); + }); + + it('unparseable timestamp falls back to nowMs (flash fires)', () => { + const feed = [mkEvent('MemorySuppressed', { timestamp: 'bad' })]; + expect(hasRecentSuppression(feed, NOW)).toBe(true); + }); + + it('ignores non-MemorySuppressed events before finding one', () => { + const feed = [ + mkEvent('MemoryCreated', { timestamp: NOW }), + mkEvent('DreamStarted', { timestamp: NOW }), + mkEvent('MemorySuppressed', { timestamp: NOW - 3_000 }), + ]; + expect(hasRecentSuppression(feed, NOW)).toBe(true); + }); +}); + +// ───────────────────────────────────────────────────────────────────────── +// formatAgo +// ───────────────────────────────────────────────────────────────────────── +describe('formatAgo', () => { + it('formats seconds', () => { + expect(formatAgo(5_000)).toBe('5s ago'); + expect(formatAgo(59_000)).toBe('59s ago'); + expect(formatAgo(0)).toBe('0s ago'); + }); + + it('formats minutes', () => { + expect(formatAgo(60_000)).toBe('1m ago'); + expect(formatAgo(59 * 60_000)).toBe('59m ago'); + }); + + it('formats hours', () => { + expect(formatAgo(60 * 60_000)).toBe('1h ago'); + expect(formatAgo(23 * 60 * 60_000)).toBe('23h ago'); + }); + + it('formats days', () => { + expect(formatAgo(24 * 60 * 60_000)).toBe('1d ago'); + expect(formatAgo(7 * 24 * 60 * 60_000)).toBe('7d ago'); + }); + + it('clamps negative input to 0', () => { + expect(formatAgo(-5_000)).toBe('0s ago'); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/ContradictionArcs.test.ts b/apps/dashboard/src/lib/components/__tests__/ContradictionArcs.test.ts new file mode 100644 index 0000000..5573d4c --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/ContradictionArcs.test.ts @@ -0,0 +1,326 @@ +/** + * Contradiction Constellation — pure-helper coverage. + * + * Runs in the vitest `node` environment (no jsdom). We only test the pure + * helpers extracted to `contradiction-helpers.ts`; the Svelte component is + * covered indirectly because every classification, opacity, radius, and + * color decision it renders routes through these functions. + */ +import { describe, it, expect } from 'vitest'; + +import { + severityColor, + severityLabel, + nodeColor, + nodeRadius, + clampTrust, + pairOpacity, + truncate, + uniqueMemoryCount, + avgTrustDelta, + NODE_COLORS, + KNOWN_NODE_TYPES, + NODE_COLOR_FALLBACK, + NODE_RADIUS_MIN, + NODE_RADIUS_RANGE, + SEVERITY_STRONG_COLOR, + SEVERITY_MODERATE_COLOR, + SEVERITY_MILD_COLOR, + UNFOCUSED_OPACITY, + type ContradictionLike, +} from '../contradiction-helpers'; + +// --------------------------------------------------------------------------- +// severityColor — strict-greater-than thresholds at 0.5 and 0.7. +// --------------------------------------------------------------------------- + +describe('severityColor', () => { + it('returns mild yellow at or below 0.5', () => { + expect(severityColor(0)).toBe(SEVERITY_MILD_COLOR); + expect(severityColor(0.29)).toBe(SEVERITY_MILD_COLOR); + expect(severityColor(0.3)).toBe(SEVERITY_MILD_COLOR); + expect(severityColor(0.5)).toBe(SEVERITY_MILD_COLOR); // boundary → lower band + }); + + it('returns moderate amber strictly above 0.5 and up to 0.7', () => { + expect(severityColor(0.51)).toBe(SEVERITY_MODERATE_COLOR); + expect(severityColor(0.6)).toBe(SEVERITY_MODERATE_COLOR); + expect(severityColor(0.7)).toBe(SEVERITY_MODERATE_COLOR); // boundary → lower band + }); + + it('returns strong red strictly above 0.7', () => { + expect(severityColor(0.71)).toBe(SEVERITY_STRONG_COLOR); + expect(severityColor(0.9)).toBe(SEVERITY_STRONG_COLOR); + expect(severityColor(1.0)).toBe(SEVERITY_STRONG_COLOR); + }); + + it('handles out-of-range numbers without crashing', () => { + expect(severityColor(-1)).toBe(SEVERITY_MILD_COLOR); + expect(severityColor(1.5)).toBe(SEVERITY_STRONG_COLOR); + }); +}); + +// --------------------------------------------------------------------------- +// severityLabel — matches severityColor thresholds. +// --------------------------------------------------------------------------- + +describe('severityLabel', () => { + it('labels mild at 0, 0.29, 0.3, 0.5', () => { + expect(severityLabel(0)).toBe('mild'); + expect(severityLabel(0.29)).toBe('mild'); + expect(severityLabel(0.3)).toBe('mild'); + expect(severityLabel(0.5)).toBe('mild'); + }); + + it('labels moderate at 0.51, 0.7', () => { + expect(severityLabel(0.51)).toBe('moderate'); + expect(severityLabel(0.7)).toBe('moderate'); + }); + + it('labels strong at 0.71, 1.0', () => { + expect(severityLabel(0.71)).toBe('strong'); + expect(severityLabel(1.0)).toBe('strong'); + }); + + it('covers all 8 ordered boundary cases from the audit', () => { + expect(severityLabel(0)).toBe('mild'); + expect(severityLabel(0.29)).toBe('mild'); + expect(severityLabel(0.3)).toBe('mild'); + expect(severityLabel(0.5)).toBe('mild'); + expect(severityLabel(0.51)).toBe('moderate'); + expect(severityLabel(0.7)).toBe('moderate'); + expect(severityLabel(0.71)).toBe('strong'); + expect(severityLabel(1.0)).toBe('strong'); + }); +}); + +// --------------------------------------------------------------------------- +// nodeColor — 8 known types plus fallback. +// --------------------------------------------------------------------------- + +describe('nodeColor', () => { + it('returns distinct colors for each of the 8 known node types', () => { + const colors = KNOWN_NODE_TYPES.map((t) => nodeColor(t)); + expect(colors.length).toBe(8); + expect(new Set(colors).size).toBe(8); // all distinct + }); + + it('matches the canonical palette exactly', () => { + expect(nodeColor('fact')).toBe(NODE_COLORS.fact); + expect(nodeColor('concept')).toBe(NODE_COLORS.concept); + expect(nodeColor('event')).toBe(NODE_COLORS.event); + expect(nodeColor('person')).toBe(NODE_COLORS.person); + expect(nodeColor('place')).toBe(NODE_COLORS.place); + expect(nodeColor('note')).toBe(NODE_COLORS.note); + expect(nodeColor('pattern')).toBe(NODE_COLORS.pattern); + expect(nodeColor('decision')).toBe(NODE_COLORS.decision); + }); + + it('falls back to violet for unknown / missing types', () => { + expect(nodeColor(undefined)).toBe(NODE_COLOR_FALLBACK); + expect(nodeColor(null)).toBe(NODE_COLOR_FALLBACK); + expect(nodeColor('')).toBe(NODE_COLOR_FALLBACK); + expect(nodeColor('bogus')).toBe(NODE_COLOR_FALLBACK); + expect(nodeColor('FACT')).toBe(NODE_COLOR_FALLBACK); // case-sensitive + }); + + it('violet fallback equals 0x8b5cf6', () => { + expect(NODE_COLOR_FALLBACK).toBe('#8b5cf6'); + }); +}); + +// --------------------------------------------------------------------------- +// nodeRadius + clampTrust — trust is defined on [0,1]. +// --------------------------------------------------------------------------- + +describe('nodeRadius', () => { + it('returns the minimum radius at trust=0', () => { + expect(nodeRadius(0)).toBe(NODE_RADIUS_MIN); + }); + + it('returns min + range at trust=1', () => { + expect(nodeRadius(1)).toBe(NODE_RADIUS_MIN + NODE_RADIUS_RANGE); + }); + + it('scales linearly in between', () => { + expect(nodeRadius(0.5)).toBeCloseTo(NODE_RADIUS_MIN + NODE_RADIUS_RANGE * 0.5); + }); + + it('clamps negative trust to 0 (minimum radius)', () => { + expect(nodeRadius(-0.5)).toBe(NODE_RADIUS_MIN); + expect(nodeRadius(-Infinity)).toBe(NODE_RADIUS_MIN); + }); + + it('clamps >1 trust to 1 (maximum radius)', () => { + expect(nodeRadius(2)).toBe(NODE_RADIUS_MIN + NODE_RADIUS_RANGE); + expect(nodeRadius(Infinity)).toBe(NODE_RADIUS_MIN); + // ^ Infinity isn't finite — falls back to min, matching "suppress suspicious data" + }); + + it('treats NaN as minimum (suppress bad data)', () => { + expect(nodeRadius(NaN)).toBe(NODE_RADIUS_MIN); + }); +}); + +describe('clampTrust', () => { + it('returns values inside [0,1] unchanged', () => { + expect(clampTrust(0)).toBe(0); + expect(clampTrust(0.5)).toBe(0.5); + expect(clampTrust(1)).toBe(1); + }); + + it('clamps negatives to 0 and >1 to 1', () => { + expect(clampTrust(-0.3)).toBe(0); + expect(clampTrust(1.3)).toBe(1); + }); + + it('collapses NaN / null / undefined / Infinity to 0', () => { + expect(clampTrust(NaN)).toBe(0); + expect(clampTrust(null)).toBe(0); + expect(clampTrust(undefined)).toBe(0); + expect(clampTrust(Infinity)).toBe(0); + expect(clampTrust(-Infinity)).toBe(0); + }); +}); + +// --------------------------------------------------------------------------- +// pairOpacity — trinary: no focus = 1, focused = 1, unfocused = 0.12. +// --------------------------------------------------------------------------- + +describe('pairOpacity', () => { + it('returns 1 when no pair is focused (null)', () => { + expect(pairOpacity(0, null)).toBe(1); + expect(pairOpacity(5, null)).toBe(1); + }); + + it('returns 1 when no pair is focused (undefined)', () => { + expect(pairOpacity(0, undefined)).toBe(1); + expect(pairOpacity(5, undefined)).toBe(1); + }); + + it('returns 1 for the focused pair', () => { + expect(pairOpacity(3, 3)).toBe(1); + expect(pairOpacity(0, 0)).toBe(1); + }); + + it('returns 0.12 for a non-focused pair when something is focused', () => { + expect(pairOpacity(0, 3)).toBe(UNFOCUSED_OPACITY); + expect(pairOpacity(7, 3)).toBe(UNFOCUSED_OPACITY); + }); + + it('does not explode for a stale focus index that matches nothing', () => { + // A focus index of 999 with only 5 pairs: every visible pair dims to 0.12. + // The missing pair renders nothing (silent no-op is correct). + for (let i = 0; i < 5; i++) { + expect(pairOpacity(i, 999)).toBe(UNFOCUSED_OPACITY); + } + }); +}); + +// --------------------------------------------------------------------------- +// truncate — length boundaries, empties, odd inputs. +// --------------------------------------------------------------------------- + +describe('truncate', () => { + it('returns strings shorter than max unchanged', () => { + expect(truncate('hi', 10)).toBe('hi'); + expect(truncate('abc', 5)).toBe('abc'); + }); + + it('returns empty strings unchanged', () => { + expect(truncate('', 5)).toBe(''); + expect(truncate('', 0)).toBe(''); + }); + + it('returns strings exactly at max unchanged', () => { + expect(truncate('12345', 5)).toBe('12345'); + expect(truncate('abcdef', 6)).toBe('abcdef'); + }); + + it('cuts strings longer than max, appending ellipsis within budget', () => { + expect(truncate('1234567890', 5)).toBe('1234…'); + expect(truncate('hello world', 6)).toBe('hello…'); + }); + + it('uses default max of 60', () => { + const long = 'a'.repeat(100); + const out = truncate(long); + expect(out.length).toBe(60); + expect(out.endsWith('…')).toBe(true); + }); + + it('null / undefined inputs return empty string', () => { + expect(truncate(null)).toBe(''); + expect(truncate(undefined)).toBe(''); + }); + + it('handles max=0 safely', () => { + expect(truncate('any string', 0)).toBe(''); + }); + + it('handles max=1 safely — one-char budget collapses to just the ellipsis', () => { + expect(truncate('abc', 1)).toBe('…'); + }); +}); + +// --------------------------------------------------------------------------- +// uniqueMemoryCount — union of memory_a_id + memory_b_id across pairs. +// --------------------------------------------------------------------------- + +describe('uniqueMemoryCount', () => { + const mkPair = (a: string, b: string): ContradictionLike => ({ + memory_a_id: a, + memory_b_id: b, + }); + + it('returns 0 for empty input', () => { + expect(uniqueMemoryCount([])).toBe(0); + }); + + it('counts both sides of every pair', () => { + expect(uniqueMemoryCount([mkPair('a', 'b')])).toBe(2); + expect(uniqueMemoryCount([mkPair('a', 'b'), mkPair('c', 'd')])).toBe(4); + }); + + it('deduplicates memories that appear in multiple pairs', () => { + // 'a' appears on both sides of two separate pairs. + expect(uniqueMemoryCount([mkPair('a', 'b'), mkPair('a', 'c')])).toBe(3); + expect(uniqueMemoryCount([mkPair('a', 'b'), mkPair('b', 'a')])).toBe(2); + }); + + it('handles a memory conflicting with itself (same id both sides)', () => { + expect(uniqueMemoryCount([mkPair('a', 'a')])).toBe(1); + }); + + it('ignores empty-string ids', () => { + expect(uniqueMemoryCount([mkPair('', '')])).toBe(0); + expect(uniqueMemoryCount([mkPair('a', '')])).toBe(1); + }); +}); + +// --------------------------------------------------------------------------- +// avgTrustDelta — safety against empty inputs. +// --------------------------------------------------------------------------- + +describe('avgTrustDelta', () => { + it('returns 0 on empty input (no NaN)', () => { + expect(avgTrustDelta([])).toBe(0); + }); + + it('computes mean absolute delta', () => { + const pairs = [ + { trust_a: 0.9, trust_b: 0.1 }, // 0.8 + { trust_a: 0.5, trust_b: 0.3 }, // 0.2 + ]; + expect(avgTrustDelta(pairs)).toBeCloseTo(0.5); + }); + + it('takes absolute value (order does not matter)', () => { + expect(avgTrustDelta([{ trust_a: 0.1, trust_b: 0.9 }])).toBeCloseTo(0.8); + expect(avgTrustDelta([{ trust_a: 0.9, trust_b: 0.1 }])).toBeCloseTo(0.8); + }); + + it('returns 0 when both sides are equal', () => { + expect(avgTrustDelta([{ trust_a: 0.5, trust_b: 0.5 }])).toBe(0); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/DreamInsightCard.test.ts b/apps/dashboard/src/lib/components/__tests__/DreamInsightCard.test.ts new file mode 100644 index 0000000..7d02844 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/DreamInsightCard.test.ts @@ -0,0 +1,258 @@ +/** + * Tests for DreamInsightCard helpers. + * + * Pure logic only — the Svelte template is a thin wrapper around these. + * Covers the boundaries of the gold-glow / muted novelty mapping, the + * formatting helpers, and the source-memory link scheme. + */ +import { describe, it, expect } from 'vitest'; + +import { + LOW_NOVELTY_THRESHOLD, + HIGH_NOVELTY_THRESHOLD, + clamp01, + noveltyBand, + formatDurationMs, + formatConfidencePct, + sourceMemoryHref, + firstSourceIds, + extraSourceCount, + shortMemoryId, +} from '../dream-helpers'; + +// --------------------------------------------------------------------------- +// clamp01 +// --------------------------------------------------------------------------- + +describe('clamp01', () => { + it.each<[number | null | undefined, number]>([ + [0, 0], + [1, 1], + [0.5, 0.5], + [-0.1, 0], + [-5, 0], + [1.1, 1], + [100, 1], + [null, 0], + [undefined, 0], + [Number.NaN, 0], + [Number.POSITIVE_INFINITY, 0], + [Number.NEGATIVE_INFINITY, 0], + ])('clamp01(%s) → %s', (input, expected) => { + expect(clamp01(input)).toBe(expected); + }); +}); + +// --------------------------------------------------------------------------- +// noveltyBand — the gold/muted visual classifier +// --------------------------------------------------------------------------- + +describe('noveltyBand — gold-glow / muted classification', () => { + it('has the documented thresholds', () => { + // These constants are contractual — the component's class bindings + // depend on them. If they change, the visual band shifts. + expect(LOW_NOVELTY_THRESHOLD).toBe(0.3); + expect(HIGH_NOVELTY_THRESHOLD).toBe(0.7); + }); + + it('classifies low-novelty (< 0.3) as muted', () => { + expect(noveltyBand(0)).toBe('low'); + expect(noveltyBand(0.1)).toBe('low'); + expect(noveltyBand(0.29)).toBe('low'); + expect(noveltyBand(0.2999)).toBe('low'); + }); + + it('classifies the boundary 0.3 exactly as neutral (NOT low)', () => { + // The component uses `novelty < 0.3`, strictly exclusive. + expect(noveltyBand(0.3)).toBe('neutral'); + }); + + it('classifies mid-range as neutral', () => { + expect(noveltyBand(0.3)).toBe('neutral'); + expect(noveltyBand(0.5)).toBe('neutral'); + expect(noveltyBand(0.7)).toBe('neutral'); + }); + + it('classifies the boundary 0.7 exactly as neutral (NOT high)', () => { + // The component uses `novelty > 0.7`, strictly exclusive. + expect(noveltyBand(0.7)).toBe('neutral'); + }); + + it('classifies high-novelty (> 0.7) as gold/high', () => { + expect(noveltyBand(0.71)).toBe('high'); + expect(noveltyBand(0.7001)).toBe('high'); + expect(noveltyBand(0.9)).toBe('high'); + expect(noveltyBand(1.0)).toBe('high'); + }); + + it('collapses null / undefined / NaN to the low band', () => { + expect(noveltyBand(null)).toBe('low'); + expect(noveltyBand(undefined)).toBe('low'); + expect(noveltyBand(Number.NaN)).toBe('low'); + }); + + it('clamps out-of-range values before classifying', () => { + // 2.0 clamps to 1.0 → high; -1 clamps to 0 → low. + expect(noveltyBand(2.0)).toBe('high'); + expect(noveltyBand(-1)).toBe('low'); + }); +}); + +// --------------------------------------------------------------------------- +// formatDurationMs +// --------------------------------------------------------------------------- + +describe('formatDurationMs', () => { + it('renders sub-second values with "ms" suffix', () => { + expect(formatDurationMs(0)).toBe('0ms'); + expect(formatDurationMs(1)).toBe('1ms'); + expect(formatDurationMs(500)).toBe('500ms'); + expect(formatDurationMs(999)).toBe('999ms'); + }); + + it('renders second-and-above values with "s" suffix, 2 decimals', () => { + expect(formatDurationMs(1000)).toBe('1.00s'); + expect(formatDurationMs(1500)).toBe('1.50s'); + expect(formatDurationMs(15000)).toBe('15.00s'); + expect(formatDurationMs(60000)).toBe('60.00s'); + }); + + it('rounds fractional millisecond values in the "ms" band', () => { + expect(formatDurationMs(0.4)).toBe('0ms'); + expect(formatDurationMs(12.7)).toBe('13ms'); + }); + + it('returns "0ms" for null / undefined / NaN / negative', () => { + expect(formatDurationMs(null)).toBe('0ms'); + expect(formatDurationMs(undefined)).toBe('0ms'); + expect(formatDurationMs(Number.NaN)).toBe('0ms'); + expect(formatDurationMs(-100)).toBe('0ms'); + expect(formatDurationMs(Number.POSITIVE_INFINITY)).toBe('0ms'); + }); +}); + +// --------------------------------------------------------------------------- +// formatConfidencePct +// --------------------------------------------------------------------------- + +describe('formatConfidencePct', () => { + it('renders 0 / 0.5 / 1 as whole-percent strings', () => { + expect(formatConfidencePct(0)).toBe('0%'); + expect(formatConfidencePct(0.5)).toBe('50%'); + expect(formatConfidencePct(1)).toBe('100%'); + }); + + it('rounds intermediate values', () => { + expect(formatConfidencePct(0.123)).toBe('12%'); + expect(formatConfidencePct(0.5049)).toBe('50%'); + expect(formatConfidencePct(0.505)).toBe('51%'); + expect(formatConfidencePct(0.999)).toBe('100%'); + }); + + it('clamps out-of-range input first', () => { + expect(formatConfidencePct(-0.5)).toBe('0%'); + expect(formatConfidencePct(2)).toBe('100%'); + }); + + it('handles null / undefined / NaN', () => { + expect(formatConfidencePct(null)).toBe('0%'); + expect(formatConfidencePct(undefined)).toBe('0%'); + expect(formatConfidencePct(Number.NaN)).toBe('0%'); + }); +}); + +// --------------------------------------------------------------------------- +// sourceMemoryHref +// --------------------------------------------------------------------------- + +describe('sourceMemoryHref — link format', () => { + it('builds the canonical /memories/:id path with no base', () => { + expect(sourceMemoryHref('abc123')).toBe('/memories/abc123'); + }); + + it('prepends the SvelteKit base path when provided', () => { + expect(sourceMemoryHref('abc123', '/dashboard')).toBe( + '/dashboard/memories/abc123', + ); + }); + + it('handles an empty base (default behaviour)', () => { + expect(sourceMemoryHref('abc', '')).toBe('/memories/abc'); + }); + + it('passes through full UUIDs untouched', () => { + const uuid = '550e8400-e29b-41d4-a716-446655440000'; + expect(sourceMemoryHref(uuid)).toBe(`/memories/${uuid}`); + }); +}); + +// --------------------------------------------------------------------------- +// firstSourceIds + extraSourceCount +// --------------------------------------------------------------------------- + +describe('firstSourceIds', () => { + it('returns [] for empty / null / undefined inputs', () => { + expect(firstSourceIds([])).toEqual([]); + expect(firstSourceIds(null)).toEqual([]); + expect(firstSourceIds(undefined)).toEqual([]); + }); + + it('returns the single element when array has one entry', () => { + expect(firstSourceIds(['a'])).toEqual(['a']); + }); + + it('returns the first 2 by default', () => { + expect(firstSourceIds(['a', 'b', 'c', 'd'])).toEqual(['a', 'b']); + }); + + it('honours a custom N', () => { + expect(firstSourceIds(['a', 'b', 'c', 'd'], 3)).toEqual(['a', 'b', 'c']); + expect(firstSourceIds(['a', 'b', 'c'], 5)).toEqual(['a', 'b', 'c']); + }); + + it('returns [] for non-positive N', () => { + expect(firstSourceIds(['a', 'b'], 0)).toEqual([]); + expect(firstSourceIds(['a', 'b'], -1)).toEqual([]); + }); +}); + +describe('extraSourceCount', () => { + it('returns 0 when there are no extras', () => { + expect(extraSourceCount([])).toBe(0); + expect(extraSourceCount(null)).toBe(0); + expect(extraSourceCount(['a'])).toBe(0); + expect(extraSourceCount(['a', 'b'])).toBe(0); + }); + + it('returns sources.length - shown when there are extras', () => { + expect(extraSourceCount(['a', 'b', 'c'])).toBe(1); + expect(extraSourceCount(['a', 'b', 'c', 'd', 'e'])).toBe(3); + }); + + it('honours a custom shown parameter', () => { + expect(extraSourceCount(['a', 'b', 'c', 'd', 'e'], 3)).toBe(2); + expect(extraSourceCount(['a', 'b'], 5)).toBe(0); + }); +}); + +// --------------------------------------------------------------------------- +// shortMemoryId +// --------------------------------------------------------------------------- + +describe('shortMemoryId', () => { + it('returns the full string when 8 chars or fewer', () => { + expect(shortMemoryId('abc')).toBe('abc'); + expect(shortMemoryId('12345678')).toBe('12345678'); + }); + + it('slices to 8 chars when longer', () => { + expect(shortMemoryId('123456789')).toBe('12345678'); + expect(shortMemoryId('550e8400-e29b-41d4-a716-446655440000')).toBe( + '550e8400', + ); + }); + + it('handles empty string defensively', () => { + expect(shortMemoryId('')).toBe(''); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/DreamStageReplay.test.ts b/apps/dashboard/src/lib/components/__tests__/DreamStageReplay.test.ts new file mode 100644 index 0000000..8d18e72 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/DreamStageReplay.test.ts @@ -0,0 +1,104 @@ +/** + * Tests for DreamStageReplay helpers. + * + * The Svelte component itself is rendered with CSS transforms + derived + * state. We can't mount it in Node without jsdom, so we test the PURE + * helpers it relies on — the same helpers also power the page's scrubber + * and the insight card. If `clampStage` is green, the scrubber can't go + * out of range; if `STAGE_NAMES` stays in sync with MemoryDreamer's 5 + * phases, the badge labels stay correct. + */ +import { describe, it, expect } from 'vitest'; + +import { + STAGE_COUNT, + STAGE_NAMES, + clampStage, + stageName, +} from '../dream-helpers'; + +describe('STAGE_NAMES — MemoryDreamer phase list', () => { + it('has exactly 5 stages matching MemoryDreamer.run()', () => { + expect(STAGE_COUNT).toBe(5); + expect(STAGE_NAMES).toHaveLength(5); + }); + + it('lists the phases in the canonical order', () => { + // Order is load-bearing: the stage replay animates in this sequence. + // Replay → Cross-reference → Strengthen → Prune → Transfer. + expect(STAGE_NAMES).toEqual([ + 'Replay', + 'Cross-reference', + 'Strengthen', + 'Prune', + 'Transfer', + ]); + }); +}); + +describe('clampStage — valid-range enforcement', () => { + it.each<[number, number]>([ + // Out-of-bounds low + [0, 1], + [-1, 1], + [-100, 1], + // In-range (exactly the valid stage indices) + [1, 1], + [2, 2], + [3, 3], + [4, 4], + [5, 5], + // Out-of-bounds high + [6, 5], + [7, 5], + [100, 5], + ])('clampStage(%s) → %s', (input, expected) => { + expect(clampStage(input)).toBe(expected); + }); + + it('floors fractional values before clamping', () => { + expect(clampStage(1.9)).toBe(1); + expect(clampStage(4.9)).toBe(4); + expect(clampStage(5.1)).toBe(5); + }); + + it('collapses NaN / Infinity / -Infinity to stage 1', () => { + expect(clampStage(Number.NaN)).toBe(1); + expect(clampStage(Number.POSITIVE_INFINITY)).toBe(1); + expect(clampStage(Number.NEGATIVE_INFINITY)).toBe(1); + }); + + it('returns a value usable as a 0-indexed STAGE_NAMES lookup', () => { + // The page uses `STAGE_NAMES[stageIdx - 1]`. Every clamped value + // must index a real name, not undefined. + for (const raw of [-5, 0, 1, 3, 5, 10, Number.NaN]) { + const idx = clampStage(raw); + expect(STAGE_NAMES[idx - 1]).toBeDefined(); + expect(typeof STAGE_NAMES[idx - 1]).toBe('string'); + } + }); +}); + +describe('stageName — resolves to the visible label', () => { + it('returns the matching name for every valid stage', () => { + expect(stageName(1)).toBe('Replay'); + expect(stageName(2)).toBe('Cross-reference'); + expect(stageName(3)).toBe('Strengthen'); + expect(stageName(4)).toBe('Prune'); + expect(stageName(5)).toBe('Transfer'); + }); + + it('falls back to the nearest valid name for out-of-range input', () => { + expect(stageName(0)).toBe('Replay'); + expect(stageName(-1)).toBe('Replay'); + expect(stageName(6)).toBe('Transfer'); + expect(stageName(100)).toBe('Transfer'); + }); + + it('never returns undefined, even for garbage input', () => { + for (const raw of [Number.NaN, Number.POSITIVE_INFINITY, -Number.MAX_VALUE]) { + expect(stageName(raw)).toBeDefined(); + expect(stageName(raw)).toMatch(/^[A-Z]/); + } + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/DuplicateCluster.test.ts b/apps/dashboard/src/lib/components/__tests__/DuplicateCluster.test.ts new file mode 100644 index 0000000..fac7c77 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/DuplicateCluster.test.ts @@ -0,0 +1,365 @@ +/** + * Pure-logic tests for the Memory Hygiene / Duplicate Detection UI. + * + * The Svelte components themselves are render-level code (no jsdom in this + * repo) — every ounce of behaviour worth testing is extracted into + * `duplicates-helpers.ts` and exercised here. If this file is green, the + * similarity bands, winner selection, suggested-action mapping, threshold + * filtering, cluster-identity keying, and the "safe render" helpers are all + * sound. + */ +import { describe, it, expect } from 'vitest'; + +import { + similarityBand, + similarityBandColor, + similarityBandLabel, + retentionColor, + pickWinner, + suggestedActionFor, + filterByThreshold, + clusterKey, + previewContent, + formatDate, + safeTags, +} from '../duplicates-helpers'; + +// --------------------------------------------------------------------------- +// Similarity band — boundaries at 0.92 (red) and 0.80 (amber). +// The boundary value MUST land in the higher band (>= semantics). +// --------------------------------------------------------------------------- +describe('similarityBand', () => { + it('0.92 exactly → near-identical (boundary)', () => { + expect(similarityBand(0.92)).toBe('near-identical'); + }); + + it('0.91 → strong (just below upper boundary)', () => { + expect(similarityBand(0.91)).toBe('strong'); + }); + + it('0.80 exactly → strong (boundary)', () => { + expect(similarityBand(0.8)).toBe('strong'); + }); + + it('0.79 → weak (just below strong boundary)', () => { + expect(similarityBand(0.79)).toBe('weak'); + }); + + it('0.50 → weak (well below)', () => { + expect(similarityBand(0.5)).toBe('weak'); + }); + + it('1.0 → near-identical', () => { + expect(similarityBand(1.0)).toBe('near-identical'); + }); + + it('0.0 → weak', () => { + expect(similarityBand(0.0)).toBe('weak'); + }); +}); + +describe('similarityBandColor', () => { + it('near-identical → decay var (red)', () => { + expect(similarityBandColor(0.95)).toBe('var(--color-decay)'); + }); + + it('strong → warning var (amber)', () => { + expect(similarityBandColor(0.85)).toBe('var(--color-warning)'); + }); + + it('weak → yellow-300 literal', () => { + expect(similarityBandColor(0.78)).toBe('#fde047'); + }); + + it('is consistent at boundary 0.92', () => { + expect(similarityBandColor(0.92)).toBe('var(--color-decay)'); + }); + + it('is consistent at boundary 0.80', () => { + expect(similarityBandColor(0.8)).toBe('var(--color-warning)'); + }); +}); + +describe('similarityBandLabel', () => { + it('labels near-identical', () => { + expect(similarityBandLabel(0.97)).toBe('Near-identical'); + }); + + it('labels strong', () => { + expect(similarityBandLabel(0.85)).toBe('Strong match'); + }); + + it('labels weak', () => { + expect(similarityBandLabel(0.75)).toBe('Weak match'); + }); +}); + +// --------------------------------------------------------------------------- +// Retention color — traffic-light: >0.7 green, >0.4 amber, else red. +// --------------------------------------------------------------------------- +describe('retentionColor', () => { + it('0.85 → green', () => expect(retentionColor(0.85)).toBe('#10b981')); + it('0.50 → amber', () => expect(retentionColor(0.5)).toBe('#f59e0b')); + it('0.30 → red', () => expect(retentionColor(0.3)).toBe('#ef4444')); + it('boundary 0.70 → amber (strict >)', () => expect(retentionColor(0.7)).toBe('#f59e0b')); + it('boundary 0.40 → red (strict >)', () => expect(retentionColor(0.4)).toBe('#ef4444')); + it('0.0 → red', () => expect(retentionColor(0)).toBe('#ef4444')); +}); + +// --------------------------------------------------------------------------- +// Winner selection — highest retention wins; ties → earliest index; empty +// list → null; NaN retentions never win. +// --------------------------------------------------------------------------- +describe('pickWinner', () => { + it('picks highest retention', () => { + const mem = [ + { id: 'a', retention: 0.3 }, + { id: 'b', retention: 0.9 }, + { id: 'c', retention: 0.5 }, + ]; + expect(pickWinner(mem)?.id).toBe('b'); + }); + + it('tie-break: earliest wins (stable)', () => { + const mem = [ + { id: 'a', retention: 0.8 }, + { id: 'b', retention: 0.8 }, + { id: 'c', retention: 0.7 }, + ]; + expect(pickWinner(mem)?.id).toBe('a'); + }); + + it('three-way tie: earliest wins', () => { + const mem = [ + { id: 'x', retention: 0.5 }, + { id: 'y', retention: 0.5 }, + { id: 'z', retention: 0.5 }, + ]; + expect(pickWinner(mem)?.id).toBe('x'); + }); + + it('all retention = 0: earliest wins (not null)', () => { + const mem = [ + { id: 'a', retention: 0 }, + { id: 'b', retention: 0 }, + ]; + expect(pickWinner(mem)?.id).toBe('a'); + }); + + it('single-member cluster: that member wins', () => { + const mem = [{ id: 'solo', retention: 0.42 }]; + expect(pickWinner(mem)?.id).toBe('solo'); + }); + + it('empty cluster: returns null', () => { + expect(pickWinner([])).toBeNull(); + }); + + it('NaN retention never wins over a real one', () => { + const mem = [ + { id: 'nan', retention: Number.NaN }, + { id: 'real', retention: 0.1 }, + ]; + expect(pickWinner(mem)?.id).toBe('real'); + }); + + it('all NaN retentions: earliest wins (stable fallback)', () => { + const mem = [ + { id: 'a', retention: Number.NaN }, + { id: 'b', retention: Number.NaN }, + ]; + expect(pickWinner(mem)?.id).toBe('a'); + }); +}); + +// --------------------------------------------------------------------------- +// Suggested action — >=0.92 merge, <0.85 review, 0.85..<0.92 null (caller +// honors upstream). +// --------------------------------------------------------------------------- +describe('suggestedActionFor', () => { + it('0.95 → merge', () => expect(suggestedActionFor(0.95)).toBe('merge')); + it('0.92 exactly → merge (boundary)', () => expect(suggestedActionFor(0.92)).toBe('merge')); + it('0.91 → null (ambiguous corridor)', () => expect(suggestedActionFor(0.91)).toBeNull()); + it('0.85 exactly → null (corridor bottom boundary)', () => + expect(suggestedActionFor(0.85)).toBeNull()); + it('0.849 → review (just below corridor)', () => + expect(suggestedActionFor(0.849)).toBe('review')); + it('0.70 → review', () => expect(suggestedActionFor(0.7)).toBe('review')); + it('0.0 → review', () => expect(suggestedActionFor(0)).toBe('review')); + it('1.0 → merge', () => expect(suggestedActionFor(1.0)).toBe('merge')); +}); + +// --------------------------------------------------------------------------- +// Threshold filter — strict >=. +// --------------------------------------------------------------------------- +describe('filterByThreshold', () => { + const clusters = [ + { similarity: 0.96, memories: [{ id: '1', retention: 1 }] }, + { similarity: 0.88, memories: [{ id: '2', retention: 1 }] }, + { similarity: 0.78, memories: [{ id: '3', retention: 1 }] }, + ]; + + it('0.80 keeps 0.96 and 0.88 (drops 0.78)', () => { + const out = filterByThreshold(clusters, 0.8); + expect(out.map((c) => c.similarity)).toEqual([0.96, 0.88]); + }); + + it('boundary: threshold = 0.88 keeps 0.88 (>=)', () => { + const out = filterByThreshold(clusters, 0.88); + expect(out.map((c) => c.similarity)).toEqual([0.96, 0.88]); + }); + + it('boundary: threshold = 0.881 drops 0.88', () => { + const out = filterByThreshold(clusters, 0.881); + expect(out.map((c) => c.similarity)).toEqual([0.96]); + }); + + it('0.95 (max) keeps only 0.96', () => { + const out = filterByThreshold(clusters, 0.95); + expect(out.map((c) => c.similarity)).toEqual([0.96]); + }); + + it('0.70 (min) keeps all three', () => { + const out = filterByThreshold(clusters, 0.7); + expect(out).toHaveLength(3); + }); + + it('empty input → empty output', () => { + expect(filterByThreshold([], 0.8)).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// Cluster identity — stable across order shuffles and re-fetches. +// --------------------------------------------------------------------------- +describe('clusterKey', () => { + it('identical member sets → identical keys (order-independent)', () => { + const a = [ + { id: 'a', retention: 0 }, + { id: 'b', retention: 0 }, + { id: 'c', retention: 0 }, + ]; + const b = [ + { id: 'c', retention: 0 }, + { id: 'a', retention: 0 }, + { id: 'b', retention: 0 }, + ]; + expect(clusterKey(a)).toBe(clusterKey(b)); + }); + + it('differing members → differing keys', () => { + const a = [ + { id: 'a', retention: 0 }, + { id: 'b', retention: 0 }, + ]; + const b = [ + { id: 'a', retention: 0 }, + { id: 'c', retention: 0 }, + ]; + expect(clusterKey(a)).not.toBe(clusterKey(b)); + }); + + it('does not mutate input order', () => { + const mem = [ + { id: 'z', retention: 0 }, + { id: 'a', retention: 0 }, + ]; + clusterKey(mem); + expect(mem.map((m) => m.id)).toEqual(['z', 'a']); + }); + + it('empty cluster → empty string', () => { + expect(clusterKey([])).toBe(''); + }); +}); + +// --------------------------------------------------------------------------- +// previewContent — trim + collapse whitespace + truncate at 80. +// --------------------------------------------------------------------------- +describe('previewContent', () => { + it('short content: unchanged', () => { + expect(previewContent('hello world')).toBe('hello world'); + }); + + it('collapses internal whitespace', () => { + expect(previewContent(' hello world ')).toBe('hello world'); + }); + + it('truncates with ellipsis', () => { + const long = 'a'.repeat(120); + const out = previewContent(long); + expect(out.length).toBe(81); // 80 + ellipsis + expect(out.endsWith('…')).toBe(true); + }); + + it('null-safe', () => { + expect(previewContent(null)).toBe(''); + expect(previewContent(undefined)).toBe(''); + }); + + it('honors custom max', () => { + expect(previewContent('abcdefghij', 5)).toBe('abcde…'); + }); +}); + +// --------------------------------------------------------------------------- +// formatDate — valid ISO → formatted; everything else → empty. +// --------------------------------------------------------------------------- +describe('formatDate', () => { + it('valid ISO → non-empty formatted string', () => { + const out = formatDate('2026-04-14T11:02:00Z'); + expect(out.length).toBeGreaterThan(0); + expect(out).not.toBe('Invalid Date'); + }); + + it('empty string → empty', () => { + expect(formatDate('')).toBe(''); + }); + + it('null → empty', () => { + expect(formatDate(null)).toBe(''); + }); + + it('undefined → empty', () => { + expect(formatDate(undefined)).toBe(''); + }); + + it('garbage string → empty (no "Invalid Date" leak)', () => { + expect(formatDate('not-a-date')).toBe(''); + }); + + it('non-string input → empty (defensive)', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(formatDate(12345 as any)).toBe(''); + }); +}); + +// --------------------------------------------------------------------------- +// safeTags — tolerant of undefined / non-array / empty. +// --------------------------------------------------------------------------- +describe('safeTags', () => { + it('normal array: slices to limit', () => { + expect(safeTags(['a', 'b', 'c', 'd', 'e'], 3)).toEqual(['a', 'b', 'c']); + }); + + it('undefined → []', () => { + expect(safeTags(undefined)).toEqual([]); + }); + + it('null → []', () => { + expect(safeTags(null)).toEqual([]); + }); + + it('empty array → []', () => { + expect(safeTags([])).toEqual([]); + }); + + it('non-array (defensive) → []', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect(safeTags('bad' as any)).toEqual([]); + }); + + it('honors default limit = 4', () => { + expect(safeTags(['a', 'b', 'c', 'd', 'e', 'f'])).toEqual(['a', 'b', 'c', 'd']); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/EvidenceCard.test.ts b/apps/dashboard/src/lib/components/__tests__/EvidenceCard.test.ts new file mode 100644 index 0000000..14a184d --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/EvidenceCard.test.ts @@ -0,0 +1,255 @@ +/** + * EvidenceCard — pure-logic coverage. + * + * The component itself mounts Svelte, which vitest cannot do in a node + * environment. Every piece of logic that was reachable via props has been + * extracted to `reasoning-helpers.ts`; this file exhaustively exercises + * those helpers through the same import surface EvidenceCard uses. If + * this file is green, the card's visual output is a 1:1 function of the + * helper output. + */ +import { describe, it, expect } from 'vitest'; + +import { + ROLE_META, + roleMetaFor, + trustColor, + trustPercent, + clampTrust, + nodeTypeColor, + formatDate, + shortenId, + CONFIDENCE_EMERALD, + CONFIDENCE_AMBER, + CONFIDENCE_RED, + DEFAULT_NODE_TYPE_COLOR, + type EvidenceRole, +} from '../reasoning-helpers'; +import { NODE_TYPE_COLORS } from '$types'; + +// ──────────────────────────────────────────────────────────────── +// clampTrust + trustPercent — numeric contract +// ──────────────────────────────────────────────────────────────── + +describe('clampTrust — 0-1 display range', () => { + it.each<[number, number]>([ + [0, 0], + [0.5, 0.5], + [1, 1], + [-0.1, 0], + [-1, 0], + [1.2, 1], + [999, 1], + ])('clamps %f → %f', (input, expected) => { + expect(clampTrust(input)).toBe(expected); + }); + + it('returns 0 for NaN (defensive — avoids NaN% in the UI)', () => { + expect(clampTrust(Number.NaN)).toBe(0); + }); + + it('returns 0 for non-finite inputs (+/-Infinity) — safe default', () => { + // Infinity indicates upstream garbage — degrade to empty bar rather + // than saturate the UI to 100%. + expect(clampTrust(-Infinity)).toBe(0); + expect(clampTrust(Infinity)).toBe(0); + }); + + it('is idempotent (clamp of clamp is the same)', () => { + for (const v of [-0.5, 0, 0.3, 0.75, 1, 2]) { + expect(clampTrust(clampTrust(v))).toBe(clampTrust(v)); + } + }); +}); + +describe('trustPercent — 0-100 rendering', () => { + it.each<[number, number]>([ + [0, 0], + [0.5, 50], + [1, 100], + [-0.1, 0], + [1.2, 100], + ])('converts trust %f → %f%%', (t, expected) => { + expect(trustPercent(t)).toBe(expected); + }); + + it('handles NaN without producing NaN', () => { + expect(trustPercent(Number.NaN)).toBe(0); + }); +}); + +// ──────────────────────────────────────────────────────────────── +// trustColor — band boundaries for the card's trust bar +// ──────────────────────────────────────────────────────────────── + +describe('trustColor — boundary analysis', () => { + it.each<[number, string]>([ + // Emerald band: strictly > 0.75 → > 75% + [1.0, CONFIDENCE_EMERALD], + [0.9, CONFIDENCE_EMERALD], + [0.751, CONFIDENCE_EMERALD], + // Amber band: 0.40 ≤ t ≤ 0.75 + [0.75, CONFIDENCE_AMBER], // boundary — amber at exactly 75% + [0.5, CONFIDENCE_AMBER], + [0.4, CONFIDENCE_AMBER], // boundary — amber at exactly 40% + // Red band: < 0.40 + [0.399, CONFIDENCE_RED], + [0.2, CONFIDENCE_RED], + [0, CONFIDENCE_RED], + ])('trust %f → %s', (t, expected) => { + expect(trustColor(t)).toBe(expected); + }); + + it('clamps negative to red and super-high to emerald (defensive)', () => { + expect(trustColor(-0.5)).toBe(CONFIDENCE_RED); + expect(trustColor(1.5)).toBe(CONFIDENCE_EMERALD); + }); + + it('returns red for NaN (lowest-confidence fallback)', () => { + expect(trustColor(Number.NaN)).toBe(CONFIDENCE_RED); + }); +}); + +// ──────────────────────────────────────────────────────────────── +// Role metadata — label + accent + icon +// ──────────────────────────────────────────────────────────────── + +describe('ROLE_META — completeness and shape', () => { + const roles: EvidenceRole[] = ['primary', 'supporting', 'contradicting', 'superseded']; + + it('defines an entry for every role', () => { + for (const r of roles) { + expect(ROLE_META[r]).toBeDefined(); + } + }); + + it.each(roles)('%s has non-empty label + icon', (r) => { + const meta = ROLE_META[r]; + expect(meta.label.length).toBeGreaterThan(0); + expect(meta.icon.length).toBeGreaterThan(0); + }); + + it('maps to the expected accent tokens used by Tailwind (synapse/recall/decay/muted)', () => { + expect(ROLE_META.primary.accent).toBe('synapse'); + expect(ROLE_META.supporting.accent).toBe('recall'); + expect(ROLE_META.contradicting.accent).toBe('decay'); + expect(ROLE_META.superseded.accent).toBe('muted'); + }); + + it('accents are unique across roles (each role is visually distinct)', () => { + const accents = roles.map((r) => ROLE_META[r].accent); + expect(new Set(accents).size).toBe(4); + }); + + it('icons are unique across roles', () => { + const icons = roles.map((r) => ROLE_META[r].icon); + expect(new Set(icons).size).toBe(4); + }); + + it('labels are human-readable (first letter capital, no accents on the word)', () => { + for (const r of roles) { + const label = ROLE_META[r].label; + expect(label[0]).toBe(label[0].toUpperCase()); + } + }); +}); + +describe('roleMetaFor — lookup with defensive fallback', () => { + it('returns the exact entry for a known role', () => { + expect(roleMetaFor('primary')).toBe(ROLE_META.primary); + expect(roleMetaFor('contradicting')).toBe(ROLE_META.contradicting); + }); + + it('falls back to Supporting when handed an unknown role (deep_reference could add new ones)', () => { + expect(roleMetaFor('unknown-role')).toBe(ROLE_META.supporting); + expect(roleMetaFor('')).toBe(ROLE_META.supporting); + }); +}); + +// ──────────────────────────────────────────────────────────────── +// nodeTypeColor — palette lookup with fallback +// ──────────────────────────────────────────────────────────────── + +describe('nodeTypeColor — palette lookup', () => { + it('returns the fallback colour when nodeType is undefined/null/empty', () => { + expect(nodeTypeColor(undefined)).toBe(DEFAULT_NODE_TYPE_COLOR); + expect(nodeTypeColor(null)).toBe(DEFAULT_NODE_TYPE_COLOR); + expect(nodeTypeColor('')).toBe(DEFAULT_NODE_TYPE_COLOR); + }); + + it('returns the palette entry for every known NODE_TYPE_COLORS key', () => { + for (const [type, colour] of Object.entries(NODE_TYPE_COLORS)) { + expect(nodeTypeColor(type)).toBe(colour); + } + }); + + it('returns the fallback for an unknown nodeType', () => { + expect(nodeTypeColor('quantum-state')).toBe(DEFAULT_NODE_TYPE_COLOR); + }); +}); + +// ──────────────────────────────────────────────────────────────── +// formatDate — invalid-date handling (the real bug fixed here) +// ──────────────────────────────────────────────────────────────── + +describe('formatDate — ISO parsing with graceful degradation', () => { + it('formats a valid ISO date into a locale string', () => { + const out = formatDate('2026-04-20T12:00:00.000Z', 'en-US'); + // Example: "Apr 20, 2026" + expect(out).toMatch(/2026/); + expect(out).toMatch(/Apr/); + }); + + it('returns em-dash for empty / null / undefined', () => { + expect(formatDate('')).toBe('—'); + expect(formatDate(null)).toBe('—'); + expect(formatDate(undefined)).toBe('—'); + expect(formatDate(' ')).toBe('—'); + }); + + it('returns the original string when the input is unparseable (never "Invalid Date")', () => { + // Regression: `new Date('not-a-date').toLocaleDateString()` returned + // the literal text "Invalid Date" — EvidenceCard rendered that. Now + // we surface the raw string so a reviewer can tell it was garbage. + const garbage = 'not-a-date'; + expect(formatDate(garbage)).toBe(garbage); + expect(formatDate(garbage)).not.toBe('Invalid Date'); + }); + + it('handles ISO dates without time component', () => { + const out = formatDate('2026-01-15', 'en-US'); + expect(out).toMatch(/2026/); + }); + + it('is pure — no global mutation between calls', () => { + const a = formatDate('2026-04-20T00:00:00.000Z', 'en-US'); + const b = formatDate('2026-04-20T00:00:00.000Z', 'en-US'); + expect(a).toBe(b); + }); +}); + +// ──────────────────────────────────────────────────────────────── +// shortenId — UUID → #abcdef01 +// ──────────────────────────────────────────────────────────────── + +describe('shortenId — 8-char display prefix', () => { + it('returns an 8-char prefix for a standard UUID', () => { + expect(shortenId('a1b2c3d4-e5f6-0000-0000-000000000000')).toBe('a1b2c3d4'); + }); + + it('returns the full string when already ≤ 8 chars', () => { + expect(shortenId('abc')).toBe('abc'); + expect(shortenId('12345678')).toBe('12345678'); + }); + + it('handles null/undefined/empty gracefully', () => { + expect(shortenId(null)).toBe(''); + expect(shortenId(undefined)).toBe(''); + expect(shortenId('')).toBe(''); + }); + + it('respects a custom length parameter', () => { + expect(shortenId('abcdefghij', 4)).toBe('abcd'); + expect(shortenId('abcdefghij', 10)).toBe('abcdefghij'); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/FSRSCalendar.test.ts b/apps/dashboard/src/lib/components/__tests__/FSRSCalendar.test.ts new file mode 100644 index 0000000..0b61494 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/FSRSCalendar.test.ts @@ -0,0 +1,311 @@ +/** + * Tests for schedule / FSRS calendar helpers. These are the pure-logic core + * of the `schedule` page + `FSRSCalendar.svelte` component — the Svelte + * runtime is not exercised here (vitest runs `environment: node`, no jsdom). + */ +import { describe, it, expect } from 'vitest'; +import type { Memory } from '$types'; +import { + MS_DAY, + startOfDay, + daysBetween, + isoDate, + classifyUrgency, + daysUntilReview, + weekBucketRange, + avgRetention, + gridCellPosition, + gridStartForAnchor, + computeScheduleStats, +} from '../schedule-helpers'; + +function makeMemory(overrides: Partial = {}): Memory { + return { + id: 'm-' + Math.random().toString(36).slice(2, 8), + content: 'test memory', + nodeType: 'fact', + tags: [], + retentionStrength: 0.7, + storageStrength: 0.5, + retrievalStrength: 0.8, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + ...overrides, + }; +} + +// Fixed anchor: 2026-04-20 12:00 local so offsets don't straddle midnight +// in the default test runner's tz. All relative timestamps are derived from +// this anchor to keep tests tz-independent. +function anchor(): Date { + const d = new Date(2026, 3, 20, 12, 0, 0, 0); // Mon Apr 20 2026 12:00 local + return d; +} + +function offsetDays(base: Date, days: number, hour = 12): Date { + const d = new Date(base); + d.setDate(d.getDate() + days); + d.setHours(hour, 0, 0, 0); + return d; +} + +describe('startOfDay', () => { + it('zeros hours / minutes / seconds / ms', () => { + const d = new Date(2026, 3, 20, 14, 35, 27, 999); + const s = startOfDay(d); + expect(s.getHours()).toBe(0); + expect(s.getMinutes()).toBe(0); + expect(s.getSeconds()).toBe(0); + expect(s.getMilliseconds()).toBe(0); + expect(s.getFullYear()).toBe(2026); + expect(s.getMonth()).toBe(3); + expect(s.getDate()).toBe(20); + }); + + it('does not mutate its input', () => { + const input = new Date(2026, 3, 20, 14, 35); + const before = input.getTime(); + startOfDay(input); + expect(input.getTime()).toBe(before); + }); + + it('accepts an ISO string', () => { + const s = startOfDay('2026-04-20T14:35:00'); + expect(s.getHours()).toBe(0); + }); +}); + +describe('daysBetween', () => { + it('returns 0 for the same calendar day at different hours', () => { + const a = new Date(2026, 3, 20, 0, 0); + const b = new Date(2026, 3, 20, 23, 59); + expect(daysBetween(a, b)).toBe(0); + expect(daysBetween(b, a)).toBe(0); + }); + + it('returns positive for future, negative for past', () => { + const today = anchor(); + expect(daysBetween(offsetDays(today, 3), today)).toBe(3); + expect(daysBetween(offsetDays(today, -3), today)).toBe(-3); + }); + + it('is day-granular across the midnight boundary', () => { + const midnight = new Date(2026, 3, 20, 0, 0, 0, 0); + const justBefore = new Date(2026, 3, 19, 23, 59, 59, 999); + expect(daysBetween(midnight, justBefore)).toBe(1); + }); +}); + +describe('isoDate', () => { + it('formats as YYYY-MM-DD with zero-padding in LOCAL time', () => { + expect(isoDate(new Date(2026, 0, 5))).toBe('2026-01-05'); // jan 5 + expect(isoDate(new Date(2026, 11, 31))).toBe('2026-12-31'); + }); + + it('uses local day even for late-evening UTC-crossing timestamps', () => { + // This is the whole reason isoDate uses get* not getUTC*: calendar cells + // should match the user's perceived day. + const d = new Date(2026, 3, 20, 23, 30); // apr 20 23:30 local + expect(isoDate(d)).toBe('2026-04-20'); + }); +}); + +describe('classifyUrgency', () => { + const now = anchor(); + + it('returns "none" for missing nextReviewAt', () => { + expect(classifyUrgency(now, null)).toBe('none'); + expect(classifyUrgency(now, undefined)).toBe('none'); + expect(classifyUrgency(now, '')).toBe('none'); + }); + + it('returns "none" for unparseable ISO strings', () => { + expect(classifyUrgency(now, 'not-a-date')).toBe('none'); + }); + + it('classifies overdue when due date is strictly before today', () => { + expect(classifyUrgency(now, offsetDays(now, -1).toISOString())).toBe('overdue'); + expect(classifyUrgency(now, offsetDays(now, -5).toISOString())).toBe('overdue'); + }); + + it('classifies today when due date is the same calendar day', () => { + // Same day, earlier hour — still today, NOT overdue (day-granular). + const earlier = new Date(now); + earlier.setHours(3, 0); + expect(classifyUrgency(now, earlier.toISOString())).toBe('today'); + const later = new Date(now); + later.setHours(22, 0); + expect(classifyUrgency(now, later.toISOString())).toBe('today'); + }); + + it('classifies 1..=7 days out as "week"', () => { + expect(classifyUrgency(now, offsetDays(now, 1).toISOString())).toBe('week'); + expect(classifyUrgency(now, offsetDays(now, 7).toISOString())).toBe('week'); + }); + + it('classifies 8+ days out as "future"', () => { + expect(classifyUrgency(now, offsetDays(now, 8).toISOString())).toBe('future'); + expect(classifyUrgency(now, offsetDays(now, 30).toISOString())).toBe('future'); + }); + + it('boundary at midnight: 1 second after midnight tomorrow is "week" not "today"', () => { + const tomorrowMidnight = startOfDay(offsetDays(now, 1, 0)); + tomorrowMidnight.setSeconds(1); + expect(classifyUrgency(now, tomorrowMidnight.toISOString())).toBe('week'); + }); +}); + +describe('daysUntilReview', () => { + const now = anchor(); + + it('returns null for missing / invalid input', () => { + expect(daysUntilReview(now, null)).toBeNull(); + expect(daysUntilReview(now, undefined)).toBeNull(); + expect(daysUntilReview(now, 'garbage')).toBeNull(); + }); + + it('returns 0 for today', () => { + expect(daysUntilReview(now, now.toISOString())).toBe(0); + }); + + it('returns signed integer days', () => { + expect(daysUntilReview(now, offsetDays(now, 5).toISOString())).toBe(5); + expect(daysUntilReview(now, offsetDays(now, -3).toISOString())).toBe(-3); + }); +}); + +describe('weekBucketRange', () => { + it('returns Sunday→Sunday exclusive for any weekday', () => { + // Apr 20 2026 is a Monday. The week starts on Sunday Apr 19. + const mon = new Date(2026, 3, 20, 14, 0); + const { start, end } = weekBucketRange(mon); + expect(start.getDay()).toBe(0); // Sunday + expect(start.getDate()).toBe(19); + expect(end.getDate()).toBe(26); // next Sunday + expect(end.getTime() - start.getTime()).toBe(7 * MS_DAY); + }); + + it('for Sunday input, returns that same Sunday as start', () => { + const sun = new Date(2026, 3, 19, 10, 0); // Sun Apr 19 2026 + const { start } = weekBucketRange(sun); + expect(start.getDate()).toBe(19); + }); +}); + +describe('avgRetention', () => { + it('returns 0 for empty array (no NaN)', () => { + expect(avgRetention([])).toBe(0); + expect(Number.isNaN(avgRetention([]))).toBe(false); + }); + + it('returns the single value for a length-1 list', () => { + expect(avgRetention([makeMemory({ retentionStrength: 0.42 })])).toBeCloseTo(0.42); + }); + + it('returns the mean for a mixed list', () => { + const ms = [ + makeMemory({ retentionStrength: 0.2 }), + makeMemory({ retentionStrength: 0.8 }), + makeMemory({ retentionStrength: 0.5 }), + ]; + expect(avgRetention(ms)).toBeCloseTo(0.5); + }); + + it('tolerates missing retentionStrength (treat as 0)', () => { + const ms = [ + makeMemory({ retentionStrength: 1.0 }), + makeMemory({ retentionStrength: undefined as unknown as number }), + ]; + expect(avgRetention(ms)).toBeCloseTo(0.5); + }); +}); + +describe('gridCellPosition', () => { + it('maps row-major: index 0 → (0,0), index 7 → (1,0), index 41 → (5,6)', () => { + expect(gridCellPosition(0)).toEqual({ row: 0, col: 0 }); + expect(gridCellPosition(6)).toEqual({ row: 0, col: 6 }); + expect(gridCellPosition(7)).toEqual({ row: 1, col: 0 }); + expect(gridCellPosition(15)).toEqual({ row: 2, col: 1 }); + expect(gridCellPosition(41)).toEqual({ row: 5, col: 6 }); + }); + + it('returns null for out-of-range or non-integer indices', () => { + expect(gridCellPosition(-1)).toBeNull(); + expect(gridCellPosition(42)).toBeNull(); + expect(gridCellPosition(100)).toBeNull(); + expect(gridCellPosition(3.5)).toBeNull(); + }); +}); + +describe('gridStartForAnchor', () => { + it('returns a Sunday at or before anchor-14 days', () => { + // Apr 20 2026 (Mon) → anchor-14 = Apr 6 2026 (Mon) → back to Sun Apr 5. + const start = gridStartForAnchor(anchor()); + expect(start.getDay()).toBe(0); + expect(start.getFullYear()).toBe(2026); + expect(start.getMonth()).toBe(3); + expect(start.getDate()).toBe(5); + expect(start.getHours()).toBe(0); + }); + + it('includes today in the 6-week window (row 2 or 3)', () => { + const today = anchor(); + const start = gridStartForAnchor(today); + const delta = daysBetween(today, start); + expect(delta).toBeGreaterThanOrEqual(14); + expect(delta).toBeLessThan(42); + }); +}); + +describe('computeScheduleStats', () => { + const now = anchor(); + + it('zeros everything for an empty corpus', () => { + const s = computeScheduleStats(now, []); + expect(s).toEqual({ + overdue: 0, + dueToday: 0, + dueThisWeek: 0, + dueThisMonth: 0, + avgDays: 0, + }); + }); + + it('counts each bucket independently (today ⊂ week ⊂ month)', () => { + const ms = [ + makeMemory({ nextReviewAt: offsetDays(now, -2).toISOString() }), // overdue + makeMemory({ nextReviewAt: new Date(now).toISOString() }), // today + makeMemory({ nextReviewAt: offsetDays(now, 3).toISOString() }), // week + makeMemory({ nextReviewAt: offsetDays(now, 15).toISOString() }), // month + makeMemory({ nextReviewAt: offsetDays(now, 45).toISOString() }), // out of month + ]; + const s = computeScheduleStats(now, ms); + expect(s.overdue).toBe(1); + expect(s.dueToday).toBe(2); // overdue + today (delta <= 0) + expect(s.dueThisWeek).toBe(3); // overdue + today + week + expect(s.dueThisMonth).toBe(4); // overdue + today + week + month + }); + + it('skips memories without a nextReviewAt or with unparseable dates', () => { + const ms = [ + makeMemory({ nextReviewAt: undefined }), + makeMemory({ nextReviewAt: 'bogus' }), + makeMemory({ nextReviewAt: offsetDays(now, 2).toISOString() }), + ]; + const s = computeScheduleStats(now, ms); + expect(s.dueThisWeek).toBe(1); + }); + + it('computes average days across future-only memories', () => { + const ms = [ + makeMemory({ nextReviewAt: offsetDays(now, -5).toISOString() }), // excluded (past) + makeMemory({ nextReviewAt: offsetDays(now, 2).toISOString() }), + makeMemory({ nextReviewAt: offsetDays(now, 4).toISOString() }), + ]; + const s = computeScheduleStats(now, ms); + // avgDays is measured from today-at-midnight (not now-mid-day), so a + // review tomorrow at noon is 1.5 days out. Two memories at +2d and +4d + // (both hour=12) → (2.5 + 4.5) / 2 = 3.5. + expect(s.avgDays).toBeCloseTo(3.5, 2); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/ImportanceRadar.test.ts b/apps/dashboard/src/lib/components/__tests__/ImportanceRadar.test.ts new file mode 100644 index 0000000..bbda296 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/ImportanceRadar.test.ts @@ -0,0 +1,417 @@ +/** + * Unit tests for importance-helpers — the pure logic backing + * ImportanceRadar.svelte + importance/+page.svelte. + * + * Runs in the vitest `node` environment (no jsdom). We exercise: + * - Composite channel weighting (matches backend ImportanceSignals) + * - 4-axis radar vertex geometry (Novelty top / Arousal right / Reward + * bottom / Attention left) + * - Value clamping at the helper boundary (defensive against a mis- + * scaled /api/importance response) + * - Size-preset mapping (sm 80 / md 180 / lg 320) + * - Trending-memory importance proxy (retention × log(reviews) / √age) + * including the age=0 division-by-zero edge case. + */ + +import { describe, it, expect } from 'vitest'; +import { + clamp01, + clampChannels, + compositeScore, + CHANNEL_WEIGHTS, + sizePreset, + radarRadius, + radarVertices, + verticesToPath, + importanceProxy, + rankByProxy, + AXIS_ORDER, + SIZE_PX, + type ProxyMemoryLike, +} from '../importance-helpers'; + +// =========================================================================== +// clamp01 +// =========================================================================== + +describe('clamp01', () => { + it('passes in-range values through', () => { + expect(clamp01(0)).toBe(0); + expect(clamp01(0.5)).toBe(0.5); + expect(clamp01(1)).toBe(1); + }); + + it('clamps below zero to 0', () => { + expect(clamp01(-0.3)).toBe(0); + expect(clamp01(-100)).toBe(0); + }); + + it('clamps above one to 1', () => { + expect(clamp01(1.0001)).toBe(1); + expect(clamp01(42)).toBe(1); + }); + + it('folds null / undefined / NaN / Infinity to 0', () => { + expect(clamp01(null)).toBe(0); + expect(clamp01(undefined)).toBe(0); + expect(clamp01(NaN)).toBe(0); + expect(clamp01(Infinity)).toBe(0); + expect(clamp01(-Infinity)).toBe(0); + }); +}); + +describe('clampChannels', () => { + it('clamps every channel independently', () => { + expect(clampChannels({ novelty: 2, arousal: -1, reward: 0.5, attention: NaN })).toEqual({ + novelty: 1, + arousal: 0, + reward: 0.5, + attention: 0, + }); + }); + + it('fills missing channels with 0', () => { + expect(clampChannels({ novelty: 0.8 })).toEqual({ + novelty: 0.8, + arousal: 0, + reward: 0, + attention: 0, + }); + }); + + it('accepts null / undefined as "all zeros"', () => { + expect(clampChannels(null)).toEqual({ novelty: 0, arousal: 0, reward: 0, attention: 0 }); + expect(clampChannels(undefined)).toEqual({ + novelty: 0, + arousal: 0, + reward: 0, + attention: 0, + }); + }); +}); + +// =========================================================================== +// compositeScore — MUST match backend ImportanceSignals weights +// =========================================================================== + +describe('compositeScore', () => { + it('sums channel contributions with the documented weights', () => { + const c = { novelty: 1, arousal: 1, reward: 1, attention: 1 }; + // 0.25 + 0.30 + 0.25 + 0.20 = 1.00 + expect(compositeScore(c)).toBeCloseTo(1.0, 5); + }); + + it('is zero for all-zero channels', () => { + expect(compositeScore({ novelty: 0, arousal: 0, reward: 0, attention: 0 })).toBe(0); + }); + + it('weights match CHANNEL_WEIGHTS exactly (backend contract)', () => { + expect(CHANNEL_WEIGHTS).toEqual({ + novelty: 0.25, + arousal: 0.3, + reward: 0.25, + attention: 0.2, + }); + // Weights sum to 1 — any drift here and the "composite ∈ [0,1]" + // invariant falls over. + const sum = + CHANNEL_WEIGHTS.novelty + + CHANNEL_WEIGHTS.arousal + + CHANNEL_WEIGHTS.reward + + CHANNEL_WEIGHTS.attention; + expect(sum).toBeCloseTo(1.0, 10); + }); + + it('matches the exact weighted formula per channel', () => { + // 0.4·0.25 + 0.6·0.30 + 0.2·0.25 + 0.8·0.20 + // = 0.10 + 0.18 + 0.05 + 0.16 = 0.49 + expect( + compositeScore({ novelty: 0.4, arousal: 0.6, reward: 0.2, attention: 0.8 }), + ).toBeCloseTo(0.49, 5); + }); + + it('clamps inputs before weighting (never escapes [0,1])', () => { + // All over-max → should pin to 1, not to 2. + expect( + compositeScore({ novelty: 2, arousal: 2, reward: 2, attention: 2 }), + ).toBeCloseTo(1.0, 5); + // Negative channels count as 0. + expect( + compositeScore({ novelty: -1, arousal: -1, reward: -1, attention: -1 }), + ).toBe(0); + }); +}); + +// =========================================================================== +// Size preset +// =========================================================================== + +describe('sizePreset', () => { + it('maps the three documented presets', () => { + expect(sizePreset('sm')).toBe(80); + expect(sizePreset('md')).toBe(180); + expect(sizePreset('lg')).toBe(320); + }); + + it('exposes the SIZE_PX mapping for external consumers', () => { + expect(SIZE_PX).toEqual({ sm: 80, md: 180, lg: 320 }); + }); + + it('falls back to md (180) for unknown / missing keys', () => { + expect(sizePreset(undefined)).toBe(180); + expect(sizePreset('' as unknown as 'md')).toBe(180); + expect(sizePreset('xl' as unknown as 'md')).toBe(180); + }); +}); + +// =========================================================================== +// radarRadius — component padding rules +// =========================================================================== + +describe('radarRadius', () => { + it('applies the correct padding per preset', () => { + // sm: 80/2 - 4 = 36 + // md: 180/2 - 28 = 62 + // lg: 320/2 - 44 = 116 + expect(radarRadius('sm')).toBe(36); + expect(radarRadius('md')).toBe(62); + expect(radarRadius('lg')).toBe(116); + }); + + it('never returns a negative radius', () => { + // Can't construct a sub-zero radius via normal presets, but the + // helper floors at 0 defensively. + expect(radarRadius('md')).toBeGreaterThanOrEqual(0); + }); +}); + +// =========================================================================== +// radarVertices — 4 SVG polygon points on the fixed axis order +// =========================================================================== + +describe('radarVertices', () => { + it('emits vertices in Novelty→Arousal→Reward→Attention order', () => { + expect(AXIS_ORDER.map((a) => a.key)).toEqual([ + 'novelty', + 'arousal', + 'reward', + 'attention', + ]); + }); + + it('places a 0-valued channel at the centre', () => { + // Centre for md is (90, 90). novelty=0 means the top vertex sits AT + // the centre — the polygon pinches inward. + const v = radarVertices( + { novelty: 0, arousal: 0, reward: 0, attention: 0 }, + 'md', + ); + expect(v).toHaveLength(4); + for (const p of v) { + expect(p.x).toBeCloseTo(90, 5); + expect(p.y).toBeCloseTo(90, 5); + } + }); + + it('places a 1-valued channel on the correct axis edge', () => { + // Size md: cx=cy=90, r=62. + // Novelty (angle -π/2, top) → (90, 90 - 62) = (90, 28) + // Arousal (angle 0, right) → (90 + 62, 90) = (152, 90) + // Reward (angle π/2, bottom) → (90, 90 + 62) = (90, 152) + // Attention (angle π, left) → (90 - 62, 90) = (28, 90) + const v = radarVertices( + { novelty: 1, arousal: 1, reward: 1, attention: 1 }, + 'md', + ); + expect(v[0].x).toBeCloseTo(90, 5); + expect(v[0].y).toBeCloseTo(28, 5); + + expect(v[1].x).toBeCloseTo(152, 5); + expect(v[1].y).toBeCloseTo(90, 5); + + expect(v[2].x).toBeCloseTo(90, 5); + expect(v[2].y).toBeCloseTo(152, 5); + + expect(v[3].x).toBeCloseTo(28, 5); + expect(v[3].y).toBeCloseTo(90, 5); + }); + + it('scales vertex radial distance linearly with the channel value', () => { + // Arousal at 0.5 should land half-way from centre to the right edge. + const v = radarVertices( + { novelty: 0, arousal: 0.5, reward: 0, attention: 0 }, + 'md', + ); + // radius=62, so right vertex x = 90 + 62*0.5 = 121. + expect(v[1].x).toBeCloseTo(121, 5); + expect(v[1].y).toBeCloseTo(90, 5); + }); + + it('clamps out-of-range inputs rather than exiting the SVG box', () => { + // novelty=2 should pin to the edge (not overshoot to 90 - 124 = -34). + const v = radarVertices( + { novelty: 2, arousal: -0.5, reward: NaN, attention: Infinity }, + 'md', + ); + // Novelty pinned to edge (y=28), arousal/reward/attention at 0 land at centre. + expect(v[0].y).toBeCloseTo(28, 5); + expect(v[1].x).toBeCloseTo(90, 5); // arousal=0 → centre + expect(v[2].y).toBeCloseTo(90, 5); // reward=0 → centre + expect(v[3].x).toBeCloseTo(90, 5); // attention=0 → centre + }); + + it('respects the active size preset', () => { + // At sm (80px), radius=36. Novelty=1 → (40, 40-36) = (40, 4). + const v = radarVertices({ novelty: 1, arousal: 0, reward: 0, attention: 0 }, 'sm'); + expect(v[0].x).toBeCloseTo(40, 5); + expect(v[0].y).toBeCloseTo(4, 5); + }); +}); + +describe('verticesToPath', () => { + it('serialises to an SVG path with M/L commands and Z close', () => { + const path = verticesToPath([ + { x: 10, y: 20 }, + { x: 30, y: 40 }, + { x: 50, y: 60 }, + { x: 70, y: 80 }, + ]); + expect(path).toBe('M10.00,20.00 L30.00,40.00 L50.00,60.00 L70.00,80.00 Z'); + }); + + it('returns an empty string for no points', () => { + expect(verticesToPath([])).toBe(''); + }); +}); + +// =========================================================================== +// importanceProxy — "Top Important Memories This Week" ranking formula +// =========================================================================== + +describe('importanceProxy', () => { + // Anchor everything to a fixed "now" so recency math is deterministic. + const NOW = new Date('2026-04-20T12:00:00Z').getTime(); + + function mem(over: Partial): ProxyMemoryLike { + return { + retentionStrength: 0.5, + reviewCount: 0, + createdAt: new Date(NOW - 2 * 86_400_000).toISOString(), + ...over, + }; + } + + it('is zero for zero retention', () => { + expect(importanceProxy(mem({ retentionStrength: 0 }), NOW)).toBe(0); + }); + + it('treats missing reviewCount as 0 (not a crash)', () => { + const m = mem({ reviewCount: undefined, retentionStrength: 0.8 }); + const v = importanceProxy(m, NOW); + expect(v).toBeGreaterThan(0); + expect(Number.isFinite(v)).toBe(true); + }); + + it('matches the documented formula: retention × log1p(reviews+1) / √age', () => { + // createdAt = 4 days before NOW → ageDays = 4, √4 = 2. + // retention = 0.6, reviews = 3 → log1p(4) ≈ 1.6094 + // expected = 0.6 × 1.6094 / 2 ≈ 0.4828 + const m = mem({ + retentionStrength: 0.6, + reviewCount: 3, + createdAt: new Date(NOW - 4 * 86_400_000).toISOString(), + }); + const v = importanceProxy(m, NOW); + const expected = (0.6 * Math.log1p(4)) / 2; + expect(v).toBeCloseTo(expected, 6); + }); + + it('clamps age to 1 day for a memory created RIGHT NOW (div-by-zero guard)', () => { + // createdAt equals NOW → raw ageDays = 0. Without the clamp, the + // recency boost would divide by zero. We assert the helper returns + // a finite value equal to the "age=1" path. + const zeroAge = importanceProxy( + mem({ + retentionStrength: 0.5, + reviewCount: 0, + createdAt: new Date(NOW).toISOString(), + }), + NOW, + ); + const oneDayAge = importanceProxy( + mem({ + retentionStrength: 0.5, + reviewCount: 0, + createdAt: new Date(NOW - 1 * 86_400_000).toISOString(), + }), + NOW, + ); + expect(Number.isFinite(zeroAge)).toBe(true); + expect(zeroAge).toBeCloseTo(oneDayAge, 10); + }); + + it('also clamps future-dated memories to ageDays=1 rather than going negative', () => { + const future = importanceProxy( + mem({ + retentionStrength: 0.5, + reviewCount: 0, + createdAt: new Date(NOW + 7 * 86_400_000).toISOString(), + }), + NOW, + ); + expect(Number.isFinite(future)).toBe(true); + expect(future).toBeGreaterThan(0); + }); + + it('returns 0 for a malformed createdAt', () => { + const m = { + retentionStrength: 0.8, + reviewCount: 3, + createdAt: 'not-a-date', + }; + expect(importanceProxy(m, NOW)).toBe(0); + }); + + it('returns 0 when retentionStrength is non-finite', () => { + expect(importanceProxy(mem({ retentionStrength: NaN }), NOW)).toBe(0); + expect(importanceProxy(mem({ retentionStrength: Infinity }), NOW)).toBe(0); + }); + + it('ranks recent + high-retention memories ahead of stale ones', () => { + const fresh: ProxyMemoryLike = { + retentionStrength: 0.9, + reviewCount: 5, + createdAt: new Date(NOW - 1 * 86_400_000).toISOString(), + }; + const stale: ProxyMemoryLike = { + retentionStrength: 0.9, + reviewCount: 5, + createdAt: new Date(NOW - 100 * 86_400_000).toISOString(), + }; + expect(importanceProxy(fresh, NOW)).toBeGreaterThan(importanceProxy(stale, NOW)); + }); +}); + +describe('rankByProxy', () => { + const NOW = new Date('2026-04-20T12:00:00Z').getTime(); + + it('sorts descending by the proxy score', () => { + const items: (ProxyMemoryLike & { id: string })[] = [ + { id: 'stale', retentionStrength: 0.9, reviewCount: 5, createdAt: new Date(NOW - 100 * 86_400_000).toISOString() }, + { id: 'fresh', retentionStrength: 0.9, reviewCount: 5, createdAt: new Date(NOW - 1 * 86_400_000).toISOString() }, + { id: 'dead', retentionStrength: 0.0, reviewCount: 0, createdAt: new Date(NOW - 2 * 86_400_000).toISOString() }, + ]; + const ranked = rankByProxy(items, NOW); + expect(ranked.map((r) => r.id)).toEqual(['fresh', 'stale', 'dead']); + }); + + it('does not mutate the input array', () => { + const items: ProxyMemoryLike[] = [ + { retentionStrength: 0.1, reviewCount: 0, createdAt: new Date(NOW - 10 * 86_400_000).toISOString() }, + { retentionStrength: 0.9, reviewCount: 9, createdAt: new Date(NOW - 1 * 86_400_000).toISOString() }, + ]; + const before = items.slice(); + rankByProxy(items, NOW); + expect(items).toEqual(before); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/MemoryAuditTrail.test.ts b/apps/dashboard/src/lib/components/__tests__/MemoryAuditTrail.test.ts new file mode 100644 index 0000000..e105b83 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/MemoryAuditTrail.test.ts @@ -0,0 +1,298 @@ +/** + * MemoryAuditTrail — pure helper coverage. + * + * Runs in vitest's Node environment (no jsdom). Every assertion exercises + * a function in `audit-trail-helpers.ts` with fully deterministic inputs. + */ +import { describe, it, expect } from 'vitest'; + +import { + ALL_ACTIONS, + META, + VISIBLE_LIMIT, + formatRetentionDelta, + generateMockAuditTrail, + hashSeed, + makeRand, + relativeTime, + splitVisible, + type AuditAction, + type AuditEvent +} from '../audit-trail-helpers'; + +// Fixed reference point for all time-based tests. Millisecond precision so +// relative-time maths are exact, not drifting with wallclock time. +const NOW = Date.UTC(2026, 3, 20, 12, 0, 0); // 2026-04-20 12:00:00 UTC + +// --------------------------------------------------------------------------- +// hashSeed + makeRand +// --------------------------------------------------------------------------- +describe('hashSeed', () => { + it('is deterministic', () => { + expect(hashSeed('abc')).toBe(hashSeed('abc')); + expect(hashSeed('memory-42')).toBe(hashSeed('memory-42')); + }); + + it('different ids hash to different seeds', () => { + expect(hashSeed('a')).not.toBe(hashSeed('b')); + expect(hashSeed('memory-1')).not.toBe(hashSeed('memory-2')); + }); + + it('empty string hashes to 0', () => { + expect(hashSeed('')).toBe(0); + }); + + it('returns an unsigned 32-bit integer', () => { + // Stress: a long id should never produce a negative or non-integer seed. + const seed = hashSeed('a'.repeat(256)); + expect(Number.isInteger(seed)).toBe(true); + expect(seed).toBeGreaterThanOrEqual(0); + expect(seed).toBeLessThan(2 ** 32); + }); +}); + +describe('makeRand', () => { + it('is deterministic given the same seed', () => { + const a = makeRand(42); + const b = makeRand(42); + for (let i = 0; i < 20; i++) expect(a()).toBe(b()); + }); + + it('produces values strictly in [0, 1)', () => { + // Seed with UINT32_MAX to force the edge case that exposed the original + // `/ 0xffffffff` bug — the divisor must be 2^32, not 2^32 - 1. + const rand = makeRand(0xffffffff); + for (let i = 0; i < 5000; i++) { + const v = rand(); + expect(v).toBeGreaterThanOrEqual(0); + expect(v).toBeLessThan(1); + } + }); + + it('different seeds produce different sequences', () => { + const a = makeRand(1); + const b = makeRand(2); + expect(a()).not.toBe(b()); + }); +}); + +// --------------------------------------------------------------------------- +// Deterministic generator +// --------------------------------------------------------------------------- +describe('generateMockAuditTrail — determinism', () => { + it('same id + same now always yields the same sequence', () => { + const a = generateMockAuditTrail('memory-xyz', NOW); + const b = generateMockAuditTrail('memory-xyz', NOW); + expect(a).toEqual(b); + }); + + it('different ids yield different sequences', () => { + const a = generateMockAuditTrail('memory-a', NOW); + const b = generateMockAuditTrail('memory-b', NOW); + // Either different lengths or different event-by-event — anything but equal. + expect(a).not.toEqual(b); + }); + + it('empty id yields no events — the panel should never fabricate history', () => { + expect(generateMockAuditTrail('', NOW)).toEqual([]); + }); + + it('count fits the default 8-15 range', () => { + // Sample a handful of ids — the distribution should stay in range. + for (const id of ['a', 'abc', 'memory-1', 'memory-2', 'memory-3', 'x'.repeat(50)]) { + const events = generateMockAuditTrail(id, NOW); + expect(events.length).toBeGreaterThanOrEqual(8); + expect(events.length).toBeLessThanOrEqual(15); + } + }); + + it('first emitted event (newest-first order → last in array) is "created"', () => { + const events = generateMockAuditTrail('deterministic-id', NOW); + expect(events[events.length - 1].action).toBe('created'); + expect(events[events.length - 1].triggered_by).toBe('smart_ingest'); + }); + + it('emits events in newest-first order', () => { + const events = generateMockAuditTrail('order-check', NOW); + for (let i = 1; i < events.length; i++) { + const prev = new Date(events[i - 1].timestamp).getTime(); + const curr = new Date(events[i].timestamp).getTime(); + expect(prev).toBeGreaterThanOrEqual(curr); + } + }); + + it('all timestamps are valid ISO strings in the past relative to NOW', () => { + const events = generateMockAuditTrail('iso-check', NOW); + for (const ev of events) { + const t = new Date(ev.timestamp).getTime(); + expect(Number.isFinite(t)).toBe(true); + expect(t).toBeLessThanOrEqual(NOW); + } + }); + + it('respects countOverride — 16 events crosses the visibility threshold', () => { + const events = generateMockAuditTrail('big', NOW, 16); + expect(events).toHaveLength(16); + }); + + it('retention values never escape [0, 1]', () => { + for (const id of ['x', 'y', 'z', 'memory-big']) { + const events = generateMockAuditTrail(id, NOW, 30); + for (const ev of events) { + if (ev.old_value !== undefined) { + expect(ev.old_value).toBeGreaterThanOrEqual(0); + expect(ev.old_value).toBeLessThanOrEqual(1); + } + if (ev.new_value !== undefined) { + expect(ev.new_value).toBeGreaterThanOrEqual(0); + expect(ev.new_value).toBeLessThanOrEqual(1); + } + } + } + }); +}); + +// --------------------------------------------------------------------------- +// Relative time +// --------------------------------------------------------------------------- +describe('relativeTime — boundary cases', () => { + // Build an ISO timestamp `offsetMs` before NOW. + const ago = (offsetMs: number) => new Date(NOW - offsetMs).toISOString(); + + const cases: Array<[string, number, string]> = [ + ['0s ago', 0, '0s ago'], + ['59s ago', 59 * 1000, '59s ago'], + ['60s flips to 1m', 60 * 1000, '1m ago'], + ['59m ago', 59 * 60 * 1000, '59m ago'], + ['60m flips to 1h', 60 * 60 * 1000, '1h ago'], + ['23h ago', 23 * 3600 * 1000, '23h ago'], + ['24h flips to 1d', 24 * 3600 * 1000, '1d ago'], + ['6d ago', 6 * 86400 * 1000, '6d ago'], + ['7d ago', 7 * 86400 * 1000, '7d ago'], + ['29d ago', 29 * 86400 * 1000, '29d ago'], + ['30d flips to 1mo', 30 * 86400 * 1000, '1mo ago'], + ['365d → 12mo flips to 1y', 365 * 86400 * 1000, '1y ago'] + ]; + + for (const [name, offset, expected] of cases) { + it(name, () => { + expect(relativeTime(ago(offset), NOW)).toBe(expected); + }); + } + + it('future timestamps clamp to "0s ago"', () => { + const future = new Date(NOW + 60_000).toISOString(); + expect(relativeTime(future, NOW)).toBe('0s ago'); + }); +}); + +// --------------------------------------------------------------------------- +// Event type → marker mapping +// --------------------------------------------------------------------------- +describe('META — action to marker mapping', () => { + it('covers all 8 audit actions exactly', () => { + expect(Object.keys(META).sort()).toEqual([...ALL_ACTIONS].sort()); + expect(ALL_ACTIONS).toHaveLength(8); + }); + + it('every action has a distinct marker kind (8 kinds → 8 glyph shapes)', () => { + const kinds = ALL_ACTIONS.map((a) => META[a].kind); + expect(new Set(kinds).size).toBe(8); + }); + + it('every action has a non-empty label and hex color', () => { + for (const action of ALL_ACTIONS) { + const m = META[action]; + expect(m.label.length).toBeGreaterThan(0); + expect(m.color).toMatch(/^#[0-9a-f]{6}$/i); + } + }); +}); + +// --------------------------------------------------------------------------- +// Retention delta formatter +// --------------------------------------------------------------------------- +describe('formatRetentionDelta', () => { + it('returns null when both values are missing', () => { + expect(formatRetentionDelta(undefined, undefined)).toBeNull(); + }); + + it('returns "set X.XX" when only new is defined', () => { + expect(formatRetentionDelta(undefined, 0.5)).toBe('set 0.50'); + // Note: toFixed(2) uses float-to-string half-to-even; assert on values + // that round unambiguously rather than on IEEE-754 tie edges. + expect(formatRetentionDelta(undefined, 0.736)).toBe('set 0.74'); + }); + + it('returns "was X.XX" when only old is defined', () => { + expect(formatRetentionDelta(0.5, undefined)).toBe('was 0.50'); + }); + + it('returns "old → new" when both are defined', () => { + expect(formatRetentionDelta(0.5, 0.7)).toBe('0.50 → 0.70'); + expect(formatRetentionDelta(0.72, 0.85)).toBe('0.72 → 0.85'); + }); + + it('handles descending deltas without changing the arrow', () => { + // Suppression / demotion paths — old > new. + expect(formatRetentionDelta(0.8, 0.6)).toBe('0.80 → 0.60'); + }); + + it('rejects non-finite numbers', () => { + expect(formatRetentionDelta(NaN, 0.5)).toBe('set 0.50'); + expect(formatRetentionDelta(0.5, NaN)).toBe('was 0.50'); + expect(formatRetentionDelta(NaN, NaN)).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// splitVisible — 15-event cap +// --------------------------------------------------------------------------- +describe('splitVisible — collapse threshold', () => { + const makeEvents = (n: number): AuditEvent[] => + Array.from({ length: n }, (_, i) => ({ + action: 'accessed' as AuditAction, + timestamp: new Date(NOW - i * 60_000).toISOString() + })); + + it('VISIBLE_LIMIT is 15', () => { + expect(VISIBLE_LIMIT).toBe(15); + }); + + it('exactly 15 events → no toggle (hiddenCount 0)', () => { + const { visible, hiddenCount } = splitVisible(makeEvents(15), false); + expect(visible).toHaveLength(15); + expect(hiddenCount).toBe(0); + }); + + it('14 events → no toggle', () => { + const { visible, hiddenCount } = splitVisible(makeEvents(14), false); + expect(visible).toHaveLength(14); + expect(hiddenCount).toBe(0); + }); + + it('16 events collapsed → visible 15, hidden 1', () => { + const { visible, hiddenCount } = splitVisible(makeEvents(16), false); + expect(visible).toHaveLength(15); + expect(hiddenCount).toBe(1); + }); + + it('16 events expanded → visible 16, hidden reports overflow count (1)', () => { + const { visible, hiddenCount } = splitVisible(makeEvents(16), true); + expect(visible).toHaveLength(16); + expect(hiddenCount).toBe(1); + }); + + it('0 events → visible empty, hidden 0', () => { + const { visible, hiddenCount } = splitVisible(makeEvents(0), false); + expect(visible).toHaveLength(0); + expect(hiddenCount).toBe(0); + }); + + it('preserves newest-first order when truncating', () => { + const events = makeEvents(20); + const { visible } = splitVisible(events, false); + expect(visible[0]).toBe(events[0]); + expect(visible[14]).toBe(events[14]); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts b/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts new file mode 100644 index 0000000..8f7fa32 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/PatternTransferHeatmap.test.ts @@ -0,0 +1,334 @@ +/** + * Unit tests for patterns-helpers — the pure logic backing + * PatternTransferHeatmap.svelte + patterns/+page.svelte. + * + * Runs in the vitest `node` environment (no jsdom). We never touch Svelte + * component internals here — only the exported helpers in patterns-helpers.ts. + * Component-level integration (click, hover, DOM wiring) is covered by the + * Playwright e2e suite; this file is pure-logic coverage of the contracts. + */ + +import { describe, it, expect } from 'vitest'; +import { + cellIntensity, + filterByCategory, + buildTransferMatrix, + matrixMaxCount, + flattenNonZero, + shortProjectName, + PATTERN_CATEGORIES, + type TransferPatternLike, +} from '../patterns-helpers'; + +// --------------------------------------------------------------------------- +// Test fixtures — mirror the mockFetchCrossProject shape in +// patterns/+page.svelte, but small enough to reason about by hand. +// --------------------------------------------------------------------------- + +const PROJECTS = ['vestige', 'api-gateway', 'desktop-app'] as const; + +const PATTERNS: TransferPatternLike[] = [ + { + name: 'Result', + category: 'ErrorHandling', + origin_project: 'vestige', + transferred_to: ['api-gateway', 'desktop-app'], + transfer_count: 2, + }, + { + name: 'Axum middleware', + category: 'ErrorHandling', + origin_project: 'api-gateway', + transferred_to: ['vestige'], + transfer_count: 1, + }, + { + name: 'proptest', + category: 'Testing', + origin_project: 'vestige', + transferred_to: ['api-gateway'], + transfer_count: 1, + }, + { + name: 'Self-reuse pattern', + category: 'Architecture', + origin_project: 'vestige', + transferred_to: ['vestige'], // diagonal — self-reuse + transfer_count: 1, + }, +]; + +// =========================================================================== +// cellIntensity — 0..1 opacity normaliser +// =========================================================================== + +describe('cellIntensity', () => { + it('returns 0 for a zero count', () => { + expect(cellIntensity(0, 10)).toBe(0); + }); + + it('returns 1 at max', () => { + expect(cellIntensity(10, 10)).toBe(1); + }); + + it('returns 1 when count exceeds max (defensive clamp)', () => { + expect(cellIntensity(15, 10)).toBe(1); + }); + + it('scales linearly between 0 and max', () => { + expect(cellIntensity(3, 10)).toBeCloseTo(0.3, 5); + expect(cellIntensity(5, 10)).toBeCloseTo(0.5, 5); + expect(cellIntensity(7, 10)).toBeCloseTo(0.7, 5); + }); + + it('returns 0 when max is 0 (div-by-zero guard)', () => { + expect(cellIntensity(5, 0)).toBe(0); + }); + + it('returns 0 for negative counts', () => { + expect(cellIntensity(-1, 10)).toBe(0); + }); + + it('returns 0 for NaN inputs', () => { + expect(cellIntensity(NaN, 10)).toBe(0); + expect(cellIntensity(5, NaN)).toBe(0); + }); + + it('returns 0 for Infinity inputs', () => { + expect(cellIntensity(Infinity, 10)).toBe(0); + expect(cellIntensity(5, Infinity)).toBe(0); + }); +}); + +// =========================================================================== +// filterByCategory — drives both heatmap + sidebar reflow +// =========================================================================== + +describe('filterByCategory', () => { + it("returns every pattern for 'All'", () => { + const out = filterByCategory(PATTERNS, 'All'); + expect(out).toHaveLength(PATTERNS.length); + // Should NOT return the same reference — helpers return a copy so + // callers can mutate freely. + expect(out).not.toBe(PATTERNS); + }); + + it('filters strictly by category equality', () => { + const errorOnly = filterByCategory(PATTERNS, 'ErrorHandling'); + expect(errorOnly).toHaveLength(2); + expect(errorOnly.every((p) => p.category === 'ErrorHandling')).toBe(true); + }); + + it('returns exactly one match for Testing', () => { + const testing = filterByCategory(PATTERNS, 'Testing'); + expect(testing).toHaveLength(1); + expect(testing[0].name).toBe('proptest'); + }); + + it('returns an empty array for a category with no patterns', () => { + const perf = filterByCategory(PATTERNS, 'Performance'); + expect(perf).toEqual([]); + }); + + it('returns an empty array for an unknown category string (no silent alias)', () => { + // This is the "unknown category fallback" contract — we do NOT + // quietly fall back to 'All'. An unknown category is a caller bug + // and yields an empty list so the empty-state UI renders. + expect(filterByCategory(PATTERNS, 'NotARealCategory')).toEqual([]); + expect(filterByCategory(PATTERNS, '')).toEqual([]); + }); + + it('accepts an empty input array for any category', () => { + expect(filterByCategory([], 'All')).toEqual([]); + expect(filterByCategory([], 'ErrorHandling')).toEqual([]); + expect(filterByCategory([], 'BogusCategory')).toEqual([]); + }); + + it('exposes all six supported categories', () => { + expect([...PATTERN_CATEGORIES]).toEqual([ + 'ErrorHandling', + 'AsyncConcurrency', + 'Testing', + 'Architecture', + 'Performance', + 'Security', + ]); + }); +}); + +// =========================================================================== +// buildTransferMatrix — directional N×N projects × projects grid +// =========================================================================== + +describe('buildTransferMatrix', () => { + it('constructs an N×N matrix over the projects axis', () => { + const m = buildTransferMatrix(PROJECTS, []); + for (const from of PROJECTS) { + for (const to of PROJECTS) { + expect(m[from][to]).toEqual({ count: 0, topNames: [] }); + } + } + }); + + it('aggregates transfer counts directionally', () => { + const m = buildTransferMatrix(PROJECTS, PATTERNS); + // vestige → api-gateway: Result + proptest = 2 + expect(m.vestige['api-gateway'].count).toBe(2); + // vestige → desktop-app: Result only = 1 + expect(m.vestige['desktop-app'].count).toBe(1); + // api-gateway → vestige: Axum middleware = 1 + expect(m['api-gateway'].vestige.count).toBe(1); + // desktop-app → anywhere: zero (no origin in desktop-app in fixtures) + expect(m['desktop-app'].vestige.count).toBe(0); + expect(m['desktop-app']['api-gateway'].count).toBe(0); + }); + + it('treats (A, B) and (B, A) as distinct directions (asymmetry confirmed)', () => { + // The component's doc-comment says "Rows = origin project · Columns = + // destination project" — the matrix MUST be directional. A copy-paste + // bug that aggregates both directions into the same cell would pass + // the "count" test above but fail this symmetry check. + const m = buildTransferMatrix(PROJECTS, PATTERNS); + expect(m.vestige['api-gateway'].count).not.toBe(m['api-gateway'].vestige.count); + }); + + it('records self-transfer on the diagonal', () => { + const m = buildTransferMatrix(PROJECTS, PATTERNS); + expect(m.vestige.vestige.count).toBe(1); + expect(m.vestige.vestige.topNames).toEqual(['Self-reuse pattern']); + }); + + it('captures top pattern names per cell, capped at 3', () => { + const manyPatterns: TransferPatternLike[] = Array.from({ length: 5 }, (_, i) => ({ + name: `pattern-${i}`, + category: 'ErrorHandling', + origin_project: 'vestige', + transferred_to: ['api-gateway'], + transfer_count: 1, + })); + const m = buildTransferMatrix(['vestige', 'api-gateway'], manyPatterns); + expect(m.vestige['api-gateway'].count).toBe(5); + expect(m.vestige['api-gateway'].topNames).toHaveLength(3); + expect(m.vestige['api-gateway'].topNames).toEqual(['pattern-0', 'pattern-1', 'pattern-2']); + }); + + it('silently drops patterns whose origin is not in the projects axis', () => { + const orphan: TransferPatternLike = { + name: 'Orphan', + category: 'Security', + origin_project: 'ghost-project', + transferred_to: ['vestige'], + transfer_count: 1, + }; + const m = buildTransferMatrix(PROJECTS, [orphan]); + // Nothing anywhere in the matrix should have ticked up. + const total = matrixMaxCount(PROJECTS, m); + expect(total).toBe(0); + // Matrix structure intact — no ghost key added. + expect((m as Record)['ghost-project']).toBeUndefined(); + }); + + it('silently drops transferred_to entries not in the projects axis', () => { + const strayDest: TransferPatternLike = { + name: 'StrayDest', + category: 'Security', + origin_project: 'vestige', + transferred_to: ['ghost-project', 'api-gateway'], + transfer_count: 2, + }; + const m = buildTransferMatrix(PROJECTS, [strayDest]); + // The known destination counts; the ghost doesn't. + expect(m.vestige['api-gateway'].count).toBe(1); + expect((m.vestige as Record)['ghost-project']).toBeUndefined(); + }); + + it('respects a custom top-name cap', () => { + const pats: TransferPatternLike[] = [ + { + name: 'a', + category: 'Testing', + origin_project: 'vestige', + transferred_to: ['api-gateway'], + transfer_count: 1, + }, + { + name: 'b', + category: 'Testing', + origin_project: 'vestige', + transferred_to: ['api-gateway'], + transfer_count: 1, + }, + ]; + const m = buildTransferMatrix(['vestige', 'api-gateway'], pats, 1); + expect(m.vestige['api-gateway'].topNames).toEqual(['a']); + }); +}); + +// =========================================================================== +// matrixMaxCount +// =========================================================================== + +describe('matrixMaxCount', () => { + it('returns 0 for an empty matrix (div-by-zero guard prerequisite)', () => { + const m = buildTransferMatrix(PROJECTS, []); + expect(matrixMaxCount(PROJECTS, m)).toBe(0); + }); + + it('returns the hottest cell count across all pairs', () => { + const m = buildTransferMatrix(PROJECTS, PATTERNS); + // vestige→api-gateway has 2; everything else is ≤1 + expect(matrixMaxCount(PROJECTS, m)).toBe(2); + }); + + it('tolerates missing rows without crashing', () => { + const partial: Record> = { + vestige: { vestige: { count: 3, topNames: [] } }, + }; + expect(matrixMaxCount(['vestige', 'absent'], partial)).toBe(3); + }); +}); + +// =========================================================================== +// flattenNonZero — mobile fallback feed +// =========================================================================== + +describe('flattenNonZero', () => { + it('returns only non-zero pairs, sorted by count descending', () => { + const m = buildTransferMatrix(PROJECTS, PATTERNS); + const rows = flattenNonZero(PROJECTS, m); + // Distinct non-zero cells in fixtures: + // vestige→api-gateway = 2 + // vestige→desktop-app = 1 + // vestige→vestige = 1 + // api-gateway→vestige = 1 + expect(rows).toHaveLength(4); + expect(rows[0]).toMatchObject({ from: 'vestige', to: 'api-gateway', count: 2 }); + // Later rows all tied at 1 — we only verify the leader. + expect(rows.slice(1).every((r) => r.count === 1)).toBe(true); + }); + + it('returns an empty list when nothing is transferred', () => { + const m = buildTransferMatrix(PROJECTS, []); + expect(flattenNonZero(PROJECTS, m)).toEqual([]); + }); +}); + +// =========================================================================== +// shortProjectName +// =========================================================================== + +describe('shortProjectName', () => { + it('passes short names through unchanged', () => { + expect(shortProjectName('vestige')).toBe('vestige'); + expect(shortProjectName('')).toBe(''); + }); + + it('keeps names at the 12-char boundary', () => { + expect(shortProjectName('123456789012')).toBe('123456789012'); + }); + + it('truncates longer names to 11 chars + ellipsis', () => { + expect(shortProjectName('1234567890123')).toBe('12345678901…'); + expect(shortProjectName('super-long-project-name')).toBe('super-long-…'); + }); +}); diff --git a/apps/dashboard/src/lib/components/__tests__/ReasoningChain.test.ts b/apps/dashboard/src/lib/components/__tests__/ReasoningChain.test.ts new file mode 100644 index 0000000..f3c5307 --- /dev/null +++ b/apps/dashboard/src/lib/components/__tests__/ReasoningChain.test.ts @@ -0,0 +1,193 @@ +/** + * ReasoningChain — pure-logic coverage. + * + * ReasoningChain renders the 8-stage cognitive pipeline. Its rendered output + * is a pure function of a handful of primitive props — confidence colours, + * intent-hint selection, and the stage hint resolver. All of that logic + * lives in `reasoning-helpers.ts` and is exercised here without mounting + * Svelte. + */ +import { describe, it, expect } from 'vitest'; + +import { + confidenceColor, + confidenceLabel, + intentHintFor, + INTENT_HINTS, + CONFIDENCE_EMERALD, + CONFIDENCE_AMBER, + CONFIDENCE_RED, + type IntentKey, +} from '../reasoning-helpers'; + +// ──────────────────────────────────────────────────────────────── +// confidenceColor — the spec-critical boundary table +// ──────────────────────────────────────────────────────────────── + +describe('confidenceColor — band boundaries (>75 emerald, 40-75 amber, <40 red)', () => { + it.each<[number, string]>([ + // Emerald band: strictly greater than 75 + [100, CONFIDENCE_EMERALD], + [99.99, CONFIDENCE_EMERALD], + [80, CONFIDENCE_EMERALD], + [76, CONFIDENCE_EMERALD], + [75.01, CONFIDENCE_EMERALD], + // Amber band: 40 <= c <= 75 + [75, CONFIDENCE_AMBER], // exactly 75 → amber (page spec: `>75` emerald) + [60, CONFIDENCE_AMBER], + [50, CONFIDENCE_AMBER], + [40.01, CONFIDENCE_AMBER], + [40, CONFIDENCE_AMBER], // exactly 40 → amber (page spec: `>=40` amber) + // Red band: strictly less than 40 + [39.99, CONFIDENCE_RED], + [20, CONFIDENCE_RED], + [0.01, CONFIDENCE_RED], + [0, CONFIDENCE_RED], + ])('confidence %f → %s', (c, expected) => { + expect(confidenceColor(c)).toBe(expected); + }); + + it('clamps negative to red (defensive — confidence should never be negative)', () => { + expect(confidenceColor(-10)).toBe(CONFIDENCE_RED); + }); + + it('over-100 stays emerald (defensive — confidence should never exceed 100)', () => { + expect(confidenceColor(150)).toBe(CONFIDENCE_EMERALD); + }); + + it('NaN → red (worst-case band)', () => { + expect(confidenceColor(Number.NaN)).toBe(CONFIDENCE_RED); + }); + + it('is pure — same input yields same output', () => { + for (const c of [0, 39.99, 40, 75, 75.01, 100]) { + expect(confidenceColor(c)).toBe(confidenceColor(c)); + } + }); + + it('never returns an empty string or undefined', () => { + for (const c of [-1, 0, 20, 40, 75, 76, 100, 200, Number.NaN]) { + const colour = confidenceColor(c); + expect(typeof colour).toBe('string'); + expect(colour.length).toBeGreaterThan(0); + } + }); +}); + +describe('confidenceLabel — human text per band', () => { + it.each<[number, string]>([ + [100, 'HIGH CONFIDENCE'], + [76, 'HIGH CONFIDENCE'], + [75.01, 'HIGH CONFIDENCE'], + [75, 'MIXED SIGNAL'], + [60, 'MIXED SIGNAL'], + [40, 'MIXED SIGNAL'], + [39.99, 'LOW CONFIDENCE'], + [0, 'LOW CONFIDENCE'], + ])('confidence %f → %s', (c, expected) => { + expect(confidenceLabel(c)).toBe(expected); + }); + + it('NaN → LOW CONFIDENCE (safe default)', () => { + expect(confidenceLabel(Number.NaN)).toBe('LOW CONFIDENCE'); + }); + + it('agrees with confidenceColor across the spec boundary sweep', () => { + // Sanity: if the label is HIGH, the colour must be emerald, etc. + const cases: Array<[number, string, string]> = [ + [100, 'HIGH CONFIDENCE', CONFIDENCE_EMERALD], + [76, 'HIGH CONFIDENCE', CONFIDENCE_EMERALD], + [75, 'MIXED SIGNAL', CONFIDENCE_AMBER], + [40, 'MIXED SIGNAL', CONFIDENCE_AMBER], + [39.99, 'LOW CONFIDENCE', CONFIDENCE_RED], + [0, 'LOW CONFIDENCE', CONFIDENCE_RED], + ]; + for (const [c, label, colour] of cases) { + expect(confidenceLabel(c)).toBe(label); + expect(confidenceColor(c)).toBe(colour); + } + }); +}); + +// ──────────────────────────────────────────────────────────────── +// Intent classification — visual hint mapping +// ──────────────────────────────────────────────────────────────── + +describe('INTENT_HINTS — one hint per deep_reference intent', () => { + const intents: IntentKey[] = [ + 'FactCheck', + 'Timeline', + 'RootCause', + 'Comparison', + 'Synthesis', + ]; + + it('defines a hint for every intent the backend emits', () => { + for (const i of intents) { + expect(INTENT_HINTS[i]).toBeDefined(); + } + }); + + it.each(intents)('%s hint has label + icon + description', (i) => { + const hint = INTENT_HINTS[i]; + expect(hint.label).toBe(i); // label doubles as canonical id + expect(hint.icon.length).toBeGreaterThan(0); + expect(hint.description.length).toBeGreaterThan(0); + }); + + it('icons are unique across intents (so the eye can distinguish them)', () => { + const icons = intents.map((i) => INTENT_HINTS[i].icon); + expect(new Set(icons).size).toBe(intents.length); + }); + + it('descriptions are distinct across intents', () => { + const descs = intents.map((i) => INTENT_HINTS[i].description); + expect(new Set(descs).size).toBe(intents.length); + }); +}); + +describe('intentHintFor — lookup with safe fallback', () => { + it('returns the exact entry for a known intent', () => { + expect(intentHintFor('FactCheck')).toBe(INTENT_HINTS.FactCheck); + expect(intentHintFor('Timeline')).toBe(INTENT_HINTS.Timeline); + expect(intentHintFor('RootCause')).toBe(INTENT_HINTS.RootCause); + expect(intentHintFor('Comparison')).toBe(INTENT_HINTS.Comparison); + expect(intentHintFor('Synthesis')).toBe(INTENT_HINTS.Synthesis); + }); + + it('falls back to Synthesis for unknown intent (most generic classification)', () => { + expect(intentHintFor('Prediction')).toBe(INTENT_HINTS.Synthesis); + expect(intentHintFor('nonsense')).toBe(INTENT_HINTS.Synthesis); + }); + + it('falls back to Synthesis for null / undefined / empty string', () => { + expect(intentHintFor(null)).toBe(INTENT_HINTS.Synthesis); + expect(intentHintFor(undefined)).toBe(INTENT_HINTS.Synthesis); + expect(intentHintFor('')).toBe(INTENT_HINTS.Synthesis); + }); + + it('is case-sensitive — backend emits Title-case strings and we honour that', () => { + // If case-folding becomes desirable, this test will force the + // change to be explicit rather than accidental. + expect(intentHintFor('factcheck')).toBe(INTENT_HINTS.Synthesis); + expect(intentHintFor('FACTCHECK')).toBe(INTENT_HINTS.Synthesis); + }); +}); + +// ──────────────────────────────────────────────────────────────── +// Stage-count invariant — the component renders exactly 8 stages +// ──────────────────────────────────────────────────────────────── + +describe('Cognitive pipeline shape', () => { + it('confidence colour constants are all distinct hex strings', () => { + const set = new Set([ + CONFIDENCE_EMERALD.toLowerCase(), + CONFIDENCE_AMBER.toLowerCase(), + CONFIDENCE_RED.toLowerCase(), + ]); + expect(set.size).toBe(3); + for (const c of set) { + expect(c).toMatch(/^#[0-9a-f]{6}$/); + } + }); +}); diff --git a/apps/dashboard/src/lib/components/activation-helpers.ts b/apps/dashboard/src/lib/components/activation-helpers.ts new file mode 100644 index 0000000..e330910 --- /dev/null +++ b/apps/dashboard/src/lib/components/activation-helpers.ts @@ -0,0 +1,237 @@ +/** + * activation-helpers — Pure logic for the Spreading Activation Live View. + * + * Extracted from ActivationNetwork.svelte + activation/+page.svelte so the + * decay / geometry / event-filtering rules can be exercised in the Vitest + * `node` environment without jsdom. Every helper in this module is a pure + * function of its inputs; no DOM, no timers, no Svelte runes. + * + * The constants in this module are the single source of truth — the Svelte + * component re-exports / re-uses them rather than hard-coding its own. + * + * References + * ---------- + * - Collins & Loftus 1975 — spreading activation with exponential decay + * - Anderson 1983 (ACT-R) — activation threshold for availability + */ +import { NODE_TYPE_COLORS } from '$types'; +import type { VestigeEvent } from '$types'; + +/** Per-tick multiplicative decay factor (Collins & Loftus 1975). */ +export const DECAY = 0.93; + +/** Activation below this floor is invisible / garbage-collected. */ +export const MIN_VISIBLE = 0.05; + +/** Fallback node colour when NODE_TYPE_COLORS has no entry for the type. */ +export const FALLBACK_COLOR = '#8B95A5'; + +/** Source node colour (synapse-glow). Distinct from any node-type colour. */ +export const SOURCE_COLOR = '#818cf8'; + +/** Radial spacing between concentric rings (px). */ +export const RING_GAP = 140; + +/** Max neighbours that fit on ring 1 before spilling to ring 2. */ +export const RING_1_CAPACITY = 8; + +/** Edge draw stagger — frames of delay per rank inside a ring. */ +export const STAGGER_PER_RANK = 4; + +/** Extra stagger added to ring-2 edges so they light up after ring 1. */ +export const STAGGER_RING_2_BONUS = 12; + +// --------------------------------------------------------------------------- +// Decay math +// --------------------------------------------------------------------------- + +/** + * Apply a single tick of exponential decay. Clamps negative input to 0 so a + * corrupt state never produces a creeping-positive value on the next tick. + */ +export function applyDecay(activation: number): number { + if (!Number.isFinite(activation) || activation <= 0) return 0; + return activation * DECAY; +} + +/** + * Compound decay over N ticks. N < 0 is treated as 0 (no change). + * Equivalent to calling `applyDecay` N times. + */ +export function compoundDecay(activation: number, ticks: number): number { + if (!Number.isFinite(activation) || activation <= 0) return 0; + if (!Number.isFinite(ticks) || ticks <= 0) return activation; + return activation * DECAY ** ticks; +} + +/** True if the node's activation is at or above the visibility floor. */ +export function isVisible(activation: number): boolean { + if (!Number.isFinite(activation)) return false; + return activation >= MIN_VISIBLE; +} + +/** + * How many ticks until `initial` decays below `MIN_VISIBLE`. Useful in tests + * and for sizing animation budgets. Initial <= threshold returns 0. + */ +export function ticksUntilInvisible(initial: number): number { + if (!Number.isFinite(initial) || initial <= MIN_VISIBLE) return 0; + // initial * DECAY^n < MIN_VISIBLE → n > log(MIN_VISIBLE/initial) / log(DECAY) + const n = Math.log(MIN_VISIBLE / initial) / Math.log(DECAY); + return Math.ceil(n); +} + +// --------------------------------------------------------------------------- +// Ring placement — concentric circles around a source +// --------------------------------------------------------------------------- + +export interface Point { + x: number; + y: number; +} + +/** + * Classify a neighbour's 0-indexed rank into a ring number. + * Ranks 0..RING_1_CAPACITY-1 → ring 1; rest → ring 2. + */ +export function computeRing(rank: number): 1 | 2 { + if (!Number.isFinite(rank) || rank < RING_1_CAPACITY) return 1; + return 2; +} + +/** + * Evenly distribute `count` positions on a circle of radius `ring * RING_GAP` + * centred at (cx, cy). `angleOffset` rotates the whole ring so overlapping + * bursts don't perfectly collide. Zero count returns `[]`. + */ +export function ringPositions( + cx: number, + cy: number, + count: number, + ring: number, + angleOffset = 0, +): Point[] { + if (!Number.isFinite(count) || count <= 0) return []; + const radius = RING_GAP * ring; + const positions: Point[] = []; + for (let i = 0; i < count; i++) { + const angle = angleOffset + (i / count) * Math.PI * 2; + positions.push({ + x: cx + Math.cos(angle) * radius, + y: cy + Math.sin(angle) * radius, + }); + } + return positions; +} + +/** + * Given the full neighbour list, produce a flat array of Points — ring 1 + * first, ring 2 after. The resulting length === neighbours.length. + */ +export function layoutNeighbours( + cx: number, + cy: number, + neighbourCount: number, + angleOffset = 0, +): Point[] { + const ring1 = Math.min(neighbourCount, RING_1_CAPACITY); + const ring2 = Math.max(0, neighbourCount - RING_1_CAPACITY); + return [ + ...ringPositions(cx, cy, ring1, 1, angleOffset), + ...ringPositions(cx, cy, ring2, 2, angleOffset), + ]; +} + +// --------------------------------------------------------------------------- +// Initial activation by rank +// --------------------------------------------------------------------------- + +/** + * Seed activation for a neighbour at 0-indexed `rank` given `total`. + * Higher-ranked (earlier) neighbours get stronger initial activation. + * Ring-2 neighbours get a 0.75× ring-factor penalty on top of the rank factor. + * Returns a value in (0, 1]. + */ +export function initialActivation(rank: number, total: number): number { + if (!Number.isFinite(total) || total <= 0) return 0; + if (!Number.isFinite(rank) || rank < 0) return 0; + const rankFactor = 1 - (rank / total) * 0.35; + const ringFactor = computeRing(rank) === 1 ? 1 : 0.75; + return Math.min(1, rankFactor * ringFactor); +} + +// --------------------------------------------------------------------------- +// Edge stagger +// --------------------------------------------------------------------------- + +/** + * Delay (in animation frames) before the edge at rank `i` starts drawing. + * Ring 1 edges light up first, then ring 2 after a bonus delay. + */ +export function edgeStagger(rank: number): number { + if (!Number.isFinite(rank) || rank < 0) return 0; + const r = Math.floor(rank); + const base = r * STAGGER_PER_RANK; + return computeRing(r) === 1 ? base : base + STAGGER_RING_2_BONUS; +} + +// --------------------------------------------------------------------------- +// Color mapping +// --------------------------------------------------------------------------- + +/** + * Colour for a node on the activation canvas. + * - source nodes always use SOURCE_COLOR (synapse-glow) + * - known node types use NODE_TYPE_COLORS + * - unknown node types fall back to FALLBACK_COLOR (soft steel) + */ +export function activationColor( + nodeType: string | null | undefined, + isSource: boolean, +): string { + if (isSource) return SOURCE_COLOR; + if (!nodeType) return FALLBACK_COLOR; + return NODE_TYPE_COLORS[nodeType] ?? FALLBACK_COLOR; +} + +// --------------------------------------------------------------------------- +// Event-feed filtering — "only fire on NEW ActivationSpread events" +// --------------------------------------------------------------------------- + +export interface SpreadPayload { + source_id: string; + target_ids: string[]; +} + +/** + * Extract ActivationSpread payloads from a websocket event feed. The feed + * is prepended (newest at index 0, oldest at the end). Stop as soon as we + * hit the reference of `lastSeen` — events at or past that point were + * already processed by a prior tick. + * + * Returned payloads are in OLDEST-FIRST order so downstream callers can + * fire them in the same narrative order they occurred. + * + * Payloads missing required fields are silently skipped. + */ +export function filterNewSpreadEvents( + feed: readonly VestigeEvent[], + lastSeen: VestigeEvent | null, +): SpreadPayload[] { + if (!feed || feed.length === 0) return []; + const fresh: SpreadPayload[] = []; + for (const ev of feed) { + if (ev === lastSeen) break; + if (ev.type !== 'ActivationSpread') continue; + const data = ev.data as { source_id?: unknown; target_ids?: unknown }; + if (typeof data.source_id !== 'string') continue; + if (!Array.isArray(data.target_ids)) continue; + const targets = data.target_ids.filter( + (t): t is string => typeof t === 'string', + ); + if (targets.length === 0) continue; + fresh.push({ source_id: data.source_id, target_ids: targets }); + } + // Reverse so oldest-first. + return fresh.reverse(); +} diff --git a/apps/dashboard/src/lib/components/audit-trail-helpers.ts b/apps/dashboard/src/lib/components/audit-trail-helpers.ts new file mode 100644 index 0000000..2dbca23 --- /dev/null +++ b/apps/dashboard/src/lib/components/audit-trail-helpers.ts @@ -0,0 +1,293 @@ +/** + * Pure helpers for MemoryAuditTrail. + * + * Extracted for isolated unit testing in a Node (vitest) environment — + * no DOM, no Svelte runtime, no fetch. Every function in this module is + * deterministic given its inputs. + */ + +export type AuditAction = + | 'created' + | 'accessed' + | 'promoted' + | 'demoted' + | 'edited' + | 'suppressed' + | 'dreamed' + | 'reconsolidated'; + +export interface AuditEvent { + action: AuditAction; + timestamp: string; // ISO + old_value?: number; + new_value?: number; + reason?: string; + triggered_by?: string; +} + +export type MarkerKind = + | 'dot' + | 'arrow-up' + | 'arrow-down' + | 'pencil' + | 'x' + | 'star' + | 'circle-arrow' + | 'ring'; + +export interface Meta { + label: string; + color: string; // hex for dot + glow + glyph: string; // optional inline symbol + kind: MarkerKind; +} + +/** + * Event type → visual metadata. Each action maps to a UNIQUE marker `kind` + * so the 8 event types are visually distinguishable without relying on the + * colour palette alone (accessibility). + */ +export const META: Record = { + created: { label: 'Created', color: '#10b981', glyph: '', kind: 'ring' }, + accessed: { label: 'Accessed', color: '#3b82f6', glyph: '', kind: 'dot' }, + promoted: { label: 'Promoted', color: '#10b981', glyph: '', kind: 'arrow-up' }, + demoted: { label: 'Demoted', color: '#f59e0b', glyph: '', kind: 'arrow-down' }, + edited: { label: 'Edited', color: '#facc15', glyph: '', kind: 'pencil' }, + suppressed: { label: 'Suppressed', color: '#a855f7', glyph: '', kind: 'x' }, + dreamed: { label: 'Dreamed', color: '#c084fc', glyph: '', kind: 'star' }, + reconsolidated: { label: 'Reconsolidated', color: '#ec4899', glyph: '', kind: 'circle-arrow' } +}; + +export const VISIBLE_LIMIT = 15; + +/** + * All 8 `AuditAction` values, in the canonical order. Used both by the + * event generator (`actionPool`) and by tests that verify uniqueness of + * the marker mapping. + */ +export const ALL_ACTIONS: readonly AuditAction[] = [ + 'created', + 'accessed', + 'promoted', + 'demoted', + 'edited', + 'suppressed', + 'dreamed', + 'reconsolidated' +] as const; + +/** + * Hash a string id into a 32-bit unsigned seed. Stable across runs. + */ +export function hashSeed(id: string): number { + let seed = 0; + for (let i = 0; i < id.length; i++) seed = (seed * 31 + id.charCodeAt(i)) >>> 0; + return seed; +} + +/** + * Linear congruential PRNG bound to a mutable seed. Returns a function + * that yields floats in `[0, 1)` — critically, NEVER 1.0, so callers + * can safely use `Math.floor(rand() * arr.length)` without off-by-one. + */ +export function makeRand(initialSeed: number): () => number { + let seed = initialSeed >>> 0; + return () => { + seed = (seed * 1664525 + 1013904223) >>> 0; + // Divide by 2^32, not 2^32 - 1 — the latter can yield exactly 1.0 + // when seed is UINT32_MAX, breaking array-index math. + return seed / 0x100000000; + }; +} + +/** + * Deterministic mock audit-trail generator. Same `memoryId` + `nowMs` + * ALWAYS yields the same event sequence (critical for snapshot stability + * and for tests). An empty `memoryId` yields no events — the audit trail + * panel should never invent history for a non-existent memory. + * + * `countOverride` lets tests force a specific number of events (e.g. + * to cross the 15-event visibility threshold, which the default range + * 8-15 cannot do). + */ +export function generateMockAuditTrail( + memoryId: string, + nowMs: number = Date.now(), + countOverride?: number +): AuditEvent[] { + if (!memoryId) return []; + + const rand = makeRand(hashSeed(memoryId)); + const count = countOverride ?? 8 + Math.floor(rand() * 8); // default 8-15 events + if (count <= 0) return []; + + const out: AuditEvent[] = []; + + const createdAt = nowMs - (14 + rand() * 21) * 86_400_000; // 14-35 days ago + out.push({ + action: 'created', + timestamp: new Date(createdAt).toISOString(), + reason: 'smart_ingest · prediction-error gate opened', + triggered_by: 'smart_ingest' + }); + + let t = createdAt; + let retention = 0.5 + rand() * 0.2; + const actionPool: AuditAction[] = [ + 'accessed', + 'accessed', + 'accessed', + 'accessed', + 'promoted', + 'demoted', + 'edited', + 'dreamed', + 'reconsolidated', + 'suppressed' + ]; + + for (let i = 1; i < count; i++) { + t += rand() * 5 * 86_400_000 + 3_600_000; // 1h-5d between events + const action = actionPool[Math.floor(rand() * actionPool.length)]; + const ev: AuditEvent = { action, timestamp: new Date(t).toISOString() }; + + switch (action) { + case 'accessed': { + const old = retention; + retention = Math.min(1, retention + rand() * 0.04 + 0.01); + ev.old_value = old; + ev.new_value = retention; + ev.triggered_by = rand() > 0.5 ? 'search' : 'deep_reference'; + break; + } + case 'promoted': { + const old = retention; + retention = Math.min(1, retention + 0.1); + ev.old_value = old; + ev.new_value = retention; + ev.reason = 'confirmed helpful by user'; + ev.triggered_by = 'memory(action=promote)'; + break; + } + case 'demoted': { + const old = retention; + retention = Math.max(0, retention - 0.15); + ev.old_value = old; + ev.new_value = retention; + ev.reason = 'user flagged as outdated'; + ev.triggered_by = 'memory(action=demote)'; + break; + } + case 'edited': { + ev.reason = 'content refined, FSRS state preserved'; + ev.triggered_by = 'memory(action=edit)'; + break; + } + case 'suppressed': { + const old = retention; + retention = Math.max(0, retention - 0.08); + ev.old_value = old; + ev.new_value = retention; + ev.reason = 'top-down inhibition (Anderson 2025)'; + ev.triggered_by = 'suppress(dashboard)'; + break; + } + case 'dreamed': { + const old = retention; + retention = Math.min(1, retention + 0.05); + ev.old_value = old; + ev.new_value = retention; + ev.reason = 'replayed during dream consolidation'; + ev.triggered_by = 'dream()'; + break; + } + case 'reconsolidated': { + ev.reason = 'edited within 5-min labile window (Nader)'; + ev.triggered_by = 'reconsolidation-manager'; + break; + } + case 'created': + // Created is only emitted once, as the first event. If the pool + // ever yields it again, treat it as a no-op access marker with + // no retention change — defensive, not expected. + ev.triggered_by = 'smart_ingest'; + break; + } + + out.push(ev); + } + + // Newest first for display. + return out.reverse(); +} + +/** + * Humanised relative time. Uses supplied `nowMs` for deterministic tests; + * defaults to `Date.now()` in production. + * + * Boundaries (strictly `<`, so 60s flips to "1m", 60m flips to "1h", etc.): + * <60s → "Ns ago" + * <60m → "Nm ago" + * <24h → "Nh ago" + * <30d → "Nd ago" + * <12mo → "Nmo ago" + * else → "Ny ago" + * + * Future timestamps (nowMs < then) clamp to "0s ago" rather than returning + * a negative string — the audit trail is a past-only view. + */ +export function relativeTime(iso: string, nowMs: number = Date.now()): string { + const then = new Date(iso).getTime(); + const diff = Math.max(0, nowMs - then); + const s = Math.floor(diff / 1000); + if (s < 60) return `${s}s ago`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + const d = Math.floor(h / 24); + if (d < 30) return `${d}d ago`; + const mo = Math.floor(d / 30); + if (mo < 12) return `${mo}mo ago`; + const y = Math.floor(mo / 12); + return `${y}y ago`; +} + +/** + * Retention delta formatter. Behaviour: + * (undef, undef) → null — no retention movement on this event + * (undef, 0.72) → "set 0.72" — initial value, no prior state + * (0.50, undef) → "was 0.50" — retention cleared (rare) + * (0.50, 0.72) → "0.50 → 0.72" + * + * The `retention ` prefix is left to the caller so tests can compare the + * core formatted value precisely. + */ +export function formatRetentionDelta( + oldValue: number | undefined, + newValue: number | undefined +): string | null { + const hasOld = typeof oldValue === 'number' && Number.isFinite(oldValue); + const hasNew = typeof newValue === 'number' && Number.isFinite(newValue); + if (!hasOld && !hasNew) return null; + if (!hasOld && hasNew) return `set ${newValue!.toFixed(2)}`; + if (hasOld && !hasNew) return `was ${oldValue!.toFixed(2)}`; + return `${oldValue!.toFixed(2)} → ${newValue!.toFixed(2)}`; +} + +/** + * Split an event list into (visible, hiddenCount) per the 15-event cap. + * Exactly 15 events → no toggle (hiddenCount = 0). 16+ → toggle. + */ +export function splitVisible( + events: AuditEvent[], + showAll: boolean +): { visible: AuditEvent[]; hiddenCount: number } { + if (showAll || events.length <= VISIBLE_LIMIT) { + return { visible: events, hiddenCount: Math.max(0, events.length - VISIBLE_LIMIT) }; + } + return { + visible: events.slice(0, VISIBLE_LIMIT), + hiddenCount: events.length - VISIBLE_LIMIT + }; +} diff --git a/apps/dashboard/src/lib/components/awareness-helpers.ts b/apps/dashboard/src/lib/components/awareness-helpers.ts new file mode 100644 index 0000000..d60a4a6 --- /dev/null +++ b/apps/dashboard/src/lib/components/awareness-helpers.ts @@ -0,0 +1,192 @@ +/** + * Pure helpers for AmbientAwarenessStrip.svelte. + * + * Extracted so the time-window, event-scan, and timestamp-parsing logic can + * be unit tested in the vitest `node` environment without jsdom, Svelte + * rendering, or fake timers bleeding into runes. + * + * Contracts + * --------- + * - `parseEventTimestamp`: handles (a) numeric ms (>1e12), (b) numeric seconds + * (<=1e12), (c) ISO-8601 string, (d) invalid/absent → null. + * - `bucketizeActivity`: given ms timestamps + `now`, returns 10 counts for a + * 5-min trailing window. Bucket 0 = oldest 30s, bucket 9 = newest 30s. + * Events outside [now-5m, now] are dropped (clock skew). + * - `findRecentDream`: returns the newest-indexed (feed is newest-first) + * DreamCompleted whose parsed timestamp is within 24h, else null. If the + * timestamp is unparseable, `now` is used as the fallback (matches the + * component's behavior). + * - `isDreaming`: a DreamStarted within the last 5 min NOT followed by a + * newer DreamCompleted. Mirrors the component's derived block exactly. + * - `hasRecentSuppression`: any MemorySuppressed event with a parsed + * timestamp within `thresholdMs` of `now`. Feed is assumed newest-first — + * we break as soon as we pass the threshold, matching component behavior. + * + * All helpers are null-safe and treat unparseable timestamps consistently + * (fall back to `now`, matching the on-screen "something just happened" feel). + */ + +export interface EventLike { + type: string; + data?: Record; +} + +/** + * Parse a VestigeEvent timestamp, checking `data.timestamp`, then `data.at`, + * then `data.occurred_at`. Supports ms-since-epoch numbers, seconds-since-epoch + * numbers, and ISO-8601 strings. Returns null for absent / invalid input. + * + * Numeric heuristic: values > 1e12 are treated as ms (2001+), values <= 1e12 + * are treated as seconds. `1e12 ms` ≈ Sept 2001, so any real ms timestamp + * lands safely on the "ms" side. + */ +export function parseEventTimestamp(event: EventLike): number | null { + const d = event.data; + if (!d || typeof d !== 'object') return null; + const raw = + (d.timestamp as string | number | undefined) ?? + (d.at as string | number | undefined) ?? + (d.occurred_at as string | number | undefined); + if (raw === undefined || raw === null) return null; + if (typeof raw === 'number') { + if (!Number.isFinite(raw)) return null; + return raw > 1e12 ? raw : raw * 1000; + } + if (typeof raw !== 'string') return null; + const ms = Date.parse(raw); + return Number.isFinite(ms) ? ms : null; +} + +export const ACTIVITY_BUCKET_COUNT = 10; +export const ACTIVITY_BUCKET_MS = 30_000; +export const ACTIVITY_WINDOW_MS = ACTIVITY_BUCKET_COUNT * ACTIVITY_BUCKET_MS; + +export interface ActivityBucket { + count: number; + ratio: number; +} + +/** + * Bucket event timestamps into 10 × 30s buckets for a 5-min trailing window. + * Events with `type === 'Heartbeat'` are skipped (noise). Events whose + * timestamp is out of window (clock skew / pre-history) are dropped. + * + * Returned `ratio` is `count / max(1, maxBucketCount)` — so a sparkline with + * zero events has all-zero ratios (no division by zero) and a sparkline with + * a single spike peaks at 1.0. + */ +export function bucketizeActivity( + events: EventLike[], + nowMs: number, +): ActivityBucket[] { + const start = nowMs - ACTIVITY_WINDOW_MS; + const counts = new Array(ACTIVITY_BUCKET_COUNT).fill(0); + for (const e of events) { + if (e.type === 'Heartbeat') continue; + const t = parseEventTimestamp(e); + if (t === null || t < start || t > nowMs) continue; + const idx = Math.min( + ACTIVITY_BUCKET_COUNT - 1, + Math.floor((t - start) / ACTIVITY_BUCKET_MS), + ); + counts[idx] += 1; + } + const max = Math.max(1, ...counts); + return counts.map((count) => ({ count, ratio: count / max })); +} + +/** + * Find the most recent DreamCompleted within 24h of `nowMs`. + * Feed is assumed newest-first — we return the FIRST match. + * Unparseable timestamps fall back to `nowMs` (matches component behavior). + */ +export function findRecentDream( + events: EventLike[], + nowMs: number, +): EventLike | null { + const dayAgo = nowMs - 24 * 60 * 60 * 1000; + for (const e of events) { + if (e.type !== 'DreamCompleted') continue; + const t = parseEventTimestamp(e) ?? nowMs; + if (t >= dayAgo) return e; + return null; // newest-first: older ones definitely won't match + } + return null; +} + +/** + * Extract `insights_generated` / `insightsGenerated` from a DreamCompleted + * event payload. Returns null if missing or non-numeric. + */ +export function dreamInsightsCount(event: EventLike | null): number | null { + if (!event || !event.data) return null; + const d = event.data; + const raw = + typeof d.insights_generated === 'number' + ? d.insights_generated + : typeof d.insightsGenerated === 'number' + ? d.insightsGenerated + : null; + return raw !== null && Number.isFinite(raw) ? raw : null; +} + +/** + * A Dream is in flight if the newest DreamStarted is within 5 min of `nowMs` + * AND there is no DreamCompleted with a timestamp >= that DreamStarted. + * + * Feed is assumed newest-first. We scan once, grabbing the first Started and + * first Completed, then compare — matching the component's derived block. + */ +export function isDreaming(events: EventLike[], nowMs: number): boolean { + let started: EventLike | null = null; + let completed: EventLike | null = null; + for (const e of events) { + if (!started && e.type === 'DreamStarted') started = e; + if (!completed && e.type === 'DreamCompleted') completed = e; + if (started && completed) break; + } + if (!started) return false; + const startedAt = parseEventTimestamp(started) ?? nowMs; + const fiveMinAgo = nowMs - 5 * 60 * 1000; + if (startedAt < fiveMinAgo) return false; + if (!completed) return true; + const completedAt = parseEventTimestamp(completed) ?? nowMs; + return completedAt < startedAt; +} + +/** + * Format an "ago" duration compactly. Pure and deterministic. + * 0-59s → "Ns ago", 60-3599s → "Nm ago", <24h → "Nh ago", else "Nd ago". + * Negative input is clamped to 0. + */ +export function formatAgo(ms: number): string { + const clamped = Math.max(0, ms); + const s = Math.floor(clamped / 1000); + if (s < 60) return `${s}s ago`; + const m = Math.floor(s / 60); + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + return `${Math.floor(h / 24)}d ago`; +} + +/** + * True if any MemorySuppressed event lies within `thresholdMs` of `nowMs`. + * Feed assumed newest-first — break as soon as we encounter one OUTSIDE + * the window (all older ones are definitely older). Unparseable timestamps + * fall back to `nowMs` so the flash fires — matches component behavior. + */ +export function hasRecentSuppression( + events: EventLike[], + nowMs: number, + thresholdMs: number = 10_000, +): boolean { + const cutoff = nowMs - thresholdMs; + for (const e of events) { + if (e.type !== 'MemorySuppressed') continue; + const t = parseEventTimestamp(e) ?? nowMs; + if (t >= cutoff) return true; + return false; // newest-first: older ones definitely won't match + } + return false; +} diff --git a/apps/dashboard/src/lib/components/contradiction-helpers.ts b/apps/dashboard/src/lib/components/contradiction-helpers.ts new file mode 100644 index 0000000..14ab90f --- /dev/null +++ b/apps/dashboard/src/lib/components/contradiction-helpers.ts @@ -0,0 +1,210 @@ +/** + * contradiction-helpers — Pure logic for the Contradiction Constellation UI. + * + * Extracted from ContradictionArcs.svelte + contradictions/+page.svelte so + * the math and classification live in one place and can be tested in the + * vitest `node` environment without jsdom / Svelte harnessing. + * + * Contracts + * --------- + * - Severity thresholds are STRICTLY exclusive: similarity > 0.7 → strong, + * similarity > 0.5 → moderate, else → mild. The boundary values 0.5 and + * 0.7 therefore fall into the LOWER band on purpose (so a similarity of + * exactly 0.7 is 'moderate', not 'strong'). + * - Node type palette has 8 known types; anything else — including + * `undefined`, `null`, empty string, or a typo — falls back to violet + * (#8b5cf6), matching the `concept` fallback tone used elsewhere. + * - Pair opacity is a trinary rule: no focus → 1, focused match → 1, + * focused non-match → 0.12. `null` and `undefined` both mean "no focus". + * - Trust is defined on [0,1]; `nodeRadius` clamps out-of-range values so + * a negative trust can't produce a sub-zero radius and a >1 trust can't + * balloon past the design maximum (14px). + * - `uniqueMemoryCount` unions memory_a_id + memory_b_id across the whole + * pair list; duplicated pairs do not double-count. + */ + +/** Shape used by the constellation. Mirrors ContradictionArcs.Contradiction. */ +export interface ContradictionLike { + memory_a_id: string; + memory_b_id: string; +} + +// --------------------------------------------------------------------------- +// Severity — similarity → colour + label. +// --------------------------------------------------------------------------- + +export type SeverityLabel = 'strong' | 'moderate' | 'mild'; + +/** Strong threshold. Similarity STRICTLY above this is red. */ +export const SEVERITY_STRONG_THRESHOLD = 0.7; +/** Moderate threshold. Similarity STRICTLY above this (and <= 0.7) is amber. */ +export const SEVERITY_MODERATE_THRESHOLD = 0.5; + +export const SEVERITY_STRONG_COLOR = '#ef4444'; +export const SEVERITY_MODERATE_COLOR = '#f59e0b'; +export const SEVERITY_MILD_COLOR = '#fde047'; + +/** + * Severity colour by similarity. Boundaries at 0.5 and 0.7 fall into the + * LOWER band (strictly-greater-than comparison). + * + * sim > 0.7 → '#ef4444' (strong / red) + * sim > 0.5 → '#f59e0b' (moderate / amber) + * otherwise → '#fde047' (mild / yellow) + */ +export function severityColor(sim: number): string { + if (sim > SEVERITY_STRONG_THRESHOLD) return SEVERITY_STRONG_COLOR; + if (sim > SEVERITY_MODERATE_THRESHOLD) return SEVERITY_MODERATE_COLOR; + return SEVERITY_MILD_COLOR; +} + +/** Severity label by similarity. Same thresholds as severityColor. */ +export function severityLabel(sim: number): SeverityLabel { + if (sim > SEVERITY_STRONG_THRESHOLD) return 'strong'; + if (sim > SEVERITY_MODERATE_THRESHOLD) return 'moderate'; + return 'mild'; +} + +// --------------------------------------------------------------------------- +// Node type palette. +// --------------------------------------------------------------------------- + +/** Fallback colour used when a memory's node_type is missing or unknown. */ +export const NODE_COLOR_FALLBACK = '#8b5cf6'; + +/** Canonical palette for the 8 known node types. */ +export const NODE_COLORS: Record = { + fact: '#3b82f6', + concept: '#8b5cf6', + event: '#f59e0b', + person: '#10b981', + place: '#06b6d4', + note: '#6b7280', + pattern: '#ec4899', + decision: '#ef4444', +}; + +/** Canonical list of known types (stable order — matches palette object). */ +export const KNOWN_NODE_TYPES = Object.freeze([ + 'fact', + 'concept', + 'event', + 'person', + 'place', + 'note', + 'pattern', + 'decision', +]) as readonly string[]; + +/** + * Map a (possibly undefined) node_type to a colour. Unknown / missing / + * empty / null strings fall back to violet (#8b5cf6). + */ +export function nodeColor(t?: string | null): string { + if (!t) return NODE_COLOR_FALLBACK; + return NODE_COLORS[t] ?? NODE_COLOR_FALLBACK; +} + +// --------------------------------------------------------------------------- +// Trust → node radius. +// --------------------------------------------------------------------------- + +/** Minimum circle radius at trust=0. */ +export const NODE_RADIUS_MIN = 5; +/** Additional radius at trust=1. `r = 5 + trust * 9`, so r ∈ [5, 14]. */ +export const NODE_RADIUS_RANGE = 9; + +/** + * Clamp `trust` to [0,1] before mapping to a radius so a bad FSRS value + * can't produce a sub-zero or oversize node. Non-finite values collapse + * to 0 (smallest radius — visually suppresses suspicious data). + */ +export function nodeRadius(trust: number): number { + if (!Number.isFinite(trust)) return NODE_RADIUS_MIN; + const t = trust < 0 ? 0 : trust > 1 ? 1 : trust; + return NODE_RADIUS_MIN + t * NODE_RADIUS_RANGE; +} + +/** Clamp trust to [0,1]. NaN/Infinity/undefined → 0. */ +export function clampTrust(trust: number | null | undefined): number { + if (trust === null || trust === undefined || !Number.isFinite(trust)) return 0; + if (trust < 0) return 0; + if (trust > 1) return 1; + return trust; +} + +// --------------------------------------------------------------------------- +// Focus / pair opacity. +// --------------------------------------------------------------------------- + +/** Opacity applied to a non-focused pair when any pair is focused. */ +export const UNFOCUSED_OPACITY = 0.12; + +/** + * Opacity for a pair given the current focus state. + * + * focus = null/undefined → 1 (nothing dimmed) + * focus === pairIndex → 1 (the focused pair is fully lit) + * focus !== pairIndex → 0.12 (dimmed) + * + * A focus index that doesn't match any rendered pair simply dims everything. + * That's the intended "silent no-op" for a stale focusedPairIndex. + */ +export function pairOpacity(pairIndex: number, focusedPairIndex: number | null | undefined): number { + if (focusedPairIndex === null || focusedPairIndex === undefined) return 1; + return focusedPairIndex === pairIndex ? 1 : UNFOCUSED_OPACITY; +} + +// --------------------------------------------------------------------------- +// Text truncation. +// --------------------------------------------------------------------------- + +/** + * Truncate a string to `max` characters with an ellipsis at the end. + * Shorter-or-equal strings return unchanged. Empty strings return unchanged. + * Non-string inputs collapse to '' rather than crashing. + * + * The ellipsis counts toward the length budget, so the cut-off content is + * `max - 1` characters, matching the component's inline truncate() helper. + */ +export function truncate(s: string | null | undefined, max = 60): string { + if (s === null || s === undefined) return ''; + if (typeof s !== 'string') return ''; + if (max <= 0) return ''; + if (s.length <= max) return s; + return s.slice(0, max - 1) + '…'; +} + +// --------------------------------------------------------------------------- +// Stats. +// --------------------------------------------------------------------------- + +/** + * Count unique memory IDs across a list of contradiction pairs. Each pair + * contributes memory_a_id and memory_b_id. Duplicates (e.g. one memory that + * appears in multiple conflicts) are counted once. + */ +export function uniqueMemoryCount(pairs: readonly ContradictionLike[]): number { + if (!pairs || pairs.length === 0) return 0; + const set = new Set(); + for (const p of pairs) { + if (p.memory_a_id) set.add(p.memory_a_id); + if (p.memory_b_id) set.add(p.memory_b_id); + } + return set.size; +} + +/** + * Average absolute trust delta across pairs. Returns 0 on empty input so + * the UI can render `0.00` instead of `NaN`. + */ +export function avgTrustDelta( + pairs: readonly { trust_a: number; trust_b: number }[], +): number { + if (!pairs || pairs.length === 0) return 0; + let sum = 0; + for (const p of pairs) { + sum += Math.abs((p.trust_a ?? 0) - (p.trust_b ?? 0)); + } + return sum / pairs.length; +} diff --git a/apps/dashboard/src/lib/components/dream-helpers.ts b/apps/dashboard/src/lib/components/dream-helpers.ts new file mode 100644 index 0000000..b740af5 --- /dev/null +++ b/apps/dashboard/src/lib/components/dream-helpers.ts @@ -0,0 +1,155 @@ +/** + * dream-helpers — Pure logic for Dream Cinema UI. + * + * Extracted so we can test it without jsdom / Svelte component harnessing. + * The Vitest setup for this package runs in a Node environment; every helper + * in this module is a pure function of its inputs, so it can be exercised + * directly in `__tests__/*.test.ts` alongside the graph helpers. + */ + +/** Stage 1..5 of the 5-phase consolidation cycle. */ +export const STAGE_COUNT = 5 as const; + +/** Display names for each stage index (1-indexed). */ +export const STAGE_NAMES = [ + 'Replay', + 'Cross-reference', + 'Strengthen', + 'Prune', + 'Transfer', +] as const; + +export type StageIndex = 1 | 2 | 3 | 4 | 5; + +/** + * Clamp an arbitrary integer to the valid 1..5 stage range. Accepts any + * number (NaN, Infinity, negatives, floats) and always returns an integer + * in [1,5]. NaN and non-finite values fall back to 1 — this matches the + * "start at stage 1" behaviour on a fresh dream. + */ +export function clampStage(n: number): StageIndex { + if (!Number.isFinite(n)) return 1; + const i = Math.floor(n); + if (i < 1) return 1; + if (i > STAGE_COUNT) return STAGE_COUNT; + return i as StageIndex; +} + +/** + * Get the human-readable stage name for a (possibly invalid) stage number. + * Uses `clampStage`, so out-of-range inputs return the nearest valid name. + */ +export function stageName(n: number): string { + return STAGE_NAMES[clampStage(n) - 1]; +} + +// --------------------------------------------------------------------------- +// Novelty classification — drives the gold-glow / muted styling on insight +// cards. Thresholds are STRICTLY exclusive so `0.3` and `0.7` map to the +// neutral band on purpose. See DreamInsightCard.svelte. +// --------------------------------------------------------------------------- + +export type NoveltyBand = 'high' | 'neutral' | 'low'; + +/** Upper bound for the muted "low novelty" band. Values BELOW this are low. */ +export const LOW_NOVELTY_THRESHOLD = 0.3; +/** Lower bound for the gold "high novelty" band. Values ABOVE this are high. */ +export const HIGH_NOVELTY_THRESHOLD = 0.7; + +/** + * Classify a novelty score into one of 3 visual bands. + * + * Thresholds are exclusive on both sides: + * novelty > 0.7 → 'high' (gold glow) + * novelty < 0.3 → 'low' (muted / desaturated) + * otherwise → 'neutral' + * + * `null` / `undefined` / `NaN` collapse to 0 → 'low'. + */ +export function noveltyBand(novelty: number | null | undefined): NoveltyBand { + const n = clamp01(novelty); + if (n > HIGH_NOVELTY_THRESHOLD) return 'high'; + if (n < LOW_NOVELTY_THRESHOLD) return 'low'; + return 'neutral'; +} + +/** Clamp a value into [0,1]. `null`/`undefined`/`NaN` → 0. */ +export function clamp01(n: number | null | undefined): number { + if (n === null || n === undefined || !Number.isFinite(n)) return 0; + if (n < 0) return 0; + if (n > 1) return 1; + return n; +} + +// --------------------------------------------------------------------------- +// Formatting helpers — mirror what the page + card render. Keeping these +// pure lets us test the exact output strings without rendering Svelte. +// --------------------------------------------------------------------------- + +/** + * Format a millisecond duration as a human-readable string. + * < 1000ms → "{n}ms" (e.g. "0ms", "500ms") + * ≥ 1000ms → "{n.nn}s" (e.g. "1.50s", "15.00s") + * Negative / NaN values collapse to "0ms". + */ +export function formatDurationMs(ms: number | null | undefined): string { + if (ms === null || ms === undefined || !Number.isFinite(ms) || ms < 0) { + return '0ms'; + } + if (ms < 1000) return `${Math.round(ms)}ms`; + return `${(ms / 1000).toFixed(2)}s`; +} + +/** + * Format a 0..1 confidence as a whole-percent string ("0%", "50%", "100%"). + * Values outside [0,1] clamp first. Uses `Math.round` so 0.505 → "51%". + */ +export function formatConfidencePct(confidence: number | null | undefined): string { + const c = clamp01(confidence); + return `${Math.round(c * 100)}%`; +} + +// --------------------------------------------------------------------------- +// Source memory link formatting. +// --------------------------------------------------------------------------- + +/** + * Build the href for a source memory link. We keep this behind a helper so + * the route format is tested in one place. `base` corresponds to SvelteKit's + * `$app/paths` base (may be ""). Invalid IDs still produce a URL — route + * handling is the page's responsibility, not ours. + */ +export function sourceMemoryHref(id: string, base = ''): string { + return `${base}/memories/${id}`; +} + +/** + * Return the first N source memory IDs from an insight's `sourceMemories` + * array, safely handling null / undefined / empty. Default N = 2, matching + * the card's "first 2 links" behaviour. + */ +export function firstSourceIds( + sources: readonly string[] | null | undefined, + n = 2, +): string[] { + if (!sources || sources.length === 0) return []; + return sources.slice(0, Math.max(0, n)); +} + +/** Count of sources beyond the first N. Used for the "(+N)" suffix. */ +export function extraSourceCount( + sources: readonly string[] | null | undefined, + shown = 2, +): number { + if (!sources) return 0; + return Math.max(0, sources.length - shown); +} + +/** + * Truncate a memory UUID for display on the chip. Matches the previous + * inline `shortId` logic: first 8 chars, or the whole string if shorter. + */ +export function shortMemoryId(id: string): string { + if (!id) return ''; + return id.length > 8 ? id.slice(0, 8) : id; +} diff --git a/apps/dashboard/src/lib/components/duplicates-helpers.ts b/apps/dashboard/src/lib/components/duplicates-helpers.ts new file mode 100644 index 0000000..3bbe0ed --- /dev/null +++ b/apps/dashboard/src/lib/components/duplicates-helpers.ts @@ -0,0 +1,149 @@ +/** + * Pure helpers for the Memory Hygiene / Duplicate Detection UI. + * + * Extracted from DuplicateCluster.svelte + duplicates/+page.svelte so the + * logic can be unit tested in the vitest `node` environment without jsdom. + * + * Contracts + * --------- + * - `similarityBand`: fixed thresholds at 0.92 (near-identical) and 0.80 + * (strong). Boundary values MATCH the higher band (>= semantics). + * - `pickWinner`: highest retention wins. Ties broken by earliest index + * (stable). Returns `null` on empty input — callers must guard. + * - `suggestedActionFor`: >= 0.92 → 'merge', < 0.85 → 'review'. The 0.85..0.92 + * corridor follows the upstream `suggestedAction` field from the MCP tool, + * so we only override the obvious cases. Default for the corridor is + * whatever the caller already had — this function returns null to signal + * "caller decides." + * - `filterByThreshold`: strict `>=` against the provided similarity. + * - `clusterKey`: stable identity across re-fetches — sorted member ids + * joined. Survives threshold changes that keep the same cluster members. + */ + +export type SimilarityBand = 'near-identical' | 'strong' | 'weak'; +export type SuggestedAction = 'merge' | 'review'; + +export interface ClusterMemoryLike { + id: string; + retention: number; + tags?: string[]; + createdAt?: string; +} + +export interface ClusterLike { + similarity: number; + memories: M[]; +} + +/** Color bands. Boundary at 0.92 → red. Boundary at 0.80 → amber. */ +export function similarityBand(similarity: number): SimilarityBand { + if (similarity >= 0.92) return 'near-identical'; + if (similarity >= 0.8) return 'strong'; + return 'weak'; +} + +export function similarityBandColor(similarity: number): string { + const band = similarityBand(similarity); + if (band === 'near-identical') return 'var(--color-decay)'; + if (band === 'strong') return 'var(--color-warning)'; + return '#fde047'; // yellow-300 — distinct from amber warning +} + +export function similarityBandLabel(similarity: number): string { + const band = similarityBand(similarity); + if (band === 'near-identical') return 'Near-identical'; + if (band === 'strong') return 'Strong match'; + return 'Weak match'; +} + +/** Retention color dot. Matches the traffic-light scheme. */ +export function retentionColor(retention: number): string { + if (retention > 0.7) return '#10b981'; + if (retention > 0.4) return '#f59e0b'; + return '#ef4444'; +} + +/** + * Pick the highest-retention memory. Stable tie-break: earliest wins. + * Returns `null` if the cluster is empty. Treats non-finite retention as + * -Infinity so a `retention=NaN` row never claims the throne. + */ +export function pickWinner(memories: M[]): M | null { + if (!memories || memories.length === 0) return null; + let best = memories[0]; + let bestScore = Number.isFinite(best.retention) ? best.retention : -Infinity; + for (let i = 1; i < memories.length; i++) { + const m = memories[i]; + const s = Number.isFinite(m.retention) ? m.retention : -Infinity; + if (s > bestScore) { + best = m; + bestScore = s; + } + } + return best; +} + +/** + * Suggested action inference. Returns null in the ambiguous 0.85..0.92 band + * so callers can honor an upstream suggestion from the backend. + */ +export function suggestedActionFor(similarity: number): SuggestedAction | null { + if (similarity >= 0.92) return 'merge'; + if (similarity < 0.85) return 'review'; + return null; +} + +/** + * Filter clusters by the >= threshold contract. Separate pure function so the + * mock fetch and any future real fetch both get the same semantics. + */ +export function filterByThreshold(clusters: C[], threshold: number): C[] { + return clusters.filter((c) => c.similarity >= threshold); +} + +/** + * Stable identity across re-fetches. Uses sorted member ids, so a cluster + * that loses/gains a member gets a new key (intentional — the cluster has + * changed). If you dismissed cluster [A,B,C] at 0.80 and refetch at 0.70 + * and it now contains [A,B,C,D], it reappears — correct behaviour: a new + * member deserves fresh attention. + */ +export function clusterKey(memories: M[]): string { + return memories + .map((m) => m.id) + .slice() + .sort() + .join('|'); +} + +/** + * Safe content preview — trims, collapses whitespace, truncates at 80 chars + * with an ellipsis. Null-safe. + */ +export function previewContent(content: string | null | undefined, max: number = 80): string { + if (!content) return ''; + const trimmed = content.trim().replace(/\s+/g, ' '); + return trimmed.length <= max ? trimmed : trimmed.slice(0, max) + '…'; +} + +/** + * Render an ISO date string safely — returns an empty string for missing, + * non-string, or invalid input so the DOM shows nothing rather than + * "Invalid Date". + */ +export function formatDate(iso: string | null | undefined): string { + if (!iso || typeof iso !== 'string') return ''; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return ''; + return d.toLocaleDateString(undefined, { + year: 'numeric', + month: 'short', + day: 'numeric', + }); +} + +/** Safe tag slice — tolerates undefined or non-array inputs. */ +export function safeTags(tags: string[] | null | undefined, limit: number = 4): string[] { + if (!Array.isArray(tags)) return []; + return tags.slice(0, limit); +} diff --git a/apps/dashboard/src/lib/components/importance-helpers.ts b/apps/dashboard/src/lib/components/importance-helpers.ts new file mode 100644 index 0000000..8516fa5 --- /dev/null +++ b/apps/dashboard/src/lib/components/importance-helpers.ts @@ -0,0 +1,226 @@ +/** + * importance-helpers — Pure logic for the Importance Radar UI + * (importance/+page.svelte + ImportanceRadar.svelte). + * + * Extracted so the radar geometry and importance-proxy maths can be unit- + * tested in the vitest `node` environment without jsdom or Svelte harness. + * + * Contracts + * --------- + * - Backend channel weights (novelty 0.25, arousal 0.30, reward 0.25, + * attention 0.20) sum to 1.0 and mirror ImportanceSignals in vestige-core. + * - `clamp01` folds NaN/Infinity/nullish → 0 and clips [0,1]. + * - `radarVertices` emits 4 SVG polygon points in the fixed axis order + * Novelty (top) → Arousal (right) → Reward (bottom) → Attention (left). + * A zero value places the vertex at centre; a one value places it at the + * unit-ring edge. + * - `importanceProxy` is the SAME formula the page uses to rank the weekly + * list: retentionStrength × log1p(reviews + 1) / sqrt(max(1, ageDays)). + * Age is clamped to 1 so a freshly-created memory never divides by zero. + * - `sizePreset` maps 'sm'|'md'|'lg' to 80|180|320 and defaults to 'md' for + * any unknown size key — matching the component's default prop. + */ + +// -- Channel model ---------------------------------------------------------- + +export type ChannelKey = 'novelty' | 'arousal' | 'reward' | 'attention'; + +/** Weights applied server-side by ImportanceSignals. Must sum to 1.0. */ +export const CHANNEL_WEIGHTS: Readonly> = { + novelty: 0.25, + arousal: 0.3, + reward: 0.25, + attention: 0.2, +} as const; + +export interface Channels { + novelty: number; + arousal: number; + reward: number; + attention: number; +} + +/** Clamp a value to [0,1]. Null / undefined / NaN / Infinity → 0. */ +export function clamp01(v: number | null | undefined): number { + if (v === null || v === undefined) return 0; + if (!Number.isFinite(v)) return 0; + if (v < 0) return 0; + if (v > 1) return 1; + return v; +} + +/** Clamp every channel to [0,1]. Safe for partial / malformed inputs. */ +export function clampChannels(ch: Partial | null | undefined): Channels { + return { + novelty: clamp01(ch?.novelty), + arousal: clamp01(ch?.arousal), + reward: clamp01(ch?.reward), + attention: clamp01(ch?.attention), + }; +} + +/** + * Composite importance score — matches backend ImportanceSignals. + * + * composite = 0.25·novelty + 0.30·arousal + 0.25·reward + 0.20·attention + * + * Every input is clamped first so out-of-range channels never puncture the + * 0..1 composite range. The return value is guaranteed to be in [0,1]. + */ +export function compositeScore(ch: Partial | null | undefined): number { + const c = clampChannels(ch); + return ( + c.novelty * CHANNEL_WEIGHTS.novelty + + c.arousal * CHANNEL_WEIGHTS.arousal + + c.reward * CHANNEL_WEIGHTS.reward + + c.attention * CHANNEL_WEIGHTS.attention + ); +} + +// -- Size preset ------------------------------------------------------------ + +export type RadarSize = 'sm' | 'md' | 'lg'; + +export const SIZE_PX: Readonly> = { + sm: 80, + md: 180, + lg: 320, +} as const; + +/** + * Resolve a size preset key to its px value. Unknown / missing keys fall + * back to 'md' (180), matching the component's default prop. `sm` loses + * axis labels in the renderer but that's rendering concern, not ours. + */ +export function sizePreset(size: RadarSize | string | undefined): number { + if (size && (size === 'sm' || size === 'md' || size === 'lg')) { + return SIZE_PX[size]; + } + return SIZE_PX.md; +} + +// -- Geometry --------------------------------------------------------------- + +/** + * Fixed axis order. Angles use SVG conventions (y grows downward): + * Novelty → angle -π/2 (top) + * Arousal → angle 0 (right) + * Reward → angle π/2 (bottom) + * Attention → angle π (left) + */ +export const AXIS_ORDER: ReadonlyArray<{ key: ChannelKey; angle: number }> = [ + { key: 'novelty', angle: -Math.PI / 2 }, + { key: 'arousal', angle: 0 }, + { key: 'reward', angle: Math.PI / 2 }, + { key: 'attention', angle: Math.PI }, +] as const; + +export interface RadarPoint { + x: number; + y: number; +} + +/** + * Compute the effective drawable radius inside the SVG box. This mirrors the + * component's padding logic: + * sm → padding 4 (edge-to-edge, no labels) + * md → padding 28 + * lg → padding 44 + * Radius = size/2 − padding, floored at 0 (a radius below zero would draw + * an inverted polygon — defensive guard). + */ +export function radarRadius(size: RadarSize | string | undefined): number { + const px = sizePreset(size); + let padding: number; + switch (size) { + case 'lg': + padding = 44; + break; + case 'sm': + padding = 4; + break; + default: + padding = 28; + } + return Math.max(0, px / 2 - padding); +} + +/** + * Compute the 4 SVG polygon vertices for a set of channel values at a given + * radar size. Values are clamped to [0,1] first so out-of-range inputs can't + * escape the radar bounds. + * + * Ordering is FIXED and matches AXIS_ORDER: [novelty, arousal, reward, attention]. + * A zero value places the vertex at the centre (cx, cy); a one value places + * it at the unit-ring edge. + */ +export function radarVertices( + ch: Partial | null | undefined, + size: RadarSize | string | undefined = 'md', +): RadarPoint[] { + const px = sizePreset(size); + const r = radarRadius(size); + const cx = px / 2; + const cy = px / 2; + const values = clampChannels(ch); + return AXIS_ORDER.map(({ key, angle }) => { + const v = values[key]; + return { + x: cx + Math.cos(angle) * v * r, + y: cy + Math.sin(angle) * v * r, + }; + }); +} + +/** Serialise vertices to an SVG "M…L…L…L… Z" path, 2-decimal precision. */ +export function verticesToPath(points: RadarPoint[]): string { + if (points.length === 0) return ''; + return ( + points + .map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(2)},${p.y.toFixed(2)}`) + .join(' ') + ' Z' + ); +} + +// -- Trending-memory proxy -------------------------------------------------- + +export interface ProxyMemoryLike { + retentionStrength: number; + reviewCount?: number | null; + createdAt: string; +} + +/** + * Proxy score for the "Top Important Memories This Week" ranking. Exact + * formula from importance/+page.svelte: + * + * ageDays = max(1, (now - createdAt) / 86_400_000) + * reviews = reviewCount ?? 0 + * recencyBoost = 1 / sqrt(ageDays) + * proxy = retentionStrength × log1p(reviews + 1) × recencyBoost + * + * Edge cases: + * - createdAt is the current instant → ageDays clamps to 1 (no div-by-0) + * - createdAt is in the future → negative age also clamps to 1 + * - reviewCount null/undefined → treated as 0 + * - non-finite retentionStrength → returns 0 defensively + * + * `now` is injectable for deterministic tests. Defaults to `Date.now()`. + */ +export function importanceProxy(m: ProxyMemoryLike, now: number = Date.now()): number { + if (!m || !Number.isFinite(m.retentionStrength)) return 0; + const created = new Date(m.createdAt).getTime(); + if (!Number.isFinite(created)) return 0; + const ageDays = Math.max(1, (now - created) / 86_400_000); + const reviews = m.reviewCount ?? 0; + const recencyBoost = 1 / Math.sqrt(ageDays); + return m.retentionStrength * Math.log1p(reviews + 1) * recencyBoost; +} + +/** Sort memories by the proxy, descending. Stable via `.sort` on a copy. */ +export function rankByProxy( + memories: readonly M[], + now: number = Date.now(), +): M[] { + return memories.slice().sort((a, b) => importanceProxy(b, now) - importanceProxy(a, now)); +} diff --git a/apps/dashboard/src/lib/components/patterns-helpers.ts b/apps/dashboard/src/lib/components/patterns-helpers.ts new file mode 100644 index 0000000..ac498de --- /dev/null +++ b/apps/dashboard/src/lib/components/patterns-helpers.ts @@ -0,0 +1,178 @@ +/** + * patterns-helpers — Pure logic for the Cross-Project Intelligence UI + * (patterns/+page.svelte + PatternTransferHeatmap.svelte). + * + * Extracted so the behaviour can be unit-tested in the vitest `node` + * environment without jsdom or Svelte component harnessing. Every helper + * in this module is a pure function of its inputs. + * + * Contracts + * --------- + * - `cellIntensity`: returns opacity in [0,1] from count / max. count=0 → 0, + * count>=max → 1. `max<=0` collapses to 0 (avoids div-by-zero — the + * component uses `max || 1` for the same reason). + * - `filterByCategory`: 'All' passes every pattern through. An unknown + * category string (not one of the 6 + 'All') returns an empty array — + * there is no hidden alias fallback. + * - `buildTransferMatrix`: directional. `matrix[origin][dest]` counts how + * many patterns originated in `origin` and were transferred to `dest`. + * `origin === dest` captures self-transfer (a project reusing its own + * pattern — rare but real per the component's doc comment). + */ + +export const PATTERN_CATEGORIES = [ + 'ErrorHandling', + 'AsyncConcurrency', + 'Testing', + 'Architecture', + 'Performance', + 'Security', +] as const; + +export type PatternCategory = (typeof PATTERN_CATEGORIES)[number]; +export type CategoryFilter = 'All' | PatternCategory; + +export interface TransferPatternLike { + name: string; + category: PatternCategory; + origin_project: string; + transferred_to: string[]; + transfer_count: number; +} + +/** + * Normalise a raw transfer count to a 0..1 opacity/intensity value against a + * known max. Used by the heatmap cell colour ramp. + * + * count <= 0 → 0 (dead cell) + * count >= max > 0 → 1 (hottest cell) + * otherwise → count / max + * + * Non-finite / negative inputs collapse to 0. When `max <= 0` the result is + * always 0 — the component's own guard (`maxCount || 1`) means this branch + * is unreachable in practice, but defensive anyway. + */ +export function cellIntensity(count: number, max: number): number { + if (!Number.isFinite(count) || count <= 0) return 0; + if (!Number.isFinite(max) || max <= 0) return 0; + if (count >= max) return 1; + return count / max; +} + +/** + * Filter a pattern list by the active category tab. + * 'All' → full pass-through (same reference-equal array is + * NOT guaranteed; callers must not rely on identity) + * one of the 6 enums → strict equality on `category` + * unknown string → empty array (no silent alias; caller bug) + */ +export function filterByCategory

          ( + patterns: readonly P[], + category: CategoryFilter | string, +): P[] { + if (category === 'All') return patterns.slice(); + if (!(PATTERN_CATEGORIES as readonly string[]).includes(category)) { + return []; + } + return patterns.filter((p) => p.category === category); +} + +/** Cell in the directional N×N transfer matrix. */ +export interface TransferCell { + count: number; + topNames: string[]; +} + +/** Dense row-major directional matrix: matrix[origin][destination]. */ +export type TransferMatrix = Record>; + +/** + * Build the directional transfer matrix from patterns + the known projects + * axis. Mirrors `PatternTransferHeatmap.svelte`'s `$derived` logic. + * + * - Every (from, to) pair in `projects × projects` gets a zero cell. + * - Each pattern P contributes `+1` to `matrix[P.origin][dest]` for every + * `dest` in `P.transferred_to` that also appears in `projects`. + * - Patterns whose origin isn't in `projects` are silently skipped — that + * matches the component's `if (!m[from]) continue` guard. + * - `topNames` holds up to 3 pattern names per cell in insertion order. + */ +export function buildTransferMatrix( + projects: readonly string[], + patterns: readonly TransferPatternLike[], + topNameCap = 3, +): TransferMatrix { + const m: TransferMatrix = {}; + for (const from of projects) { + m[from] = {}; + for (const to of projects) { + m[from][to] = { count: 0, topNames: [] }; + } + } + for (const p of patterns) { + const from = p.origin_project; + if (!m[from]) continue; + for (const to of p.transferred_to) { + if (!m[from][to]) continue; + m[from][to].count += 1; + m[from][to].topNames.push(p.name); + } + } + const cap = Math.max(0, topNameCap); + for (const from of projects) { + for (const to of projects) { + m[from][to].topNames = m[from][to].topNames.slice(0, cap); + } + } + return m; +} + +/** + * Maximum single-cell transfer count across the matrix. Floors at 0 for an + * empty matrix, which callers should treat as "scale by 1" to avoid a div- + * by-zero in the colour ramp. + */ +export function matrixMaxCount( + projects: readonly string[], + matrix: TransferMatrix, +): number { + let max = 0; + for (const from of projects) { + const row = matrix[from]; + if (!row) continue; + for (const to of projects) { + const cell = row[to]; + if (cell && cell.count > max) max = cell.count; + } + } + return max; +} + +/** + * Flatten a matrix into sorted-desc rows for the mobile fallback. Only + * non-zero pairs are emitted, matching the component. + */ +export function flattenNonZero( + projects: readonly string[], + matrix: TransferMatrix, +): Array<{ from: string; to: string; count: number; topNames: string[] }> { + const rows: Array<{ from: string; to: string; count: number; topNames: string[] }> = []; + for (const from of projects) { + for (const to of projects) { + const cell = matrix[from]?.[to]; + if (cell && cell.count > 0) { + rows.push({ from, to, count: cell.count, topNames: cell.topNames }); + } + } + } + return rows.sort((a, b) => b.count - a.count); +} + +/** + * Truncate long project names for axis labels. Match the component's + * `shortProject` behaviour: keep ≤12 chars, otherwise 11-char prefix + ellipsis. + */ +export function shortProjectName(name: string): string { + if (!name) return ''; + return name.length > 12 ? name.slice(0, 11) + '…' : name; +} diff --git a/apps/dashboard/src/lib/components/reasoning-helpers.ts b/apps/dashboard/src/lib/components/reasoning-helpers.ts new file mode 100644 index 0000000..83b90d1 --- /dev/null +++ b/apps/dashboard/src/lib/components/reasoning-helpers.ts @@ -0,0 +1,229 @@ +/** + * reasoning-helpers — Pure logic for the Reasoning Theater UI. + * + * Extracted so we can test it without jsdom / Svelte component harnessing. + * The Vitest setup for this package runs in a Node environment; every helper + * in this module is a pure function of its inputs, so it can be exercised + * directly in `__tests__/*.test.ts` alongside the graph helpers. + */ +import { NODE_TYPE_COLORS } from '$types'; + +// ──────────────────────────────────────────────────────────────── +// Shared palette — keep in sync with Tailwind @theme values. +// ──────────────────────────────────────────────────────────────── + +export const CONFIDENCE_EMERALD = '#10b981'; +export const CONFIDENCE_AMBER = '#f59e0b'; +export const CONFIDENCE_RED = '#ef4444'; + +/** Fallback colour when a node-type has no mapping. */ +export const DEFAULT_NODE_TYPE_COLOR = '#8B95A5'; + +// ──────────────────────────────────────────────────────────────── +// Roles +// ──────────────────────────────────────────────────────────────── + +export type EvidenceRole = 'primary' | 'supporting' | 'contradicting' | 'superseded'; + +export interface RoleMeta { + label: string; + /** Tailwind / CSS colour token — see app.css. */ + accent: 'synapse' | 'recall' | 'decay' | 'muted'; + icon: string; +} + +export const ROLE_META: Record = { + primary: { label: 'Primary', accent: 'synapse', icon: '◈' }, + supporting: { label: 'Supporting', accent: 'recall', icon: '◇' }, + contradicting: { label: 'Contradicting', accent: 'decay', icon: '⚠' }, + superseded: { label: 'Superseded', accent: 'muted', icon: '⊘' }, +}; + +/** Look up role metadata with a defensive fallback. */ +export function roleMetaFor(role: EvidenceRole | string): RoleMeta { + return (ROLE_META as Record)[role] ?? ROLE_META.supporting; +} + +// ──────────────────────────────────────────────────────────────── +// Intent classification (deep_reference `intent` field) +// ──────────────────────────────────────────────────────────────── + +export type IntentKey = + | 'FactCheck' + | 'Timeline' + | 'RootCause' + | 'Comparison' + | 'Synthesis'; + +export interface IntentHint { + label: string; + icon: string; + description: string; +} + +export const INTENT_HINTS: Record = { + FactCheck: { + label: 'FactCheck', + icon: '◆', + description: 'Direct verification of a single claim.', + }, + Timeline: { + label: 'Timeline', + icon: '↗', + description: 'Ordered evolution of a fact over time.', + }, + RootCause: { + label: 'RootCause', + icon: '⚡', + description: 'Why did this happen — causal chain.', + }, + Comparison: { + label: 'Comparison', + icon: '⬡', + description: 'Contrasting two or more options side-by-side.', + }, + Synthesis: { + label: 'Synthesis', + icon: '❖', + description: 'Cross-memory composition into a new insight.', + }, +}; + +/** + * Map an arbitrary intent string to a hint. Unknown intents degrade to + * Synthesis, which is the most generic classification. + */ +export function intentHintFor(intent: string | undefined | null): IntentHint { + if (!intent) return INTENT_HINTS.Synthesis; + const key = intent as IntentKey; + return INTENT_HINTS[key] ?? INTENT_HINTS.Synthesis; +} + +// ──────────────────────────────────────────────────────────────── +// Confidence bands +// ──────────────────────────────────────────────────────────────── + +/** + * Confidence colour band. + * + * > 75 → emerald (HIGH) + * 40-75 → amber (MIXED) + * < 40 → red (LOW) + * + * Boundaries: 75 is amber (strictly greater than 75 is emerald), 40 is amber + * (>=40 is amber). Any non-finite input (NaN) is treated as lowest confidence + * and returns red. + */ +export function confidenceColor(c: number): string { + if (!Number.isFinite(c)) return CONFIDENCE_RED; + if (c > 75) return CONFIDENCE_EMERALD; + if (c >= 40) return CONFIDENCE_AMBER; + return CONFIDENCE_RED; +} + +/** Human-readable label for a confidence score (0-100). */ +export function confidenceLabel(c: number): string { + if (!Number.isFinite(c)) return 'LOW CONFIDENCE'; + if (c > 75) return 'HIGH CONFIDENCE'; + if (c >= 40) return 'MIXED SIGNAL'; + return 'LOW CONFIDENCE'; +} + +/** + * Convert a 0-1 trust score to the same confidence band. + * + * Thresholds: >0.75 emerald, 0.40-0.75 amber, <0.40 red. + * Matches `confidenceColor` semantics so the trust bar on an evidence card + * and the confidence meter on the page agree at the boundaries. + */ +export function trustColor(t: number): string { + if (!Number.isFinite(t)) return CONFIDENCE_RED; + return confidenceColor(t * 100); +} + +/** Clamp a trust score into the display range [0, 1]. */ +export function clampTrust(t: number): number { + if (!Number.isFinite(t)) return 0; + if (t < 0) return 0; + if (t > 1) return 1; + return t; +} + +/** Trust as a 0-100 percentage suitable for width / label rendering. */ +export function trustPercent(t: number): number { + return clampTrust(t) * 100; +} + +// ──────────────────────────────────────────────────────────────── +// Node-type colouring +// ──────────────────────────────────────────────────────────────── + +/** Resolve a node-type colour with a soft-steel fallback. */ +export function nodeTypeColor(nodeType?: string | null): string { + if (!nodeType) return DEFAULT_NODE_TYPE_COLOR; + return NODE_TYPE_COLORS[nodeType] ?? DEFAULT_NODE_TYPE_COLOR; +} + +// ──────────────────────────────────────────────────────────────── +// Date formatting +// ──────────────────────────────────────────────────────────────── + +/** + * Format an ISO date string for EvidenceCard display. + * + * Handles three failure modes that `new Date(str)` alone does not: + * 1. Empty / null / undefined → returns '—' + * 2. Unparseable string (NaN) → returns the original string unchanged + * 3. Non-ISO but parseable → best-effort locale format + * + * The previous try/catch-only approach silently rendered the literal text + * "Invalid Date" because `Date` never throws on bad input — it produces a + * valid object whose getTime() is NaN. + */ +export function formatDate( + iso: string | null | undefined, + locale?: string, +): string { + if (iso == null) return '—'; + if (typeof iso !== 'string' || iso.trim() === '') return '—'; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + try { + return d.toLocaleDateString(locale, { + month: 'short', + day: 'numeric', + year: 'numeric', + }); + } catch { + return iso; + } +} + +/** Compact month/day formatter for the evolution timeline. */ +export function formatShortDate( + iso: string | null | undefined, + locale?: string, +): string { + if (iso == null) return '—'; + if (typeof iso !== 'string' || iso.trim() === '') return '—'; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + try { + return d.toLocaleDateString(locale, { month: 'short', day: 'numeric' }); + } catch { + return iso; + } +} + +// ──────────────────────────────────────────────────────────────── +// Short-id for #abcdef01 style display +// ──────────────────────────────────────────────────────────────── + +/** + * Return the first 8 characters of an id, or the full string if shorter. + * Never throws on null/undefined — returns '' so the caller can render '#'. + */ +export function shortenId(id: string | null | undefined, length = 8): string { + if (!id) return ''; + return id.length > length ? id.slice(0, length) : id; +} diff --git a/apps/dashboard/src/lib/components/schedule-helpers.ts b/apps/dashboard/src/lib/components/schedule-helpers.ts new file mode 100644 index 0000000..93ca985 --- /dev/null +++ b/apps/dashboard/src/lib/components/schedule-helpers.ts @@ -0,0 +1,161 @@ +/** + * Pure helpers for the FSRS review schedule page + calendar. + * + * Extracted from `FSRSCalendar.svelte` and `routes/(app)/schedule/+page.svelte` + * so that bucket / grid / urgency / retention math can be tested in isolation + * (vitest `environment: node`, no jsdom required). + */ +import type { Memory } from '$types'; + +export const MS_DAY = 24 * 60 * 60 * 1000; + +/** + * Zero-out the time component of a date, returning a NEW Date at local + * midnight. Used for day-granular bucketing so comparisons are stable across + * any hour-of-day the user loads the page. + */ +export function startOfDay(d: Date | string): Date { + const x = typeof d === 'string' ? new Date(d) : new Date(d); + x.setHours(0, 0, 0, 0); + return x; +} + +/** + * Signed integer count of whole local days between two timestamps, normalized + * to midnight. Positive means `a` is in the future relative to `b`, negative + * means `a` is in the past. Zero means same calendar day. + */ +export function daysBetween(a: Date, b: Date): number { + return Math.floor((startOfDay(a).getTime() - startOfDay(b).getTime()) / MS_DAY); +} + +/** YYYY-MM-DD in LOCAL time (not UTC) so calendar cells align with user's day. */ +export function isoDate(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${y}-${m}-${day}`; +} + +/** + * Urgency bucket for a review date relative to "now". Used by the right-hand + * list and the calendar cell color. Day-granular (not hour-granular) so a + * memory due at 23:59 today does not suddenly become "in 1d" at 00:01 + * tomorrow UX-wise — it becomes "overdue" cleanly at midnight. + * + * - `none` — no valid `nextReviewAt` + * - `overdue` — due date's calendar day is strictly before today + * - `today` — due date's calendar day is today + * - `week` — due in 1..=7 whole days + * - `future` — due in 8+ whole days + */ +export type Urgency = 'none' | 'overdue' | 'today' | 'week' | 'future'; + +export function classifyUrgency(now: Date, nextReviewAt: string | null | undefined): Urgency { + if (!nextReviewAt) return 'none'; + const d = new Date(nextReviewAt); + if (Number.isNaN(d.getTime())) return 'none'; + const delta = daysBetween(d, now); + if (delta < 0) return 'overdue'; + if (delta === 0) return 'today'; + if (delta <= 7) return 'week'; + return 'future'; +} + +/** + * Signed whole-day count from today → due date. Negative means overdue by + * |n| days; zero means today; positive means n days out. Returns `null` + * if the ISO string is invalid or missing. + */ +export function daysUntilReview(now: Date, nextReviewAt: string | null | undefined): number | null { + if (!nextReviewAt) return null; + const d = new Date(nextReviewAt); + if (Number.isNaN(d.getTime())) return null; + return daysBetween(d, now); +} + +/** + * The [start, end) window for the week containing `d`, starting Sunday at + * local midnight. End is the following Sunday at local midnight — exclusive. + */ +export function weekBucketRange(d: Date): { start: Date; end: Date } { + const start = startOfDay(d); + start.setDate(start.getDate() - start.getDay()); // back to Sunday + const end = new Date(start); + end.setDate(end.getDate() + 7); + return { start, end }; +} + +/** + * Mean retention strength across a list of memories. Returns 0 for an empty + * list (never NaN) so the sidebar can safely render "0%". + */ +export function avgRetention(memories: Memory[]): number { + if (memories.length === 0) return 0; + let sum = 0; + for (const m of memories) sum += m.retentionStrength ?? 0; + return sum / memories.length; +} + +/** + * Given a day-index `i` into a 42-cell calendar grid (6 rows × 7 cols), return + * its row / column. The grid is laid out row-major: cell 0 = row 0 col 0, + * cell 7 = row 1 col 0, cell 41 = row 5 col 6. Returns `null` for indices + * outside `[0, 42)`. + */ +export function gridCellPosition(i: number): { row: number; col: number } | null { + if (!Number.isInteger(i) || i < 0 || i >= 42) return null; + return { row: Math.floor(i / 7), col: i % 7 }; +} + +/** + * The inverse: given a calendar anchor date (today), compute the Sunday + * at-or-before `anchor - 14 days` that seeds row 0 of the 6×7 grid. Pure, + * deterministic, local-time. + */ +export function gridStartForAnchor(anchor: Date): Date { + const base = startOfDay(anchor); + base.setDate(base.getDate() - 14); + base.setDate(base.getDate() - base.getDay()); // back to Sunday + return base; +} + +/** + * Bucket counts used by the sidebar stats block. Day-granular, consistent + * with `classifyUrgency`. + */ +export interface ScheduleStats { + overdue: number; + dueToday: number; + dueThisWeek: number; + dueThisMonth: number; + avgDays: number; +} + +export function computeScheduleStats(now: Date, scheduled: Memory[]): ScheduleStats { + let overdue = 0; + let dueToday = 0; + let dueThisWeek = 0; + let dueThisMonth = 0; + let sumDays = 0; + let futureCount = 0; + const today = startOfDay(now); + for (const m of scheduled) { + if (!m.nextReviewAt) continue; + const d = new Date(m.nextReviewAt); + if (Number.isNaN(d.getTime())) continue; + const delta = daysBetween(d, now); + if (delta < 0) overdue++; + if (delta <= 0) dueToday++; + if (delta <= 7) dueThisWeek++; + if (delta <= 30) dueThisMonth++; + if (delta >= 0) { + // Use hour-resolution days-until for the average so "due in 2.3 days" + // is informative even when bucketing is day-granular elsewhere. + sumDays += (d.getTime() - today.getTime()) / MS_DAY; + futureCount++; + } + } + const avgDays = futureCount > 0 ? sumDays / futureCount : 0; + return { overdue, dueToday, dueThisWeek, dueThisMonth, avgDays }; +} diff --git a/apps/dashboard/src/lib/graph/__tests__/color-mode.test.ts b/apps/dashboard/src/lib/graph/__tests__/color-mode.test.ts index 6eb1bbe..e59fb9d 100644 --- a/apps/dashboard/src/lib/graph/__tests__/color-mode.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/color-mode.test.ts @@ -16,7 +16,9 @@ vi.mock('three', async () => { import { NodeManager, getMemoryState, + getAhaGraphColor, getNodeColor, + AHAGRAPH_COLORS, MEMORY_STATE_COLORS, MEMORY_STATE_DESCRIPTIONS, type MemoryState, @@ -210,6 +212,31 @@ describe('getNodeColor — state mode', () => { }); }); +describe('getNodeColor — AhaGraph mode', () => { + it.each([ + [['ahagraph', 'aha'], AHAGRAPH_COLORS.aha], + [['ahagraph', 'confusion'], AHAGRAPH_COLORS.confusion], + [['ahagraph', 'weak-spot'], AHAGRAPH_COLORS.confusion], + [['ahagraph', 'failure'], AHAGRAPH_COLORS.failure], + [['ahagraph', 'guardrail'], AHAGRAPH_COLORS.failure], + ] as Array<[string[], string]>)('maps tags %j to %s', (tags, color) => { + const node = makeNode({ type: 'concept', tags }); + expect(getAhaGraphColor(node)).toBe(color); + expect(getNodeColor(node, 'ahagraph')).toBe(color); + }); + + it('prioritizes aha when a note also mentions confusion tags', () => { + const node = makeNode({ type: 'note', tags: ['ahagraph', 'aha', 'confusion'] }); + expect(getNodeColor(node, 'ahagraph')).toBe(AHAGRAPH_COLORS.aha); + }); + + it('falls back to node type when no AhaGraph learning tag is present', () => { + const node = makeNode({ type: 'event', tags: ['ahagraph'] }); + expect(getAhaGraphColor(node)).toBeNull(); + expect(getNodeColor(node, 'ahagraph')).toBe(NODE_TYPE_COLORS.event); + }); +}); + // ---------------------------------------------------------------------------- // NodeManager — default state + colorMode field // ---------------------------------------------------------------------------- @@ -236,6 +263,11 @@ describe('NodeManager — colorMode field', () => { expect(manager.colorMode).toBe('state'); }); + it('setColorMode("ahagraph") updates the field', () => { + manager.setColorMode('ahagraph'); + expect(manager.colorMode).toBe('ahagraph'); + }); + it('setColorMode("type") is no-op when already "type" (idempotent early return)', () => { // Spy on the meshMap iteration indirectly: if the early-return fires, // calling setColorMode on an empty manager still leaves us in 'type'. diff --git a/apps/dashboard/src/lib/graph/__tests__/effects.test.ts b/apps/dashboard/src/lib/graph/__tests__/effects.test.ts index 9c80986..e495046 100644 --- a/apps/dashboard/src/lib/graph/__tests__/effects.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/effects.test.ts @@ -497,4 +497,505 @@ describe('EffectManager', () => { expect(effects.pulseEffects.length).toBe(0); }); }); + + describe('createBirthOrb (v2.3 Memory Birth Ritual)', () => { + // Build a camera with a Quaternion for createBirthOrb's view-space + // projection. The three-mock's applyQuaternion is identity, so the + // start position collapses to `camera.position + (0, 0, -distance)`. + function makeCamera() { + return { + position: new Vector3(0, 30, 80), + quaternion: new (class { + x = 0; y = 0; z = 0; w = 1; + })(), + } as any; + } + + it('adds exactly 2 sprites to the scene on spawn', () => { + const cam = makeCamera(); + const baseline = scene.children.length; + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + expect(scene.children.length).toBe(baseline + 2); + }); + + it('both sprite and core use additive blending', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0xff8800) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + const halo = scene.children[0] as any; + const core = scene.children[1] as any; + // AdditiveBlending constant from three-mock is 2 + expect(halo.material.blending).toBe(2); + expect(core.material.blending).toBe(2); + // depthTest:false is passed to the SpriteMaterial constructor in + // effects.ts so the orb stays visible through other nodes. The + // three-mock's SpriteMaterial constructor does not persist this + // param, so we can't assert it at the instance level here; the + // production behavior is covered by ui-fixes.test.ts source grep. + expect(halo.material.transparent).toBe(true); + expect(core.material.transparent).toBe(true); + }); + + it('positions the orb at camera-relative cosmic center on spawn', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {}, + { distanceFromCamera: 40 } + ); + const halo = scene.children[0] as any; + const core = scene.children[1] as any; + // mock applyQuaternion is identity, so startPos = camera.pos + (0,0,-40) + expect(halo.position.x).toBeCloseTo(0); + expect(halo.position.y).toBeCloseTo(30); + expect(halo.position.z).toBeCloseTo(40); // 80 + (-40) + expect(core.position.x).toBeCloseTo(halo.position.x); + expect(core.position.y).toBeCloseTo(halo.position.y); + expect(core.position.z).toBeCloseTo(halo.position.z); + }); + + it('gestation phase: position stays at startPos for all 48 frames', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(100, 100, 100) as any, // far-away target + () => {} + ); + const halo = scene.children[0] as any; + const startX = halo.position.x; + const startY = halo.position.y; + const startZ = halo.position.z; + + for (let f = 0; f < 48; f++) { + effects.update(nodeMeshMap, cam); + expect(halo.position.x).toBeCloseTo(startX); + expect(halo.position.y).toBeCloseTo(startY); + expect(halo.position.z).toBeCloseTo(startZ); + } + }); + + it('gestation phase: opacity rises from 0 toward 0.95', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + const halo = scene.children[0] as any; + const core = scene.children[1] as any; + + // Spawn opacity + expect(halo.material.opacity).toBe(0); + expect(core.material.opacity).toBe(0); + + effects.update(nodeMeshMap, cam); // age 1 + const earlyHaloOp = halo.material.opacity; + expect(earlyHaloOp).toBeGreaterThan(0); + expect(earlyHaloOp).toBeLessThan(0.2); + + // Run to end of gestation + for (let f = 0; f < 47; f++) effects.update(nodeMeshMap, cam); + expect(halo.material.opacity).toBeCloseTo(0.95, 1); + expect(core.material.opacity).toBeCloseTo(1.0, 1); + // Monotonic-ish growth: late gestation > early gestation + expect(halo.material.opacity).toBeGreaterThan(earlyHaloOp); + }); + + it('gestation phase: sprite scale grows substantially', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + const halo = scene.children[0] as any; + + effects.update(nodeMeshMap, cam); // age 1 + const earlyScale = halo.scale.x; + + for (let f = 0; f < 47; f++) effects.update(nodeMeshMap, cam); // age 48 + const lateScale = halo.scale.x; + + // Halo grows from ~0.5 toward ~5 during gestation (with pulse variation). + expect(lateScale).toBeGreaterThan(earlyScale); + expect(lateScale).toBeGreaterThan(2); + }); + + it('gestation phase: halo color tints toward event color', () => { + const cam = makeCamera(); + const eventColor = new Color(0xff0000); // pure red + effects.createBirthOrb( + cam, + eventColor as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + const halo = scene.children[0] as any; + + effects.update(nodeMeshMap, cam); // age 1 — factor ≈ 0.72 + const earlyR = halo.material.color.r; + + for (let f = 0; f < 47; f++) effects.update(nodeMeshMap, cam); // age 48 — factor = 1.0 + const lateR = halo.material.color.r; + + // Red channel should approach the event color's red (1.0) from a dimmer value + expect(lateR).toBeGreaterThan(earlyR); + expect(lateR).toBeCloseTo(1.0, 1); + // Green/blue stay at 0 (event color is pure red) + expect(halo.material.color.g).toBeCloseTo(0); + expect(halo.material.color.b).toBeCloseTo(0); + }); + + it('flight phase: Bezier arc passes ABOVE the linear midpoint at t=0.5', () => { + const cam = makeCamera(); + // startPos = (0, 30, 40), target = (0, 0, 0) + // linear midpoint y = 15; control point y = 15 + 30 + dist*0.15 = 52.5 + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + const halo = scene.children[0] as any; + + // Drive past gestation (48) + half of flight (45) = 93 frames → t=0.5 + for (let f = 0; f < 93; f++) effects.update(nodeMeshMap, cam); + + // Linear midpoint y is 15; Bezier midpoint should be notably higher. + expect(halo.position.y).toBeGreaterThan(15); + // And not as high as the control point itself (52.5) — Bezier + // passes through midpoint-ish at t=0.5, biased upward by the arc. + expect(halo.position.y).toBeLessThan(52.5); + }); + + it('flight phase: orb moves from startPos toward target', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + const halo = scene.children[0] as any; + + // End of gestation + for (let f = 0; f < 48; f++) effects.update(nodeMeshMap, cam); + const gestZ = halo.position.z; + + // One tick into flight + effects.update(nodeMeshMap, cam); + const earlyFlightZ = halo.position.z; + + // Near end of flight + for (let f = 0; f < 88; f++) effects.update(nodeMeshMap, cam); + const lateFlightZ = halo.position.z; + + // Z moves from 40 toward 0 + expect(earlyFlightZ).toBeLessThan(gestZ); + expect(lateFlightZ).toBeLessThan(earlyFlightZ); + expect(lateFlightZ).toBeLessThan(5); // close to target z=0 + }); + + it('dynamic target tracking: changing getTargetPos mid-flight redirects the orb', () => { + const cam = makeCamera(); + let target = new Vector3(0, 0, 0); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => target as any, + () => {} + ); + const halo = scene.children[0] as any; + + // Drive to mid-flight (gestation 48 + 30 flight frames = 78) + for (let f = 0; f < 78; f++) effects.update(nodeMeshMap, cam); + const xBeforeRedirect = halo.position.x; + + // Redirect target far to the +X side + target = new Vector3(200, 0, 0); + + // A few more flight frames — orb should track the new target + for (let f = 0; f < 10; f++) effects.update(nodeMeshMap, cam); + const xAfterRedirect = halo.position.x; + + // With the original target at (0,0,0), x stays near 0 throughout. + // After redirect, x should swing toward the new target's +200. + expect(xAfterRedirect).toBeGreaterThan(xBeforeRedirect + 5); + }); + + it('onArrive fires exactly once at frame 139 (totalFrames + 1)', () => { + const cam = makeCamera(); + let arriveCount = 0; + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => { + arriveCount++; + } + ); + + // Drive through gestation (48) + flight (90) = 138 frames. Should NOT have fired. + for (let f = 0; f < 138; f++) effects.update(nodeMeshMap, cam); + expect(arriveCount).toBe(0); + + // Frame 139 — fires onArrive + effects.update(nodeMeshMap, cam); + expect(arriveCount).toBe(1); + + // Drive many more frames — must stay at 1 + for (let f = 0; f < 50; f++) effects.update(nodeMeshMap, cam); + expect(arriveCount).toBe(1); + }); + + it('post-arrival fade: orb disposes from scene after ~8 fade frames', () => { + const cam = makeCamera(); + const baseline = scene.children.length; + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + expect(scene.children.length).toBe(baseline + 2); + + // Gestation + flight + arrive + fade = 138 + 1 + 8 = 147 frames + for (let f = 0; f < 150; f++) effects.update(nodeMeshMap, cam); + + // Both orb sprites should be gone + expect(scene.children.length).toBe(baseline); + }); + + it('onArrive callback wrapped in try/catch so a throw does not crash the loop', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => { + throw new Error('caller blew up'); + } + ); + + // Should not throw — the production code swallows arrival-callback errors. + expect(() => { + for (let f = 0; f < 160; f++) effects.update(nodeMeshMap, cam); + }).not.toThrow(); + }); + + it('Sanhedrin Shatter: onArrive NEVER fires when target vanishes mid-flight', () => { + const cam = makeCamera(); + let arriveCount = 0; + let target: Vector3 | undefined = new Vector3(0, 0, 0); + + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => target as any, + () => { + arriveCount++; + } + ); + + // Finish gestation (48 frames) with target present + for (let f = 0; f < 48; f++) effects.update(nodeMeshMap, cam); + expect(arriveCount).toBe(0); + + // Stop hook yanks the target mid-flight + target = undefined; + + // Run enough frames to cover the entire orb lifecycle + for (let f = 0; f < 200; f++) effects.update(nodeMeshMap, cam); + + // onArrive must NEVER fire on aborted orbs + expect(arriveCount).toBe(0); + }); + + it('Sanhedrin Shatter: implosion is spawned when target vanishes mid-flight', () => { + const cam = makeCamera(); + let target: Vector3 | undefined = new Vector3(0, 0, 0); + + const baseline = scene.children.length; + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => target as any, + () => {} + ); + // baseline + 2 sprites + expect(scene.children.length).toBe(baseline + 2); + + // Finish gestation + for (let f = 0; f < 48; f++) effects.update(nodeMeshMap, cam); + + // Yank target → abort triggers on next tick + target = undefined; + const beforeAbort = scene.children.length; + effects.update(nodeMeshMap, cam); + // Scene should have grown by at least 1 (the implosion particles) + expect(scene.children.length).toBeGreaterThan(beforeAbort); + }); + + it('Sanhedrin Shatter: halo turns blood-red on abort', () => { + const cam = makeCamera(); + let target: Vector3 | undefined = new Vector3(0, 0, 0); + + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, // cyan — NOT red + () => target as any, + () => {} + ); + const halo = scene.children[0] as any; + + // Finish gestation + for (let f = 0; f < 48; f++) effects.update(nodeMeshMap, cam); + + // Sanity: halo is NOT red yet (event color cyan has r≈0) + expect(halo.material.color.r).toBeLessThan(0.5); + + // Yank target; abort triggers next tick + target = undefined; + effects.update(nodeMeshMap, cam); + + // Halo should now be blood red (1.0, 0.15, 0.2) + expect(halo.material.color.r).toBeGreaterThan(0.9); + expect(halo.material.color.g).toBeLessThan(0.3); + expect(halo.material.color.b).toBeLessThan(0.3); + }); + + it('Sanhedrin Shatter: orb eventually disposes from scene', () => { + const cam = makeCamera(); + let target: Vector3 | undefined = new Vector3(0, 0, 0); + + const baseline = scene.children.length; + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => target as any, + () => {} + ); + + // Finish gestation + for (let f = 0; f < 48; f++) effects.update(nodeMeshMap, cam); + // Yank target + target = undefined; + + // Drive a long time — orb + implosion should both dispose + // (orb fade ~8 frames, implosion lifetime ~80 frames) + for (let f = 0; f < 200; f++) effects.update(nodeMeshMap, cam); + + expect(scene.children.length).toBe(baseline); + }); + + it('dispose() removes active birth orbs from the scene', () => { + const cam = makeCamera(); + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => {} + ); + effects.createBirthOrb( + cam, + new Color(0xff00ff) as any, + () => new Vector3(10, 10, 10) as any, + () => {} + ); + // 4 sprites in scene (2 per orb) + expect(scene.children.length).toBeGreaterThanOrEqual(4); + + effects.dispose(); + + // All orb sprites should be gone + expect(scene.children.length).toBe(0); + }); + + it('multiple orbs in flight: all 3 onArrive callbacks fire exactly once each', () => { + const cam = makeCamera(); + let c1 = 0, c2 = 0, c3 = 0; + + effects.createBirthOrb( + cam, + new Color(0xff0000) as any, + () => new Vector3(10, 0, 0) as any, + () => { c1++; } + ); + effects.createBirthOrb( + cam, + new Color(0x00ff00) as any, + () => new Vector3(-10, 0, 0) as any, + () => { c2++; } + ); + effects.createBirthOrb( + cam, + new Color(0x0000ff) as any, + () => new Vector3(0, 0, -10) as any, + () => { c3++; } + ); + + // Drive past arrival (139) with margin + for (let f = 0; f < 160; f++) effects.update(nodeMeshMap, cam); + + expect(c1).toBe(1); + expect(c2).toBe(1); + expect(c3).toBe(1); + }); + + it('custom gestation/flight frame counts are honored', () => { + const cam = makeCamera(); + let arriveCount = 0; + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(0, 0, 0) as any, + () => { arriveCount++; }, + { gestationFrames: 10, flightFrames: 20 } + ); + + // Before frame 31 — no arrival + for (let f = 0; f < 30; f++) effects.update(nodeMeshMap, cam); + expect(arriveCount).toBe(0); + + // Frame 31 — fires + effects.update(nodeMeshMap, cam); + expect(arriveCount).toBe(1); + }); + + it('zero-alloc invariant (advisory): flight phase runs without throwing across many orbs', () => { + // Advisory test — vitest has no allocator introspection, but the + // inline algebraic Bezier eval in effects.ts is intentionally zero- + // allocation per frame (no `new Vector3`, no `new QuadraticBezierCurve3`). + // Here we just smoke-test that running many orbs across the full + // flight phase does not throw and completes cleanly. + const cam = makeCamera(); + for (let k = 0; k < 6; k++) { + effects.createBirthOrb( + cam, + new Color(0x00ffd1) as any, + () => new Vector3(k * 5, 0, 0) as any, + () => {} + ); + } + expect(() => { + for (let f = 0; f < 150; f++) effects.update(nodeMeshMap, cam); + }).not.toThrow(); + // All orbs should have cleaned up + expect(scene.children.length).toBe(0); + }); + }); }); diff --git a/apps/dashboard/src/lib/graph/__tests__/events.test.ts b/apps/dashboard/src/lib/graph/__tests__/events.test.ts index 5ffe198..c4061a1 100644 --- a/apps/dashboard/src/lib/graph/__tests__/events.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/events.test.ts @@ -10,7 +10,7 @@ import { NodeManager } from '../nodes'; import { EdgeManager } from '../edges'; import { EffectManager } from '../effects'; import { ForceSimulation } from '../force-sim'; -import { Vector3, Scene } from './three-mock'; +import { Vector3, Scene, RingGeometry, Mesh, Points, Sprite } from './three-mock'; import { makeNode, makeEdge, makeEvent, resetNodeCounter } from './helpers'; import type { GraphNode, VestigeEvent } from '$types'; @@ -155,7 +155,7 @@ describe('Event-to-Mutation Pipeline', () => { expect(distToN1).toBeLessThan(20); }); - it('triggers rainbow burst effect', () => { + it('spawns a v2.3 birth orb in the scene', () => { const childrenBefore = scene.children.length; mapEventToEffects( @@ -168,16 +168,19 @@ describe('Event-to-Mutation Pipeline', () => { allNodes ); - // Scene should have new particles (rainbow burst + shockwave + possibly more) - expect(scene.children.length).toBeGreaterThan(childrenBefore); + // Birth orb adds a halo sprite + bright core sprite to the scene + // immediately. The arrival-cascade effects (rainbow burst, shockwaves, + // ripple wave) are deferred to the orb's onArrive callback — covered + // by the "fires arrival cascade after ritual" test below. + expect(scene.children.length).toBeGreaterThanOrEqual(childrenBefore + 2); }); - it('triggers double shockwave (second delayed)', () => { + it('fires the arrival cascade after the birth ritual completes', () => { vi.useFakeTimers(); mapEventToEffects( makeEvent('MemoryCreated', { - id: 'double-shock', + id: 'cascade-check', content: 'test', node_type: 'fact', }), @@ -185,13 +188,23 @@ describe('Event-to-Mutation Pipeline', () => { allNodes ); - const initialChildren = scene.children.length; + const afterSpawn = scene.children.length; - // Advance past the setTimeout - vi.advanceTimersByTime(200); + // Drive the effects update loop past the full ritual duration + // (gestation 48 + flight 90 = 138 frames). Each tick is one frame; + // we run 150 to give onArrive room to fire. + for (let i = 0; i < 150; i++) { + effects.update(nodeManager.meshMap, camera, nodeManager.positions); + } - // Second shockwave should have been added - expect(scene.children.length).toBeGreaterThan(initialChildren); + // Advance the setTimeout that schedules the delayed second shockwave. + vi.advanceTimersByTime(250); + + // The arrival cascade should have added a rainbow burst, shockwave, + // ripple wave, and delayed second shockwave to the scene. Even after + // the orb fades out and is removed, the burst particles persist long + // enough that children.length should exceed the post-spawn count. + expect(scene.children.length).toBeGreaterThan(afterSpawn); vi.useRealTimers(); }); @@ -861,4 +874,270 @@ describe('Event-to-Mutation Pipeline', () => { expect(mutations.some((m) => m.type === 'edgeAdded')).toBe(true); }); }); + + describe('v2.3 Birth Ritual wiring', () => { + /** Count shockwave rings currently in the scene by their RingGeometry. */ + function countRings(s: InstanceType): number { + let n = 0; + for (const child of s.children) { + if (child instanceof Mesh && child.geometry instanceof RingGeometry) n++; + } + return n; + } + + /** Count Points children — rainbow bursts, spawn bursts, implosions. */ + function countPoints(s: InstanceType): number { + let n = 0; + for (const child of s.children) if (child instanceof Points) n++; + return n; + } + + /** Count Sprite children — birth orb adds a halo + core sprite. */ + function countSprites(s: InstanceType): number { + let n = 0; + for (const child of s.children) if (child instanceof Sprite) n++; + return n; + } + + it('node mesh is hidden immediately after MemoryCreated dispatch', () => { + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'ritual-create', + content: 'fresh memory', + node_type: 'fact', + }), + ctx, + allNodes + ); + + // Ritual path: mesh/glow/label are all .visible = false until + // igniteNode fires on orb arrival. + const mesh = nodeManager.meshMap.get('ritual-create')!; + const glow = nodeManager.glowMap.get('ritual-create')!; + const label = nodeManager.labelSprites.get('ritual-create')!; + expect(mesh.visible).toBe(false); + expect(glow.visible).toBe(false); + expect(label.visible).toBe(false); + + // Pending sentinel is stamped on userData. + expect(mesh.userData.birthRitualPending).toBeDefined(); + }); + + it('does NOT fire burst/ripple/shockwave at spawn (only the birth orb)', () => { + const ringsBefore = countRings(scene); + const pointsBefore = countPoints(scene); + const spritesBefore = countSprites(scene); + + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'spawn-quiet', + content: 'test', + node_type: 'fact', + }), + ctx, + allNodes + ); + + // Birth orb adds exactly 2 sprites (halo + core). NodeManager's + // addNode also adds a glow Sprite + label Sprite to the NodeManager + // GROUP, not to the scene — so spritesBefore -> after delta is +2. + expect(countSprites(scene) - spritesBefore).toBe(2); + + // No arrival-cascade effects yet: no shockwave rings, no rainbow + // burst/spawn burst/ripple particles. + expect(countRings(scene)).toBe(ringsBefore); + expect(countPoints(scene)).toBe(pointsBefore); + }); + + it('drives through the full ritual: onArrive fires, node becomes visible, scale grows', () => { + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'full-ritual', + content: 'visible after arrival', + node_type: 'fact', + }), + ctx, + allNodes + ); + + const mesh = nodeManager.meshMap.get('full-ritual')!; + expect(mesh.visible).toBe(false); + + // Drive the effects update loop past the full ritual duration + // (gestation 48 + flight 90 = 138 frames). After frame 138 the + // orb fires onArrive which ignites the node and queues materialization. + for (let i = 0; i < 140; i++) { + effects.update(nodeManager.meshMap, camera, nodeManager.positions); + } + + // Node is now visible and sentinel is cleared. + expect(mesh.visible).toBe(true); + expect(mesh.userData.birthRitualPending).toBeUndefined(); + + // Run node animation a few frames to let materialization scale grow. + // Note: onArrive bumped scale by 1.8x (from 0.001 -> 0.0018), then + // materialization easeOutElastic pulls it toward targetScale. + for (let f = 0; f < 10; f++) { + nodeManager.animate(f * 0.016, allNodes, camera); + } + expect(mesh.scale.x).toBeGreaterThan(0.001); + }); + + it("Newton's Cradle — target mesh scale is multiplied by 1.8x on arrival", () => { + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'newton-cradle', + content: 'recoil test', + node_type: 'fact', + }), + ctx, + allNodes + ); + + const mesh = nodeManager.meshMap.get('newton-cradle')!; + // Pre-arrival: scale is the addNode initial 0.001. + expect(mesh.scale.x).toBeCloseTo(0.001, 6); + + // Drive just to the moment onArrive fires. Gestation (48) + + // flight (90) = 138 frames. Arrival bumps scale by 1.8x BEFORE + // materialization has run any ticks, so the scale should be + // exactly 0.001 * 1.8 = 0.0018 at that instant. We check right + // after onArrive (frame 139) — but effects.update progresses the + // orb's age counter by one each call, and on the tick where + // orb.age > totalFrames, onArrive fires. We then must NOT tick + // nodeManager.animate (or materialization would diverge the scale). + for (let i = 0; i < 140; i++) { + effects.update(nodeManager.meshMap, camera, nodeManager.positions); + } + + // onArrive fired. Scale was 0.001, got multiplied by 1.8 -> 0.0018. + // Materialization is queued but hasn't run yet (no animate() calls). + expect(mesh.scale.x).toBeCloseTo(0.0018, 6); + }); + + it('dual shockwave — arrival cascade adds TWO RingGeometry meshes, not one', () => { + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'dual-shock', + content: 'layered crash', + node_type: 'fact', + }), + ctx, + allNodes + ); + + const ringsBefore = countRings(scene); + + // Drive past full ritual so onArrive fires. + for (let i = 0; i < 140; i++) { + effects.update(nodeManager.meshMap, camera, nodeManager.positions); + } + + // Both shockwaves fire synchronously in the onArrive callback + // (the previous setTimeout-delayed second shockwave was dropped + // because it could outlive the scene on route change). + const ringsAfter = countRings(scene); + expect(ringsAfter - ringsBefore).toBe(2); + }); + + it('re-reads position on arrival — fires cascade at force-sim-moved position', () => { + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'moving-target', + content: 'follow the node', + node_type: 'fact', + }), + ctx, + allNodes + ); + + // Grab the spawn position, then mutate it to simulate the force + // simulation pushing the node during the ritual. + const movedPos = new Vector3(123, 456, -789); + nodeManager.positions.set('moving-target', movedPos); + + // Drive past full ritual. + for (let i = 0; i < 140; i++) { + effects.update(nodeManager.meshMap, camera, nodeManager.positions); + } + + // The onArrive callback re-reads nodeManager.positions and fires + // the cascade at the LIVE position. The two shockwave Ring meshes + // should have been created at movedPos. Find them and check. + const rings = scene.children.filter( + (c) => c instanceof Mesh && c.geometry instanceof RingGeometry + ); + expect(rings.length).toBeGreaterThanOrEqual(2); + // Rings for this node: their .position copies from arrivePos at + // spawn time inside createShockwave. + const atMovedPos = rings.filter( + (r) => r.position.x === 123 && r.position.y === 456 && r.position.z === -789 + ); + expect(atMovedPos.length).toBe(2); + }); + + it('Sanhedrin abort path — removeNode before arrival prevents the regular cascade', () => { + // Spy on the three arrival-cascade emitters so we can assert + // they were NEVER called when the target is vetoed mid-ritual. + const burstSpy = vi.spyOn(effects, 'createRainbowBurst'); + const shockwaveSpy = vi.spyOn(effects, 'createShockwave'); + const rippleSpy = vi.spyOn(effects, 'createRippleWave'); + + mapEventToEffects( + makeEvent('MemoryCreated', { + id: 'vetoed', + content: 'about to be shattered', + node_type: 'fact', + }), + ctx, + allNodes + ); + + // The orb's getTargetPos() closure reads + // nodeManager.positions.get('vetoed'). Dropping the position + // directly simulates the "target gone" state that the Sanhedrin + // veto produces after dissolution completes — without needing to + // drive the full 60-frame dissolution animation. + nodeManager.positions.delete('vetoed'); + expect(nodeManager.positions.has('vetoed')).toBe(false); + + // Snapshot the orb reference before the update loop disposes it. + // The abort branch flips `aborted` and tints the halo red; we + // assert on those fields after the ritual unwinds. + const orbs = (effects as any).birthOrbs as Array<{ + sprite: { material: { color: any } }; + core: { material: { color: any } }; + aborted: boolean; + }>; + expect(orbs.length).toBe(1); + const orbRef = orbs[0]; + + // Drive effects past the full ritual. During flight the orb will + // see getTargetPos() === undefined, enter the Sanhedrin branch, + // call createImplosion (anti-birth visual) and SKIP onArrive — + // so the regular rainbow-burst + dual-shockwave + ripple cascade + // never fires. + for (let i = 0; i < 200; i++) { + effects.update(nodeManager.meshMap, camera, nodeManager.positions); + } + + // Core assertion: the three regular-cascade emitters were never + // invoked for the vetoed node. + expect(burstSpy).not.toHaveBeenCalled(); + expect(shockwaveSpy).not.toHaveBeenCalled(); + expect(rippleSpy).not.toHaveBeenCalled(); + + // Also confirm the orb actually took the abort branch, not the + // gestation-only no-op path (otherwise this test would pass for + // the wrong reason). The aborted flag is set exactly once inside + // the Sanhedrin branch. + expect(orbRef.aborted).toBe(true); + expect(orbRef.sprite.material.color.r).toBeCloseTo(1.0, 3); + expect(orbRef.sprite.material.color.g).toBeCloseTo(0.15, 3); + + burstSpy.mockRestore(); + shockwaveSpy.mockRestore(); + rippleSpy.mockRestore(); + }); + }); }); diff --git a/apps/dashboard/src/lib/graph/__tests__/nodes.test.ts b/apps/dashboard/src/lib/graph/__tests__/nodes.test.ts index 3d533f8..26b997f 100644 --- a/apps/dashboard/src/lib/graph/__tests__/nodes.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/nodes.test.ts @@ -453,4 +453,201 @@ describe('NodeManager', () => { // The dispose method clears materializingNodes, dissolvingNodes, growingNodes }); }); + + describe('Birth Ritual integration', () => { + it('addNode with isBirthRitual:true hides mesh, glow, and label immediately', () => { + const node = makeNode({ id: 'ritual-1' }); + manager.addNode(node, new Vector3(5, 5, 5), { isBirthRitual: true }); + + const mesh = manager.meshMap.get('ritual-1')!; + const glow = manager.glowMap.get('ritual-1')!; + const label = manager.labelSprites.get('ritual-1')!; + + expect(mesh.visible).toBe(false); + expect(glow.visible).toBe(false); + expect(label.visible).toBe(false); + }); + + it('addNode with isBirthRitual:true stores a pending sentinel on mesh.userData', () => { + const node = makeNode({ id: 'ritual-sentinel', retention: 0.75 }); + manager.addNode(node, new Vector3(0, 0, 0), { isBirthRitual: true }); + + const mesh = manager.meshMap.get('ritual-sentinel')!; + const pending = mesh.userData.birthRitualPending as any; + expect(pending).toBeDefined(); + expect(pending.totalFrames).toBe(30); + // targetScale = 0.5 + retention * 2 = 0.5 + 0.75 * 2 = 2.0 + expect(pending.targetScale).toBeCloseTo(2.0, 3); + }); + + it('addNode with isBirthRitual:true does NOT enqueue materialization', () => { + const ritualNode = makeNode({ id: 'ritual-pending', retention: 0.8 }); + manager.addNode(ritualNode, new Vector3(10, 10, 10), { isBirthRitual: true }); + + // In the real runtime the ritual-pending node is .visible=false + // AND is not yet in the GraphNode[] list — it only gets added to + // the visible node list once igniteNode flips its visibility and + // materialization kicks in. So we pass an empty `nodes` array to + // animate(), which also exercises that the breathing loop skips + // meshes absent from the nodes array. + const camera = { position: new Vector3(0, 30, 80) } as any; + for (let f = 0; f < 40; f++) { + manager.animate(f * 0.016, [], camera); + } + + const mesh = manager.meshMap.get('ritual-pending')!; + // Materialization queue never pushed — a regular materializing + // node would be at scale ≈ targetScale = 2.1 by frame 40. The + // ritual-pending node stays at its addNode initial 0.001 because + // no animation loop is mutating its scale. + expect(mesh.scale.x).toBeCloseTo(0.001, 3); + + // Stronger invariant — the sentinel is still there, confirming + // the node never got handed off to the materialization queue. + expect(mesh.userData.birthRitualPending).toBeDefined(); + }); + + it('addNode without opts proceeds with normal materialization (old behavior)', () => { + const node = makeNode({ id: 'normal-spawn' }); + manager.addNode(node, new Vector3(1, 2, 3)); + + const mesh = manager.meshMap.get('normal-spawn')!; + const glow = manager.glowMap.get('normal-spawn')!; + const label = manager.labelSprites.get('normal-spawn')!; + + // Default mesh.visible is true in three-mock (Object3D has no explicit field). + // Key invariant: visible is NOT explicitly false like the ritual path. + expect(mesh.visible).not.toBe(false); + expect(glow.visible).not.toBe(false); + expect(label.visible).not.toBe(false); + + // And no pending sentinel + expect(mesh.userData.birthRitualPending).toBeUndefined(); + + // Animation should proceed — scale grows via easeOutElastic + const camera = { position: new Vector3(0, 30, 80) } as any; + for (let f = 0; f < 20; f++) { + manager.animate(f * 0.016, [node], camera); + } + expect(mesh.scale.x).toBeGreaterThan(0.1); + }); + + it('igniteNode flips all three visibility flags and queues materialization', () => { + const node = makeNode({ id: 'to-ignite', retention: 0.6 }); + manager.addNode(node, new Vector3(0, 0, 0), { isBirthRitual: true }); + + // Pre-ignite: hidden + const mesh = manager.meshMap.get('to-ignite')!; + const glow = manager.glowMap.get('to-ignite')!; + const label = manager.labelSprites.get('to-ignite')!; + expect(mesh.visible).toBe(false); + + manager.igniteNode('to-ignite'); + + // Post-ignite: visible + expect(mesh.visible).toBe(true); + expect(glow.visible).toBe(true); + expect(label.visible).toBe(true); + + // Sentinel is gone + expect(mesh.userData.birthRitualPending).toBeUndefined(); + + // Materialization was queued — drive animation and the scale + // should grow past the initial 0.001. + const camera = { position: new Vector3(0, 30, 80) } as any; + for (let f = 0; f < 15; f++) { + manager.animate(f * 0.016, [node], camera); + } + expect(mesh.scale.x).toBeGreaterThan(0.1); + }); + + it('igniteNode called twice is idempotent (second call is a no-op)', () => { + const node = makeNode({ id: 'double-ignite', retention: 0.5 }); + manager.addNode(node, new Vector3(0, 0, 0), { isBirthRitual: true }); + + manager.igniteNode('double-ignite'); + // Capture scale after one round of animation + const camera = { position: new Vector3(0, 30, 80) } as any; + for (let f = 0; f < 10; f++) { + manager.animate(f * 0.016, [node], camera); + } + const scaleAfterFirst = manager.meshMap.get('double-ignite')!.scale.x; + + // Second ignite — should NOT push a duplicate materialization entry. + // If it did, the extra entry (starting at frame 0) would restart + // the scale back near 0.001 or at least visibly reset it. + manager.igniteNode('double-ignite'); + for (let f = 0; f < 5; f++) { + manager.animate((f + 10) * 0.016, [node], camera); + } + const scaleAfterSecond = manager.meshMap.get('double-ignite')!.scale.x; + + // Scale after second ignite should be greater than or roughly equal + // to scale after first, NOT reset toward 0.001. A duplicate entry + // starting at frame 0 would pull the mesh back near zero on the + // very first subsequent animate() tick via mn.mesh.scale.setScalar. + expect(scaleAfterSecond).toBeGreaterThanOrEqual(scaleAfterFirst * 0.5); + }); + + it('igniteNode on a regular (non-ritual) node is a no-op', () => { + const node = makeNode({ id: 'regular', retention: 0.5 }); + manager.addNode(node, new Vector3(0, 0, 0)); + // Regular addNode already queued materialization. Capture state. + const mesh = manager.meshMap.get('regular')!; + const visBefore = mesh.visible; + + // Call igniteNode — there's no pending sentinel, should short-circuit. + expect(() => manager.igniteNode('regular')).not.toThrow(); + + // No pending sentinel means the function returns early after the + // sentinel check, so nothing about the mesh changes. + expect(mesh.visible).toBe(visBefore); + expect(mesh.userData.birthRitualPending).toBeUndefined(); + }); + + it('igniteNode on unknown id is a no-op (no throw)', () => { + expect(() => manager.igniteNode('does-not-exist')).not.toThrow(); + expect(manager.meshMap.has('does-not-exist')).toBe(false); + }); + + it('position is stored in positions map even when the node is invisible', () => { + const node = makeNode({ id: 'invisible-but-positioned' }); + const spawnPos = new Vector3(42, -17, 8); + manager.addNode(node, spawnPos, { isBirthRitual: true }); + + // Force simulation + orb getTargetPos() both rely on positions + // being live immediately — the ritual only hides visuals, not + // physics state. + const stored = manager.positions.get('invisible-but-positioned'); + expect(stored).toBeDefined(); + expect(stored!.x).toBe(42); + expect(stored!.y).toBe(-17); + expect(stored!.z).toBe(8); + + // And the mesh itself is still hidden + expect(manager.meshMap.get('invisible-but-positioned')!.visible).toBe(false); + }); + + it('removeNode during pending ritual cancels without materialization', () => { + // Sanhedrin abort path at the NodeManager level: a ritual-pending + // node gets removed before igniteNode fires. The remove path + // should still work (dissolution queue takes over) and igniteNode + // called later must not resurrect it. + const node = makeNode({ id: 'aborted-ritual' }); + manager.addNode(node, new Vector3(0, 0, 0), { isBirthRitual: true }); + + manager.removeNode('aborted-ritual'); + + // Dissolution progresses past totalFrames = 60 and clears state. + const camera = { position: new Vector3(0, 30, 80) } as any; + for (let f = 0; f < 65; f++) { + manager.animate(f * 0.016, [node], camera); + } + + expect(manager.meshMap.has('aborted-ritual')).toBe(false); + + // And a late igniteNode call on the dead id is a safe no-op. + expect(() => manager.igniteNode('aborted-ritual')).not.toThrow(); + }); + }); }); diff --git a/apps/dashboard/src/lib/graph/__tests__/three-mock.ts b/apps/dashboard/src/lib/graph/__tests__/three-mock.ts index b665ed7..e6afd57 100644 --- a/apps/dashboard/src/lib/graph/__tests__/three-mock.ts +++ b/apps/dashboard/src/lib/graph/__tests__/three-mock.ts @@ -93,6 +93,52 @@ export class Vector3 { this.z = s; return this; } + + addVectors(a: Vector3, b: Vector3) { + this.x = a.x + b.x; + this.y = a.y + b.y; + this.z = a.z + b.z; + return this; + } + + applyQuaternion(_q: Quaternion) { + // Mock: identity transform. Tests don't care about actual + // camera-relative positioning; production uses real THREE math. + return this; + } +} + +export class Quaternion { + x = 0; + y = 0; + z = 0; + w = 1; +} + +export class QuadraticBezierCurve3 { + v0: Vector3; + v1: Vector3; + v2: Vector3; + constructor(v0: Vector3, v1: Vector3, v2: Vector3) { + this.v0 = v0; + this.v1 = v1; + this.v2 = v2; + } + getPoint(t: number): Vector3 { + // Standard quadratic Bezier evaluation, faithful enough for tests + // that only care that points land on the curve. + const one = 1 - t; + return new Vector3( + one * one * this.v0.x + 2 * one * t * this.v1.x + t * t * this.v2.x, + one * one * this.v0.y + 2 * one * t * this.v1.y + t * t * this.v2.y, + one * one * this.v0.z + 2 * one * t * this.v1.z + t * t * this.v2.z + ); + } +} + +export class Texture { + needsUpdate = false; + dispose() {} } export class Vector2 { @@ -157,6 +203,20 @@ export class Color { offsetHSL(_h: number, _s: number, _l: number) { return this; } + + multiplyScalar(s: number) { + this.r *= s; + this.g *= s; + this.b *= s; + return this; + } + + setRGB(r: number, g: number, b: number) { + this.r = r; + this.g = g; + this.b = b; + return this; + } } export class BufferAttribute { @@ -329,6 +389,8 @@ export class SpriteMaterial extends BaseMaterial { export class Object3D { position = new Vector3(); scale = new Vector3(1, 1, 1); + quaternion = new Quaternion(); + renderOrder = 0; userData: Record = {}; children: Object3D[] = []; parent: Object3D | null = null; @@ -428,6 +490,9 @@ export function installThreeMock() { Vector3, Vector2, Color, + Quaternion, + QuadraticBezierCurve3, + Texture, BufferAttribute, BufferGeometry, SphereGeometry, diff --git a/apps/dashboard/src/lib/graph/effects.ts b/apps/dashboard/src/lib/graph/effects.ts index 1402476..ccae736 100644 --- a/apps/dashboard/src/lib/graph/effects.ts +++ b/apps/dashboard/src/lib/graph/effects.ts @@ -1,4 +1,5 @@ import * as THREE from 'three'; +import { getGlowTexture } from './nodes'; export interface PulseEffect { nodeId: string; @@ -49,6 +50,33 @@ interface ConnectionFlash { intensity: number; } +// v2.3 Memory Birth Ritual. The orb gestates at a camera-relative "cosmic +// center" point for `gestationFrames`, then flies along a dynamic quadratic +// Bezier curve to the live position of its target node for `flightFrames`, +// then calls `onArrive` and disposes itself. The target position is +// resolved via `getTargetPos` on every frame so the force simulation can +// move the node during the flight and the orb stays glued to it. +interface BirthOrb { + sprite: THREE.Sprite; + core: THREE.Sprite; + startPos: THREE.Vector3; + getTargetPos: () => THREE.Vector3 | undefined; + color: THREE.Color; + age: number; + gestationFrames: number; + flightFrames: number; + arriveFired: boolean; + onArrive: () => void; + /** Last known target position. If the target disappears mid-flight we keep + * using this snapshot so the orb still lands somewhere sensible. */ + lastTargetPos: THREE.Vector3; + /** v2.3: Sanhedrin-Shatter state. Set true when getTargetPos returns + * undefined after gestation — the Stop hook deleted the target node + * mid-ritual, so we short-circuit the arrival cascade and implode + * the orb in place as the "cognitive immune system" visual. */ + aborted: boolean; +} + export class EffectManager { pulseEffects: PulseEffect[] = []; private spawnBursts: SpawnBurst[] = []; @@ -57,6 +85,7 @@ export class EffectManager { private implosions: ImplosionEffect[] = []; private shockwaves: Shockwave[] = []; private connectionFlashes: ConnectionFlash[] = []; + private birthOrbs: BirthOrb[] = []; private scene: THREE.Scene; constructor(scene: THREE.Scene) { @@ -231,6 +260,89 @@ export class EffectManager { this.connectionFlashes.push({ line, intensity: 1.0 }); } + /** + * v2.3 Memory Birth Ritual. Spawn a glowing orb at a point in front of the + * camera ("cosmic center"), gestate for ~800ms, then arc along a quadratic + * Bezier curve to the live position of the target node, which is resolved + * on every frame via `getTargetPos`. On arrival, `onArrive` fires — caller + * is responsible for adding the real node to the graph and triggering + * arrival-time bursts. + * + * The target getter can return undefined if the node has been removed + * mid-flight; the orb then flies to the last known target position so the + * burst still fires somewhere coherent rather than snapping to origin. + */ + createBirthOrb( + camera: THREE.Camera, + color: THREE.Color, + getTargetPos: () => THREE.Vector3 | undefined, + onArrive: () => void, + opts: { gestationFrames?: number; flightFrames?: number; distanceFromCamera?: number } = {} + ) { + const gestationFrames = opts.gestationFrames ?? 48; // ~800ms + const flightFrames = opts.flightFrames ?? 90; // ~1500ms + const distanceFromCamera = opts.distanceFromCamera ?? 40; + + // Place the orb slightly in front of the camera, in view-space, + // projected back into world coordinates. This way the orb always + // appears "right in front of the user's face" regardless of where + // the camera has been orbited to. + const startPos = new THREE.Vector3(0, 0, -distanceFromCamera) + .applyQuaternion(camera.quaternion) + .add(camera.position); + + // Outer glow halo + const haloMat = new THREE.SpriteMaterial({ + map: getGlowTexture(), + color: color.clone(), + transparent: true, + opacity: 0.0, + blending: THREE.AdditiveBlending, + depthWrite: false, + depthTest: false, // always visible, even through other nodes + }); + const sprite = new THREE.Sprite(haloMat); + sprite.position.copy(startPos); + sprite.scale.set(0.5, 0.5, 1); + sprite.renderOrder = 999; + + // Inner bright core — stays hot white during gestation, tints at launch + const coreMat = new THREE.SpriteMaterial({ + map: getGlowTexture(), + color: new THREE.Color(0xffffff), + transparent: true, + opacity: 0.0, + blending: THREE.AdditiveBlending, + depthWrite: false, + depthTest: false, + }); + const core = new THREE.Sprite(coreMat); + core.position.copy(startPos); + core.scale.set(0.2, 0.2, 1); + core.renderOrder = 1000; + + this.scene.add(sprite); + this.scene.add(core); + + // Snapshot the current target so we have a fallback. + const initialTarget = getTargetPos()?.clone() ?? startPos.clone(); + + this.birthOrbs.push({ + sprite, + core, + startPos, + getTargetPos, + color: color.clone(), + age: 0, + gestationFrames, + flightFrames, + arriveFired: false, + onArrive, + lastTargetPos: initialTarget, + aborted: false, + }); + } + update( nodeMeshMap: Map, camera: THREE.Camera, @@ -431,6 +543,122 @@ export class EffectManager { } (flash.line.material as THREE.LineBasicMaterial).opacity = flash.intensity; } + + // v2.3 Birth orbs — gestate at cosmic center, then arc to live node + // position along a quadratic Bezier curve. Target position is + // re-resolved every frame so the force simulation can move the + // destination during flight without the orb losing its mark. + for (let i = this.birthOrbs.length - 1; i >= 0; i--) { + const orb = this.birthOrbs[i]; + orb.age++; + const totalFrames = orb.gestationFrames + orb.flightFrames; + + const haloMat = orb.sprite.material as THREE.SpriteMaterial; + const coreMat = orb.core.material as THREE.SpriteMaterial; + + // Refresh the live target snapshot. If the target getter returns + // undefined DURING flight (not just at spawn), the node was + // aborted mid-ritual — typically a Sanhedrin veto deleting a + // hallucination node while the orb was still in transit. Trigger + // the anti-birth: turn red, implode in place, stop tracking. + const live = orb.getTargetPos(); + if (live) { + orb.lastTargetPos.copy(live); + } else if (orb.age > orb.gestationFrames && !orb.aborted) { + orb.aborted = true; + // Fire an implosion where the orb currently is, then splice + // out on the next tick by jumping age to the end of life. + const pos = orb.sprite.position; + haloMat.color.setRGB(1.0, 0.15, 0.2); // blood red + coreMat.color.setRGB(1.0, 0.6, 0.6); + this.createImplosion(pos, new THREE.Color(0xff2533)); + orb.arriveFired = true; + orb.age = totalFrames + 1; + } + + if (orb.age <= orb.gestationFrames) { + // Gestation phase — pulse brighter + grow from a tiny spark + // into a full orb. Sits still at the cosmic center. + const t = orb.age / orb.gestationFrames; + const ease = 1 - Math.pow(1 - t, 3); // easeOutCubic + const pulse = 0.85 + Math.sin(orb.age * 0.35) * 0.15; + const haloScale = 0.5 + ease * 4.5 * pulse; + const coreScale = 0.2 + ease * 1.8 * pulse; + orb.sprite.scale.set(haloScale, haloScale, 1); + orb.core.scale.set(coreScale, coreScale, 1); + haloMat.opacity = ease * 0.95; + coreMat.opacity = ease; + // Subtle warm-up — core white, halo tints toward the event + // color as gestation completes. + haloMat.color.copy(orb.color).multiplyScalar(0.7 + ease * 0.3); + orb.sprite.position.copy(orb.startPos); + orb.core.position.copy(orb.startPos); + } else if (orb.age <= totalFrames) { + // Flight phase — inline quadratic Bezier eval. Zero-alloc: + // no new Vector3 or QuadraticBezierCurve3 per frame, which + // would flood the GC when several orbs are in flight. + const t = (orb.age - orb.gestationFrames) / orb.flightFrames; + const ease = t < 0.5 + ? 2 * t * t + : 1 - Math.pow(-2 * t + 2, 2) / 2; // easeInOutQuad + + const s = orb.startPos; + const tgt = orb.lastTargetPos; + const dx = tgt.x - s.x; + const dy = tgt.y - s.y; + const dz = tgt.z - s.z; + const dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + const cx = (s.x + tgt.x) * 0.5; + const cy = (s.y + tgt.y) * 0.5 + 30 + dist * 0.15; + const cz = (s.z + tgt.z) * 0.5; + + const oneMinusE = 1 - ease; + const w0 = oneMinusE * oneMinusE; + const w1 = 2 * oneMinusE * ease; + const w2 = ease * ease; + const px = w0 * s.x + w1 * cx + w2 * tgt.x; + const py = w0 * s.y + w1 * cy + w2 * tgt.y; + const pz = w0 * s.z + w1 * cz + w2 * tgt.z; + + orb.sprite.position.set(px, py, pz); + orb.core.position.set(px, py, pz); + + // Trail effect — shrink + brighten as it approaches target + const shrink = 1 - ease * 0.35; + orb.sprite.scale.setScalar(5 * shrink); + orb.core.scale.setScalar(2 * shrink); + haloMat.opacity = 0.95; + coreMat.opacity = 1.0; + // Shift halo fully to the event color during flight + haloMat.color.copy(orb.color); + } else if (!orb.arriveFired) { + // Docking — fire the arrival callback once. Let the caller + // trigger burst/ripple effects at the exact target point. + orb.arriveFired = true; + try { + orb.onArrive(); + } catch (e) { + // Effects must never take down the render loop. + // eslint-disable-next-line no-console + console.warn('[birth-orb] onArrive threw', e); + } + // Fade the orb out over a few more frames instead of popping. + } else { + // Post-arrival fade (8 frames ≈ 130ms) + const fadeAge = orb.age - totalFrames; + const fade = Math.max(0, 1 - fadeAge / 8); + haloMat.opacity = 0.95 * fade; + coreMat.opacity = 1.0 * fade; + orb.sprite.scale.setScalar(5 * (1 + (1 - fade) * 2)); + if (fade <= 0) { + this.scene.remove(orb.sprite); + this.scene.remove(orb.core); + haloMat.dispose(); + coreMat.dispose(); + this.birthOrbs.splice(i, 1); + } + } + } } dispose() { @@ -464,6 +692,12 @@ export class EffectManager { flash.line.geometry.dispose(); (flash.line.material as THREE.Material).dispose(); } + for (const orb of this.birthOrbs) { + this.scene.remove(orb.sprite); + this.scene.remove(orb.core); + (orb.sprite.material as THREE.Material).dispose(); + (orb.core.material as THREE.Material).dispose(); + } this.pulseEffects = []; this.spawnBursts = []; this.rainbowBursts = []; @@ -471,5 +705,6 @@ export class EffectManager { this.implosions = []; this.shockwaves = []; this.connectionFlashes = []; + this.birthOrbs = []; } } diff --git a/apps/dashboard/src/lib/graph/events.ts b/apps/dashboard/src/lib/graph/events.ts index b13ce22..31fd3cc 100644 --- a/apps/dashboard/src/lib/graph/events.ts +++ b/apps/dashboard/src/lib/graph/events.ts @@ -125,25 +125,59 @@ export function mapEventToEffects( // Find spawn position near related nodes const spawnPos = findSpawnPosition(newNode, allNodes, nodePositions); - // Add to all managers - const pos = nodeManager.addNode(newNode, spawnPos); + // Reserve the physics slot but hide the node until the orb docks. + // `isBirthRitual:true` skips the 30-frame materialization push, so + // the mesh/glow/label stay invisible; `igniteNode` below flips + // visibility and kicks off the elastic scale-up AT the exact + // millisecond the orb lands — not 100 frames before. + const pos = nodeManager.addNode(newNode, spawnPos, { isBirthRitual: true }); forceSim.addNode(data.id, pos); // FIFO eviction liveSpawnedNodes.push(data.id); evictOldestLiveNode(ctx, allNodes); - // Spectacular effects: rainbow burst + double shockwave + ripple wave + // v2.3 Memory Birth Ritual — cosmic-center orb, Bezier flight, + // arrival burst cascade. The burst/ripple/shockwave cascade + // fires on arrival at the docking target, not at spawn, so the + // eye tracks the orb in and the visuals peak on contact. const color = new THREE.Color(NODE_TYPE_COLORS[newNode.type] || '#00ffd1'); - effects.createRainbowBurst(spawnPos, color); - effects.createShockwave(spawnPos, color, camera); - // Second shockwave, hue-shifted, delayed via smaller initial scale const hueShifted = color.clone(); hueShifted.offsetHSL(0.15, 0, 0); - setTimeout(() => { - effects.createShockwave(spawnPos, hueShifted, camera); - }, 166); // ~10 frames at 60fps - effects.createRippleWave(spawnPos); + + effects.createBirthOrb( + camera, + color, + // Re-resolve the live target position every frame — the node + // is being pushed around by the force sim during flight. + // Returning undefined here signals "node was aborted" and + // triggers the Sanhedrin-Shatter anti-birth in effects.ts. + () => nodeManager.positions.get(newNode.id), + () => { + // Dock. Ignite the node (flips visibility + starts + // materialization) and fire the arrival cascade at the + // node's CURRENT position — the force sim may have moved + // the target during the ritual, so we re-read positions. + nodeManager.igniteNode(newNode.id); + const arrivePos = nodeManager.positions.get(newNode.id) ?? spawnPos; + + // Newton's Cradle — kinetic transfer into the graph. + // Bump the mesh scale on impact so the easeOutElastic + // materialization + force-sim springs physically recoil + // instead of the orb docking silently. + const mesh = nodeManager.meshMap.get(newNode.id); + if (mesh) mesh.scale.multiplyScalar(1.8); + + effects.createRainbowBurst(arrivePos, color); + effects.createShockwave(arrivePos, color, camera); + // Fire BOTH shockwaves immediately (different scales / + // colors for layered crash feel). The previous 166ms + // setTimeout could outlive the scene on route change + // and throw an unhandled rejection. + effects.createShockwave(arrivePos, hueShifted, camera); + effects.createRippleWave(arrivePos); + } + ); onMutation({ type: 'nodeAdded', node: newNode }); break; diff --git a/apps/dashboard/src/lib/graph/nodes.ts b/apps/dashboard/src/lib/graph/nodes.ts index 0fef301..e247fae 100644 --- a/apps/dashboard/src/lib/graph/nodes.ts +++ b/apps/dashboard/src/lib/graph/nodes.ts @@ -47,10 +47,24 @@ export const MEMORY_STATE_DESCRIPTIONS: Record = { unavailable: 'Needs reinforcement (< 10%)', }; -/// Color mode controls whether node spheres are tinted by node type -/// (fact / concept / event / …) or by FSRS memory state. +export type AhaGraphKind = 'aha' | 'confusion' | 'failure'; + +export const AHAGRAPH_COLORS: Record = { + aha: '#FFD700', + confusion: '#EF4444', + failure: '#9CA3AF', +}; + +export const AHAGRAPH_DESCRIPTIONS: Record = { + aha: 'Aha moments and breakthroughs', + confusion: 'Confusions and weak spots', + failure: 'Failures and guardrails', +}; + +/// Color mode controls whether node spheres are tinted by node type, +/// FSRS memory state, or AhaGraph learning tags. /// Type mode is the long-standing default; state mode is the v2.0.8 addition. -export type ColorMode = 'type' | 'state'; +export type ColorMode = 'type' | 'state' | 'ahagraph'; /// Pick a hex colour for a node given the active colour mode. /// Falls back to the grey `unavailable` tone if the node's type is unknown. @@ -58,16 +72,27 @@ export function getNodeColor(node: GraphNode, mode: ColorMode): string { if (mode === 'state') { return MEMORY_STATE_COLORS[getMemoryState(node.retention)]; } + if (mode === 'ahagraph') { + return getAhaGraphColor(node) ?? NODE_TYPE_COLORS[node.type] ?? '#8B95A5'; + } return NODE_TYPE_COLORS[node.type] || '#8B95A5'; } +export function getAhaGraphColor(node: Pick): string | null { + const tags = new Set((node.tags ?? []).map((tag) => tag.toLowerCase())); + if (tags.has('aha')) return AHAGRAPH_COLORS.aha; + if (tags.has('confusion') || tags.has('weak-spot')) return AHAGRAPH_COLORS.confusion; + if (tags.has('failure') || tags.has('guardrail')) return AHAGRAPH_COLORS.failure; + return null; +} + // Shared radial-gradient texture used for every node's glow Sprite. // Without a map, THREE.Sprite renders as a flat coloured plane — additive- // blending + UnrealBloomPass then amplifies its square edges into the // hard-edged "glowing cubes" artefact reported in issue #31. Using a // soft radial gradient gives a real round halo and lets bloom do its job. let sharedGlowTexture: THREE.Texture | null = null; -function getGlowTexture(): THREE.Texture { +export function getGlowTexture(): THREE.Texture { if (sharedGlowTexture) return sharedGlowTexture; const size = 128; const canvas = document.createElement('canvas'); @@ -139,8 +164,8 @@ export class NodeManager { labelSprites = new Map(); hoveredNode: string | null = null; selectedNode: string | null = null; - /// v2.0.8: colour nodes by FSRS memory state (active/dormant/silent/unavailable) - /// instead of node type. Switched at runtime via `setColorMode`. + /// Colour nodes by type, FSRS state, or AhaGraph learning tags. + /// Switched at runtime via `setColorMode`. colorMode: ColorMode = 'type'; private materializingNodes: MaterializingNode[] = []; @@ -161,12 +186,15 @@ export class NodeManager { for (const [id, mesh] of this.meshMap) { const retention = (mesh.userData.retention as number | undefined) ?? 0; const type = (mesh.userData.type as string | undefined) ?? 'fact'; + const tags = Array.isArray(mesh.userData.tags) + ? (mesh.userData.tags as string[]) + : []; const stubNode = { id, label: '', type, retention, - tags: [], + tags, createdAt: '', updatedAt: '', isCenter: false, @@ -236,7 +264,7 @@ export class NodeManager { const mesh = new THREE.Mesh(geometry, material); mesh.position.copy(pos); mesh.scale.setScalar(initialScale); - mesh.userData = { nodeId: node.id, type: node.type, retention: node.retention }; + mesh.userData = { nodeId: node.id, type: node.type, retention: node.retention, tags: node.tags }; this.meshMap.set(node.id, mesh); this.group.add(mesh); @@ -271,7 +299,11 @@ export class NodeManager { return { mesh, glow: sprite, label: labelSprite, size }; } - addNode(node: GraphNode, initialPosition?: THREE.Vector3): THREE.Vector3 { + addNode( + node: GraphNode, + initialPosition?: THREE.Vector3, + options: { isBirthRitual?: boolean } = {} + ): THREE.Vector3 { const pos = initialPosition?.clone() ?? new THREE.Vector3( @@ -289,17 +321,62 @@ export class NodeManager { (glow.material as THREE.SpriteMaterial).opacity = 0; (label.material as THREE.SpriteMaterial).opacity = 0; + if (options.isBirthRitual) { + // v2.3 Birth Ritual: reserve the physics slot but don't show + // anything until the orb docks. Hiding via .visible keeps the + // force simulation + positions map fully active, so getTargetPos() + // can still resolve the live destination for the orb. `igniteNode` + // below flips visibility and kicks off the materialization anim. + mesh.visible = false; + glow.visible = false; + label.visible = false; + mesh.userData.birthRitualPending = { + totalFrames: 30, + targetScale: 0.5 + node.retention * 2, + }; + } else { + this.materializingNodes.push({ + id: node.id, + frame: 0, + totalFrames: 30, + mesh, + glow, + label, + targetScale: 0.5 + node.retention * 2, + }); + } + + return pos; + } + + /** + * v2.3 Birth Ritual docking. Flip visibility and hand the node over to + * the materialization queue so it springs up via easeOutElastic at the + * exact moment the orb hits. No-op if the node wasn't created with + * `isBirthRitual:true` or was already ignited. + */ + igniteNode(id: string) { + const mesh = this.meshMap.get(id); + const glow = this.glowMap.get(id); + const label = this.labelSprites.get(id); + if (!mesh || !glow || !label) return; + const pending = mesh.userData.birthRitualPending as + | { totalFrames: number; targetScale: number } + | undefined; + if (!pending) return; + mesh.visible = true; + glow.visible = true; + label.visible = true; + delete mesh.userData.birthRitualPending; this.materializingNodes.push({ - id: node.id, + id, frame: 0, - totalFrames: 30, + totalFrames: pending.totalFrames, mesh, glow, label, - targetScale: 0.5 + node.retention * 2, + targetScale: pending.targetScale, }); - - return pos; } removeNode(id: string) { @@ -344,7 +421,7 @@ export class NodeManager { /// The scene runs an UnrealBloomPass with threshold 0.2, so any bright /// canvas pixels get smeared into a halo. Previously the labels were /// near-white (#e2e8f0) text on a transparent background, which bloomed - /// into unreadable white blobs (issue filed by Sam 2026-04-19). The fix: + /// into unreadable white blobs (issue filed 2026-04-19). The fix: /// /// 1. A ~85%-opaque dark pill under the text so the background is /// well below the bloom threshold, stopping the halo before it @@ -446,7 +523,12 @@ export class NodeManager { }); } - animate(time: number, nodes: GraphNode[], camera: THREE.PerspectiveCamera) { + animate( + time: number, + nodes: GraphNode[], + camera: THREE.PerspectiveCamera, + brightness: number = 1.0 + ) { // Materialization animations — elastic scale-up from 0 for (let i = this.materializingNodes.length - 1; i >= 0; i--) { const mn = this.materializingNodes[i]; @@ -552,16 +634,38 @@ export class NodeManager { 1 + Math.sin(time * 1.5 + nodes.indexOf(node) * 0.5) * 0.15 * node.retention; mesh.scale.setScalar(breathe); + // Distance compensation: FogExp2 attenuates exponentially with camera + // distance, so nodes past ~80 units go nearly black unless we push + // emissive harder. Boost runs 1.0x at <60 units → ~2.4x at 200 units. + // Combined with the user brightness multiplier this gives a visible + // floor at every zoom level without blowing out close-up highlights. + const pos = this.positions.get(id); + const dist = pos ? camera.position.distanceTo(pos) : 0; + const distanceBoost = 1 + Math.min(1.4, Math.max(0, (dist - 60) / 100)); + const mat = mesh.material as THREE.MeshStandardMaterial; if (id === this.hoveredNode) { - mat.emissiveIntensity = 1.0; + mat.emissiveIntensity = 1.0 * brightness; } else if (id === this.selectedNode) { - mat.emissiveIntensity = 0.8; + mat.emissiveIntensity = 0.8 * brightness; } else { const baseIntensity = 0.3 + node.retention * 0.5; const breatheIntensity = baseIntensity + Math.sin(time * (0.8 + node.retention * 0.7)) * 0.1 * node.retention; - mat.emissiveIntensity = breatheIntensity; + mat.emissiveIntensity = breatheIntensity * brightness * distanceBoost; + } + + // Opacity also gets the distance boost (capped at 1.0) so the node + // body stays visible against the dark void at far zoom. + const baseOpacity = 0.3 + node.retention * 0.7; + mat.opacity = Math.min(1.0, baseOpacity * brightness * distanceBoost); + + // Mirror the boost onto the glow sprite so the halo tracks the core. + const glow = this.glowMap.get(id); + if (glow) { + const glowMat = glow.material as THREE.SpriteMaterial; + const baseGlow = 0.3 + node.retention * 0.35; + glowMat.opacity = Math.min(0.95, baseGlow * brightness * distanceBoost); } }); diff --git a/apps/dashboard/src/lib/graph/scene.ts b/apps/dashboard/src/lib/graph/scene.ts index 6f05cca..00713d7 100644 --- a/apps/dashboard/src/lib/graph/scene.ts +++ b/apps/dashboard/src/lib/graph/scene.ts @@ -90,8 +90,15 @@ export function createScene(container: HTMLDivElement): SceneContext { controls.dampingFactor = 0.05; controls.rotateSpeed = 0.5; controls.zoomSpeed = 0.8; - controls.minDistance = 10; - controls.maxDistance = 500; + // Distance clamps — the camera starts at ~86 units from origin + // (position.set(0, 30, 80)). The graph's force-directed layout seats + // most nodes within a ~120-unit radius. 500 was dramatically out of + // scale — the user could zoom out until every node was one pixel on + // a black starfield (issue reported 2026-04-23). 180 keeps the full + // graph in frame with nodes still readable; 12 prevents zooming inside + // a node and losing orientation. + controls.minDistance = 12; + controls.maxDistance = 180; controls.autoRotate = true; controls.autoRotateSpeed = 0.3; diff --git a/apps/dashboard/src/lib/stores/__tests__/theme.test.ts b/apps/dashboard/src/lib/stores/__tests__/theme.test.ts new file mode 100644 index 0000000..46d8e33 --- /dev/null +++ b/apps/dashboard/src/lib/stores/__tests__/theme.test.ts @@ -0,0 +1,496 @@ +/** + * Unit tests for the theme store. + * + * Scope: pure-store behavior — setter validation, cycle order, derived + * resolution, localStorage persistence + fallback, matchMedia listener + * wiring, idempotent style injection, SSR safety. + * + * Environment notes: + * - Vitest runs in Node (no jsdom). We fabricate the window / document / + * localStorage / matchMedia globals the store touches, then mock + * `$app/environment` so `browser` flips between true and false per + * test group. SSR tests leave `browser` false and verify no globals + * are touched. + * - The store caches module-level state (mediaQuery, listener, + * resolvedUnsub). We `vi.resetModules()` before every test so each + * loadTheme() returns a pristine instance. + */ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { get } from 'svelte/store'; + +// --- Controllable `browser` flag ------------------------------------------ +// vi.mock is hoisted — we reference a module-level `browserFlag` the tests +// mutate between blocks. Casting via globalThis keeps the hoist happy. +const browserState = { value: true }; +vi.mock('$app/environment', () => ({ + get browser() { + return browserState.value; + }, +})); + +// --- Fabricated DOM / storage / matchMedia -------------------------------- +// Each test's setup wires these onto globalThis so the store's `browser` +// branch can read them. They are intentionally minimal — only the methods +// theme.ts actually calls are implemented. + +type FakeMediaListener = (e: { matches: boolean }) => void; + +interface FakeMediaQueryList { + matches: boolean; + addEventListener: (type: 'change', listener: FakeMediaListener) => void; + removeEventListener: (type: 'change', listener: FakeMediaListener) => void; + // Test-only helpers + _emit: (matches: boolean) => void; + _listenerCount: () => number; +} + +function createFakeMediaQuery(initialMatches: boolean): FakeMediaQueryList { + const listeners = new Set(); + return { + matches: initialMatches, + addEventListener: (_type, listener) => { + listeners.add(listener); + }, + removeEventListener: (_type, listener) => { + listeners.delete(listener); + }, + _emit(matches: boolean) { + this.matches = matches; + for (const l of listeners) l({ matches }); + }, + _listenerCount() { + return listeners.size; + }, + }; +} + +interface FakeStorageBehavior { + throwOnGet?: boolean; + throwOnSet?: boolean; + corruptRaw?: string | null; +} + +function installFakeLocalStorage(behavior: FakeStorageBehavior = {}) { + const store = new Map(); + if (behavior.corruptRaw !== undefined && behavior.corruptRaw !== null) { + store.set('vestige.theme', behavior.corruptRaw); + } + const fake = { + getItem: (key: string) => { + if (behavior.throwOnGet) throw new Error('SecurityError: storage disabled'); + return store.has(key) ? store.get(key)! : null; + }, + setItem: (key: string, value: string) => { + if (behavior.throwOnSet) throw new Error('QuotaExceededError'); + store.set(key, value); + }, + removeItem: (key: string) => { + store.delete(key); + }, + clear: () => store.clear(), + key: () => null, + length: 0, + _store: store, // test-only peek + }; + vi.stubGlobal('localStorage', fake); + return fake; +} + +/** + * Install a fake `document` with only the APIs theme.ts calls: + * - getElementById (style-dedup check) + * - createElement('style') + * - head.appendChild + * - documentElement.dataset + * Returns handles so tests can inspect the head children and data-theme. + */ +function installFakeDocument() { + const headChildren: Array<{ id: string; textContent: string; tagName: string }> = []; + const docEl = { + dataset: {} as Record, + }; + const fakeDocument = { + getElementById: (id: string) => + headChildren.find((el) => el.id === id) ?? null, + createElement: (tag: string) => ({ + id: '', + textContent: '', + tagName: tag.toUpperCase(), + }), + head: { + appendChild: (el: { id: string; textContent: string; tagName: string }) => { + headChildren.push(el); + return el; + }, + }, + documentElement: docEl, + }; + vi.stubGlobal('document', fakeDocument); + return { fakeDocument, headChildren, docEl }; +} + +/** + * Install a fake `window` with just `matchMedia`. We keep the returned + * MQL handle so tests can emit change events. + */ +function installFakeWindow(initialPrefersDark: boolean) { + const mql = createFakeMediaQuery(initialPrefersDark); + const fakeWindow = { + matchMedia: vi.fn(() => mql), + }; + vi.stubGlobal('window', fakeWindow); + return { fakeWindow, mql }; +} + +/** + * Fresh module import. The theme store caches matchMedia/listener handles + * at module level, so every test that exercises initTheme wants a clean + * copy. Returns the full export surface. + */ +async function loadTheme() { + vi.resetModules(); + return await import('../theme'); +} + +// Baseline: every test starts with browser=true, fake window/doc/storage +// installed, and fresh module state. SSR-specific tests override these. +beforeEach(() => { + browserState.value = true; + installFakeDocument(); + installFakeWindow(true); // system prefers dark by default + installFakeLocalStorage(); +}); + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +// --------------------------------------------------------------------------- +// Export surface +// --------------------------------------------------------------------------- +describe('theme store — exports', () => { + it('exports theme writable, resolvedTheme derived, setTheme, cycleTheme, initTheme', async () => { + const mod = await loadTheme(); + expect(mod.theme).toBeDefined(); + expect(typeof mod.theme.subscribe).toBe('function'); + expect(typeof mod.theme.set).toBe('function'); + expect(mod.resolvedTheme).toBeDefined(); + expect(typeof mod.resolvedTheme.subscribe).toBe('function'); + // Derived stores do NOT expose .set — this guards against accidental + // conversion to a writable during refactors. + expect((mod.resolvedTheme as unknown as { set?: unknown }).set).toBeUndefined(); + expect(typeof mod.setTheme).toBe('function'); + expect(typeof mod.cycleTheme).toBe('function'); + expect(typeof mod.initTheme).toBe('function'); + }); + + it('theme defaults to dark before initTheme is called', async () => { + const mod = await loadTheme(); + expect(get(mod.theme)).toBe('dark'); + }); +}); + +// --------------------------------------------------------------------------- +// setTheme — input validation + persistence +// --------------------------------------------------------------------------- +describe('setTheme', () => { + it('accepts dark/light/auto and updates the store', async () => { + const { theme, setTheme } = await loadTheme(); + setTheme('light'); + expect(get(theme)).toBe('light'); + setTheme('auto'); + expect(get(theme)).toBe('auto'); + setTheme('dark'); + expect(get(theme)).toBe('dark'); + }); + + it('rejects invalid values — store is unchanged, localStorage untouched', async () => { + const { theme, setTheme } = await loadTheme(); + setTheme('light'); // seed a known value + const ls = installFakeLocalStorage(); + // Reset any prior writes so we only see what happens during the bad call. + ls._store.clear(); + + // Cast a bad value through the public API. + setTheme('midnight' as unknown as 'dark'); + expect(get(theme)).toBe('light'); // unchanged + expect(ls._store.has('vestige.theme')).toBe(false); + + setTheme('' as unknown as 'dark'); + setTheme(undefined as unknown as 'dark'); + setTheme(null as unknown as 'dark'); + expect(get(theme)).toBe('light'); + }); + + it('persists the valid value to localStorage under the vestige.theme key', async () => { + const ls = installFakeLocalStorage(); + const { setTheme } = await loadTheme(); + setTheme('auto'); + expect(ls._store.get('vestige.theme')).toBe('auto'); + }); + + it('swallows localStorage write errors (private mode / disabled storage)', async () => { + installFakeLocalStorage({ throwOnSet: true }); + const { theme, setTheme } = await loadTheme(); + // Must not throw. + expect(() => setTheme('light')).not.toThrow(); + // Store still updated even though persistence failed — UI should + // reflect the click; the next session will just start fresh. + expect(get(theme)).toBe('light'); + }); + + it('no-ops localStorage write when browser=false (SSR safety)', async () => { + browserState.value = false; + const ls = installFakeLocalStorage(); + const { theme, setTheme } = await loadTheme(); + setTheme('light'); + // Store update is still safe (pure JS object), but persistence is skipped. + expect(get(theme)).toBe('light'); + expect(ls._store.has('vestige.theme')).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// cycleTheme — dark → light → auto → dark +// --------------------------------------------------------------------------- +describe('cycleTheme', () => { + it('cycles dark → light', async () => { + const { theme, cycleTheme } = await loadTheme(); + // Default is 'dark'. + expect(get(theme)).toBe('dark'); + cycleTheme(); + expect(get(theme)).toBe('light'); + }); + + it('cycles light → auto', async () => { + const { theme, setTheme, cycleTheme } = await loadTheme(); + setTheme('light'); + cycleTheme(); + expect(get(theme)).toBe('auto'); + }); + + it('cycles auto → dark (closes the loop)', async () => { + const { theme, setTheme, cycleTheme } = await loadTheme(); + setTheme('auto'); + cycleTheme(); + expect(get(theme)).toBe('dark'); + }); + + it('full triple-click returns to the starting value', async () => { + const { theme, cycleTheme } = await loadTheme(); + const start = get(theme); + cycleTheme(); + cycleTheme(); + cycleTheme(); + expect(get(theme)).toBe(start); + }); + + it('persists each step to localStorage', async () => { + const ls = installFakeLocalStorage(); + const { cycleTheme } = await loadTheme(); + cycleTheme(); + expect(ls._store.get('vestige.theme')).toBe('light'); + cycleTheme(); + expect(ls._store.get('vestige.theme')).toBe('auto'); + cycleTheme(); + expect(ls._store.get('vestige.theme')).toBe('dark'); + }); +}); + +// --------------------------------------------------------------------------- +// resolvedTheme — derived from theme + systemPrefersDark +// --------------------------------------------------------------------------- +describe('resolvedTheme', () => { + it('dark → dark (independent of system preference)', async () => { + const { resolvedTheme, setTheme } = await loadTheme(); + setTheme('dark'); + expect(get(resolvedTheme)).toBe('dark'); + }); + + it('light → light (independent of system preference)', async () => { + const { resolvedTheme, setTheme } = await loadTheme(); + setTheme('light'); + expect(get(resolvedTheme)).toBe('light'); + }); + + it('auto + system prefers dark → dark', async () => { + const { mql } = installFakeWindow(true); + const { resolvedTheme, setTheme, initTheme } = await loadTheme(); + initTheme(); // primes systemPrefersDark from matchMedia + setTheme('auto'); + expect(mql.matches).toBe(true); + expect(get(resolvedTheme)).toBe('dark'); + }); + + it('auto + system prefers light → light', async () => { + installFakeWindow(false); + const { resolvedTheme, setTheme, initTheme } = await loadTheme(); + initTheme(); // primes systemPrefersDark=false + setTheme('auto'); + expect(get(resolvedTheme)).toBe('light'); + }); + + it('auto flips live when the matchMedia listener fires (OS changes scheme)', async () => { + const { mql } = installFakeWindow(true); + const { resolvedTheme, setTheme, initTheme } = await loadTheme(); + initTheme(); + setTheme('auto'); + expect(get(resolvedTheme)).toBe('dark'); + // OS user toggles to light mode — matchMedia fires 'change' with matches=false. + mql._emit(false); + expect(get(resolvedTheme)).toBe('light'); + // And back to dark. + mql._emit(true); + expect(get(resolvedTheme)).toBe('dark'); + }); +}); + +// --------------------------------------------------------------------------- +// initTheme — idempotence, teardown, localStorage hydration +// --------------------------------------------------------------------------- +describe('initTheme', () => { + it('returns a teardown function', async () => { + const { initTheme } = await loadTheme(); + const teardown = initTheme(); + expect(typeof teardown).toBe('function'); + teardown(); + }); + + it('injects exactly one diff --git a/apps/dashboard/src/routes/(app)/duplicates/+page.svelte b/apps/dashboard/src/routes/(app)/duplicates/+page.svelte new file mode 100644 index 0000000..28860f9 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/duplicates/+page.svelte @@ -0,0 +1,387 @@ + + + +

          + +
          +

          + Memory Hygiene — Duplicate Detection +

          +

          + Cosine-similarity clustering over embeddings. Merges reinforce the winner's FSRS state; + losers inherit into the merged node. Dismissed clusters are hidden for this session only. +

          +
          + + +
          + + + + +
          + {#if loading} + + Detecting… + {:else if error} + + Error + {:else} + + + {visibleClusters.length} + {visibleClusters.length === 1 ? 'cluster' : 'clusters'}, + {totalDuplicates} potential duplicate{totalDuplicates === 1 ? '' : 's'} + + {/if} +
          + + +
          + + + {#if error} +
          +
          Couldn't detect duplicates
          +
          {error}
          + +
          + {:else if loading} +
          + {#each Array(3) as _} +
          + {/each} +
          + {:else if visibleClusters.length === 0} +
          +
          ·
          +
          + No duplicates found above threshold. +
          +
          Memory is clean.
          +
          + {:else} +
          + {#if overflowed} +
          + Showing first {CLUSTER_RENDER_CAP} of {visibleClusters.length} clusters. Raise the + threshold to narrow results. +
          + {/if} + {#each renderedClusters as { c, key } (key)} +
          + dismissCluster(key)} + onMerge={(winnerId, loserIds) => mergeCluster(key, winnerId, loserIds)} + /> +
          + {/each} +
          + {/if} +
          + + diff --git a/apps/dashboard/src/routes/(app)/graph/+page.svelte b/apps/dashboard/src/routes/(app)/graph/+page.svelte index 28842a2..1df2683 100644 --- a/apps/dashboard/src/routes/(app)/graph/+page.svelte +++ b/apps/dashboard/src/routes/(app)/graph/+page.svelte @@ -7,9 +7,10 @@ import MemoryStateLegend from '$components/MemoryStateLegend.svelte'; import { api } from '$stores/api'; import { eventFeed } from '$stores/websocket'; + import { graphState } from '$stores/graph-state.svelte'; import type { GraphResponse, GraphNode, GraphEdge, Memory } from '$types'; import type { GraphMutation } from '$lib/graph/events'; - import type { ColorMode } from '$lib/graph/nodes'; + import { AHAGRAPH_COLORS, AHAGRAPH_DESCRIPTIONS, type ColorMode } from '$lib/graph/nodes'; import { filterByDate } from '$lib/graph/temporal'; let graphData: GraphResponse | null = $state(null); @@ -21,10 +22,12 @@ let maxNodes = $state(150); let temporalEnabled = $state(false); let temporalDate = $state(new Date()); - // v2.0.8: colour spheres by node type (default) or by FSRS memory state - // (Active / Dormant / Silent / Unavailable). Legend overlay renders when - // state mode is active. + // Colour spheres by node type, FSRS memory state, or AhaGraph learning tags. let colorMode: ColorMode = $state('type'); + const ahagraphLegendEntries = Object.entries(AHAGRAPH_COLORS) as Array<[ + keyof typeof AHAGRAPH_COLORS, + string + ]>; // Live counts that update on mutations let liveNodeCount = $state(0); @@ -77,43 +80,97 @@ } } - onMount(() => loadGraph()); + onMount(() => { + const requestedMode = new URLSearchParams(window.location.search).get('colorMode'); + if (isColorMode(requestedMode)) { + colorMode = requestedMode; + } + void loadGraph(); + }); + + function isColorMode(value: string | null): value is ColorMode { + return value === 'type' || value === 'state' || value === 'ahagraph'; + } async function loadGraph(query?: string, centerId?: string) { loading = true; error = ''; try { + const isDefault = !query && !centerId; graphData = await api.graph({ max_nodes: maxNodes, depth: 3, query: query || undefined, - center_id: centerId || undefined + center_id: centerId || undefined, + // Center on the newest memory by default. Prevents the old + // "most-connected" behaviour from clustering on historical + // hotspots and hiding today's memories behind the 150-node + // cap. Future UI toggle can flip this to 'connected'. + sort: isDefault ? 'recent' : undefined }); + + // Fallback: if the newest memory is isolated (1 node, 0 edges), + // fall back to the connected hotspot so the user sees context + // instead of a lonely orb. Only applies to the default load — + // explicit queries/centerId honor the user's choice even if the + // subgraph is sparse. + if ( + isDefault && + graphData && + graphData.nodeCount <= 1 && + graphData.edgeCount === 0 + ) { + const connected = await api.graph({ + max_nodes: maxNodes, + depth: 3, + sort: 'connected' + }); + if (connected && connected.nodeCount > graphData.nodeCount) { + graphData = connected; + } + } + if (graphData) { liveNodeCount = graphData.nodeCount; liveEdgeCount = graphData.edgeCount; } } catch (e) { - // Distinguish "cold-start / empty database" from "actual API failure". - // Before v2.0.7 both surfaced as "No memories yet..." which masked - // real errors (network down, dashboard disabled, 500s) and looked - // identical to a first-run install. Split the two so debugging - // isn't a guessing game. - // - // Sanitize the error string before rendering: strip filesystem - // paths and crate-file references (the backend occasionally wraps - // raw rusqlite / fs errors) and cap length at 200 chars so a - // stack-trace-sized error doesn't dominate the page. + // Distinguish three failure modes so the error message is actually + // helpful. Before: all failures (backend offline, empty DB, real + // 500) rendered identical cryptic text. That made the dashboard + // look broken on first-run or on backend-down, when the root + // cause is ALWAYS "the MCP server isn't running." + // (1) Backend offline — vite dev proxy returns 500 with no body + // (upstream EHOSTUNREACH / ECONNREFUSED). Surface clearly: + // tell the user to start vestige-mcp. + // (2) Empty database — fresh install, no memories yet. Happy + // first-run state, not an error. + // (3) Real backend error — a genuine 500 with a response body, + // or a 4xx with content. Show the sanitized upstream msg. const rawMsg = e instanceof Error ? e.message : String(e); const safeMsg = rawMsg .replace(/\/[\w./-]+\.(sqlite|rs|db|toml|lock)\b/g, '[path]') .slice(0, 200); + + // Network-level failure: fetch itself rejects (TypeError) OR vite + // proxy passes back a body-less 500 when upstream :3927 is + // unreachable. Both mean "backend offline." + const isBackendOffline = + e instanceof TypeError || + /failed to fetch|NetworkError|load failed/i.test(rawMsg) || + /^API 500:?\s*(Internal Server Error)?\s*$/i.test(rawMsg.trim()); + const isEmpty = (graphData?.nodeCount ?? 0) === 0 && /not found|404|empty|no memor/i.test(rawMsg); - error = isEmpty - ? 'No memories yet. Start using Vestige to populate your graph.' - : `Failed to load graph: ${safeMsg}`; + + if (isBackendOffline) { + error = 'OFFLINE'; + } else if (isEmpty) { + error = 'EMPTY'; + } else { + error = `Failed to load graph: ${safeMsg}`; + } } finally { loading = false; } @@ -149,6 +206,40 @@

          Loading memory graph...

          + {:else if error === 'OFFLINE'} +
          +
          +
          +

          MCP Backend Offline

          +

          + The Vestige MCP server isn't reachable on :3927. + The dashboard is running but has nothing to query. +

          +
          +
          Start the backend:
          + nohup bash -c 'tail -f /dev/null | VESTIGE_DASHBOARD_ENABLED=true ~/.local/bin/vestige-mcp' > /tmp/vestige.log 2>&1 & +disown +
          +
          + + + Try demos (no backend needed) + +
          +
          +
          + {:else if error === 'EMPTY'} +
          +
          +
          +

          Your Mind Awaits

          +

          No memories yet. Start using Vestige to populate your graph.

          +
          +
          {:else if error}
          @@ -213,6 +304,16 @@ > State +
          @@ -224,6 +325,27 @@ + + + + ⌘/Ctrl + Enter + {#if scoreError} + {scoreError} + {/if} +
          +
          + + +
          + {#if score} +
          +
          Composite
          +
          + {(score.composite * 100).toFixed(0)}% +
          +
          + {#key radarKey} + + {/key} + + + {#if score.composite > 0.6} +
          +
          ✓ Save
          +

          + Composite {(score.composite * 100).toFixed(0)}% > 60% threshold. + {#if topChannel} + Driven by {topChannel.key} — {CHANNEL_BLURBS[topChannel.key].high}. + {/if} +

          +
          + {:else} +
          +
          ⨯ Skip
          +

          + Composite {(score.composite * 100).toFixed(0)}% < 60% threshold. + {#if weakestChannel} + Weakest channel: {weakestChannel} — {CHANNEL_BLURBS[weakestChannel].low}. + {/if} +

          +
          + {/if} + {:else} +
          +
          +

          Type some content above to score its importance.

          +

          + Composite = 0.25·novelty + 0.30·arousal + 0.25·reward + 0.20·attention. + Threshold for save: 60%. +

          +
          + {/if} +
          + + + + +
          +
          +
          +

          + Top Important Memories This Week +

          +

          + Ranked by retention × reviews ÷ age. Click any card to open it. +

          +
          + +
          + + {#if loadingMemories} +
          + {#each Array(6) as _} +
          + {/each} +
          + {:else if memories.length === 0} +
          +

          No memories yet.

          +
          + {:else} +
          + {#each memories as memory (memory.id)} + {@const ch = perMemoryScores[memory.id]} + + {/each} +
          + {/if} +
          + diff --git a/apps/dashboard/src/routes/(app)/memories/+page.svelte b/apps/dashboard/src/routes/(app)/memories/+page.svelte index e1000cd..03e9808 100644 --- a/apps/dashboard/src/routes/(app)/memories/+page.svelte +++ b/apps/dashboard/src/routes/(app)/memories/+page.svelte @@ -3,6 +3,7 @@ import { api } from '$stores/api'; import type { Memory } from '$types'; import { NODE_TYPE_COLORS } from '$types'; + import MemoryAuditTrail from '$lib/components/MemoryAuditTrail.svelte'; let memories: Memory[] = $state([]); let searchQuery = $state(''); @@ -11,6 +12,9 @@ let minRetention = $state(0); let loading = $state(true); let selectedMemory: Memory | null = $state(null); + // Which inner tab of the expanded card is active. Keyed by memory id so + // switching between cards remembers each one's last view independently. + let expandedTab: Record = $state({}); let debounceTimer: ReturnType; onMount(() => loadMemories()); @@ -116,13 +120,45 @@ {#if selectedMemory?.id === memory.id} + {@const activeTab = expandedTab[memory.id] ?? 'content'}
          -

          {memory.content}

          -
          -
          Storage: {(memory.storageStrength * 100).toFixed(1)}%
          -
          Retrieval: {(memory.retrievalStrength * 100).toFixed(1)}%
          -
          Created: {new Date(memory.createdAt).toLocaleDateString()}
          + +
          + { e.stopPropagation(); expandedTab[memory.id] = 'content'; }} + onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); expandedTab[memory.id] = 'content'; } }} + class="px-3 py-1.5 rounded-lg cursor-pointer select-none transition + {activeTab === 'content' ? 'bg-synapse/20 text-synapse-glow border border-synapse/40' : 'bg-white/[0.03] text-dim hover:text-text border border-transparent'}" + >Content + { e.stopPropagation(); expandedTab[memory.id] = 'audit'; }} + onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.stopPropagation(); expandedTab[memory.id] = 'audit'; } }} + class="px-3 py-1.5 rounded-lg cursor-pointer select-none transition + {activeTab === 'audit' ? 'bg-synapse/20 text-synapse-glow border border-synapse/40' : 'bg-white/[0.03] text-dim hover:text-text border border-transparent'}" + >Audit Trail
          + + {#if activeTab === 'content'} +

          {memory.content}

          +
          +
          Storage: {(memory.storageStrength * 100).toFixed(1)}%
          +
          Retrieval: {(memory.retrievalStrength * 100).toFixed(1)}%
          +
          Created: {new Date(memory.createdAt).toLocaleDateString()}
          +
          + {:else} +
          e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + > + +
          + {/if} +
          { e.stopPropagation(); api.memories.promote(memory.id); }} onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); api.memories.promote(memory.id); } }} diff --git a/apps/dashboard/src/routes/(app)/patterns/+page.svelte b/apps/dashboard/src/routes/(app)/patterns/+page.svelte new file mode 100644 index 0000000..e436b12 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/patterns/+page.svelte @@ -0,0 +1,567 @@ + + + +
          + +
          +

          Cross-Project Intelligence

          +

          Patterns learned here, applied there.

          +
          + + +
          + + {#each CATEGORIES as cat (cat)} + + {/each} +
          + + {#if error} +
          +
          Couldn't load pattern transfers
          +
          {error}
          + +
          + {:else if loading} +
          +
          +
          +
          + {:else} + +
          + +
          + + + {#if selectedCell} +
          +
          + Filtered to + {selectedCell.from} + + {selectedCell.to} +
          + +
          + {/if} +
          + + + +
          + + +
          +
          + {patternCount} + pattern{patternCount === 1 ? '' : 's'} across + {projectCount} + project{projectCount === 1 ? '' : 's'}, + {totalTransfers} + total transfer{totalTransfers === 1 ? '' : 's'} +
          +
          + {activeCategory === 'All' ? 'All categories' : activeCategory} +
          +
          + {/if} +
          diff --git a/apps/dashboard/src/routes/(app)/reasoning/+page.svelte b/apps/dashboard/src/routes/(app)/reasoning/+page.svelte new file mode 100644 index 0000000..2a208b2 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/reasoning/+page.svelte @@ -0,0 +1,668 @@ + + + + Reasoning Theater · Vestige + + +
          + +
          +
          + +

          Reasoning Theater

          + + deep_reference + +
          +

          + Watch Vestige reason. Your query runs the 8-stage cognitive pipeline — broad retrieval, + spreading activation, FSRS trust scoring, intent classification, supersession, contradiction + analysis, relation assessment, template reasoning — and returns a pre-built answer with + trust-scored evidence. +

          +
          + + +
          +
          + + e.key === 'Enter' && ask()} + placeholder="Ask your memory anything..." + class="flex-1 bg-transparent text-bright text-lg placeholder:text-muted focus:outline-none font-mono" + /> + + +
          + + {#if !response && !loading} +
          + Try + {#each exampleQueries as ex} + + {/each} +
          + {/if} +
          + + + {#if error} +
          + Error: + {error} +
          + {/if} + + + {#if loading} +
          +
          + + Running cognitive pipeline +
          + +
          + {/if} + + + {#if response && !loading} + {@const conf = response.confidence} + {@const confColor = confidenceColor(conf)} + + + {#if response.reasoning} +
          +
          +

          + + Reasoning +

          +
          + intent: {response.intent} + · + {response.memoriesAnalyzed} analyzed + · + {conf}% {confidenceLabel(conf)} +
          +
          +
          {response.reasoning}
          +
          + {/if} + + +
          + +
          + Confidence +
          + + {conf}% + +
          + + {confidenceLabel(conf)} + + + + + + + + +
          + intent: {response.intent} + · + {response.memoriesAnalyzed} analyzed +
          +
          + + +
          +
          + Primary Source + + #{response.recommended.memory_id.slice(0, 8)} + +
          +

          {response.recommended.answer_preview}

          +
          + + + Trust {(response.recommended.trust_score * 100).toFixed(0)}% + + · + {new Date(response.recommended.date).toLocaleDateString()} +
          +
          +
          + + +
          +

          + + Cognitive Pipeline +

          +
          + +
          +
          + + +
          +
          +

          + + Evidence + ({response.evidence.length}) +

          +
          + + primary + + + supporting + + + contradicting + + + superseded + +
          +
          + +
          + {#each response.evidence as ev, i (ev.id)} + + {/each} + + + {#if arcs.length > 0} + + {/if} +
          +
          + + + {#if response.contradictions.length > 0} +
          +

          + + Contradictions Detected + ({response.contradictions.length}) +

          +
          + {#each response.contradictions as c, i} +
          + +
          +
          + #{c.a_id.slice(0, 8)} + + #{c.b_id.slice(0, 8)} +
          +

          {c.summary}

          +
          + pair {i + 1} +
          + {/each} +
          +
          + {/if} + + + {#if response.superseded.length > 0} +
          +

          + + Superseded + ({response.superseded.length}) +

          +
          + {#each response.superseded as s} +
          + #{s.old_id.slice(0, 8)} + + #{s.new_id.slice(0, 8)} + {s.reason} +
          + {/each} +
          +
          + {/if} + + +
          + {#if response.evolution.length > 0} +
          +

          + + Evolution +

          +
          + {#each response.evolution as ev} +
          + + {new Date(ev.date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} + + + {ev.summary} +
          + {/each} +
          +
          + {/if} + + {#if response.related_insights.length > 0} +
          +

          + + Related Insights +

          +
          + {#each response.related_insights as ins} +

          + {ins} +

          + {/each} +
          +
          + {/if} +
          + {/if} + + + {#if !response && !loading && !error} +
          +
          +

          + Ask anything. Vestige will run the full reasoning pipeline and show you its work. +

          +

          + 8-stage pipeline: retrieval → rerank → activation → trust-score → supersession → + contradiction → relations → chain. Zero LLM calls, 100% local. +

          +
          + {/if} +
          + + diff --git a/apps/dashboard/src/routes/(app)/schedule/+page.svelte b/apps/dashboard/src/routes/(app)/schedule/+page.svelte new file mode 100644 index 0000000..781b284 --- /dev/null +++ b/apps/dashboard/src/routes/(app)/schedule/+page.svelte @@ -0,0 +1,252 @@ + + +
          +
          +
          +

          Review Schedule

          +

          FSRS-6 next-review dates across your memory corpus

          +
          +
          + {#each FILTERS as f} + + {/each} +
          +
          + + {#if !loading && !errored && truncated} +
          + Showing the first {memories.length.toLocaleString()} of {totalMemories.toLocaleString()} memories. + Schedule reflects this slice only. +
          + {/if} + + {#if loading} +
          +
          +
          +
          + {#each Array(42) as _} +
          + {/each} +
          +
          +
          + {#each Array(5) as _} +
          + {/each} +
          +
          + {:else if errored} +
          +

          API unavailable.

          +

          Could not fetch memories from /api/memories.

          +
          + {:else if scheduled.length === 0} +
          +
          +

          FSRS review schedule not yet populated.

          +

          + None of your {memories.length} memor{memories.length === 1 ? 'y has' : 'ies have'} a + nextReviewAt timestamp yet. Run consolidation to compute + next-review dates via FSRS-6. +

          + +
          + {:else} +
          + +
          + +
          + + + +
          + {/if} +
          diff --git a/apps/dashboard/src/routes/(app)/settings/+page.svelte b/apps/dashboard/src/routes/(app)/settings/+page.svelte index 90d141e..f7a82eb 100644 --- a/apps/dashboard/src/routes/(app)/settings/+page.svelte +++ b/apps/dashboard/src/routes/(app)/settings/+page.svelte @@ -1,7 +1,29 @@ - - - - +{#if isMarketingRoute} + {@render children()} +{:else} + + + + - - -
          - - - -
          -
          - {@render children()} -
          -
          + +
          + + +
          + {@render children()} +
          +
          - -
          + +
          + + + +{/if} -{#if showCommandPalette} +{#if showCommandPalette && !isMarketingRoute}
          + import { onMount } from 'svelte'; + import { base } from '$app/paths'; + + type SubmitState = 'idle' | 'submitting' | 'success' | 'error'; + type SupportMessage = { + role: 'bot' | 'user'; + content: string; + }; + + let canvas: HTMLCanvasElement; + let name = $state(''); + let email = $state(''); + let role = $state('solo'); + let priority = $state('sync'); + let notes = $state(''); + let companySite = $state(''); + let submitState = $state('idle'); + let submitMessage = $state(''); + let botQuestion = $state(''); + let botBusy = $state(false); + let botMessages = $state([ + { + role: 'bot', + content: 'Ask me about installing Vestige, whether heavy models are required, Solo vs Team Pro, sync, pricing, or what happens after you join the June list.' + } + ]); + + const waitlistEndpoint = import.meta.env.VITE_WAITLIST_ENDPOINT as string | undefined; + const supportBotEndpoint = import.meta.env.VITE_SUPPORT_BOT_ENDPOINT as string | undefined; + + const proofPoints = [ + { value: 'Local', label: 'SQLite memory, no hosted memory service' }, + { value: 'MCP', label: 'Claude Code, Cursor, Cline, Codex, Goose' }, + { value: 'June', label: 'Pro sync, backup, team memory early access' } + ]; + + const proTracks = [ + { + name: 'Solo Pro', + accent: '#22c55e', + copy: 'Multi-device sync, encrypted backups, managed updates, and a cleaner memory dashboard for developers living inside AI coding agents.' + }, + { + name: 'Team Pro', + accent: '#06b6d4', + copy: 'Shared project memory, admin review, audit trails, PostgreSQL-backed deployments, and async support for engineering teams.' + } + ]; + + const launchPillars = [ + 'Private by default', + 'Sync without lock-in', + 'Team memory controls', + 'Bot-assisted support' + ]; + + const supportPrompts = [ + { label: 'Install', prompt: 'How do I install Vestige and connect it to Claude Code?' }, + { label: 'No 20GB?', prompt: 'Do I need the Sanhedrin model or 20GB of RAM?' }, + { label: 'Solo vs Team', prompt: 'Should I choose Solo Pro or Team Pro?' }, + { label: 'Sync', prompt: 'How will Pro sync and backups work?' }, + { label: 'Pricing', prompt: 'How much will Vestige Pro cost?' }, + { label: 'Human help', prompt: 'When does a human get involved?' } + ]; + + onMount(() => { + const rawContext = canvas.getContext('2d'); + if (!rawContext) return; + const context: CanvasRenderingContext2D = rawContext; + + let frame = 0; + let width = 0; + let height = 0; + const nodeCount = 62; + const nodes = Array.from({ length: nodeCount }, (_, index) => ({ + x: Math.random(), + y: Math.random(), + vx: (Math.random() - 0.5) * 0.00016, + vy: (Math.random() - 0.5) * 0.00016, + phase: Math.random() * Math.PI * 2, + kind: index % 5 + })); + + function resize() { + const dpr = Math.min(window.devicePixelRatio || 1, 2); + width = window.innerWidth; + height = window.innerHeight; + canvas.width = Math.floor(width * dpr); + canvas.height = Math.floor(height * dpr); + canvas.style.width = `${width}px`; + canvas.style.height = `${height}px`; + context.setTransform(dpr, 0, 0, dpr, 0, 0); + } + + function draw(time: number) { + context.clearRect(0, 0, width, height); + const gradient = context.createLinearGradient(0, 0, width, height); + gradient.addColorStop(0, '#07100f'); + gradient.addColorStop(0.45, '#0b1221'); + gradient.addColorStop(1, '#15100a'); + context.fillStyle = gradient; + context.fillRect(0, 0, width, height); + + context.strokeStyle = 'rgba(148, 163, 184, 0.08)'; + context.lineWidth = 1; + for (let x = 0; x < width; x += 72) { + context.beginPath(); + context.moveTo(x, 0); + context.lineTo(x + Math.sin(time / 3000 + x) * 12, height); + context.stroke(); + } + for (let y = 0; y < height; y += 72) { + context.beginPath(); + context.moveTo(0, y); + context.lineTo(width, y + Math.cos(time / 3300 + y) * 12); + context.stroke(); + } + + for (const node of nodes) { + node.x += node.vx; + node.y += node.vy; + if (node.x < 0.04 || node.x > 0.96) node.vx *= -1; + if (node.y < 0.06 || node.y > 0.94) node.vy *= -1; + } + + for (let i = 0; i < nodes.length; i++) { + const a = nodes[i]; + const ax = a.x * width; + const ay = a.y * height; + for (let j = i + 1; j < nodes.length; j++) { + const b = nodes[j]; + const bx = b.x * width; + const by = b.y * height; + const dx = ax - bx; + const dy = ay - by; + const distance = Math.sqrt(dx * dx + dy * dy); + if (distance < 168) { + const alpha = (1 - distance / 168) * 0.18; + context.strokeStyle = `rgba(34, 197, 94, ${alpha})`; + context.beginPath(); + context.moveTo(ax, ay); + context.lineTo(bx, by); + context.stroke(); + } + } + } + + const colors = ['#22c55e', '#06b6d4', '#f59e0b', '#ef4444', '#a3e635']; + for (const node of nodes) { + const pulse = 0.5 + Math.sin(time / 900 + node.phase) * 0.5; + const x = node.x * width; + const y = node.y * height; + context.fillStyle = colors[node.kind]; + context.globalAlpha = 0.45 + pulse * 0.35; + context.beginPath(); + context.arc(x, y, 1.6 + pulse * 1.8, 0, Math.PI * 2); + context.fill(); + context.globalAlpha = 1; + } + + frame = requestAnimationFrame(draw); + } + + resize(); + window.addEventListener('resize', resize); + frame = requestAnimationFrame(draw); + + return () => { + cancelAnimationFrame(frame); + window.removeEventListener('resize', resize); + }; + }); + + function githubWaitlistUrl() { + const body = [ + '## Vestige Pro waitlist', + '', + `Plan: ${role}`, + `Priority: ${priority}`, + notes.trim() ? `Use case: ${notes.trim()}` : 'Use case:', + '', + 'Please do not include private email addresses in this public issue.' + ].join('\n'); + + return `https://github.com/samvallad33/vestige/issues/new?title=${encodeURIComponent('Vestige Pro waitlist')}&body=${encodeURIComponent(body)}`; + } + + async function joinWaitlist(event: SubmitEvent) { + event.preventDefault(); + submitState = 'submitting'; + submitMessage = ''; + + if (companySite.trim()) { + submitState = 'success'; + submitMessage = 'You are on the list.'; + return; + } + + if (!email.includes('@')) { + submitState = 'error'; + submitMessage = 'Enter an email so the early-access invite can reach you.'; + return; + } + + const payload = { + name: name.trim(), + email: email.trim(), + plan: role, + priority, + notes: notes.trim(), + source: 'vestige-pro-waitlist', + createdAt: new Date().toISOString() + }; + + if (!waitlistEndpoint) { + submitState = 'success'; + submitMessage = 'Email capture is ready for an endpoint. Opening the GitHub waitlist fallback with your email omitted.'; + window.open(githubWaitlistUrl(), '_blank', 'noopener,noreferrer'); + return; + } + + try { + const response = await fetch(waitlistEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!response.ok) throw new Error(`Waitlist endpoint returned ${response.status}`); + submitState = 'success'; + submitMessage = 'You are on the June early-access list.'; + name = ''; + email = ''; + notes = ''; + } catch (error) { + submitState = 'error'; + submitMessage = error instanceof Error + ? error.message + : 'The waitlist endpoint did not accept the request.'; + } + } + + function localSupportAnswer(question: string) { + const query = question.toLowerCase(); + + if (/(install|setup|onboard|claude|cursor|cline|codex|connect)/.test(query)) { + return [ + 'Start with the open-source install:', + '1. `npm install -g vestige-mcp-server@latest`', + '2. Claude Code: `claude mcp add vestige vestige-mcp -s user`', + '3. Codex: `codex mcp add vestige -- vestige-mcp`', + 'Then test it by asking your agent to remember a preference, opening a fresh session, and asking for that preference back.' + ].join('\n'); + } + + if (/(sanhedrin|20gb|20 gb|ram|heavy|model|mlx|preflight|hook)/.test(query)) { + return 'No. The default Vestige path is the local MCP memory server. Sanhedrin, preflight hooks, and large local verifier models are optional. Pro should keep that promise: nobody should need a 20GB machine just to use memory.'; + } + + if (/(solo|team|plan|seat|buying)/.test(query)) { + return 'Choose Solo Pro if you want your own multi-device memory, backups, smoother updates, and personal support. Choose Team Pro if multiple people need shared project memory, admin controls, PostgreSQL-backed storage, audit trails, or team onboarding.'; + } + + if (/(sync|backup|device|dropbox|icloud|syncthing|postgres|postgresql|pg|central)/.test(query)) { + return 'Open-source Vestige should stay local-first. Pro is where guided sync, encrypted backups, conflict handling, and Team Pro PostgreSQL-backed storage belong. The important design rule: users own memory and can export it.'; + } + + if (/(price|pricing|cost|pay|billing|stripe|lemon|subscription|monthly|yearly)/.test(query)) { + return 'Pricing is not final yet. The current plan is simple: Solo Pro for individual developers, Team Pro for engineering teams. Join the waitlist so early users can shape pricing before the June launch.'; + } + + if (/(update|upgrade|curl|reinstall|version)/.test(query)) { + return 'Use `vestige update` for existing installs. The goal is that users should not need to keep copying curl commands just to stay current.'; + } + + if (/(privacy|local|cloud|telemetry|data|where.*stored|sqlite)/.test(query)) { + return 'Vestige core stores memory locally in SQLite and does not need a hosted memory service. Pro should add convenience around sync, backup, and teams without turning private local memory into a black box.'; + } + + if (/(support|bot|human|email|question|help|available|awake|discord)/.test(query)) { + return 'The support bot should answer common install, sync, plan, and onboarding questions instantly. Hard cases should escalate with context so a human teammate only handles the issues that actually need human judgment.'; + } + + if (/(waitlist|june|early|launch|invite|after)/.test(query)) { + return 'After you join the waitlist, the June early-access flow should invite you into the right lane: Solo Pro for personal memory, Team Pro for shared memory and admin controls. The bot will keep onboarding answers available while the launch scales.'; + } + + return 'I can help with install, updates, optional heavy models, Solo vs Team Pro, sync, backups, privacy, pricing, and support escalation. For now, the fastest next step is to join the waitlist and include your use case so the June onboarding can prioritize the right workflows.'; + } + + async function askSupportBot(event?: SubmitEvent, prompt?: string) { + event?.preventDefault(); + const question = (prompt ?? botQuestion).trim(); + if (!question || botBusy) return; + + botQuestion = ''; + botBusy = true; + botMessages = [...botMessages, { role: 'user', content: question }]; + + if (!supportBotEndpoint) { + botMessages = [...botMessages, { role: 'bot', content: localSupportAnswer(question) }]; + botBusy = false; + return; + } + + try { + const response = await fetch(supportBotEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + question, + plan: role, + priority, + source: 'vestige-pro-waitlist', + history: botMessages.slice(-6) + }) + }); + if (!response.ok) throw new Error(`Support bot endpoint returned ${response.status}`); + const data = await response.json(); + botMessages = [ + ...botMessages, + { role: 'bot', content: String(data.answer ?? data.message ?? localSupportAnswer(question)) } + ]; + } catch { + botMessages = [ + ...botMessages, + { role: 'bot', content: localSupportAnswer(question) } + ]; + } finally { + botBusy = false; + } + } + + + + Vestige Pro Waitlist + + + +
          + + + +
          + + V + Vestige Pro + + +
          + +
          +
          +
          +

          June early access

          +

          Vestige Pro

          +

          + The paid layer for developers and teams who already trust Vestige as local memory for AI agents. + Sync, backups, team memory, and bot-assisted support come next. +

          + + + +
          + {#each proofPoints as point} +
          + {point.value} + {point.label} +
          + {/each} +
          +
          + +
          +
          +

          Early access

          +

          Reserve a Pro seat

          +
          + + + + + + + + + + + + + + + + {#if submitMessage} +

          + {submitMessage} +

          + {/if} +
          +
          + +
          + {#each launchPillars as pillar} +
          {pillar}
          + {/each} +
          + +
          +
          +

          Why Pro exists

          +

          Two plans. One promise: agent memory you can depend on.

          +
          +
          + {#each proTracks as track} +
          +
          +

          {track.name}

          +

          {track.copy}

          +
          + {/each} +
          +
          + +
          +
          +

          Always-on answers

          +

          The support bot handles the first wave.

          +

          + This is the first support layer: instant onboarding answers before anyone has to write an email. + It can run locally from the FAQ now and call a hosted support endpoint later. +

          +
          +
          +
          + + Onboarding bot + {supportBotEndpoint ? 'Connected' : 'FAQ mode'} +
          + +
          + {#each botMessages as message} +
          + {#each message.content.split('\n') as line} +

          {line}

          + {/each} +
          + {/each} + {#if botBusy} +
          +

          Checking the onboarding notes...

          +
          + {/if} +
          + +
          + {#each supportPrompts as prompt} + + {/each} +
          + +
          + + +
          +
          +
          + +
          +
          +

          May to June

          +

          The plan is simple.

          +
          +
            +
          1. + May + Get Vestige into every MCP, Claude Code, Cursor, local AI, Rust, and self-hosted channel that cares about agent memory. +
          2. +
          3. + June + Invite the first Solo Pro and Team Pro users into sync, backups, shared memory, PostgreSQL-backed deployments, and bot-assisted support. +
          4. +
          5. + After + Use paid feedback to turn Vestige from a beloved local tool into durable agent-memory infrastructure. +
          6. +
          +
          +
          +
          + + diff --git a/apps/dashboard/svelte.config.js b/apps/dashboard/svelte.config.js index 4298fa7..731bd4a 100644 --- a/apps/dashboard/svelte.config.js +++ b/apps/dashboard/svelte.config.js @@ -1,6 +1,8 @@ import adapter from '@sveltejs/adapter-static'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; +const appVersion = process.env.VESTIGE_DASHBOARD_VERSION ?? process.env.npm_package_version ?? 'dev'; + /** @type {import('@sveltejs/kit').Config} */ const config = { preprocess: vitePreprocess(), @@ -15,6 +17,9 @@ const config = { paths: { base: '/dashboard' }, + version: { + name: appVersion + }, alias: { $lib: 'src/lib', $components: 'src/lib/components', diff --git a/assets/vestige-icon.png b/assets/vestige-icon.png new file mode 100644 index 0000000..2f7deaa Binary files /dev/null and b/assets/vestige-icon.png differ diff --git a/crates/vestige-core/Cargo.toml b/crates/vestige-core/Cargo.toml index 0d02e0b..e84625e 100644 --- a/crates/vestige-core/Cargo.toml +++ b/crates/vestige-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vestige-core" -version = "2.0.7" +version = "2.1.22" edition = "2024" rust-version = "1.91" authors = ["Vestige Team"] @@ -11,36 +11,51 @@ keywords = ["memory", "spaced-repetition", "fsrs", "embeddings", "knowledge-grap categories = ["science", "database"] [features] -default = ["embeddings", "vector-search", "bundled-sqlite"] +default = ["embeddings", "ort-download", "vector-search", "bundled-sqlite"] # SQLite backend (default, unencrypted) bundled-sqlite = ["rusqlite/bundled"] # Encrypted SQLite via SQLCipher (mutually exclusive with bundled-sqlite) -# Use: --no-default-features --features encryption,embeddings,vector-search +# Use: --no-default-features --features encryption,embeddings,ort-download,vector-search # Set VESTIGE_ENCRYPTION_KEY env var to enable encryption encryption = ["rusqlite/bundled-sqlcipher"] -# Core embeddings with fastembed (ONNX-based, local inference) -# Downloads a pre-built ONNX Runtime binary at build time (requires glibc >= 2.38) -embeddings = ["dep:fastembed", "fastembed/ort-download-binaries-native-tls"] +# Embedding code paths (fastembed dep, hf-hub, image-models). This feature +# enables the #[cfg(feature = "embeddings")] gates throughout the crate but +# does NOT pick an ort backend. Pair with EXACTLY ONE of `ort-download` +# (prebuilt ONNX Runtime, default) or `ort-dynamic` (runtime-linked system +# libonnxruntime, required on targets without prebuilts). +embeddings = ["dep:fastembed", "fastembed/hf-hub-native-tls", "fastembed/image-models"] + +# Default ort backend: ort-sys downloads prebuilt ONNX Runtime at build time. +# Requires glibc >= 2.38. Fails on x86_64-apple-darwin (Microsoft is +# discontinuing Intel Mac prebuilts after ONNX Runtime v1.23.0). +ort-download = ["embeddings", "fastembed/ort-download-binaries-native-tls"] # HNSW vector search with USearch (20x faster than FAISS) vector-search = ["dep:usearch"] -# Use runtime-loaded ORT instead of the downloaded pre-built binary. -# Required on systems with glibc < 2.38 (Ubuntu 22.04, Debian 12, RHEL/Rocky 9). -# Mutually exclusive with the default `embeddings` feature's download strategy. -# Usage: --no-default-features --features ort-dynamic,vector-search,bundled-sqlite -# Runtime requirement: libonnxruntime.so must be on LD_LIBRARY_PATH or ORT_DYLIB_PATH set. -ort-dynamic = ["dep:fastembed", "fastembed/ort-load-dynamic", "fastembed/hf-hub-native-tls", "fastembed/image-models"] +# Alternative ort backend: runtime-linked against a system libonnxruntime via +# dlopen. Required on Intel Mac and on systems with glibc < 2.38 (Ubuntu +# 22.04, Debian 12, RHEL/Rocky 9). Transitively enables `embeddings` so the +# #[cfg] gates stay active. +# +# Usage: cargo build --no-default-features \ +# --features ort-dynamic,vector-search,bundled-sqlite +# Runtime: export ORT_DYLIB_PATH=/path/to/libonnxruntime.{dylib,so} +# (e.g. $(brew --prefix onnxruntime)/lib/libonnxruntime.dylib) +ort-dynamic = ["embeddings", "fastembed/ort-load-dynamic"] # Nomic Embed Text v2 MoE (475M params, 305M active, Candle backend) # Requires: fastembed with nomic-v2-moe feature nomic-v2 = ["embeddings", "fastembed/nomic-v2-moe"] -# Qwen3 Reranker (Candle backend, high-precision cross-encoder) -qwen3-reranker = ["embeddings", "fastembed/qwen3"] +# Qwen3 Embeddings (Candle backend, opt-in for v2.1.1 re-embedding) +qwen3-embeddings = ["embeddings", "fastembed/qwen3", "dep:candle-core"] + +# Backwards-compatible feature alias from the original v2.1.0 naming. +qwen3-reranker = ["qwen3-embeddings"] # Metal GPU acceleration on Apple Silicon (significantly faster inference) metal = ["fastembed/metal"] @@ -84,8 +99,9 @@ notify = "8" # OPTIONAL: Embeddings (fastembed v5 - local ONNX inference, 2026 bleeding edge) # ============================================================================ # nomic-embed-text-v1.5: 768 dimensions, 8192 token context, Matryoshka support -# v5.11: Adds Nomic v2 MoE (nomic-v2-moe feature) + Qwen3 reranker (qwen3 feature) +# fastembed v5 provides Nomic v2 MoE and Qwen3 feature-gated model loaders. fastembed = { version = "5.11", default-features = false, features = ["hf-hub-native-tls", "image-models"], optional = true } +candle-core = { version = "0.10.2", optional = true } # ============================================================================ # OPTIONAL: Vector Search (USearch - HNSW, 20x faster than FAISS) diff --git a/crates/vestige-core/src/advanced/dreams.rs b/crates/vestige-core/src/advanced/dreams.rs index d01d8e2..5cf3492 100644 --- a/crates/vestige-core/src/advanced/dreams.rs +++ b/crates/vestige-core/src/advanced/dreams.rs @@ -93,6 +93,9 @@ const CONNECTION_DECAY_FACTOR: f64 = 0.95; /// Minimum connection strength to keep const MIN_CONNECTION_STRENGTH: f64 = 0.1; +/// Maximum discovered connections kept in the live dreamer buffer. +const MAX_STORED_DREAM_CONNECTIONS: usize = 200_000; + /// Maximum memories to replay per cycle const MAX_REPLAY_MEMORIES: usize = 100; @@ -1129,44 +1132,81 @@ impl MemoryDreamer { /// Run a dream cycle on provided memories pub async fn dream(&self, memories: &[DreamMemory]) -> DreamResult { + self.dream_with_connections(memories).await.0 + } + + /// Run a dream cycle with a temporary config. + pub async fn dream_with_config( + &self, + memories: &[DreamMemory], + config: DreamConfig, + ) -> DreamResult { + self.dream_with_config_and_connections(memories, config) + .await + .0 + } + + /// Run a dream cycle and return the exact connections found in that run. + pub async fn dream_with_connections( + &self, + memories: &[DreamMemory], + ) -> (DreamResult, Vec) { + self.run_dream(memories, &self.config).await + } + + /// Run a dream cycle with a temporary config and return this run's connections. + pub async fn dream_with_config_and_connections( + &self, + memories: &[DreamMemory], + config: DreamConfig, + ) -> (DreamResult, Vec) { + self.run_dream(memories, &config).await + } + + async fn run_dream( + &self, + memories: &[DreamMemory], + config: &DreamConfig, + ) -> (DreamResult, Vec) { let start = std::time::Instant::now(); let mut stats = DreamStats::default(); // Filter memories based on config - let working_memories: Vec<_> = if self.config.focus_tags.is_empty() { + let working_memories: Vec<_> = if config.focus_tags.is_empty() { memories .iter() - .take(self.config.max_memories_per_dream) + .take(config.max_memories_per_dream) .collect() } else { memories .iter() - .filter(|m| m.tags.iter().any(|t| self.config.focus_tags.contains(t))) - .take(self.config.max_memories_per_dream) + .filter(|m| m.tags.iter().any(|t| config.focus_tags.contains(t))) + .take(config.max_memories_per_dream) .collect() }; stats.memories_analyzed = working_memories.len(); // Phase 1: Discover new connections - let new_connections = self.discover_connections(&working_memories, &mut stats); + let new_connections = + self.discover_connections(&working_memories, &mut stats, config.min_similarity); // Phase 2: Find clusters/patterns let clusters = self.find_clusters(&working_memories, &new_connections); stats.clusters_found = clusters.len(); // Phase 3: Generate insights - let insights = self.generate_insights(&working_memories, &clusters, &mut stats); + let insights = self.generate_insights(&working_memories, &clusters, &mut stats, config); // Phase 4: Strengthen important memories (would update storage) - let memories_strengthened = if self.config.enable_strengthening { + let memories_strengthened = if config.enable_strengthening { self.identify_memories_to_strengthen(&working_memories, &new_connections) } else { 0 }; // Phase 5: Identify compression candidates (would compress in storage) - let memories_compressed = if self.config.enable_compression { + let memories_compressed = if config.enable_compression { self.identify_compression_candidates(&working_memories) } else { 0 @@ -1195,7 +1235,7 @@ impl MemoryDreamer { } } - result + (result, new_connections) } /// Synthesize insights from memories without full dream cycle @@ -1203,12 +1243,20 @@ impl MemoryDreamer { let mut stats = DreamStats::default(); // Find clusters - let connections = - self.discover_connections(&memories.iter().collect::>(), &mut stats); + let connections = self.discover_connections( + &memories.iter().collect::>(), + &mut stats, + self.config.min_similarity, + ); let clusters = self.find_clusters(&memories.iter().collect::>(), &connections); // Generate insights - self.generate_insights(&memories.iter().collect::>(), &clusters, &mut stats) + self.generate_insights( + &memories.iter().collect::>(), + &clusters, + &mut stats, + &self.config, + ) } /// Get all generated insights @@ -1257,6 +1305,7 @@ impl MemoryDreamer { &self, memories: &[&DreamMemory], stats: &mut DreamStats, + min_similarity: f64, ) -> Vec { let mut connections = Vec::new(); @@ -1271,7 +1320,7 @@ impl MemoryDreamer { // Calculate similarity let similarity = self.calculate_similarity(mem_a, mem_b); - if similarity >= self.config.min_similarity { + if similarity >= min_similarity { let connection_type = self.determine_connection_type(mem_a, mem_b, similarity); let reasoning = self.generate_connection_reasoning(mem_a, mem_b, &connection_type); @@ -1464,6 +1513,7 @@ impl MemoryDreamer { memories: &[&DreamMemory], clusters: &[Vec], stats: &mut DreamStats, + config: &DreamConfig, ) -> Vec { let mut insights = Vec::new(); let memory_map: HashMap<_, _> = memories.iter().map(|m| (&m.id, *m)).collect(); @@ -1483,12 +1533,12 @@ impl MemoryDreamer { // Try to generate insight from this cluster if let Some(insight) = self.generate_insight_from_cluster(&cluster_memories) - && insight.novelty_score >= self.config.min_novelty + && insight.novelty_score >= config.min_novelty { insights.push(insight); } - if insights.len() >= self.config.max_insights { + if insights.len() >= config.max_insights { break; } } @@ -1706,7 +1756,7 @@ impl MemoryDreamer { fn store_connections(&self, connections: &[DiscoveredConnection]) { if let Ok(mut stored) = self.connections.write() { stored.extend(connections.iter().cloned()); - // Keep the 1000 highest-scoring connections using a composite score + // Keep the highest-scoring connections using a composite score // that balances quality (similarity) and recency (age-based decay). // // score = similarity * 0.6 + recency * 0.4 @@ -1721,7 +1771,7 @@ impl MemoryDreamer { // Strong old connections are retained longer than weak new ones, // but eventually yield to fresh high-quality discoveries. let len = stored.len(); - if len > 1000 { + if len > MAX_STORED_DREAM_CONNECTIONS { let now = Utc::now(); stored.sort_unstable_by(|a, b| { let score = |c: &DiscoveredConnection| -> f64 { @@ -1737,7 +1787,7 @@ impl MemoryDreamer { .partial_cmp(&score(a)) .unwrap_or(std::cmp::Ordering::Equal) }); - stored.truncate(1000); + stored.truncate(MAX_STORED_DREAM_CONNECTIONS); } } } @@ -1854,6 +1904,31 @@ mod tests { assert!(result.stats.connections_evaluated > 0); } + #[tokio::test] + async fn test_dense_dream_keeps_more_than_legacy_connection_cap() { + let dreamer = MemoryDreamer::with_config(DreamConfig { + max_memories_per_dream: 50, + min_similarity: 0.1, + ..DreamConfig::default() + }); + + let memories: Vec<_> = (0..50) + .map(|i| { + make_memory( + &format!("dense-{i}"), + &format!("Dense single-domain memory {i} about shared identity systems"), + vec!["dense", "identity"], + ) + }) + .collect(); + + let (result, connections) = dreamer.dream_with_connections(&memories).await; + + assert_eq!(result.new_connections_found, 1_225); + assert_eq!(connections.len(), 1_225); + assert_eq!(dreamer.get_connections().len(), 1_225); + } + #[test] fn test_tag_similarity() { let dreamer = MemoryDreamer::new(); diff --git a/crates/vestige-core/src/embeddings/local.rs b/crates/vestige-core/src/embeddings/local.rs index 3dfc363..a6ec555 100644 --- a/crates/vestige-core/src/embeddings/local.rs +++ b/crates/vestige-core/src/embeddings/local.rs @@ -7,7 +7,13 @@ //! - **Default**: Nomic Embed Text v1.5 (ONNX, 768d → 256d Matryoshka, 8192 context) //! - **Optional**: Nomic Embed Text v2 MoE (Candle, 475M params, 305M active, 8 experts) //! Enable with `nomic-v2` feature flag + `metal` for Apple Silicon acceleration. +//! - **Optional**: Qwen3 Embedding 0.6B (Candle, 1024d → 256d Matryoshka) +//! Enable with `qwen3-embeddings` and `VESTIGE_EMBEDDING_MODEL=qwen3-0.6b`. +#[cfg(feature = "qwen3-embeddings")] +use candle_core::{DType, Device}; +#[cfg(feature = "qwen3-embeddings")] +use fastembed::Qwen3TextEmbedding; use fastembed::{EmbeddingModel, InitOptions, TextEmbedding}; use std::sync::{Mutex, OnceLock}; @@ -31,7 +37,82 @@ pub const BATCH_SIZE: usize = 32; // ============================================================================ /// Result type for model initialization -static EMBEDDING_MODEL_RESULT: OnceLock, String>> = OnceLock::new(); +static EMBEDDING_BACKEND_RESULT: OnceLock, String>> = + OnceLock::new(); + +const NOMIC_V15_MODEL_ID: &str = "nomic-ai/nomic-embed-text-v1.5"; +#[cfg(feature = "qwen3-embeddings")] +const QWEN3_06B_MODEL_ID: &str = "Qwen/Qwen3-Embedding-0.6B"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum EmbeddingModelSpec { + NomicV15, + #[cfg(feature = "qwen3-embeddings")] + Qwen3Embedding06B, +} + +impl EmbeddingModelSpec { + fn selected() -> Result { + let requested = std::env::var("VESTIGE_EMBEDDING_MODEL") + .ok() + .map(|value| value.trim().to_ascii_lowercase()) + .filter(|value| !value.is_empty()) + .unwrap_or_else(|| "nomic-v1.5".to_string()); + + match requested.as_str() { + "nomic" | "nomic-v1.5" | "nomic-embed-text-v1.5" | NOMIC_V15_MODEL_ID => { + Ok(Self::NomicV15) + } + "qwen3" | "qwen3-0.6b" | "qwen3-embedding-0.6b" | "qwen/qwen3-embedding-0.6b" => { + #[cfg(feature = "qwen3-embeddings")] + { + Ok(Self::Qwen3Embedding06B) + } + #[cfg(not(feature = "qwen3-embeddings"))] + { + Err( + "VESTIGE_EMBEDDING_MODEL requests Qwen3, but vestige-core was not built with the qwen3-embeddings feature" + .to_string(), + ) + } + } + other => Err(format!( + "Unsupported VESTIGE_EMBEDDING_MODEL '{}'. Expected 'nomic-v1.5' or 'qwen3-0.6b'.", + other + )), + } + } + + fn model_name(self) -> &'static str { + match self { + Self::NomicV15 => NOMIC_V15_MODEL_ID, + #[cfg(feature = "qwen3-embeddings")] + Self::Qwen3Embedding06B => QWEN3_06B_MODEL_ID, + } + } +} + +enum EmbeddingBackend { + NomicV15(TextEmbedding), + #[cfg(feature = "qwen3-embeddings")] + Qwen3Embedding06B(Qwen3TextEmbedding), +} + +impl EmbeddingBackend { + fn model_name(&self) -> &'static str { + match self { + Self::NomicV15(_) => NOMIC_V15_MODEL_ID, + #[cfg(feature = "qwen3-embeddings")] + Self::Qwen3Embedding06B(_) => QWEN3_06B_MODEL_ID, + } + } +} + +fn qwen3_format_query(query: &str) -> String { + format!( + "Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: {query}" + ) +} /// Get the default cache directory for fastembed models. /// @@ -65,10 +146,10 @@ pub(crate) fn get_cache_dir() -> std::path::PathBuf { std::path::PathBuf::from(".fastembed_cache") } -/// Initialize the global embedding model -/// Using nomic-embed-text-v1.5 (768d) - 8192 token context, Matryoshka support -fn get_model() -> Result, EmbeddingError> { - let result = EMBEDDING_MODEL_RESULT.get_or_init(|| { +/// Initialize the global embedding backend selected by `VESTIGE_EMBEDDING_MODEL`. +fn get_backend() -> Result, EmbeddingError> { + let result = EMBEDDING_BACKEND_RESULT.get_or_init(|| { + let spec = EmbeddingModelSpec::selected()?; // Get cache directory (respects FASTEMBED_CACHE_PATH env var) let cache_dir = get_cache_dir(); @@ -77,31 +158,66 @@ fn get_model() -> Result, Embeddin tracing::warn!("Failed to create cache directory {:?}: {}", cache_dir, e); } - // nomic-embed-text-v1.5: 768 dimensions, 8192 token context - // Matryoshka representation learning, fully open source - let options = InitOptions::new(EmbeddingModel::NomicEmbedTextV15) - .with_show_download_progress(true) - .with_cache_dir(cache_dir); + match spec { + EmbeddingModelSpec::NomicV15 => { + let options = InitOptions::new(EmbeddingModel::NomicEmbedTextV15) + .with_show_download_progress(true) + .with_cache_dir(cache_dir); - TextEmbedding::try_new(options) - .map(Mutex::new) - .map_err(|e| { - format!( - "Failed to initialize nomic-embed-text-v1.5 embedding model: {}. \ - Ensure ONNX runtime is available and model files can be downloaded.", - e + TextEmbedding::try_new(options) + .map(EmbeddingBackend::NomicV15) + .map(Mutex::new) + .map_err(|e| { + format!( + "Failed to initialize {} embedding model: {}. \ + Ensure ONNX runtime is available and model files can be downloaded.", + spec.model_name(), + e + ) + }) + } + #[cfg(feature = "qwen3-embeddings")] + EmbeddingModelSpec::Qwen3Embedding06B => { + let device = qwen3_device(); + Qwen3TextEmbedding::from_hf( + QWEN3_06B_MODEL_ID, + &device, + DType::F32, + MAX_TEXT_LENGTH, ) - }) + .map(EmbeddingBackend::Qwen3Embedding06B) + .map(Mutex::new) + .map_err(|e| { + format!( + "Failed to initialize {} embedding model: {}. \ + Ensure Hugging Face model files can be downloaded.", + spec.model_name(), + e + ) + }) + } + } }); match result { - Ok(model) => model + Ok(backend) => backend .lock() .map_err(|e| EmbeddingError::ModelInit(format!("Lock poisoned: {}", e))), Err(err) => Err(EmbeddingError::ModelInit(err.clone())), } } +#[cfg(feature = "qwen3-embeddings")] +fn qwen3_device() -> Device { + #[cfg(feature = "metal")] + { + if let Ok(device) = Device::new_metal(0) { + return device; + } + } + Device::Cpu +} + // ============================================================================ // ERROR TYPES // ============================================================================ @@ -223,7 +339,7 @@ impl EmbeddingService { /// Check if the model is ready pub fn is_ready(&self) -> bool { - match get_model() { + match get_backend() { Ok(_) => true, Err(e) => { tracing::warn!("Embedding model not ready: {}", e); @@ -234,25 +350,26 @@ impl EmbeddingService { /// Check if the model is ready and return the error if not pub fn check_ready(&self) -> Result<(), EmbeddingError> { - get_model().map(|_| ()) + get_backend().map(|_| ()) } /// Initialize the model (downloads if necessary) pub fn init(&self) -> Result<(), EmbeddingError> { - let _model = get_model()?; // Ensures model is loaded and returns any init errors + let _model = get_backend()?; // Ensures model is loaded and returns any init errors Ok(()) } /// Get the model name pub fn model_name(&self) -> &'static str { - #[cfg(feature = "nomic-v2")] + if let Some(Ok(backend)) = EMBEDDING_BACKEND_RESULT.get() + && let Ok(backend) = backend.lock() { - "nomic-ai/nomic-embed-text-v2-moe" - } - #[cfg(not(feature = "nomic-v2"))] - { - "nomic-ai/nomic-embed-text-v1.5" + return backend.model_name(); } + + EmbeddingModelSpec::selected() + .unwrap_or(EmbeddingModelSpec::NomicV15) + .model_name() } /// Get the embedding dimensions @@ -268,7 +385,7 @@ impl EmbeddingService { )); } - let mut model = get_model()?; + let mut backend = get_backend()?; // Truncate if too long (char-boundary safe) let text = if text.len() > MAX_TEXT_LENGTH { @@ -281,9 +398,15 @@ impl EmbeddingService { text }; - let embeddings = model - .embed(vec![text], None) - .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?; + let embeddings = match &mut *backend { + EmbeddingBackend::NomicV15(model) => model + .embed(vec![text], None) + .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, + #[cfg(feature = "qwen3-embeddings")] + EmbeddingBackend::Qwen3Embedding06B(model) => model + .embed(&[text]) + .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, + }; if embeddings.is_empty() { return Err(EmbeddingError::EmbeddingFailed( @@ -294,13 +417,26 @@ impl EmbeddingService { Ok(Embedding::new(matryoshka_truncate(embeddings[0].clone()))) } + /// Generate an embedding for retrieval queries. + /// + /// Qwen3 uses instruction-formatted queries against raw document embeddings; + /// Nomic remains symmetric and receives the query unchanged. + pub fn embed_query(&self, query: &str) -> Result { + if self.model_name().to_ascii_lowercase().contains("qwen3") { + let formatted = qwen3_format_query(query); + self.embed(&formatted) + } else { + self.embed(query) + } + } + /// Generate embeddings for multiple texts (batch processing) pub fn embed_batch(&self, texts: &[&str]) -> Result, EmbeddingError> { if texts.is_empty() { return Ok(vec![]); } - let mut model = get_model()?; + let mut backend = get_backend()?; let mut all_embeddings = Vec::with_capacity(texts.len()); // Process in batches for efficiency @@ -320,9 +456,15 @@ impl EmbeddingService { }) .collect(); - let embeddings = model - .embed(truncated, None) - .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?; + let embeddings = match &mut *backend { + EmbeddingBackend::NomicV15(model) => model + .embed(truncated, None) + .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, + #[cfg(feature = "qwen3-embeddings")] + EmbeddingBackend::Qwen3Embedding06B(model) => model + .embed(&truncated) + .map_err(|e| EmbeddingError::EmbeddingFailed(e.to_string()))?, + }; for emb in embeddings { all_embeddings.push(Embedding::new(matryoshka_truncate(emb))); @@ -481,6 +623,14 @@ mod tests { } } + #[test] + fn test_qwen3_query_format() { + let formatted = qwen3_format_query("rust memory portability"); + assert!(formatted.starts_with("Instruct:")); + assert!(formatted.contains("retrieve relevant passages")); + assert!(formatted.ends_with("Query: rust memory portability")); + } + #[test] fn test_embedding_normalize() { let mut emb = Embedding::new(vec![3.0, 4.0]); diff --git a/crates/vestige-core/src/embeddings/mod.rs b/crates/vestige-core/src/embeddings/mod.rs index 5d89c10..d38497b 100644 --- a/crates/vestige-core/src/embeddings/mod.rs +++ b/crates/vestige-core/src/embeddings/mod.rs @@ -13,6 +13,7 @@ mod code; mod hybrid; mod local; +#[cfg(feature = "vector-search")] pub(crate) use local::get_cache_dir; pub use local::{ BATCH_SIZE, EMBEDDING_DIMENSIONS, Embedding, EmbeddingError, EmbeddingService, MAX_TEXT_LENGTH, diff --git a/crates/vestige-core/src/fts.rs b/crates/vestige-core/src/fts.rs index e4cadfb..efebb71 100644 --- a/crates/vestige-core/src/fts.rs +++ b/crates/vestige-core/src/fts.rs @@ -7,6 +7,54 @@ /// Dangerous FTS5 operators that could be used for injection or DoS const FTS5_OPERATORS: &[&str] = &["OR", "AND", "NOT", "NEAR"]; +/// Sanitize input for FTS5 MATCH queries using individual term matching. +/// +/// Unlike `sanitize_fts5_query` which wraps in quotes for a phrase search, +/// this function produces individual terms joined with implicit AND. +/// This matches documents that contain ALL the query words in any order. +/// +/// Use this when you want "find all records containing these words" rather +/// than "find records with this exact phrase". +pub fn sanitize_fts5_terms(query: &str) -> Option { + let limited: String = query.chars().take(1000).collect(); + let mut sanitized = limited; + + sanitized = sanitized + .chars() + .map(|c| match c { + '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' | '.' | '/' | '\\' + | '=' | '@' => ' ', + _ => c, + }) + .collect(); + + for op in FTS5_OPERATORS { + let pattern = format!(" {} ", op); + sanitized = sanitized.replace(&pattern, " "); + sanitized = sanitized.replace(&pattern.to_lowercase(), " "); + let upper = sanitized.to_uppercase(); + let start_pattern = format!("{} ", op); + if upper.starts_with(&start_pattern) { + sanitized = sanitized.chars().skip(op.len()).collect(); + } + let end_pattern = format!(" {}", op); + if upper.ends_with(&end_pattern) { + let char_count = sanitized.chars().count(); + sanitized = sanitized + .chars() + .take(char_count.saturating_sub(op.len())) + .collect(); + } + } + + let terms: Vec<&str> = sanitized.split_whitespace().collect(); + if terms.is_empty() { + return None; + } + // Join with space: FTS5 implicit AND — all terms must appear + Some(terms.join(" ")) +} + /// Sanitize input for FTS5 MATCH queries /// /// Prevents: @@ -21,11 +69,13 @@ pub fn sanitize_fts5_query(query: &str) -> String { // Remove FTS5 special characters and operators let mut sanitized = limited.to_string(); - // Remove special characters: * : ^ - " ( ) + // Remove special characters: * : ^ - " ( ) and common identifier/path + // punctuation that FTS5 otherwise treats as syntax. sanitized = sanitized .chars() .map(|c| match c { - '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' => ' ', + '*' | ':' | '^' | '-' | '"' | '(' | ')' | '{' | '}' | '[' | ']' | '.' | '/' | '\\' + | '=' | '@' => ' ', _ => c, }) .collect(); diff --git a/crates/vestige-core/src/lib.rs b/crates/vestige-core/src/lib.rs index 306b2d2..640ba4a 100644 --- a/crates/vestige-core/src/lib.rs +++ b/crates/vestige-core/src/lib.rs @@ -153,7 +153,8 @@ pub use fsrs::{ // Storage layer pub use storage::{ ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, - IntentionRecord, Result, SmartIngestResult, StateTransitionRecord, Storage, StorageError, + IntentionRecord, PORTABLE_ARCHIVE_FORMAT, PortableArchive, PortableImportMode, + PortableImportReport, Result, SmartIngestResult, StateTransitionRecord, Storage, StorageError, }; // Consolidation (sleep-inspired memory processing) diff --git a/crates/vestige-core/src/memory/mod.rs b/crates/vestige-core/src/memory/mod.rs index e7c61d4..e8c3f32 100644 --- a/crates/vestige-core/src/memory/mod.rs +++ b/crates/vestige-core/src/memory/mod.rs @@ -247,8 +247,14 @@ pub struct MemoryStats { pub newest_memory: Option>, /// Number of nodes with semantic embeddings pub nodes_with_embeddings: i64, + /// Number of nodes with embeddings generated by the active embedding model family + pub nodes_with_active_embeddings: i64, + /// Number of nodes whose stored embeddings belong to a different model family + pub nodes_with_mismatched_embeddings: i64, /// Embedding model used (if any) pub embedding_model: Option, + /// Embedding model family currently configured for new queries and writes + pub active_embedding_model: Option, } impl Default for MemoryStats { @@ -262,7 +268,10 @@ impl Default for MemoryStats { oldest_memory: None, newest_memory: None, nodes_with_embeddings: 0, + nodes_with_active_embeddings: 0, + nodes_with_mismatched_embeddings: 0, embedding_model: None, + active_embedding_model: None, } } } diff --git a/crates/vestige-core/src/neuroscience/emotional_memory.rs b/crates/vestige-core/src/neuroscience/emotional_memory.rs index bc5a527..2f7dc86 100644 --- a/crates/vestige-core/src/neuroscience/emotional_memory.rs +++ b/crates/vestige-core/src/neuroscience/emotional_memory.rs @@ -103,21 +103,6 @@ pub enum EmotionCategory { Neutral, } -impl EmotionCategory { - /// Get the base arousal level for this category - #[allow(dead_code)] - fn base_arousal(&self) -> f64 { - match self { - Self::Joy => 0.6, - Self::Frustration => 0.7, - Self::Urgency => 0.9, - Self::Surprise => 0.8, - Self::Confusion => 0.4, - Self::Neutral => 0.1, - } - } -} - impl std::fmt::Display for EmotionCategory { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/vestige-core/src/storage/migrations.rs b/crates/vestige-core/src/storage/migrations.rs index 566561f..2c66a2d 100644 --- a/crates/vestige-core/src/storage/migrations.rs +++ b/crates/vestige-core/src/storage/migrations.rs @@ -59,6 +59,16 @@ pub const MIGRATIONS: &[Migration] = &[ description: "v2.0.7 Cleanup: drop dead knowledge_edges and compressed_memories tables", up: MIGRATION_V11_UP, }, + Migration { + version: 12, + description: "v2.1.1 Sync: tombstones for merge-capable portable storage", + up: MIGRATION_V12_UP, + }, + Migration { + version: 13, + description: "v2.1.2 Honest Memory: non-content purge tombstones", + up: MIGRATION_V13_UP, + }, ]; /// A database migration @@ -681,6 +691,50 @@ DROP TABLE IF EXISTS compressed_memories; UPDATE schema_version SET version = 11, applied_at = datetime('now'); "#; +/// V12: Merge-capable sync tombstones. +/// +/// Portable sync needs to propagate deletions between devices. `knowledge_nodes` +/// remains the source of truth for live memories; this table records deletes so +/// another device can remove the same memory during a merge import. +const MIGRATION_V12_UP: &str = r#" +CREATE TABLE IF NOT EXISTS sync_tombstones ( + table_name TEXT NOT NULL, + row_id TEXT NOT NULL, + deleted_at TEXT NOT NULL, + reason TEXT, + PRIMARY KEY (table_name, row_id) +); + +CREATE INDEX IF NOT EXISTS idx_sync_tombstones_deleted_at +ON sync_tombstones(deleted_at); + +UPDATE schema_version SET version = 12, applied_at = datetime('now'); +"#; + +/// V13: non-content purge tombstones. +/// +/// `memory(action="purge")` permanently removes memory content and embeddings, +/// but keeps a content-free audit/sync record so users can verify that a memory +/// was removed without Vestige retaining the text it was told to forget. +const MIGRATION_V13_UP: &str = r#" +CREATE TABLE IF NOT EXISTS deletion_tombstones ( + memory_id TEXT PRIMARY KEY, + deleted_at TEXT NOT NULL, + reason TEXT, + node_type TEXT NOT NULL, + tags TEXT NOT NULL DEFAULT '[]', + edges_pruned INTEGER NOT NULL DEFAULT 0, + insights_rewritten INTEGER NOT NULL DEFAULT 0, + insights_deleted INTEGER NOT NULL DEFAULT 0, + children_orphaned INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX IF NOT EXISTS idx_deletion_tombstones_deleted_at +ON deletion_tombstones(deleted_at); + +UPDATE schema_version SET version = 13, applied_at = datetime('now'); +"#; + /// Get current schema version from database pub fn get_current_version(conn: &rusqlite::Connection) -> rusqlite::Result { conn.query_row( @@ -730,15 +784,18 @@ mod tests { /// version after `apply_migrations` runs all migrations end-to-end, and /// neither of the dead tables V11 drops must exist afterwards. #[test] - fn test_apply_migrations_advances_to_v11_and_drops_dead_tables() { + fn test_apply_migrations_advances_to_v13_and_drops_dead_tables() { let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); // Pre-requisite: schema_version must be bootstrapped by V1. apply_migrations(&conn).expect("apply_migrations succeeds"); - // 1. schema_version advanced to V11 + // 1. schema_version advanced to V13 let version = get_current_version(&conn).expect("read schema_version"); - assert_eq!(version, 11, "schema_version must be 11 after all migrations"); + assert_eq!( + version, 13, + "schema_version must be 13 after all migrations" + ); // 2. knowledge_edges is gone (V11 drops it) let knowledge_edges_rows: i64 = conn @@ -765,6 +822,32 @@ mod tests { compressed_memories_rows, 0, "compressed_memories table must be dropped by V11" ); + + // 4. sync_tombstones exists (V12 creates it) + let sync_tombstone_rows: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='sync_tombstones'", + [], + |row| row.get(0), + ) + .expect("query sqlite_master"); + assert_eq!( + sync_tombstone_rows, 1, + "sync_tombstones table must be created by V12" + ); + + // 5. deletion_tombstones exists (V13 creates it) + let deletion_tombstone_rows: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='deletion_tombstones'", + [], + |row| row.get(0), + ) + .expect("query sqlite_master"); + assert_eq!( + deletion_tombstone_rows, 1, + "deletion_tombstones table must be created by V13" + ); } /// V11 must be idempotent on replay — if the tables were already dropped @@ -786,6 +869,6 @@ mod tests { apply_migrations(&conn).expect("V11 replay must be idempotent"); let version = get_current_version(&conn).expect("read schema_version"); - assert_eq!(version, 11, "schema_version back at 11 after replay"); + assert_eq!(version, 13, "schema_version back at 13 after replay"); } } diff --git a/crates/vestige-core/src/storage/mod.rs b/crates/vestige-core/src/storage/mod.rs index eb224fa..1660529 100644 --- a/crates/vestige-core/src/storage/mod.rs +++ b/crates/vestige-core/src/storage/mod.rs @@ -7,10 +7,16 @@ //! - Temporal memory support mod migrations; +mod portable; mod sqlite; pub use migrations::MIGRATIONS; -pub use sqlite::{ - ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, InsightRecord, - IntentionRecord, Result, SmartIngestResult, StateTransitionRecord, Storage, StorageError, +pub use portable::{ + PORTABLE_ARCHIVE_FORMAT, PortableArchive, PortableImportMode, PortableImportReport, + PortableTable, PortableValue, +}; +pub use sqlite::{ + ConnectionRecord, ConsolidationHistoryRecord, DreamHistoryRecord, FilePortableSyncBackend, + InsightRecord, IntentionRecord, PortableSyncBackend, PortableSyncReport, Result, + SmartIngestResult, StateTransitionRecord, Storage, StorageError, }; diff --git a/crates/vestige-core/src/storage/portable.rs b/crates/vestige-core/src/storage/portable.rs new file mode 100644 index 0000000..dda0a74 --- /dev/null +++ b/crates/vestige-core/src/storage/portable.rs @@ -0,0 +1,171 @@ +//! Portable archive types for exact Vestige-to-Vestige transfer. +//! +//! This format preserves SQLite row data instead of re-ingesting memories. It is +//! intentionally storage-level: import can keep IDs, FSRS state, graph edges, +//! suppression state, embeddings, and audit/history rows intact. + +use chrono::{DateTime, Utc}; +use rusqlite::types::Value; +use serde::{Deserialize, Serialize}; + +/// Current portable archive format identifier. +pub const PORTABLE_ARCHIVE_FORMAT: &str = "vestige.portable.v1"; + +/// Full exact portable archive. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PortableArchive { + /// Stable format marker used for compatibility checks. + pub archive_format: String, + /// Vestige version that produced the archive. + pub vestige_version: String, + /// SQLite schema version of the source database. + pub schema_version: u32, + /// Archive creation timestamp. + pub exported_at: DateTime, + /// Export mode. v1 only writes "exact". + pub mode: String, + /// Dumped storage tables in deterministic import order. + pub tables: Vec, +} + +impl PortableArchive { + /// Count all rows across all tables. + pub fn total_rows(&self) -> usize { + self.tables.iter().map(|table| table.rows.len()).sum() + } +} + +/// One table in a portable archive. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PortableTable { + /// SQLite table name. + pub name: String, + /// Column names in row value order. + pub columns: Vec, + /// Raw rows. Each row has the same order as `columns`. + pub rows: Vec>, +} + +/// SQLite value encoded in JSON. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type", content = "value", rename_all = "camelCase")] +pub enum PortableValue { + /// SQL NULL. + Null, + /// SQL INTEGER. + Integer(i64), + /// SQL REAL. + Real(f64), + /// SQL TEXT. + Text(String), + /// SQL BLOB, hex encoded. + Blob(String), +} + +impl PortableValue { + /// Convert this portable value back into a rusqlite owned value. + pub(crate) fn to_sql_value(&self) -> Result { + match self { + Self::Null => Ok(Value::Null), + Self::Integer(value) => Ok(Value::Integer(*value)), + Self::Real(value) => Ok(Value::Real(*value)), + Self::Text(value) => Ok(Value::Text(value.clone())), + Self::Blob(value) => decode_hex(value).map(Value::Blob), + } + } +} + +/// Import behavior for duplicate primary keys. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PortableImportMode { + /// Reject import if user data already exists, then insert rows exactly. + EmptyOnly, + /// Merge archive rows into an existing database. + /// + /// This mode is intended for file-backed sync between devices. It applies + /// tombstones, upserts row-keyed state, and appends audit/history rows. + Merge, +} + +/// Summary of an exact portable import. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PortableImportReport { + /// Number of imported tables. + pub tables_imported: usize, + /// Number of imported rows. + pub rows_imported: usize, + /// Number of archive tables skipped because the target schema lacks them. + pub tables_skipped: usize, + /// Whether FTS was rebuilt after import. + pub fts_rebuilt: bool, + /// Number of rows inserted. + #[serde(default)] + pub rows_inserted: usize, + /// Number of existing rows updated/replaced. + #[serde(default)] + pub rows_updated: usize, + /// Number of rows skipped because local state was newer or unsupported. + #[serde(default)] + pub rows_skipped: usize, + /// Number of local rows deleted by imported tombstones. + #[serde(default)] + pub rows_deleted: usize, + /// Number of merge conflicts resolved by keeping local state. + #[serde(default)] + pub conflicts_kept_local: usize, +} + +pub(crate) fn encode_hex(bytes: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut out = String::with_capacity(bytes.len() * 2); + for &byte in bytes { + out.push(HEX[(byte >> 4) as usize] as char); + out.push(HEX[(byte & 0x0f) as usize] as char); + } + out +} + +fn decode_hex(input: &str) -> Result, String> { + if !input.len().is_multiple_of(2) { + return Err("hex blob has odd length".to_string()); + } + + let mut out = Vec::with_capacity(input.len() / 2); + let bytes = input.as_bytes(); + for chunk in bytes.chunks_exact(2) { + let high = hex_value(chunk[0])?; + let low = hex_value(chunk[1])?; + out.push((high << 4) | low); + } + Ok(out) +} + +fn hex_value(byte: u8) -> Result { + match byte { + b'0'..=b'9' => Ok(byte - b'0'), + b'a'..=b'f' => Ok(byte - b'a' + 10), + b'A'..=b'F' => Ok(byte - b'A' + 10), + _ => Err(format!("invalid hex byte: {}", byte as char)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_round_trip() { + let bytes = vec![0, 1, 2, 15, 16, 127, 128, 255]; + let encoded = encode_hex(&bytes); + assert_eq!(decode_hex(&encoded).unwrap(), bytes); + } + + #[test] + fn rejects_invalid_hex() { + assert!(decode_hex("f").is_err()); + assert!(decode_hex("zz").is_err()); + } +} diff --git a/crates/vestige-core/src/storage/sqlite.rs b/crates/vestige-core/src/storage/sqlite.rs index 81197cb..dc0150c 100644 --- a/crates/vestige-core/src/storage/sqlite.rs +++ b/crates/vestige-core/src/storage/sqlite.rs @@ -3,13 +3,16 @@ //! Core storage layer with integrated embeddings and vector search. use chrono::{DateTime, Duration, Utc}; -use directories::ProjectDirs; -#[cfg(feature = "embeddings")] +use directories::{BaseDirs, ProjectDirs}; +#[cfg(all(feature = "embeddings", feature = "vector-search"))] use lru::LruCache; -use rusqlite::{Connection, OptionalExtension, params}; -#[cfg(feature = "embeddings")] +use rusqlite::types::{Type, Value, ValueRef}; +use rusqlite::{Connection, OptionalExtension, params, params_from_iter}; +use std::collections::{HashMap, HashSet}; +use std::io::Write; +#[cfg(all(feature = "embeddings", feature = "vector-search"))] use std::num::NonZeroUsize; -use std::path::PathBuf; +use std::path::{Component, Path, PathBuf}; use std::sync::Mutex; use uuid::Uuid; @@ -18,13 +21,20 @@ use crate::fsrs::{ }; use crate::fts::sanitize_fts5_query; use crate::memory::{ - ConsolidationResult, IngestInput, KnowledgeNode, MemoryStats, RecallInput, SearchMode, + ConsolidationResult, IngestInput, KnowledgeNode, MatchType, MemoryStats, RecallInput, + SearchMode, SearchResult, }; #[cfg(all(feature = "embeddings", feature = "vector-search"))] -use crate::memory::{EmbeddingResult, MatchType, SearchResult, SimilarityResult}; +use crate::memory::{EmbeddingResult, SimilarityResult}; +use crate::storage::portable::{ + PORTABLE_ARCHIVE_FORMAT, PortableArchive, PortableImportMode, PortableImportReport, + PortableTable, PortableValue, encode_hex, +}; #[cfg(feature = "embeddings")] -use crate::embeddings::{EMBEDDING_DIMENSIONS, Embedding, EmbeddingService, matryoshka_truncate}; +use crate::embeddings::EmbeddingService; +#[cfg(all(feature = "embeddings", feature = "vector-search"))] +use crate::embeddings::{EMBEDDING_DIMENSIONS, Embedding, matryoshka_truncate}; #[cfg(feature = "vector-search")] use crate::search::{VectorIndex, linear_combination}; @@ -78,16 +88,204 @@ pub struct SmartIngestResult { pub reason: String, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum MergeWrite { + Inserted, + Updated, +} + +/// Backend interface for portable sync storage. +/// +/// The first shipped backend is a local file, which works with Dropbox, iCloud, +/// Syncthing, Git, shared volumes, or any other folder sync tool. Remote stores +/// can implement this trait without changing merge semantics. +pub trait PortableSyncBackend { + /// Human-readable backend label for reports. + fn label(&self) -> String; + /// Read the current remote archive. `Ok(None)` means no remote exists yet. + fn read_archive(&self) -> Result>; + /// Atomically write the merged archive back to the backend when possible. + fn write_archive(&self, archive: &PortableArchive) -> Result<()>; +} + +/// File-backed portable sync backend. +#[derive(Debug, Clone)] +pub struct FilePortableSyncBackend { + path: PathBuf, +} + +impl FilePortableSyncBackend { + /// Create a file-backed sync backend for a portable archive path. + pub fn new(path: impl Into) -> Self { + Self { path: path.into() } + } + + /// Archive path backing this sync store. + pub fn path(&self) -> &Path { + &self.path + } +} + +impl PortableSyncBackend for FilePortableSyncBackend { + fn label(&self) -> String { + format!("file:{}", self.path.display()) + } + + fn read_archive(&self) -> Result> { + if !self.path.exists() { + return Ok(None); + } + let file = std::fs::File::open(&self.path)?; + let archive: PortableArchive = serde_json::from_reader(file).map_err(|e| { + StorageError::Init(format!( + "Failed to parse portable sync archive '{}': {}", + self.path.display(), + e + )) + })?; + Ok(Some(archive)) + } + + fn write_archive(&self, archive: &PortableArchive) -> Result<()> { + let parent = self.path.parent().unwrap_or_else(|| Path::new(".")); + std::fs::create_dir_all(parent)?; + let filename = self + .path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("vestige-sync.json"); + let temp_path = parent.join(format!(".{}.tmp-{}", filename, Uuid::new_v4())); + + #[cfg(unix)] + let mut file = { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(&temp_path)? + }; + #[cfg(not(unix))] + let mut file = std::fs::File::create(&temp_path)?; + if let Err(e) = serde_json::to_writer_pretty(&mut file, archive) { + let _ = std::fs::remove_file(&temp_path); + return Err(StorageError::Init(format!( + "Failed to write portable sync archive '{}': {}", + self.path.display(), + e + ))); + } + file.flush()?; + file.sync_all()?; + drop(file); + + if let Err(rename_err) = std::fs::rename(&temp_path, &self.path) { + if self.path.exists() { + std::fs::remove_file(&self.path)?; + std::fs::rename(&temp_path, &self.path)?; + } else { + let _ = std::fs::remove_file(&temp_path); + return Err(rename_err.into()); + } + } + Ok(()) + } +} + +/// Summary of a pull-merge-push sync operation. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PortableSyncReport { + /// Backend label that was synced. + pub backend: String, + /// Whether an existing remote archive was pulled before pushing. + pub pulled: bool, + /// Merge report from the pull phase, if a remote archive existed. + pub pull: Option, + /// Number of tables written to the backend during push. + pub pushed_tables: usize, + /// Number of rows written to the backend during push. + pub pushed_rows: usize, + /// Portable archive format written during push. + pub archive_format: String, +} + +/// Report returned by an irreversible content purge. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct PurgeReport { + /// Memory ID requested for purge. + pub memory_id: String, + /// Whether a live memory row was found and removed. + pub deleted: bool, + /// Non-content tombstone timestamp. + pub deleted_at: DateTime, + /// Number of graph edges removed by foreign-key cascade. + pub edges_pruned: i64, + /// Number of insight rows whose source list was rewritten. + pub insights_rewritten: i64, + /// Number of insight rows dropped because fewer than two source memories remained. + pub insights_deleted: i64, + /// Number of temporal-summary children detached from this parent. + pub children_orphaned: i64, +} + // ============================================================================ // STORAGE // ============================================================================ +const PORTABLE_TABLES: &[&str] = &[ + "knowledge_nodes", + "node_embeddings", + "fsrs_cards", + "memory_states", + "memory_connections", + "memory_access_log", + "state_transitions", + "intentions", + "insights", + "sessions", + "fsrs_config", + "consolidation_history", + "dream_history", + "retention_snapshots", + "sync_tombstones", + "deletion_tombstones", +]; + +const PORTABLE_USER_DATA_TABLES: &[&str] = &[ + "knowledge_nodes", + "node_embeddings", + "fsrs_cards", + "memory_states", + "memory_connections", + "memory_access_log", + "state_transitions", + "intentions", + "insights", + "sessions", + "consolidation_history", + "dream_history", + "retention_snapshots", + "sync_tombstones", + "deletion_tombstones", +]; + +#[derive(Default)] +struct PortableMergeState { + locally_newer_nodes: HashSet, +} + +const DATA_DIR_ENV: &str = "VESTIGE_DATA_DIR"; +const DATABASE_FILE: &str = "vestige.db"; + /// Main storage struct with integrated embedding and vector search /// /// Uses separate reader/writer connections for interior mutability. /// All methods take `&self` (not `&mut self`), making Storage `Send + Sync` /// so the MCP layer can use `Arc` instead of `Arc>`. pub struct Storage { + db_path: PathBuf, writer: Mutex, reader: Mutex, scheduler: Mutex, @@ -96,11 +294,75 @@ pub struct Storage { #[cfg(feature = "vector-search")] vector_index: Mutex, /// LRU cache for query embeddings to avoid re-embedding repeated queries - #[cfg(feature = "embeddings")] + #[cfg(all(feature = "embeddings", feature = "vector-search"))] query_cache: Mutex>>, } impl Storage { + fn data_dir_from_env() -> Option { + std::env::var_os(DATA_DIR_ENV).and_then(|value| { + if value.is_empty() { + None + } else { + Some(PathBuf::from(value)) + } + }) + } + + fn expand_tilde(path: PathBuf) -> PathBuf { + let rest = { + let mut components = path.components(); + match components.next() { + Some(Component::Normal(first)) if first == "~" => { + Some(components.as_path().to_path_buf()) + } + _ => None, + } + }; + + match rest { + Some(rest) => BaseDirs::new() + .map(|dirs| dirs.home_dir().join(rest)) + .unwrap_or(path), + None => path, + } + } + + fn prepare_data_dir(data_dir: PathBuf) -> Result { + let data_dir = Self::expand_tilde(data_dir); + std::fs::create_dir_all(&data_dir)?; + // Restrict directory permissions to owner-only on Unix + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let perms = std::fs::Permissions::from_mode(0o700); + let _ = std::fs::set_permissions(&data_dir, perms); + } + Ok(data_dir.join(DATABASE_FILE)) + } + + /// Resolve a Vestige database path from an explicit data directory. + pub fn db_path_for_data_dir(data_dir: PathBuf) -> Result { + Self::prepare_data_dir(data_dir) + } + + /// Resolve the default Vestige database path. + /// + /// `VESTIGE_DATA_DIR` is treated as a directory and wins over the platform + /// per-user data directory. The database file is always `vestige.db` inside + /// that directory. + pub fn default_db_path() -> Result { + if let Some(data_dir) = Self::data_dir_from_env() { + return Self::prepare_data_dir(data_dir); + } + + let proj_dirs = ProjectDirs::from("com", "vestige", "core").ok_or_else(|| { + StorageError::Init("Could not determine project directories".to_string()) + })?; + + Self::prepare_data_dir(proj_dirs.data_dir().to_path_buf()) + } + /// Apply PRAGMAs and optional encryption to a connection fn configure_connection(conn: &Connection) -> Result<()> { // Apply encryption key if SQLCipher is enabled and key is provided @@ -133,22 +395,7 @@ impl Storage { pub fn new(db_path: Option) -> Result { let path = match db_path { Some(p) => p, - None => { - let proj_dirs = ProjectDirs::from("com", "vestige", "core").ok_or_else(|| { - StorageError::Init("Could not determine project directories".to_string()) - })?; - - let data_dir = proj_dirs.data_dir(); - std::fs::create_dir_all(data_dir)?; - // Restrict directory permissions to owner-only on Unix - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - let perms = std::fs::Permissions::from_mode(0o700); - let _ = std::fs::set_permissions(data_dir, perms); - } - data_dir.join("vestige.db") - } + None => Self::default_db_path()?, }; // Open writer connection @@ -180,12 +427,13 @@ impl Storage { // Initialize LRU cache for query embeddings (capacity: 100 queries) // SAFETY: 100 is always non-zero, this cannot fail - #[cfg(feature = "embeddings")] + #[cfg(all(feature = "embeddings", feature = "vector-search"))] let query_cache = Mutex::new(LruCache::new( NonZeroUsize::new(100).expect("100 is non-zero"), )); let storage = Self { + db_path: path, writer: Mutex::new(writer_conn), reader: Mutex::new(reader_conn), scheduler: Mutex::new(FSRSScheduler::default()), @@ -193,7 +441,7 @@ impl Storage { embedding_service, #[cfg(feature = "vector-search")] vector_index: Mutex::new(vector_index), - #[cfg(feature = "embeddings")] + #[cfg(all(feature = "embeddings", feature = "vector-search"))] query_cache, }; @@ -203,35 +451,74 @@ impl Storage { Ok(storage) } + /// Absolute path of the SQLite database this storage instance uses. + pub fn db_path(&self) -> &Path { + &self.db_path + } + + /// Data directory containing the SQLite database and sidecar folders. + pub fn data_dir(&self) -> &Path { + self.db_path.parent().unwrap_or_else(|| Path::new(".")) + } + + /// Sidecar directory for files belonging to this storage instance. + pub fn sidecar_dir(&self, name: &str) -> PathBuf { + self.data_dir().join(name) + } + /// Load existing embeddings into vector index #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn load_embeddings_into_index(&self) -> Result<()> { + let mut index = self + .vector_index + .lock() + .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; let reader = self .reader .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare("SELECT node_id, embedding FROM node_embeddings")?; + let mut stmt = reader.prepare("SELECT node_id, embedding, model FROM node_embeddings")?; - let embeddings: Vec<(String, Vec)> = stmt - .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? + let embeddings: Vec<(String, Vec, String)> = stmt + .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))? .filter_map(|r| r.ok()) .collect(); drop(stmt); drop(reader); - let mut index = self - .vector_index - .lock() - .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; + *index = VectorIndex::new().map_err(|e| { + StorageError::Init(format!("Failed to rebuild vector index before load: {}", e)) + })?; let mut load_failures = 0u32; - for (node_id, embedding_bytes) in embeddings { + let mut skipped_model_mismatches = 0u32; + let active_model = self.embedding_service.model_name(); + for (node_id, embedding_bytes, model_name) in embeddings { + if !Self::embedding_model_matches_active(&model_name, active_model) { + skipped_model_mismatches += 1; + continue; + } + if let Some(embedding) = Embedding::from_bytes(&embedding_bytes) { - // Handle Matryoshka migration: old 768-dim → truncate to 256-dim + // Handle Matryoshka models explicitly. Do not silently truncate + // unknown embedding families into the active 256d index. let vector = if embedding.dimensions != EMBEDDING_DIMENSIONS { - matryoshka_truncate(embedding.vector) + let model_lower = model_name.to_ascii_lowercase(); + if model_lower.contains("nomic") || model_lower.contains("qwen3") { + matryoshka_truncate(embedding.vector) + } else { + load_failures += 1; + tracing::warn!( + node_id = %node_id, + model = %model_name, + dimensions = embedding.dimensions, + expected = EMBEDDING_DIMENSIONS, + "Skipping embedding with incompatible dimensions" + ); + continue; + } } else { embedding.vector }; @@ -248,6 +535,13 @@ impl Storage { load_failures ); } + if skipped_model_mismatches > 0 { + tracing::info!( + count = skipped_model_mismatches, + active_model = active_model, + "Vector index skipped embeddings from a different model family; run consolidation to re-embed them" + ); + } Ok(()) } @@ -569,14 +863,19 @@ impl Storage { .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = - reader.prepare("SELECT embedding FROM node_embeddings WHERE node_id = ?1")?; + reader.prepare("SELECT embedding, model FROM node_embeddings WHERE node_id = ?1")?; - let embedding_bytes: Option> = stmt - .query_row(params![node_id], |row| row.get(0)) + let embedding_row: Option<(Vec, String)> = stmt + .query_row(params![node_id], |row| Ok((row.get(0)?, row.get(1)?))) .optional()?; - Ok(embedding_bytes - .and_then(|bytes| crate::embeddings::Embedding::from_bytes(&bytes).map(|e| e.vector))) + Ok(embedding_row.and_then(|(bytes, model)| { + Self::embedding_vector_for_active_model( + &bytes, + &model, + self.embedding_service.model_name(), + ) + })) } /// Get all embedding vectors for duplicate detection @@ -586,23 +885,32 @@ impl Storage { .reader .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare("SELECT node_id, embedding FROM node_embeddings")?; + let mut stmt = reader.prepare("SELECT node_id, embedding, model FROM node_embeddings")?; + let active_model = self.embedding_service.model_name(); let results: Vec<(String, Vec)> = stmt .query_map([], |row| { let node_id: String = row.get(0)?; let embedding_bytes: Vec = row.get(1)?; - Ok((node_id, embedding_bytes)) + let model: String = row.get(2)?; + Ok((node_id, embedding_bytes, model)) })? .filter_map(|r| r.ok()) - .filter_map(|(id, bytes)| { - crate::embeddings::Embedding::from_bytes(&bytes).map(|e| (id, e.vector)) + .filter_map(|(id, bytes, model)| { + Self::embedding_vector_for_active_model(&bytes, &model, active_model) + .map(|vector| (id, vector)) }) .collect(); Ok(results) } + /// Fallback for builds without local embeddings/vector search. + #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] + pub fn get_node_embedding(&self, _node_id: &str) -> Result>> { + Ok(None) + } + /// Update the content of an existing node pub fn update_node_content(&self, id: &str, new_content: &str) -> Result<()> { let now = Utc::now(); @@ -645,6 +953,7 @@ impl Storage { .embedding_service .embed(content) .map_err(|e| StorageError::Init(format!("Embedding failed: {}", e)))?; + let model_name = self.embedding_service.model_name(); let now = Utc::now(); @@ -659,15 +968,15 @@ impl Storage { params![ node_id, embedding.to_bytes(), - EMBEDDING_DIMENSIONS as i32, - "nomic-embed-text-v1.5", + embedding.dimensions as i32, + model_name, now.to_rfc3339(), ], )?; writer.execute( - "UPDATE knowledge_nodes SET has_embedding = 1, embedding_model = 'nomic-embed-text-v1.5' WHERE id = ?1", - params![node_id], + "UPDATE knowledge_nodes SET has_embedding = 1, embedding_model = ?2 WHERE id = ?1", + params![node_id, model_name], )?; } @@ -1449,11 +1758,56 @@ impl Storage { |row| row.get(0), )?; - let embedding_model: Option = if nodes_with_embeddings > 0 { - Some("nomic-embed-text-v1.5".to_string()) - } else { - None + let embedding_model: Option = reader + .query_row( + "SELECT model + FROM node_embeddings + GROUP BY model + ORDER BY COUNT(*) DESC, model ASC + LIMIT 1", + [], + |row| row.get(0), + ) + .optional()?; + + #[cfg(feature = "embeddings")] + let active_embedding_model = Some(self.embedding_service.model_name().to_string()); + #[cfg(not(feature = "embeddings"))] + let active_embedding_model = None; + + #[cfg(feature = "embeddings")] + let (nodes_with_active_embeddings, nodes_with_mismatched_embeddings) = { + let active_model = active_embedding_model.as_deref().unwrap_or_default(); + let model_pattern = Self::active_embedding_model_like_pattern(active_model); + let active_count: i64 = reader.query_row( + "SELECT COUNT(*) + FROM knowledge_nodes kn + WHERE kn.has_embedding = 1 + AND EXISTS ( + SELECT 1 FROM node_embeddings ne + WHERE ne.node_id = kn.id + AND ne.model LIKE ?1 + )", + params![&model_pattern], + |row| row.get(0), + )?; + let mismatched_count: i64 = reader.query_row( + "SELECT COUNT(*) + FROM knowledge_nodes kn + WHERE kn.has_embedding = 1 + AND NOT EXISTS ( + SELECT 1 FROM node_embeddings ne + WHERE ne.node_id = kn.id + AND ne.model LIKE ?1 + )", + params![&model_pattern], + |row| row.get(0), + )?; + (active_count, mismatched_count) }; + #[cfg(not(feature = "embeddings"))] + let (nodes_with_active_embeddings, nodes_with_mismatched_embeddings) = + (nodes_with_embeddings, 0); Ok(MemoryStats { total_nodes: total, @@ -1472,17 +1826,25 @@ impl Storage { .ok() }), nodes_with_embeddings, + nodes_with_active_embeddings, + nodes_with_mismatched_embeddings, embedding_model, + active_embedding_model, }) } /// Delete a node pub fn delete_node(&self, id: &str) -> Result { - let writer = self + let mut writer = self .writer .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let rows = writer.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; + let tx = writer.transaction()?; + if Self::node_exists(&tx, id)? { + Self::record_sync_tombstone(&tx, "knowledge_nodes", id, "delete_node")?; + } + let rows = tx.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; + tx.commit()?; // Clean up vector index to prevent stale search results #[cfg(all(feature = "embeddings", feature = "vector-search"))] @@ -1495,6 +1857,155 @@ impl Storage { Ok(rows > 0) } + /// Permanently purge a memory's content and embeddings. + /// + /// Unlike `delete_node`, purge also scrubs non-FK JSON references in + /// `insights.source_memories`, detaches temporal-summary children, and + /// writes a content-free deletion tombstone for audit/sync. + pub fn purge_node(&self, id: &str, reason: Option<&str>) -> Result { + let deleted_at = Utc::now(); + let mut writer = self + .writer + .lock() + .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; + let tx = writer.transaction()?; + + let node = tx + .prepare("SELECT * FROM knowledge_nodes WHERE id = ?1")? + .query_row(params![id], Self::row_to_node) + .optional()?; + + let Some(node) = node else { + return Ok(PurgeReport { + memory_id: id.to_string(), + deleted: false, + deleted_at, + edges_pruned: 0, + insights_rewritten: 0, + insights_deleted: 0, + children_orphaned: 0, + }); + }; + + let edges_pruned: i64 = tx.query_row( + "SELECT COUNT(*) FROM memory_connections WHERE source_id = ?1 OR target_id = ?1", + params![id], + |row| row.get(0), + )?; + + let insight_refs: Vec<(String, String)> = { + let mut stmt = tx.prepare( + "SELECT id, source_memories FROM insights WHERE source_memories LIKE ?1", + )?; + let pattern = format!("%{}%", id); + stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))? + .filter_map(|row| row.ok()) + .collect() + }; + + let mut insights_rewritten = 0_i64; + let mut insights_deleted = 0_i64; + for (insight_id, source_json) in insight_refs { + let mut sources: Vec = serde_json::from_str(&source_json).unwrap_or_default(); + let before = sources.len(); + sources.retain(|source_id| source_id != id); + + if sources.len() == before { + continue; + } + + if sources.len() < 2 { + insights_deleted += + tx.execute("DELETE FROM insights WHERE id = ?1", params![insight_id])? as i64; + } else { + let rewritten = serde_json::to_string(&sources).unwrap_or_else(|_| "[]".into()); + insights_rewritten += tx.execute( + "UPDATE insights SET source_memories = ?1 WHERE id = ?2", + params![rewritten, insight_id], + )? as i64; + } + } + + let children_orphaned = tx.execute( + "UPDATE knowledge_nodes SET summary_parent_id = NULL WHERE summary_parent_id = ?1", + params![id], + )? as i64; + + let tags_json = serde_json::to_string(&node.tags).unwrap_or_else(|_| "[]".to_string()); + tx.execute( + "INSERT INTO deletion_tombstones ( + memory_id, deleted_at, reason, node_type, tags, + edges_pruned, insights_rewritten, insights_deleted, children_orphaned + ) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9) + ON CONFLICT(memory_id) DO UPDATE SET + deleted_at = excluded.deleted_at, + reason = excluded.reason, + node_type = excluded.node_type, + tags = excluded.tags, + edges_pruned = excluded.edges_pruned, + insights_rewritten = excluded.insights_rewritten, + insights_deleted = excluded.insights_deleted, + children_orphaned = excluded.children_orphaned", + params![ + id, + deleted_at.to_rfc3339(), + reason, + node.node_type, + tags_json, + edges_pruned, + insights_rewritten, + insights_deleted, + children_orphaned, + ], + )?; + + Self::record_sync_tombstone(&tx, "knowledge_nodes", id, "purge_node")?; + tx.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; + tx.commit()?; + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + if let Ok(mut index) = self.vector_index.lock() { + let _ = index.remove(id); + } + + Ok(PurgeReport { + memory_id: id.to_string(), + deleted: true, + deleted_at, + edges_pruned, + insights_rewritten, + insights_deleted, + children_orphaned, + }) + } + + fn node_exists(conn: &Connection, id: &str) -> Result { + let count: i64 = conn.query_row( + "SELECT COUNT(*) FROM knowledge_nodes WHERE id = ?1", + params![id], + |row| row.get(0), + )?; + Ok(count > 0) + } + + fn record_sync_tombstone( + conn: &Connection, + table_name: &str, + row_id: &str, + reason: &str, + ) -> Result<()> { + conn.execute( + "INSERT INTO sync_tombstones (table_name, row_id, deleted_at, reason) + VALUES (?1, ?2, ?3, ?4) + ON CONFLICT(table_name, row_id) DO UPDATE SET + deleted_at = excluded.deleted_at, + reason = excluded.reason", + params![table_name, row_id, Utc::now().to_rfc3339(), reason], + )?; + Ok(()) + } + /// Search with full-text search pub fn search(&self, query: &str, limit: i32) -> Result> { let sanitized_query = sanitize_fts5_query(query); @@ -1520,6 +2031,234 @@ impl Storage { Ok(result) } + /// FTS5 keyword search using individual-term matching (implicit AND). + /// + /// Unlike `search()` which uses phrase matching (words must be adjacent), + /// this returns documents containing ALL query words in any order and position. + /// This is more useful for free-text queries from external callers. + pub fn search_terms(&self, query: &str, limit: i32) -> Result> { + use crate::fts::sanitize_fts5_terms; + let Some(terms) = sanitize_fts5_terms(query) else { + return Ok(vec![]); + }; + + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare( + "SELECT n.* FROM knowledge_nodes n + JOIN knowledge_fts fts ON n.id = fts.id + WHERE knowledge_fts MATCH ?1 + ORDER BY rank + LIMIT ?2", + )?; + + let nodes = stmt.query_map(params![terms, limit], Self::row_to_node)?; + + let mut result = Vec::new(); + for node in nodes { + result.push(node?); + } + Ok(result) + } + + /// Concrete keyword/literal search that skips semantic expansion and + /// cognitive reranking. + /// + /// This path is for identifiers, paths, quoted strings, env vars, UUIDs, + /// and other exact user intent where "close enough" is wrong. + pub fn concrete_search_filtered( + &self, + query: &str, + limit: i32, + include_types: Option<&[String]>, + exclude_types: Option<&[String]>, + ) -> Result> { + let literal = Self::normalize_literal_query(query); + if literal.is_empty() { + return Ok(vec![]); + } + + let limit = limit.max(1) as usize; + let fetch_limit = ((limit * 10).min(500)) as i32; + let mut by_id: HashMap = HashMap::new(); + + if let Some(terms) = crate::fts::sanitize_fts5_terms(&literal) { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare( + "SELECT n.*, rank AS fts_rank FROM knowledge_nodes n + JOIN knowledge_fts fts ON n.id = fts.id + WHERE knowledge_fts MATCH ?1 + ORDER BY rank + LIMIT ?2", + )?; + + let rows = stmt.query_map(params![terms, fetch_limit], |row| { + let node = Self::row_to_node(row)?; + let rank = row.get::<_, f64>("fts_rank").unwrap_or(0.0); + Ok((node, rank)) + })?; + + for (idx, row) in rows.enumerate() { + let (node, rank) = row?; + if !Self::node_matches_type_filters(&node, include_types, exclude_types) { + continue; + } + let base_score = (1.0 / (idx as f32 + 1.0)).max((-rank as f32).max(0.0)); + Self::upsert_concrete_result(&mut by_id, node, base_score, Some(base_score)); + } + } + + let escaped = Self::escape_like(&literal.to_lowercase()); + let pattern = format!("%{}%", escaped); + let prefix_pattern = format!("{}%", escaped); + { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare( + "SELECT n.* FROM knowledge_nodes n + WHERE lower(n.id) = ?2 + OR lower(n.content) LIKE ?1 ESCAPE '\\' + OR lower(COALESCE(n.source, '')) LIKE ?1 ESCAPE '\\' + OR lower(n.tags) LIKE ?1 ESCAPE '\\' + ORDER BY + CASE + WHEN lower(n.id) = ?2 THEN 0 + WHEN lower(n.content) = ?2 THEN 1 + WHEN lower(n.content) LIKE ?3 ESCAPE '\\' THEN 2 + ELSE 3 + END, + n.updated_at DESC + LIMIT ?4", + )?; + + let rows = stmt.query_map( + params![pattern, literal.to_lowercase(), prefix_pattern, fetch_limit], + Self::row_to_node, + )?; + + for row in rows { + let node = row?; + if !Self::node_matches_type_filters(&node, include_types, exclude_types) { + continue; + } + if let Some(score) = Self::literal_match_score(&literal, &node) { + Self::upsert_concrete_result(&mut by_id, node, score, Some(score)); + } + } + } + + let mut results: Vec = by_id.into_values().collect(); + results.sort_by(|a, b| { + b.combined_score + .partial_cmp(&a.combined_score) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| b.node.updated_at.cmp(&a.node.updated_at)) + }); + results.truncate(limit); + Ok(results) + } + + fn upsert_concrete_result( + by_id: &mut HashMap, + node: KnowledgeNode, + score: f32, + keyword_score: Option, + ) { + by_id + .entry(node.id.clone()) + .and_modify(|existing| { + existing.combined_score = existing.combined_score.max(score); + existing.keyword_score = match (existing.keyword_score, keyword_score) { + (Some(a), Some(b)) => Some(a.max(b)), + (None, Some(b)) => Some(b), + (a, None) => a, + }; + }) + .or_insert(SearchResult { + node, + keyword_score, + semantic_score: None, + combined_score: score, + match_type: MatchType::Keyword, + }); + } + + fn normalize_literal_query(query: &str) -> String { + let trimmed = query.trim(); + if trimmed.len() >= 2 { + let bytes = trimmed.as_bytes(); + let quoted = (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"') + || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''); + if quoted { + return trimmed[1..trimmed.len() - 1].trim().to_string(); + } + } + trimmed.to_string() + } + + fn escape_like(value: &str) -> String { + let mut escaped = String::with_capacity(value.len()); + for ch in value.chars() { + match ch { + '\\' | '%' | '_' => { + escaped.push('\\'); + escaped.push(ch); + } + _ => escaped.push(ch), + } + } + escaped + } + + fn literal_match_score(query: &str, node: &KnowledgeNode) -> Option { + let q = query.to_lowercase(); + let content = node.content.to_lowercase(); + let tags = node.tags.join(" ").to_lowercase(); + let source = node.source.as_deref().unwrap_or("").to_lowercase(); + let id = node.id.to_lowercase(); + + if id == q { + Some(3.0) + } else if content == q { + Some(2.5) + } else if content.starts_with(&q) { + Some(2.0) + } else if content.contains(&q) { + Some(1.6) + } else if source.contains(&q) { + Some(1.4) + } else if tags.contains(&q) { + Some(1.2) + } else { + None + } + } + + fn node_matches_type_filters( + node: &KnowledgeNode, + include_types: Option<&[String]>, + exclude_types: Option<&[String]>, + ) -> bool { + if let Some(includes) = include_types + && !includes.is_empty() + { + return includes.iter().any(|t| t == &node.node_type); + } + if let Some(excludes) = exclude_types + && !excludes.is_empty() + { + return !excludes.iter().any(|t| t == &node.node_type); + } + true + } + /// Get all nodes (paginated) pub fn get_all_nodes(&self, limit: i32, offset: i32) -> Result> { let reader = self @@ -1620,15 +2359,16 @@ impl Storage { } /// Get query embedding from cache or compute it - #[cfg(feature = "embeddings")] + #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn get_query_embedding(&self, query: &str) -> Result> { + let cache_key = format!("{}\0{}", self.embedding_service.model_name(), query); // Check cache first { let mut cache = self .query_cache .lock() .map_err(|_| StorageError::Init("Query cache lock poisoned".to_string()))?; - if let Some(cached) = cache.get(query) { + if let Some(cached) = cache.get(&cache_key) { return Ok(cached.clone()); } } @@ -1636,7 +2376,7 @@ impl Storage { // Not in cache, compute embedding let embedding = self .embedding_service - .embed(query) + .embed_query(query) .map_err(|e| StorageError::Init(format!("Failed to embed query: {}", e)))?; // Store in cache @@ -1645,7 +2385,7 @@ impl Storage { .query_cache .lock() .map_err(|_| StorageError::Init("Query cache lock poisoned".to_string()))?; - cache.put(query.to_string(), embedding.vector.clone()); + cache.put(cache_key, embedding.vector.clone()); } Ok(embedding.vector) @@ -1832,6 +2572,60 @@ impl Storage { Ok(results) } + /// Keyword-only fallback for builds without local embeddings/vector search. + #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] + pub fn hybrid_search( + &self, + query: &str, + limit: i32, + _keyword_weight: f32, + _semantic_weight: f32, + ) -> Result> { + self.hybrid_search_filtered(query, limit, 1.0, 0.0, None, None) + } + + /// Keyword-only fallback for builds without local embeddings/vector search. + #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] + pub fn hybrid_search_filtered( + &self, + query: &str, + limit: i32, + _keyword_weight: f32, + _semantic_weight: f32, + include_types: Option<&[String]>, + exclude_types: Option<&[String]>, + ) -> Result> { + let nodes = self.search_terms(query, limit.max(1) * 4)?; + let mut results = Vec::new(); + + for node in nodes { + if let Some(includes) = include_types { + if !includes.iter().any(|t| t == &node.node_type) { + continue; + } + } else if let Some(excludes) = exclude_types + && excludes.iter().any(|t| t == &node.node_type) + { + continue; + } + + let score = 1.0 / (results.len() as f32 + 1.0); + results.push(SearchResult { + node, + keyword_score: Some(score), + semantic_score: None, + combined_score: score, + match_type: MatchType::Keyword, + }); + + if results.len() >= limit.max(1) as usize { + break; + } + } + + Ok(results) + } + /// Keyword search returning scores, with optional type filtering in the SQL query. #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn keyword_search_with_scores( @@ -1841,7 +2635,12 @@ impl Storage { include_types: Option<&[String]>, exclude_types: Option<&[String]>, ) -> Result> { - let sanitized_query = sanitize_fts5_query(query); + // Use individual-term matching (implicit AND) so multi-word queries find + // documents where all words appear anywhere, not just as adjacent phrases. + use crate::fts::sanitize_fts5_terms; + let Some(terms_query) = sanitize_fts5_terms(query) else { + return Ok(vec![]); + }; // Build the type filter clause and collect parameter values. // We use numbered parameters: ?1 = query, ?2 = limit, ?3.. = type strings. @@ -1887,7 +2686,7 @@ impl Storage { // Build the parameter list: [query, limit, ...type_values] let mut param_values: Vec> = Vec::new(); - param_values.push(Box::new(sanitized_query.clone())); + param_values.push(Box::new(terms_query)); param_values.push(Box::new(limit)); for tv in &type_values { param_values.push(Box::new(tv.to_string())); @@ -1971,66 +2770,30 @@ impl Storage { } let mut result = EmbeddingResult::default(); + let active_model = self.embedding_service.model_name(); + let nodes = self.embedding_regeneration_candidates(node_ids, force)?; - let nodes: Vec<(String, String)> = { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - if let Some(ids) = node_ids { - let placeholders = ids.iter().map(|_| "?").collect::>().join(","); - let query = format!( - "SELECT id, content FROM knowledge_nodes WHERE id IN ({})", - placeholders - ); - - let mut result_nodes = Vec::new(); - { - let mut stmt = reader.prepare(&query)?; - let params: Vec<&dyn rusqlite::ToSql> = - ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect(); - - let rows = stmt.query_map(params.as_slice(), |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })?; - - for r in rows.flatten() { - result_nodes.push(r); - } - } - result_nodes - } else if force { - let mut stmt = reader.prepare("SELECT id, content FROM knowledge_nodes")?; - let rows = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })?; - rows.filter_map(|r| r.ok()).collect() - } else { - let mut stmt = reader.prepare( - "SELECT id, content FROM knowledge_nodes - WHERE has_embedding = 0 OR has_embedding IS NULL", - )?; - let rows = stmt.query_map([], |row| { - Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?)) - })?; - rows.filter_map(|r| r.ok()).collect() - } - }; - - for (id, content) in nodes { + for (id, content, stored_model) in nodes { if !force { - let has_emb: i32 = self + let (has_emb, stored_model): (i32, Option) = self .reader .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))? .query_row( - "SELECT COALESCE(has_embedding, 0) FROM knowledge_nodes WHERE id = ?1", - params![id], - |row| row.get(0), + "SELECT COALESCE(kn.has_embedding, 0), COALESCE(ne.model, kn.embedding_model) + FROM knowledge_nodes kn + LEFT JOIN node_embeddings ne ON ne.node_id = kn.id + WHERE kn.id = ?1", + params![&id], + |row| Ok((row.get(0)?, row.get(1)?)), ) - .unwrap_or(0); + .unwrap_or((0, stored_model)); - if has_emb == 1 { + if has_emb == 1 + && stored_model.as_deref().is_some_and(|model| { + Self::embedding_model_matches_active(model, active_model) + }) + { result.skipped += 1; continue; } @@ -2048,6 +2811,78 @@ impl Storage { Ok(result) } + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + fn embedding_regeneration_candidates( + &self, + node_ids: Option<&[String]>, + force: bool, + ) -> Result)>> { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + + if let Some(ids) = node_ids { + if ids.is_empty() { + return Ok(Vec::new()); + } + + let placeholders = ids.iter().map(|_| "?").collect::>().join(","); + let query = format!( + "SELECT kn.id, kn.content, COALESCE(ne.model, kn.embedding_model) AS embedding_model + FROM knowledge_nodes kn + LEFT JOIN node_embeddings ne ON ne.node_id = kn.id + WHERE kn.id IN ({})", + placeholders + ); + + let mut stmt = reader.prepare(&query)?; + let params: Vec<&dyn rusqlite::ToSql> = + ids.iter().map(|s| s as &dyn rusqlite::ToSql).collect(); + let rows = stmt.query_map(params.as_slice(), |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)?, + )) + })?; + return Ok(rows.filter_map(|r| r.ok()).collect()); + } + + if force { + let mut stmt = + reader.prepare("SELECT id, content, embedding_model FROM knowledge_nodes")?; + let rows = stmt.query_map([], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)?, + )) + })?; + return Ok(rows.filter_map(|r| r.ok()).collect()); + } + + let active_model = self.embedding_service.model_name(); + let model_pattern = Self::active_embedding_model_like_pattern(active_model); + let mut stmt = reader.prepare( + "SELECT kn.id, kn.content, COALESCE(ne.model, kn.embedding_model) AS embedding_model + FROM knowledge_nodes kn + LEFT JOIN node_embeddings ne ON ne.node_id = kn.id + WHERE kn.has_embedding = 0 + OR kn.has_embedding IS NULL + OR ne.node_id IS NULL + OR COALESCE(ne.model, kn.embedding_model, '') NOT LIKE ?1", + )?; + let rows = stmt.query_map(params![model_pattern], |row| { + Ok(( + row.get::<_, String>(0)?, + row.get::<_, String>(1)?, + row.get::<_, Option>(2)?, + )) + })?; + Ok(rows.filter_map(|r| r.ok()).collect()) + } + /// Query memories valid at a specific time pub fn query_at_time( &self, @@ -2321,7 +3156,8 @@ impl Storage { } } - // 3. Generate missing embeddings + // 3. Generate missing and model-mismatched embeddings. + // This must drain the whole set so embedder upgrades do not strand v1 corpora. #[cfg(all(feature = "embeddings", feature = "vector-search"))] let embeddings_generated = self.generate_missing_embeddings()?; #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] @@ -2875,7 +3711,7 @@ impl Storage { Ok(Some(optimized_w20)) } - /// Generate missing embeddings + /// Generate all missing or active-model-mismatched embeddings. #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn generate_missing_embeddings(&self) -> Result { if !self.embedding_service.is_ready() @@ -2885,33 +3721,15 @@ impl Storage { return Ok(0); } - let nodes: Vec<(String, String)> = { - let reader = self - .reader - .lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - reader - .prepare( - "SELECT id, content FROM knowledge_nodes - WHERE has_embedding = 0 OR has_embedding IS NULL - LIMIT 100", - )? - .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? - .filter_map(|r| r.ok()) - .collect() - }; - - let mut count = 0i64; - - for (id, content) in nodes { - if let Err(e) = self.generate_embedding_for_node(&id, &content) { - tracing::warn!("Failed to generate embedding for {}: {}", id, e); - } else { - count += 1; - } + let result = self.generate_embeddings(None, false)?; + if result.failed > 0 { + tracing::warn!( + failed = result.failed, + "Some embeddings could not be regenerated during consolidation" + ); } - Ok(count) + Ok(result.successful) } } @@ -3826,19 +4644,14 @@ impl Storage { Ok(count) } - /// Get last backup timestamp by scanning the backups directory. - /// Parses `vestige-YYYYMMDD-HHMMSS.db` filenames. - pub fn get_last_backup_timestamp() -> Option> { - let proj_dirs = directories::ProjectDirs::from("com", "vestige", "core")?; - let backup_dir = proj_dirs.data_dir().parent()?.join("backups"); - + fn scan_last_backup_timestamp(backup_dir: &Path) -> Option> { if !backup_dir.exists() { return None; } let mut latest: Option> = None; - if let Ok(entries) = std::fs::read_dir(&backup_dir) { + if let Ok(entries) = std::fs::read_dir(backup_dir) { for entry in entries.flatten() { let name = entry.file_name(); let name_str = name.to_string_lossy(); @@ -3860,6 +4673,1034 @@ impl Storage { latest } + /// Get last backup timestamp for this storage instance. + /// Parses `vestige-YYYYMMDD-HHMMSS.db` filenames. + pub fn last_backup_timestamp(&self) -> Option> { + Self::scan_last_backup_timestamp(&self.sidecar_dir("backups")) + } + + /// Get last backup timestamp in the default backups directory. + /// Kept for compatibility with older callers. + pub fn get_last_backup_timestamp() -> Option> { + let backup_dir = Self::default_db_path().ok()?.parent()?.join("backups"); + Self::scan_last_backup_timestamp(&backup_dir) + } + + /// Export an exact portable archive preserving raw Vestige storage rows. + /// + /// Unlike the user-facing JSON export, this preserves IDs, timestamps, + /// FSRS state, graph edges, suppression state, history tables, and raw + /// embedding blobs. It is intended for Vestige-to-Vestige device transfer. + pub fn export_portable_archive(&self) -> Result { + let mut reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let tx = reader.transaction()?; + + let schema_version = Self::current_schema_version(&tx)?; + let mut tables = Vec::new(); + + for table_name in PORTABLE_TABLES { + if !Self::table_exists(&tx, table_name)? { + continue; + } + + let quoted_table = Self::quote_ident(table_name); + let mut stmt = tx.prepare(&format!("SELECT * FROM {} ORDER BY rowid", quoted_table))?; + let columns: Vec = stmt + .column_names() + .iter() + .map(|name| (*name).to_string()) + .collect(); + let column_count = columns.len(); + + let rows = stmt.query_map([], |row| { + let mut values = Vec::with_capacity(column_count); + for idx in 0..column_count { + values.push(Self::portable_value_from_ref(row.get_ref(idx)?)?); + } + Ok(values) + })?; + + let mut portable_rows = Vec::new(); + for row in rows { + portable_rows.push(row?); + } + + tables.push(PortableTable { + name: (*table_name).to_string(), + columns, + rows: portable_rows, + }); + } + + let archive = PortableArchive { + archive_format: PORTABLE_ARCHIVE_FORMAT.to_string(), + vestige_version: crate::VERSION.to_string(), + schema_version, + exported_at: Utc::now(), + mode: "exact".to_string(), + tables, + }; + tx.commit()?; + Ok(archive) + } + + /// Write an exact portable archive to a JSON file. + pub fn export_portable_archive_to_path( + &self, + path: &std::path::Path, + ) -> Result { + let archive = self.export_portable_archive()?; + let parent = path.parent().unwrap_or_else(|| std::path::Path::new(".")); + let filename = path + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or("vestige-portable.json"); + let temp_path = parent.join(format!(".{}.tmp-{}", filename, Uuid::new_v4())); + + #[cfg(unix)] + let mut file = { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .mode(0o600) + .open(&temp_path)? + }; + #[cfg(not(unix))] + let mut file = std::fs::File::create(&temp_path)?; + if let Err(e) = serde_json::to_writer_pretty(&mut file, &archive) { + let _ = std::fs::remove_file(&temp_path); + return Err(StorageError::Init(format!( + "Failed to write portable archive: {}", + e + ))); + } + file.flush()?; + file.sync_all()?; + drop(file); + + if let Err(rename_err) = std::fs::rename(&temp_path, path) { + if path.exists() { + std::fs::remove_file(path)?; + std::fs::rename(&temp_path, path)?; + } else { + let _ = std::fs::remove_file(&temp_path); + return Err(rename_err.into()); + } + } + Ok(archive) + } + + /// Import an exact portable archive. + /// + /// `EmptyOnly` preserves the conservative migration path. `Merge` is used + /// by portable sync to combine non-empty databases with tombstones and + /// newer-local conflict handling. + pub fn import_portable_archive( + &self, + archive: &PortableArchive, + mode: PortableImportMode, + ) -> Result { + if archive.archive_format != PORTABLE_ARCHIVE_FORMAT { + return Err(StorageError::Init(format!( + "Unsupported portable archive format '{}'", + archive.archive_format + ))); + } + if archive.mode != "exact" { + return Err(StorageError::Init(format!( + "Unsupported portable archive mode '{}'", + archive.mode + ))); + } + + let mut seen_tables = std::collections::HashSet::new(); + let mut tables_by_name = std::collections::HashMap::new(); + for table in &archive.tables { + if !PORTABLE_TABLES.contains(&table.name.as_str()) { + return Err(StorageError::Init(format!( + "Portable archive contains unsupported table '{}'", + table.name + ))); + } + if !seen_tables.insert(table.name.as_str()) { + return Err(StorageError::Init(format!( + "Portable archive contains duplicate table '{}'", + table.name + ))); + } + tables_by_name.insert(table.name.as_str(), table); + } + + let mut report = PortableImportReport { + tables_imported: 0, + rows_imported: 0, + tables_skipped: 0, + fts_rebuilt: false, + rows_inserted: 0, + rows_updated: 0, + rows_skipped: 0, + rows_deleted: 0, + conflicts_kept_local: 0, + }; + + { + let mut writer = self + .writer + .lock() + .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; + + let current_schema = Self::current_schema_version(&writer)?; + if archive.schema_version > current_schema { + return Err(StorageError::Init(format!( + "Archive schema version {} is newer than this Vestige database schema {}", + archive.schema_version, current_schema + ))); + } + + match mode { + PortableImportMode::EmptyOnly => { + Self::ensure_portable_import_target_empty(&writer)? + } + PortableImportMode::Merge => {} + } + + let tx = writer.transaction()?; + let mut merge_state = PortableMergeState::default(); + + for table_name in PORTABLE_TABLES { + let Some(table) = tables_by_name.get(table_name) else { + continue; + }; + + if !Self::table_exists(&tx, table_name)? { + report.tables_skipped += 1; + continue; + } + + if mode == PortableImportMode::Merge { + Self::merge_portable_table( + &tx, + table_name, + table, + &mut report, + &mut merge_state, + )?; + report.tables_imported += 1; + continue; + } + + let target_columns = Self::table_columns(&tx, table_name)?; + let mut insert_columns = Vec::new(); + let mut source_indexes = Vec::new(); + + for (idx, column) in table.columns.iter().enumerate() { + if target_columns.iter().any(|target| target == column) { + insert_columns.push(column.clone()); + source_indexes.push(idx); + } + } + + if insert_columns.is_empty() { + report.tables_skipped += 1; + continue; + } + + let quoted_table = Self::quote_ident(table_name); + let quoted_columns = insert_columns + .iter() + .map(|column| Self::quote_ident(column)) + .collect::>() + .join(", "); + let placeholders = std::iter::repeat_n("?", insert_columns.len()) + .collect::>() + .join(", "); + let verb = if *table_name == "fsrs_config" { + "INSERT OR REPLACE" + } else { + "INSERT" + }; + let sql = format!( + "{} INTO {} ({}) VALUES ({})", + verb, quoted_table, quoted_columns, placeholders + ); + + for row in &table.rows { + if row.len() != table.columns.len() { + return Err(StorageError::Init(format!( + "Portable archive row in table '{}' has {} values for {} columns", + table_name, + row.len(), + table.columns.len() + ))); + } + + let values = source_indexes + .iter() + .map(|idx| row[*idx].to_sql_value()) + .collect::, _>>() + .map_err(|e| { + StorageError::Init(format!("Invalid portable value: {}", e)) + })?; + tx.execute(&sql, params_from_iter(values))?; + report.rows_imported += 1; + report.rows_inserted += 1; + } + + report.tables_imported += 1; + } + + if Self::table_exists(&tx, "knowledge_fts")? { + tx.execute( + "INSERT INTO knowledge_fts(knowledge_fts) VALUES('rebuild')", + [], + )?; + report.fts_rebuilt = true; + } + + tx.commit()?; + } + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + self.load_embeddings_into_index()?; + + Ok(report) + } + + /// Read and import an exact portable archive JSON file. + pub fn import_portable_archive_from_path( + &self, + path: &std::path::Path, + mode: PortableImportMode, + ) -> Result { + let file = std::fs::File::open(path)?; + let archive: PortableArchive = serde_json::from_reader(file) + .map_err(|e| StorageError::Init(format!("Failed to parse portable archive: {}", e)))?; + self.import_portable_archive(&archive, mode) + } + + /// Synchronize this database with a pluggable portable archive backend. + /// + /// Sync is pull-merge-push: + /// 1. read remote archive if present, + /// 2. merge it into the local database using tombstones and conflict rules, + /// 3. export the merged local database, + /// 4. write the archive back through the backend. + pub fn sync_portable_archive( + &self, + backend: &B, + ) -> Result { + let (pulled, pull) = match backend.read_archive()? { + Some(remote) => ( + true, + Some(self.import_portable_archive(&remote, PortableImportMode::Merge)?), + ), + None => (false, None), + }; + + let archive = self.export_portable_archive()?; + let pushed_tables = archive.tables.len(); + let pushed_rows = archive.total_rows(); + let archive_format = archive.archive_format.clone(); + backend.write_archive(&archive)?; + + Ok(PortableSyncReport { + backend: backend.label(), + pulled, + pull, + pushed_tables, + pushed_rows, + archive_format, + }) + } + + /// Synchronize this database with a file-backed portable archive. + pub fn sync_portable_archive_file(&self, path: &std::path::Path) -> Result { + let backend = FilePortableSyncBackend::new(path); + self.sync_portable_archive(&backend) + } + + fn merge_portable_table( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + table: &PortableTable, + report: &mut PortableImportReport, + state: &mut PortableMergeState, + ) -> Result<()> { + match table_name { + "sync_tombstones" => Self::merge_sync_tombstones(tx, table, report), + "knowledge_nodes" => Self::merge_knowledge_nodes(tx, table, report, state), + "memory_access_log" + | "state_transitions" + | "consolidation_history" + | "dream_history" + | "retention_snapshots" => Self::merge_append_only_table(tx, table_name, table, report), + "node_embeddings" => { + Self::merge_keyed_table(tx, table_name, table, &["node_id"], report, state) + } + "fsrs_cards" | "memory_states" => { + Self::merge_keyed_table(tx, table_name, table, &["memory_id"], report, state) + } + "deletion_tombstones" => Self::merge_deletion_tombstones(tx, table, report), + "memory_connections" => Self::merge_keyed_table( + tx, + table_name, + table, + &["source_id", "target_id"], + report, + state, + ), + "intentions" | "insights" | "sessions" => { + Self::merge_keyed_table(tx, table_name, table, &["id"], report, state) + } + "fsrs_config" => { + Self::merge_keyed_table(tx, table_name, table, &["key"], report, state) + } + _ => { + report.tables_skipped += 1; + Ok(()) + } + } + } + + fn merge_knowledge_nodes( + tx: &rusqlite::Transaction<'_>, + table: &PortableTable, + report: &mut PortableImportReport, + state: &mut PortableMergeState, + ) -> Result<()> { + for row in &table.rows { + let Some(id) = Self::portable_text(table, row, "id") else { + report.rows_skipped += 1; + continue; + }; + let incoming_updated = Self::portable_timestamp(table, row, "updated_at"); + + if let Some(deleted_at) = Self::tombstone_timestamp(tx, "knowledge_nodes", id)? + && incoming_updated.is_some_and(|updated| deleted_at >= updated) + { + report.conflicts_kept_local += 1; + report.rows_skipped += 1; + continue; + } + + let existing_updated: Option = tx + .query_row( + "SELECT updated_at FROM knowledge_nodes WHERE id = ?1", + params![id], + |row| row.get(0), + ) + .optional()?; + + if let (Some(existing), Some(incoming)) = ( + existing_updated + .as_deref() + .and_then(Self::parse_rfc3339_opt), + incoming_updated, + ) && existing > incoming + { + state.locally_newer_nodes.insert(id.to_string()); + report.conflicts_kept_local += 1; + report.rows_skipped += 1; + continue; + } + + let affected = Self::insert_or_replace_row(tx, "knowledge_nodes", table, row)?; + report.rows_imported += 1; + if affected == MergeWrite::Inserted { + report.rows_inserted += 1; + } else { + report.rows_updated += 1; + } + } + Ok(()) + } + + fn merge_sync_tombstones( + tx: &rusqlite::Transaction<'_>, + table: &PortableTable, + report: &mut PortableImportReport, + ) -> Result<()> { + for row in &table.rows { + let Some(table_name) = Self::portable_text(table, row, "table_name") else { + report.rows_skipped += 1; + continue; + }; + let Some(row_id) = Self::portable_text(table, row, "row_id") else { + report.rows_skipped += 1; + continue; + }; + let incoming_deleted_at = Self::portable_timestamp(table, row, "deleted_at"); + let incoming_reason = Self::portable_text(table, row, "reason").map(ToOwned::to_owned); + + let existing_tombstone: Option<(String, Option)> = tx + .query_row( + "SELECT deleted_at, reason FROM sync_tombstones WHERE table_name = ?1 AND row_id = ?2", + params![table_name, row_id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .optional()?; + let existing_deleted_at = existing_tombstone + .as_ref() + .and_then(|(deleted_at, _)| Self::parse_rfc3339_opt(deleted_at)); + let incoming_wins = match (existing_deleted_at, incoming_deleted_at) { + (Some(existing), Some(incoming)) => incoming >= existing, + (Some(_), None) => false, + (None, _) => true, + }; + + let (effective_deleted_at, effective_reason) = if incoming_wins { + let affected = Self::insert_or_replace_row(tx, "sync_tombstones", table, row)?; + report.rows_imported += 1; + if affected == MergeWrite::Inserted { + report.rows_inserted += 1; + } else { + report.rows_updated += 1; + } + (incoming_deleted_at, incoming_reason) + } else { + report.rows_skipped += 1; + ( + existing_deleted_at, + existing_tombstone.and_then(|(_, reason)| reason), + ) + }; + + if table_name == "knowledge_nodes" { + let local_updated: Option = tx + .query_row( + "SELECT updated_at FROM knowledge_nodes WHERE id = ?1", + params![row_id], + |row| row.get(0), + ) + .optional()?; + let should_delete = match ( + local_updated.as_deref().and_then(Self::parse_rfc3339_opt), + effective_deleted_at, + ) { + (Some(local), Some(deleted)) => { + effective_reason.as_deref() == Some("purge_node") || deleted >= local + } + (Some(_), None) => true, + (None, _) => false, + }; + if should_delete { + let deleted = + tx.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![row_id])?; + report.rows_deleted += deleted; + } + } + } + Ok(()) + } + + fn merge_deletion_tombstones( + tx: &rusqlite::Transaction<'_>, + table: &PortableTable, + report: &mut PortableImportReport, + ) -> Result<()> { + for row in &table.rows { + let Some(memory_id) = Self::portable_text(table, row, "memory_id") else { + report.rows_skipped += 1; + continue; + }; + let incoming_deleted_at = Self::portable_timestamp(table, row, "deleted_at"); + let existing_deleted_at: Option = tx + .query_row( + "SELECT deleted_at FROM deletion_tombstones WHERE memory_id = ?1", + params![memory_id], + |row| row.get(0), + ) + .optional()?; + + if let (Some(existing), Some(incoming)) = ( + existing_deleted_at + .as_deref() + .and_then(Self::parse_rfc3339_opt), + incoming_deleted_at, + ) && existing > incoming + { + report.rows_skipped += 1; + continue; + } + + let affected = Self::insert_or_replace_row(tx, "deletion_tombstones", table, row)?; + report.rows_imported += 1; + if affected == MergeWrite::Inserted { + report.rows_inserted += 1; + } else { + report.rows_updated += 1; + } + } + Ok(()) + } + + fn merge_keyed_table( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + table: &PortableTable, + key_columns: &[&str], + report: &mut PortableImportReport, + state: &PortableMergeState, + ) -> Result<()> { + for row in &table.rows { + if !Self::parent_rows_exist(tx, table_name, table, row)? { + report.rows_skipped += 1; + continue; + } + if key_columns + .iter() + .any(|column| Self::portable_value(table, row, column).is_none()) + { + report.rows_skipped += 1; + continue; + } + if Self::row_references_locally_newer_node(table_name, table, row, state) { + report.conflicts_kept_local += 1; + report.rows_skipped += 1; + continue; + } + let affected = Self::insert_or_replace_row(tx, table_name, table, row)?; + report.rows_imported += 1; + if affected == MergeWrite::Inserted { + report.rows_inserted += 1; + } else { + report.rows_updated += 1; + } + } + Ok(()) + } + + fn row_references_locally_newer_node( + table_name: &str, + table: &PortableTable, + row: &[PortableValue], + state: &PortableMergeState, + ) -> bool { + match table_name { + "node_embeddings" => Self::portable_text(table, row, "node_id") + .is_some_and(|id| state.locally_newer_nodes.contains(id)), + "fsrs_cards" | "memory_states" => Self::portable_text(table, row, "memory_id") + .is_some_and(|id| state.locally_newer_nodes.contains(id)), + "memory_connections" => { + Self::portable_text(table, row, "source_id") + .is_some_and(|id| state.locally_newer_nodes.contains(id)) + || Self::portable_text(table, row, "target_id") + .is_some_and(|id| state.locally_newer_nodes.contains(id)) + } + _ => false, + } + } + + fn merge_append_only_table( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + table: &PortableTable, + report: &mut PortableImportReport, + ) -> Result<()> { + for row in &table.rows { + if !Self::parent_rows_exist(tx, table_name, table, row)? { + report.rows_skipped += 1; + continue; + } + + let insert_columns: Vec = table + .columns + .iter() + .filter(|column| column.as_str() != "id") + .cloned() + .collect(); + if insert_columns.is_empty() { + report.rows_skipped += 1; + continue; + } + + let values = Self::row_values_for_columns(table, row, &insert_columns)?; + if Self::row_exists_by_values(tx, table_name, &insert_columns, &values)? { + report.rows_skipped += 1; + continue; + } + + Self::insert_row_with_columns(tx, table_name, &insert_columns, values)?; + report.rows_imported += 1; + report.rows_inserted += 1; + } + Ok(()) + } + + fn parent_rows_exist( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + table: &PortableTable, + row: &[PortableValue], + ) -> Result { + match table_name { + "node_embeddings" | "memory_access_log" => Self::portable_text(table, row, "node_id") + .map(|id| Self::node_exists(tx, id)) + .transpose() + .map(|v| v.unwrap_or(false)), + "fsrs_cards" | "memory_states" | "state_transitions" => { + Self::portable_text(table, row, "memory_id") + .map(|id| Self::node_exists(tx, id)) + .transpose() + .map(|v| v.unwrap_or(false)) + } + "memory_connections" => { + let source_exists = Self::portable_text(table, row, "source_id") + .map(|id| Self::node_exists(tx, id)) + .transpose()? + .unwrap_or(false); + let target_exists = Self::portable_text(table, row, "target_id") + .map(|id| Self::node_exists(tx, id)) + .transpose()? + .unwrap_or(false); + Ok(source_exists && target_exists) + } + _ => Ok(true), + } + } + + fn insert_or_replace_row( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + table: &PortableTable, + row: &[PortableValue], + ) -> Result { + let key_exists = Self::merge_row_exists(tx, table_name, table, row)?; + let values = Self::row_values_for_columns(table, row, &table.columns)?; + Self::upsert_row_with_columns(tx, table_name, &table.columns, values)?; + Ok(if key_exists { + MergeWrite::Updated + } else { + MergeWrite::Inserted + }) + } + + fn merge_key_columns(table_name: &str) -> &'static [&'static str] { + match table_name { + "knowledge_nodes" | "intentions" | "insights" | "sessions" => &["id"], + "node_embeddings" => &["node_id"], + "fsrs_cards" | "memory_states" | "deletion_tombstones" => &["memory_id"], + "memory_connections" => &["source_id", "target_id"], + "fsrs_config" => &["key"], + "sync_tombstones" => &["table_name", "row_id"], + _ => &[], + } + } + + fn upsert_row_with_columns( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + columns: &[String], + values: Vec, + ) -> Result<()> { + let key_columns = Self::merge_key_columns(table_name); + if key_columns.is_empty() { + return Self::insert_row_with_columns(tx, table_name, columns, values); + } + + let quoted_table = Self::quote_ident(table_name); + let quoted_columns = columns + .iter() + .map(|column| Self::quote_ident(column)) + .collect::>() + .join(", "); + let placeholders = std::iter::repeat_n("?", columns.len()) + .collect::>() + .join(", "); + let conflict_target = key_columns + .iter() + .map(|column| Self::quote_ident(column)) + .collect::>() + .join(", "); + let update_columns = columns + .iter() + .filter(|column| !key_columns.iter().any(|key| key == &column.as_str())) + .map(|column| { + let quoted = Self::quote_ident(column); + format!("{quoted} = excluded.{quoted}") + }) + .collect::>(); + + let conflict_action = if update_columns.is_empty() { + "DO NOTHING".to_string() + } else { + format!("DO UPDATE SET {}", update_columns.join(", ")) + }; + + let sql = format!( + "INSERT INTO {} ({}) VALUES ({}) ON CONFLICT({}) {}", + quoted_table, quoted_columns, placeholders, conflict_target, conflict_action + ); + tx.execute(&sql, params_from_iter(values))?; + Ok(()) + } + + fn insert_row_with_columns( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + columns: &[String], + values: Vec, + ) -> Result<()> { + let quoted_table = Self::quote_ident(table_name); + let quoted_columns = columns + .iter() + .map(|column| Self::quote_ident(column)) + .collect::>() + .join(", "); + let placeholders = std::iter::repeat_n("?", columns.len()) + .collect::>() + .join(", "); + let sql = format!( + "INSERT OR REPLACE INTO {} ({}) VALUES ({})", + quoted_table, quoted_columns, placeholders + ); + tx.execute(&sql, params_from_iter(values))?; + Ok(()) + } + + fn merge_row_exists( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + table: &PortableTable, + row: &[PortableValue], + ) -> Result { + let key_columns = Self::merge_key_columns(table_name); + if key_columns.is_empty() { + return Ok(false); + } + let mut columns = Vec::new(); + for key in key_columns { + columns.push((*key).to_string()); + } + let values = Self::row_values_for_columns(table, row, &columns)?; + Self::row_exists_by_values(tx, table_name, &columns, &values) + } + + fn row_exists_by_values( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + columns: &[String], + values: &[Value], + ) -> Result { + let quoted_table = Self::quote_ident(table_name); + let where_clause = columns + .iter() + .map(|column| format!("{} IS ?", Self::quote_ident(column))) + .collect::>() + .join(" AND "); + let sql = format!( + "SELECT COUNT(*) FROM {} WHERE {}", + quoted_table, where_clause + ); + let count: i64 = tx.query_row(&sql, params_from_iter(values.iter()), |row| row.get(0))?; + Ok(count > 0) + } + + fn row_values_for_columns( + table: &PortableTable, + row: &[PortableValue], + columns: &[String], + ) -> Result> { + columns + .iter() + .map(|column| { + Self::portable_value(table, row, column) + .ok_or_else(|| { + StorageError::Init(format!( + "Portable archive row in table '{}' is missing column '{}'", + table.name, column + )) + })? + .to_sql_value() + .map_err(|e| StorageError::Init(format!("Invalid portable value: {}", e))) + }) + .collect() + } + + fn portable_value<'a>( + table: &PortableTable, + row: &'a [PortableValue], + column: &str, + ) -> Option<&'a PortableValue> { + table + .columns + .iter() + .position(|name| name == column) + .and_then(|idx| row.get(idx)) + } + + fn portable_text<'a>( + table: &PortableTable, + row: &'a [PortableValue], + column: &str, + ) -> Option<&'a str> { + match Self::portable_value(table, row, column) { + Some(PortableValue::Text(value)) => Some(value.as_str()), + _ => None, + } + } + + fn portable_timestamp( + table: &PortableTable, + row: &[PortableValue], + column: &str, + ) -> Option> { + Self::portable_text(table, row, column).and_then(Self::parse_rfc3339_opt) + } + + fn parse_rfc3339_opt(value: &str) -> Option> { + DateTime::parse_from_rfc3339(value) + .map(|dt| dt.with_timezone(&Utc)) + .ok() + } + + fn tombstone_timestamp( + tx: &rusqlite::Transaction<'_>, + table_name: &str, + row_id: &str, + ) -> Result>> { + let deleted_at: Option = tx + .query_row( + "SELECT deleted_at FROM sync_tombstones WHERE table_name = ?1 AND row_id = ?2", + params![table_name, row_id], + |row| row.get(0), + ) + .optional()?; + Ok(deleted_at.as_deref().and_then(Self::parse_rfc3339_opt)) + } + + fn current_schema_version(conn: &Connection) -> Result { + let version: i64 = conn.query_row( + "SELECT COALESCE(MAX(version), 0) FROM schema_version", + [], + |row| row.get(0), + )?; + Ok(version as u32) + } + + fn ensure_portable_import_target_empty(conn: &Connection) -> Result<()> { + for table_name in PORTABLE_USER_DATA_TABLES { + if Self::table_exists(conn, table_name)? { + let count = Self::table_row_count(conn, table_name)?; + if count > 0 { + return Err(StorageError::Init(format!( + "Portable import requires an empty target database; table '{}' has {} rows", + table_name, count + ))); + } + } + } + Ok(()) + } + + fn table_exists(conn: &Connection, table_name: &str) -> Result { + let exists: i64 = conn.query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type IN ('table', 'view') AND name = ?1", + params![table_name], + |row| row.get(0), + )?; + Ok(exists > 0) + } + + fn table_row_count(conn: &Connection, table_name: &str) -> Result { + let sql = format!("SELECT COUNT(*) FROM {}", Self::quote_ident(table_name)); + Ok(conn.query_row(&sql, [], |row| row.get(0))?) + } + + fn table_columns(conn: &Connection, table_name: &str) -> Result> { + let sql = format!("PRAGMA table_info({})", Self::quote_ident(table_name)); + let mut stmt = conn.prepare(&sql)?; + let rows = stmt.query_map([], |row| row.get::<_, String>(1))?; + + let mut columns = Vec::new(); + for row in rows { + columns.push(row?); + } + Ok(columns) + } + + fn portable_value_from_ref(value: ValueRef<'_>) -> rusqlite::Result { + Ok(match value { + ValueRef::Null => PortableValue::Null, + ValueRef::Integer(value) => PortableValue::Integer(value), + ValueRef::Real(value) => PortableValue::Real(value), + ValueRef::Text(value) => PortableValue::Text( + std::str::from_utf8(value) + .map_err(|e| { + rusqlite::Error::FromSqlConversionFailure(0, Type::Text, Box::new(e)) + })? + .to_string(), + ), + ValueRef::Blob(value) => PortableValue::Blob(encode_hex(value)), + }) + } + + fn quote_ident(identifier: &str) -> String { + format!("\"{}\"", identifier.replace('"', "\"\"")) + } + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + fn embedding_model_matches_active(stored_model: &str, active_model: &str) -> bool { + if stored_model == active_model { + return true; + } + + let stored = stored_model.to_ascii_lowercase(); + let active = active_model.to_ascii_lowercase(); + + if active.contains("qwen3") { + return stored.contains("qwen3"); + } + + if active.contains("nomic-embed-text-v1.5") { + return stored.contains("nomic") && stored.contains("v1.5"); + } + + false + } + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + fn embedding_model_supports_matryoshka(model_name: &str) -> bool { + let model = model_name.to_ascii_lowercase(); + model.contains("nomic") || model.contains("qwen3") + } + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + fn embedding_vector_for_active_model( + embedding_bytes: &[u8], + stored_model: &str, + active_model: &str, + ) -> Option> { + if !Self::embedding_model_matches_active(stored_model, active_model) { + return None; + } + + let embedding = Embedding::from_bytes(embedding_bytes)?; + if embedding.dimensions == EMBEDDING_DIMENSIONS { + Some(embedding.vector) + } else if Self::embedding_model_supports_matryoshka(stored_model) { + Some(matryoshka_truncate(embedding.vector)) + } else { + None + } + } + + #[cfg(feature = "embeddings")] + fn active_embedding_model_like_pattern(active_model: &str) -> String { + let active = active_model.to_ascii_lowercase(); + if active.contains("qwen3") { + "%qwen3%".to_string() + } else if active.contains("nomic-embed-text-v1.5") { + "%nomic%v1.5%".to_string() + } else { + active_model.to_string() + } + } + // ======================================================================== // STATE TRANSITIONS (Audit Trail) // ======================================================================== @@ -3916,6 +5757,13 @@ impl Storage { .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; // VACUUM INTO doesn't support parameterized queries; escape single quotes reader.execute_batch(&format!("VACUUM INTO '{}'", path_str.replace('\'', "''")))?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(path)?.permissions(); + perms.set_mode(0o600); + std::fs::set_permissions(path, perms)?; + } Ok(()) } @@ -4040,8 +5888,7 @@ impl Storage { pub fn gc_below_retention(&self, threshold: f64, min_age_days: i64) -> Result { let cutoff = (Utc::now() - Duration::days(min_age_days)).to_rfc3339(); - // Collect IDs first for vector index cleanup - #[cfg(all(feature = "embeddings", feature = "vector-search"))] + // Collect IDs first for sync tombstones and vector index cleanup. let doomed_ids: Vec = { let reader = self .reader @@ -4059,6 +5906,9 @@ impl Storage { .writer .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; + for id in &doomed_ids { + Self::record_sync_tombstone(&writer, "knowledge_nodes", id, "gc_below_retention")?; + } let deleted = writer.execute( "DELETE FROM knowledge_nodes WHERE retention_strength < ?1 AND created_at < ?2", params![threshold, cutoff], @@ -4292,6 +6142,96 @@ mod tests { Storage::new(Some(db_path)).unwrap() } + fn create_test_storage_at(dir: &tempfile::TempDir, name: &str) -> Storage { + Storage::new(Some(dir.path().join(name))).unwrap() + } + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + #[test] + fn test_embedding_model_family_matching() { + assert!(Storage::embedding_model_matches_active( + "nomic-embed-text-v1.5", + "nomic-ai/nomic-embed-text-v1.5", + )); + assert!(Storage::embedding_model_matches_active( + "Qwen/Qwen3-Embedding-0.6B", + "Qwen/Qwen3-Embedding-0.6B", + )); + assert!(!Storage::embedding_model_matches_active( + "nomic-ai/nomic-embed-text-v1.5", + "Qwen/Qwen3-Embedding-0.6B", + )); + + let bytes = Embedding::new(vec![1.0; EMBEDDING_DIMENSIONS]).to_bytes(); + assert!( + Storage::embedding_vector_for_active_model( + &bytes, + "nomic-ai/nomic-embed-text-v1.5", + "Qwen/Qwen3-Embedding-0.6B", + ) + .is_none() + ); + assert!( + Storage::embedding_vector_for_active_model( + &bytes, + "Qwen/Qwen3-Embedding-0.6B", + "Qwen/Qwen3-Embedding-0.6B", + ) + .is_some() + ); + } + + #[cfg(all(feature = "embeddings", feature = "vector-search"))] + #[test] + fn test_embedding_regeneration_candidates_include_entire_mismatched_corpus() { + let storage = create_test_storage(); + let stale_model = "all-MiniLM-L6-v2"; + let stale_embedding = Embedding::new(vec![0.0; EMBEDDING_DIMENSIONS]).to_bytes(); + + for i in 0..125 { + let node = storage + .ingest(IngestInput { + content: format!("legacy embedded memory {}", i), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let writer = storage.writer.lock().unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO node_embeddings + (node_id, embedding, dimensions, model, created_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + rusqlite::params![ + &node.id, + &stale_embedding, + EMBEDDING_DIMENSIONS as i32, + stale_model, + Utc::now().to_rfc3339() + ], + ) + .unwrap(); + writer + .execute( + "UPDATE knowledge_nodes + SET has_embedding = 1, embedding_model = ?2 + WHERE id = ?1", + rusqlite::params![&node.id, stale_model], + ) + .unwrap(); + } + + let stats = storage.get_stats().unwrap(); + assert_eq!(stats.nodes_with_mismatched_embeddings, 125); + assert_eq!(stats.nodes_with_active_embeddings, 0); + + let candidates = storage + .embedding_regeneration_candidates(None, false) + .unwrap(); + assert_eq!(candidates.len(), 125); + } + #[test] fn test_storage_creation() { let storage = create_test_storage(); @@ -4432,6 +6372,528 @@ mod tests { assert_eq!(count_future, 0); } + #[test] + fn test_portable_archive_exact_round_trip() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + + let first = source + .ingest(IngestInput { + content: "Portable archive alpha memory".to_string(), + node_type: "fact".to_string(), + tags: vec!["portable".to_string()], + source: Some("test".to_string()), + ..Default::default() + }) + .unwrap(); + let second = source + .ingest(IngestInput { + content: "Portable archive beta memory".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + source.mark_reviewed(&first.id, Rating::Good).unwrap(); + source + .save_connection(&ConnectionRecord { + source_id: first.id.clone(), + target_id: second.id.clone(), + strength: 0.75, + link_type: "semantic".to_string(), + created_at: Utc::now(), + last_activated: Utc::now(), + activation_count: 1, + }) + .unwrap(); + + let archive = source.export_portable_archive().unwrap(); + assert_eq!(archive.archive_format, PORTABLE_ARCHIVE_FORMAT); + assert!(archive.total_rows() >= 3); + assert!( + archive + .tables + .iter() + .any(|table| table.name == "knowledge_nodes" && table.rows.len() == 2) + ); + + let target = create_test_storage_at(&target_dir, "target.db"); + let report = target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap(); + assert!(report.rows_imported >= 3); + assert!(report.fts_rebuilt); + + let restored = target.get_node(&first.id).unwrap().unwrap(); + assert_eq!(restored.id, first.id); + assert_eq!(restored.content, first.content); + assert_eq!(restored.tags, first.tags); + assert_eq!(restored.reps, 1); + + let connections = target.get_connections_for_memory(&first.id).unwrap(); + assert_eq!(connections.len(), 1); + assert_eq!(connections[0].target_id, second.id); + + let results = target.search("alpha", 10).unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0].id, first.id); + } + + #[test] + fn test_portable_import_rejects_non_empty_target() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + source + .ingest(IngestInput { + content: "Source memory".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let archive = source.export_portable_archive().unwrap(); + + let target = create_test_storage_at(&target_dir, "target.db"); + target + .ingest(IngestInput { + content: "Existing target memory".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let err = target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap_err(); + assert!( + err.to_string() + .contains("requires an empty target database") + ); + } + + #[test] + fn test_portable_import_rejects_unknown_mode() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + source + .ingest(IngestInput { + content: "Source memory".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let mut archive = source.export_portable_archive().unwrap(); + archive.mode = "merge".to_string(); + + let target = create_test_storage_at(&target_dir, "target.db"); + let err = target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap_err(); + assert!( + err.to_string() + .contains("Unsupported portable archive mode") + ); + } + + #[test] + fn test_portable_import_rejects_malformed_table_list() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + source + .ingest(IngestInput { + content: "Source memory".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let mut duplicate_archive = source.export_portable_archive().unwrap(); + let duplicate_table = duplicate_archive + .tables + .iter() + .find(|table| table.name == "knowledge_nodes") + .unwrap() + .clone(); + duplicate_archive.tables.push(duplicate_table); + + let target = create_test_storage_at(&target_dir, "target-duplicate.db"); + let err = target + .import_portable_archive(&duplicate_archive, PortableImportMode::EmptyOnly) + .unwrap_err(); + assert!( + err.to_string() + .contains("Portable archive contains duplicate table") + ); + + let mut unknown_archive = source.export_portable_archive().unwrap(); + unknown_archive.tables.push(PortableTable { + name: "sqlite_sequence".to_string(), + columns: vec!["name".to_string(), "seq".to_string()], + rows: vec![], + }); + + let target = create_test_storage_at(&target_dir, "target-unknown.db"); + let err = target + .import_portable_archive(&unknown_archive, PortableImportMode::EmptyOnly) + .unwrap_err(); + assert!( + err.to_string() + .contains("Portable archive contains unsupported table") + ); + } + + #[test] + fn test_portable_merge_import_combines_non_empty_databases() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + let target = create_test_storage_at(&target_dir, "target.db"); + + let source_node = source + .ingest(IngestInput { + content: "Source sync memory".to_string(), + node_type: "fact".to_string(), + tags: vec!["sync".to_string()], + ..Default::default() + }) + .unwrap(); + let target_node = target + .ingest(IngestInput { + content: "Target local memory".to_string(), + node_type: "fact".to_string(), + tags: vec!["local".to_string()], + ..Default::default() + }) + .unwrap(); + + let archive = source.export_portable_archive().unwrap(); + let report = target + .import_portable_archive(&archive, PortableImportMode::Merge) + .unwrap(); + + assert!(report.rows_inserted > 0); + assert!(target.get_node(&source_node.id).unwrap().is_some()); + assert!(target.get_node(&target_node.id).unwrap().is_some()); + } + + #[test] + fn test_portable_merge_import_keeps_newer_local_memory() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + let target = create_test_storage_at(&target_dir, "target.db"); + + let node = source + .ingest(IngestInput { + content: "Original shared memory".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let archive = source.export_portable_archive().unwrap(); + target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap(); + + let newer = (Utc::now() + Duration::hours(1)).to_rfc3339(); + { + let writer = target.writer.lock().unwrap(); + writer + .execute( + "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", + params!["Newer local edit", newer, &node.id], + ) + .unwrap(); + } + + let report = target + .import_portable_archive(&archive, PortableImportMode::Merge) + .unwrap(); + + assert!(report.conflicts_kept_local >= 1); + let restored = target.get_node(&node.id).unwrap().unwrap(); + assert_eq!(restored.content, "Newer local edit"); + } + + #[test] + fn test_portable_merge_import_keeps_children_for_newer_local_memory() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + let target = create_test_storage_at(&target_dir, "target.db"); + + let node = source + .ingest(IngestInput { + content: "Shared parent with child rows".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let source_time = Utc::now().to_rfc3339(); + { + let writer = source.writer.lock().unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO node_embeddings + (node_id, embedding, dimensions, model, created_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![&node.id, vec![1_u8, 2, 3, 4], 4, "test-model", &source_time], + ) + .unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO fsrs_cards + (memory_id, difficulty, stability, state, reps, lapses) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![&node.id, 3.0_f64, 2.0_f64, "review", 2_i64, 0_i64], + ) + .unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO memory_states + (memory_id, state, last_access, access_count, state_entered_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![&node.id, "active", &source_time, 1_i64, &source_time], + ) + .unwrap(); + } + + let archive = source.export_portable_archive().unwrap(); + target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap(); + + let local_time = (Utc::now() + Duration::hours(1)).to_rfc3339(); + { + let writer = target.writer.lock().unwrap(); + writer + .execute( + "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", + params!["Newer local parent edit", &local_time, &node.id], + ) + .unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO node_embeddings + (node_id, embedding, dimensions, model, created_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![&node.id, vec![9_u8, 8, 7, 6], 4, "test-model", &local_time], + ) + .unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO fsrs_cards + (memory_id, difficulty, stability, state, reps, lapses) + VALUES (?1, ?2, ?3, ?4, ?5, ?6)", + params![&node.id, 9.0_f64, 8.0_f64, "review", 9_i64, 1_i64], + ) + .unwrap(); + writer + .execute( + "INSERT OR REPLACE INTO memory_states + (memory_id, state, last_access, access_count, state_entered_at) + VALUES (?1, ?2, ?3, ?4, ?5)", + params![&node.id, "silent", &local_time, 42_i64, &local_time], + ) + .unwrap(); + } + + let report = target + .import_portable_archive(&archive, PortableImportMode::Merge) + .unwrap(); + + assert!(report.conflicts_kept_local >= 4); + let restored = target.get_node(&node.id).unwrap().unwrap(); + assert_eq!(restored.content, "Newer local parent edit"); + + let reader = target.reader.lock().unwrap(); + let embedding: Vec = reader + .query_row( + "SELECT embedding FROM node_embeddings WHERE node_id = ?1", + params![&node.id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(embedding, vec![9_u8, 8, 7, 6]); + + let difficulty: f64 = reader + .query_row( + "SELECT difficulty FROM fsrs_cards WHERE memory_id = ?1", + params![&node.id], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(difficulty, 9.0); + + let (state, access_count): (String, i64) = reader + .query_row( + "SELECT state, access_count FROM memory_states WHERE memory_id = ?1", + params![&node.id], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .unwrap(); + assert_eq!(state, "silent"); + assert_eq!(access_count, 42); + } + + #[test] + fn test_portable_merge_import_applies_delete_tombstones() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + let target = create_test_storage_at(&target_dir, "target.db"); + + let node = source + .ingest(IngestInput { + content: "Memory deleted on source".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let archive = source.export_portable_archive().unwrap(); + target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap(); + assert!(target.get_node(&node.id).unwrap().is_some()); + + source.delete_node(&node.id).unwrap(); + let delete_archive = source.export_portable_archive().unwrap(); + let report = target + .import_portable_archive(&delete_archive, PortableImportMode::Merge) + .unwrap(); + + assert!(report.rows_deleted >= 1); + assert!(target.get_node(&node.id).unwrap().is_none()); + } + + #[test] + fn test_portable_merge_import_preserves_purge_tombstones() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + let target = create_test_storage_at(&target_dir, "target.db"); + + let node = source + .ingest(IngestInput { + content: "Memory purged on source".to_string(), + node_type: "fact".to_string(), + tags: vec!["sync".to_string()], + ..Default::default() + }) + .unwrap(); + let archive = source.export_portable_archive().unwrap(); + target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap(); + assert!(target.get_node(&node.id).unwrap().is_some()); + + source + .purge_node(&node.id, Some("sync purge test")) + .unwrap(); + let purge_archive = source.export_portable_archive().unwrap(); + let report = target + .import_portable_archive(&purge_archive, PortableImportMode::Merge) + .unwrap(); + + assert!(report.rows_deleted >= 1); + assert!(target.get_node(&node.id).unwrap().is_none()); + + let writer = target.writer.lock().unwrap(); + let tombstone_count: i64 = writer + .query_row( + "SELECT COUNT(*) FROM deletion_tombstones WHERE memory_id = ?1 AND reason = ?2", + params![node.id, "sync purge test"], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(tombstone_count, 1); + } + + #[test] + fn test_portable_merge_import_purge_wins_over_newer_local_edit() { + let source_dir = tempdir().unwrap(); + let target_dir = tempdir().unwrap(); + let source = create_test_storage_at(&source_dir, "source.db"); + let target = create_test_storage_at(&target_dir, "target.db"); + + let node = source + .ingest(IngestInput { + content: "Memory that will be purged on source".to_string(), + node_type: "fact".to_string(), + tags: vec!["sync".to_string()], + ..Default::default() + }) + .unwrap(); + let archive = source.export_portable_archive().unwrap(); + target + .import_portable_archive(&archive, PortableImportMode::EmptyOnly) + .unwrap(); + + let newer = (Utc::now() + Duration::hours(1)).to_rfc3339(); + { + let writer = target.writer.lock().unwrap(); + writer + .execute( + "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", + params!["Newer local edit before purge arrives", newer, &node.id], + ) + .unwrap(); + } + + source + .purge_node(&node.id, Some("hard purge wins sync conflict")) + .unwrap(); + let purge_archive = source.export_portable_archive().unwrap(); + let report = target + .import_portable_archive(&purge_archive, PortableImportMode::Merge) + .unwrap(); + + assert!(report.rows_deleted >= 1); + assert!(target.get_node(&node.id).unwrap().is_none()); + } + + #[test] + fn test_file_portable_sync_round_trips_between_devices() { + let sync_dir = tempdir().unwrap(); + let first_dir = tempdir().unwrap(); + let second_dir = tempdir().unwrap(); + let sync_path = sync_dir.path().join("vestige-sync.json"); + let first = create_test_storage_at(&first_dir, "first.db"); + let second = create_test_storage_at(&second_dir, "second.db"); + + let first_node = first + .ingest(IngestInput { + content: "First device memory".to_string(), + node_type: "fact".to_string(), + tags: vec!["sync".to_string()], + ..Default::default() + }) + .unwrap(); + let first_push = first.sync_portable_archive_file(&sync_path).unwrap(); + assert!(!first_push.pulled); + assert!(sync_path.exists()); + + let second_node = second + .ingest(IngestInput { + content: "Second device memory".to_string(), + node_type: "fact".to_string(), + tags: vec!["sync".to_string()], + ..Default::default() + }) + .unwrap(); + let second_sync = second.sync_portable_archive_file(&sync_path).unwrap(); + assert!(second_sync.pulled); + assert!(second.get_node(&first_node.id).unwrap().is_some()); + + let first_sync = first.sync_portable_archive_file(&sync_path).unwrap(); + assert!(first_sync.pulled); + assert!(first.get_node(&second_node.id).unwrap().is_some()); + assert!(first_sync.pushed_rows >= 2); + } + #[test] fn test_get_last_backup_timestamp_no_panic() { // Static method should not panic even if no backups exist @@ -4589,4 +7051,169 @@ mod tests { assert!(!results.is_empty()); assert!(results[0].node.content.contains("Neurons")); } + + #[test] + fn test_concrete_search_literal_identifier_lands_first() { + let storage = create_test_storage(); + + storage + .ingest(IngestInput { + content: "General OpenAI API setup notes without the exact env var".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let target = storage + .ingest(IngestInput { + content: "Set OPENAI_API_KEY before running the release smoke tests".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + storage + .ingest(IngestInput { + content: "API keys should be handled carefully in shell profiles".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let results = storage + .concrete_search_filtered("OPENAI_API_KEY", 10, None, None) + .unwrap(); + + assert!(!results.is_empty()); + assert_eq!(results[0].node.id, target.id); + assert_eq!(results[0].match_type, MatchType::Keyword); + assert!(results[0].semantic_score.is_none()); + } + + #[test] + fn test_purge_scrubs_insight_json_orphans_children_and_writes_tombstone() { + let storage = create_test_storage(); + let doomed = storage + .ingest(IngestInput { + content: "Sensitive purge target memory".to_string(), + node_type: "fact".to_string(), + tags: vec!["sensitive".to_string()], + ..Default::default() + }) + .unwrap(); + let other_a = storage + .ingest(IngestInput { + content: "Other source memory A".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let other_b = storage + .ingest(IngestInput { + content: "Other source memory B".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + let child = storage + .ingest(IngestInput { + content: "Temporal summary child".to_string(), + node_type: "summary".to_string(), + ..Default::default() + }) + .unwrap(); + + { + let writer = storage.writer.lock().unwrap(); + writer + .execute( + "INSERT INTO memory_connections ( + source_id, target_id, strength, link_type, created_at, last_activated, activation_count + ) VALUES (?1, ?2, 0.9, 'semantic', ?3, ?3, 0)", + params![doomed.id, other_a.id, Utc::now().to_rfc3339()], + ) + .unwrap(); + writer + .execute( + "INSERT INTO insights ( + id, insight, source_memories, confidence, novelty_score, insight_type, generated_at + ) VALUES (?1, 'drop me', ?2, 0.9, 0.2, 'synthesis', ?3)", + params![ + Uuid::new_v4().to_string(), + serde_json::to_string(&vec![doomed.id.clone(), other_a.id.clone()]).unwrap(), + Utc::now().to_rfc3339() + ], + ) + .unwrap(); + writer + .execute( + "INSERT INTO insights ( + id, insight, source_memories, confidence, novelty_score, insight_type, generated_at + ) VALUES (?1, 'rewrite me', ?2, 0.9, 0.2, 'synthesis', ?3)", + params![ + Uuid::new_v4().to_string(), + serde_json::to_string(&vec![ + doomed.id.clone(), + other_a.id.clone(), + other_b.id.clone() + ]) + .unwrap(), + Utc::now().to_rfc3339() + ], + ) + .unwrap(); + writer + .execute( + "UPDATE knowledge_nodes SET summary_parent_id = ?1 WHERE id = ?2", + params![doomed.id, child.id], + ) + .unwrap(); + } + + let report = storage + .purge_node(&doomed.id, Some("user requested hard purge")) + .unwrap(); + assert!(report.deleted); + assert_eq!(report.edges_pruned, 1); + assert_eq!(report.insights_deleted, 1); + assert_eq!(report.insights_rewritten, 1); + assert_eq!(report.children_orphaned, 1); + assert!(storage.get_node(&doomed.id).unwrap().is_none()); + + let writer = storage.writer.lock().unwrap(); + let remaining_refs: Vec = writer + .prepare("SELECT source_memories FROM insights") + .unwrap() + .query_map([], |row| row.get(0)) + .unwrap() + .filter_map(|row| row.ok()) + .collect(); + assert_eq!(remaining_refs.len(), 1); + assert!(!remaining_refs[0].contains(&doomed.id)); + + let child_parent: Option = writer + .query_row( + "SELECT summary_parent_id FROM knowledge_nodes WHERE id = ?1", + params![child.id], + |row| row.get(0), + ) + .unwrap(); + assert!(child_parent.is_none()); + + let tombstone_count: i64 = writer + .query_row( + "SELECT COUNT(*) FROM deletion_tombstones WHERE memory_id = ?1 AND reason = ?2", + params![doomed.id, "user requested hard purge"], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(tombstone_count, 1); + + let has_content_column: i64 = writer + .query_row( + "SELECT COUNT(*) FROM pragma_table_info('deletion_tombstones') WHERE name = 'content'", + [], + |row| row.get(0), + ) + .unwrap(); + assert_eq!(has_content_column, 0); + } } diff --git a/crates/vestige-mcp/Cargo.toml b/crates/vestige-mcp/Cargo.toml index cc72f18..b0a5490 100644 --- a/crates/vestige-mcp/Cargo.toml +++ b/crates/vestige-mcp/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "vestige-mcp" -version = "2.0.7" +version = "2.1.22" edition = "2024" -description = "Cognitive memory MCP server for Claude - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research" +description = "Cognitive memory MCP server for AI agents - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research" authors = ["samvallad33"] license = "AGPL-3.0-only" keywords = ["mcp", "ai", "memory", "fsrs", "neuroscience", "cognitive-science", "spaced-repetition"] @@ -10,12 +10,20 @@ categories = ["command-line-utilities", "database"] repository = "https://github.com/samvallad33/vestige" [features] -default = ["embeddings", "vector-search"] +default = ["embeddings", "ort-download", "vector-search"] embeddings = ["vestige-core/embeddings"] vector-search = ["vestige-core/vector-search"] -# For systems with glibc < 2.38 — use runtime-loaded ORT instead of the downloaded pre-built binary. -# Usage: cargo install --path crates/vestige-mcp --no-default-features --features ort-dynamic,vector-search -ort-dynamic = ["vestige-core/ort-dynamic"] +# Default ort backend: downloads prebuilt ONNX Runtime at build time. +# Fails on targets without prebuilts (notably x86_64-apple-darwin). +ort-download = ["embeddings", "vestige-core/ort-download"] +# Alternative ort backend: runtime-linked system libonnxruntime via dlopen. +# Required on Intel Mac and on systems with glibc < 2.38. +# Usage: cargo build --no-default-features --features ort-dynamic,vector-search +# Runtime: export ORT_DYLIB_PATH=$(brew --prefix onnxruntime)/lib/libonnxruntime.dylib +ort-dynamic = ["embeddings", "vestige-core/ort-dynamic"] +qwen3-embeddings = ["embeddings", "vestige-core/qwen3-embeddings"] +qwen3-reranker = ["qwen3-embeddings"] +metal = ["embeddings", "vestige-core/metal"] [[bin]] name = "vestige-mcp" @@ -39,7 +47,7 @@ path = "src/bin/cli.rs" # Only `bundled-sqlite` is always on. `embeddings` and `vector-search` are # toggled via vestige-mcp's own feature flags below so `--no-default-features` # actually works (previously hardcoded here, which silently defeated the flag). -vestige-core = { version = "2.0.5", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] } +vestige-core = { version = "2.1.22", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] } # ============================================================================ # MCP Server Dependencies diff --git a/crates/vestige-mcp/README.md b/crates/vestige-mcp/README.md index 10bdfa3..92f53d8 100644 --- a/crates/vestige-mcp/README.md +++ b/crates/vestige-mcp/README.md @@ -1,115 +1,75 @@ # Vestige MCP Server -A bleeding-edge Rust MCP (Model Context Protocol) server for Vestige - providing Claude and other AI assistants with long-term memory capabilities. +Local cognitive memory for MCP-compatible AI agents. -## Features +This crate provides the `vestige-mcp` stdio MCP server plus the `vestige` CLI. +The cognitive engine lives in `vestige-core`; this crate owns protocol handling, +tool dispatch, optional dashboard serving, backups, restore, update, and +portable import/export commands. -- **FSRS-6 Algorithm**: State-of-the-art spaced repetition (21 parameters, personalized decay) -- **Dual-Strength Memory Model**: Based on Bjork & Bjork 1992 cognitive science research -- **Local Semantic Embeddings**: nomic-embed-text-v1.5 (768d) via fastembed v5 (no external API) -- **HNSW Vector Search**: USearch-based, 20x faster than FAISS -- **Hybrid Search**: BM25 + semantic with RRF fusion -- **Codebase Memory**: Remember patterns, decisions, and context +## Install -## Installation +For normal users, prefer the release package: ```bash -cd /path/to/vestige/crates/vestige-mcp -cargo build --release +npm install -g vestige-mcp-server ``` -Binary will be at `target/release/vestige-mcp` +For local development: -## Claude Desktop Configuration +```bash +cargo build --release -p vestige-mcp +``` -Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): +## Register With An MCP Client + +Use the command `vestige-mcp` in any stdio MCP client: ```json { "mcpServers": { "vestige": { - "command": "/path/to/vestige-mcp" + "command": "vestige-mcp" } } } ``` -## Available Tools +Examples: -### Core Memory - -| Tool | Description | -|------|-------------| -| `ingest` | Add new knowledge to memory | -| `recall` | Search and retrieve memories | -| `semantic_search` | Find conceptually similar content | -| `hybrid_search` | Combined keyword + semantic search | -| `get_knowledge` | Retrieve a specific memory by ID | -| `delete_knowledge` | Delete a memory | -| `mark_reviewed` | Review with FSRS rating (1-4) | - -### Statistics & Maintenance - -| Tool | Description | -|------|-------------| -| `get_stats` | Memory system statistics | -| `health_check` | System health status | -| `run_consolidation` | Apply decay, generate embeddings | - -### Codebase Tools - -| Tool | Description | -|------|-------------| -| `remember_pattern` | Remember code patterns | -| `remember_decision` | Remember architectural decisions | -| `get_codebase_context` | Get patterns and decisions | - -## Available Resources - -### Memory Resources - -| URI | Description | -|-----|-------------| -| `memory://stats` | Current statistics | -| `memory://recent?n=10` | Recent memories | -| `memory://decaying` | Low retention memories | -| `memory://due` | Memories due for review | - -### Codebase Resources - -| URI | Description | -|-----|-------------| -| `codebase://structure` | Known codebases | -| `codebase://patterns` | Remembered patterns | -| `codebase://decisions` | Architectural decisions | - -## Example Usage (with Claude) - -``` -User: Remember that we decided to use FSRS-6 instead of SM-2 because it's 20-30% more efficient. - -Claude: [calls remember_decision] -I've recorded that architectural decision. - -User: What decisions have we made about algorithms? - -Claude: [calls get_codebase_context] -I found 1 decision: -- We decided to use FSRS-6 instead of SM-2 because it's 20-30% more efficient. +```bash +claude mcp add vestige vestige-mcp -s user +codex mcp add vestige -- vestige-mcp ``` -## Data Storage +## Transports -- Database: `~/Library/Application Support/com.vestige.mcp/vestige-mcp.db` (macOS) -- Uses SQLite with FTS5 for full-text search -- Vector embeddings stored in separate table +- Default: JSON-RPC 2.0 over stdio. +- Optional: MCP-over-HTTP on `/mcp`, enabled only with `--http`, + `--http-port`, or `VESTIGE_HTTP_ENABLED=1`. +- Dashboard: `vestige dashboard` or `VESTIGE_DASHBOARD_ENABLED=1`. -## Protocol +HTTP and dashboard bearer tokens are generated locally; see +[`docs/CONFIGURATION.md`](../../docs/CONFIGURATION.md). -- JSON-RPC 2.0 over stdio -- MCP Protocol Version: 2024-11-05 -- Logging to stderr (stdout reserved for JSON-RPC) +## Current Tool Surface + +The server exposes the current unified MCP tools from +[`src/server.rs`](src/server.rs), including: + +- `session_context` +- `search`, `smart_ingest`, `memory`, `codebase`, `intention` +- `deep_reference`, `cross_reference`, `contradictions` +- `dream`, `explore_connections`, `predict` +- `memory_health`, `memory_graph`, `system_status` +- `importance_score`, `find_duplicates` +- `consolidate`, `memory_timeline`, `memory_changelog` +- `backup`, `export`, `restore`, `gc`, `suppress` + +See the root [`README.md`](../../README.md) and +[`docs/AGENT-MEMORY-PROTOCOL.md`](../../docs/AGENT-MEMORY-PROTOCOL.md) for +agent instructions. ## License -MIT +AGPL-3.0-only diff --git a/crates/vestige-mcp/src/autopilot.rs b/crates/vestige-mcp/src/autopilot.rs new file mode 100644 index 0000000..2db04a8 --- /dev/null +++ b/crates/vestige-mcp/src/autopilot.rs @@ -0,0 +1,494 @@ +//! Autopilot — v2.0.9 event-subscriber task. +//! +//! Subscribes to the shared `VestigeEvent` broadcast bus and routes every +//! live event into the cognitive modules that already have trigger methods +//! implemented. Without this layer, Vestige's 30 cognitive modules are a +//! passive library that only responds to MCP tool queries — the event bus +//! emits 20 event types but every one of them terminates at the dashboard. +//! +//! This module closes that gap. It turns Vestige from "fast retrieval with +//! neuroscience modules" into "self-managing cognitive surface that acts +//! without being asked." See `docs/VESTIGE_STATE_AND_PLAN.md` §15 for the +//! full architectural rationale. +//! +//! ## What fires autonomously after v2.0.9 +//! +//! - **`MemoryCreated`** → `synaptic_tagging.trigger_prp()` (9h retroactive +//! PRP window on every save) + `predictive_memory.record_memory_access()` +//! (pattern learning for `predict` tool). +//! - **`SearchPerformed`** → `predictive_memory.record_query()` (keeps the +//! query-interest model warm without waiting for the next `predict` call). +//! - **`MemoryPromoted`** → `activation_network.activate()` (spreads a small +//! reinforcement ripple from the promoted node to its neighbors). +//! - **`MemorySuppressed`** → emits the previously-declared-never-emitted +//! `Rac1CascadeSwept` event so the dashboard can render the cascade wave. +//! - **`ImportanceScored` with `composite_score > 0.85`** → auto-`promote` +//! when the score refers to a stored memory. +//! - **`Heartbeat` with `memory_count > DUPLICATES_THRESHOLD`** → +//! opportunistic `find_duplicates` sweep (rate-limited). +//! +//! ## What polls on a timer +//! +//! A 60-second `tokio::interval` calls `prospective_memory.check_triggers()` +//! with the best context we can infer from recent WebSocket activity. +//! Matched intentions are logged at `info!` level today; v2.5 "Autonomic" +//! will promote this to MCP sampling/createMessage notifications that +//! actually reach the agent mid-session. + +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use tokio::sync::{Mutex, broadcast}; +use tracing::{debug, info, warn}; +use vestige_core::Storage; +use vestige_core::neuroscience::prospective_memory::Context as ProspectiveContext; +use vestige_core::neuroscience::synaptic_tagging::{ImportanceEvent, ImportanceEventType}; + +use crate::cognitive::CognitiveEngine; +use crate::dashboard::events::VestigeEvent; + +/// Composite-score threshold above which `ImportanceScored` auto-promotes +/// the referenced memory. Conservative default — tune in telemetry. +const AUTO_PROMOTE_THRESHOLD: f64 = 0.85; + +/// Memory-count threshold above which a `Heartbeat` triggers a +/// `find_duplicates` sweep. Matches the CLAUDE.md guidance ("totalMemories > 700"). +const DUPLICATES_THRESHOLD: usize = 700; + +/// Minimum interval between autopilot-triggered `find_duplicates` sweeps, +/// regardless of Heartbeat cadence. Prevents sweep-storms when the count +/// hovers near the threshold. +const DUPLICATES_SWEEP_COOLDOWN_SECS: u64 = 6 * 3600; // 6 hours + +/// Interval for polling `prospective_memory.check_triggers()`. +const PROSPECTIVE_POLL_SECS: u64 = 60; + +/// Backoff between supervisor restarts after a panicked child task. Short +/// enough that a single bad memory doesn't meaningfully degrade the system, +/// long enough to avoid a tight crash loop if the panic source is persistent. +const SUPERVISOR_RESTART_BACKOFF_SECS: u64 = 5; + +/// Tracks an in-flight Heartbeat-triggered dedup sweep so the next Heartbeat +/// can skip spawning a second sweep while the first is still running. The +/// previous implementation stored only the *start* time, which allowed two +/// concurrent scans on databases where `find_duplicates` exceeds the 6h +/// cooldown window. +struct DedupSweepState { + last_fired: Option, + in_flight: Option>, +} + +impl DedupSweepState { + fn new() -> Self { + Self { + last_fired: None, + in_flight: None, + } + } + + /// True if a previous sweep is still running. Drops a finished handle so + /// a long-dead sweep doesn't keep us from firing the next one. + fn is_running(&mut self) -> bool { + match &self.in_flight { + Some(h) if !h.is_finished() => true, + _ => { + self.in_flight = None; + false + } + } + } +} + +/// Launch the Autopilot event-subscriber task + prospective-memory poller. +/// +/// Both tasks are supervised: if the inner loop panics on a single bad +/// memory, the supervisor logs the panic and restarts it after a short +/// backoff. This turns a permanent silent-failure mode ("task dies, every +/// future cognitive event lost") into a transient hiccup ("one bad memory +/// skipped, subsystem resumes"). The event loop holds the `CognitiveEngine` +/// mutex only for the duration of a single handler, and never inside an +/// `await`, so it never starves MCP tool dispatch. +pub fn spawn( + cognitive: Arc>, + storage: Arc, + event_tx: broadcast::Sender, +) { + // Opt-out: users upgrading in place from v2.0.8 may want to keep the + // "passive library" contract. Set VESTIGE_AUTOPILOT_ENABLED=0 to skip + // spawning both background tasks. Anything else (unset, "1", "true", etc.) + // enables the default v2.0.9 Autopilot behavior. + match std::env::var("VESTIGE_AUTOPILOT_ENABLED").as_deref() { + Ok("0") | Ok("false") | Ok("no") | Ok("off") => { + info!( + "Autopilot disabled via VESTIGE_AUTOPILOT_ENABLED — \ + cognitive modules remain passive (v2.0.8 behavior)" + ); + return; + } + _ => {} + } + + // Event-subscriber supervisor. + { + let cognitive = cognitive.clone(); + let storage = storage.clone(); + let event_tx = event_tx.clone(); + tokio::spawn(async move { + loop { + let rx = event_tx.subscribe(); + let cog = cognitive.clone(); + let sto = storage.clone(); + let etx = event_tx.clone(); + let handle = tokio::spawn(async move { + run_event_subscriber(rx, cog, sto, etx).await; + }); + match handle.await { + Ok(()) => { + info!("Autopilot event subscriber exited cleanly"); + break; + } + Err(e) if e.is_panic() => { + warn!( + error = ?e, + backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS, + "Autopilot event subscriber panicked — supervisor restarting" + ); + tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS)) + .await; + } + Err(e) => { + warn!(error = ?e, "Autopilot event subscriber join error — exiting"); + break; + } + } + } + }); + } + + // Prospective-memory poller supervisor — symmetric restart semantics. + { + let cognitive = cognitive.clone(); + tokio::spawn(async move { + loop { + let cog = cognitive.clone(); + let handle = tokio::spawn(async move { + run_prospective_poller(cog).await; + }); + match handle.await { + Ok(()) => { + info!("Autopilot prospective poller exited cleanly"); + break; + } + Err(e) if e.is_panic() => { + warn!( + error = ?e, + backoff_secs = SUPERVISOR_RESTART_BACKOFF_SECS, + "Autopilot prospective poller panicked — supervisor restarting" + ); + tokio::time::sleep(Duration::from_secs(SUPERVISOR_RESTART_BACKOFF_SECS)) + .await; + } + Err(e) => { + warn!(error = ?e, "Autopilot prospective poller join error — exiting"); + break; + } + } + } + }); + } + + info!("Autopilot spawned (event-subscriber + prospective poller, supervised)"); +} + +async fn run_event_subscriber( + mut rx: broadcast::Receiver, + cognitive: Arc>, + storage: Arc, + event_tx: broadcast::Sender, +) { + // Tracks Heartbeat-triggered auto-sweeps so the next Heartbeat skips + // spawning a second sweep while the first is still running — essential + // on large DBs where `find_duplicates` can outrun the cooldown. + let mut dedup_state = DedupSweepState::new(); + + loop { + match rx.recv().await { + Ok(event) => { + handle_event(event, &cognitive, &storage, &event_tx, &mut dedup_state).await; + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!("Autopilot lagged {n} events — increase channel capacity if this persists"); + } + Err(broadcast::error::RecvError::Closed) => { + info!("Autopilot event bus closed — subscriber exiting"); + break; + } + } + } +} + +async fn handle_event( + event: VestigeEvent, + cognitive: &Arc>, + storage: &Arc, + event_tx: &broadcast::Sender, + dedup_state: &mut DedupSweepState, +) { + match event { + VestigeEvent::MemoryCreated { + id, + content_preview, + tags, + timestamp, + .. + } => { + // Synaptic tagging: every save is a CrossReference event candidate + // for Frey & Morris 1997 PRP (retroactive importance within a 9h + // window). The system dedups internally, so firing per-save is safe. + let ev = ImportanceEvent { + event_type: ImportanceEventType::CrossReference, + memory_id: Some(id.clone()), + timestamp, + strength: 0.5, + context: None, + }; + let tag_outcome = { + let mut cog = cognitive.lock().await; + let outcome = cog.synaptic_tagging.trigger_prp(ev); + // Predictive memory learns the ingested tags for pattern-match + // against future `predict` queries. Method is `&self` (interior + // RwLock), so we keep the cognitive mutex guard for ordering + // but don't actually need &mut on this call. + let _ = cog + .predictive_memory + .record_memory_access(&id, &content_preview, &tags); + outcome + }; + debug!( + memory_id = %id, + captured = ?tag_outcome, + "Autopilot: MemoryCreated routed to synaptic_tagging + predictive_memory" + ); + } + + VestigeEvent::SearchPerformed { + query, result_ids, .. + } => { + // Feed the search into the predictive-retrieval model so the + // speculative prefetch path warms up for the NEXT query. The + // event doesn't carry per-result content, so we record with an + // empty preview — the model only needs the id + tag signal. + let cog = cognitive.lock().await; + let empty_tags_str: [&str; 0] = []; + let empty_tags_string: [String; 0] = []; + let _ = cog.predictive_memory.record_query(&query, &empty_tags_str); + for mid in result_ids.iter().take(10) { + let _ = cog + .predictive_memory + .record_memory_access(mid, "", &empty_tags_string); + } + debug!( + query = %query, + n_results = result_ids.len(), + "Autopilot: SearchPerformed routed to predictive_memory" + ); + } + + VestigeEvent::MemoryPromoted { id, .. } => { + // Spread a small activation ripple from the promoted node. The + // ActivationNetwork internally handles decay (0.7/hop) so this + // cannot over-amplify. + let mut cog = cognitive.lock().await; + let spread = cog.activation_network.activate(&id, 0.3); + debug!( + memory_id = %id, + n_activated = spread.len(), + "Autopilot: MemoryPromoted triggered activation spread" + ); + } + + VestigeEvent::MemorySuppressed { + id, + estimated_cascade, + timestamp, + .. + } => { + // Surface the previously-declared-never-emitted Rac1CascadeSwept + // event so the dashboard's cascade animation actually fires. The + // per-suppress work happens synchronously inside `suppress_memory` + // on the handler path; this is the observable shadow for the UI. + let _ = event_tx.send(VestigeEvent::Rac1CascadeSwept { + seeds: 1, + neighbors_affected: estimated_cascade, + timestamp, + }); + debug!( + memory_id = %id, + cascade_size = estimated_cascade, + "Autopilot: MemorySuppressed → Rac1CascadeSwept emitted" + ); + } + + VestigeEvent::ImportanceScored { + memory_id, + composite_score, + .. + } => { + // Auto-promote only when the score refers to a stored memory AND + // exceeds the threshold. None means "score was computed for + // arbitrary content via the importance tool" — nothing to promote. + if let Some(mid) = memory_id + && composite_score > AUTO_PROMOTE_THRESHOLD + { + match storage.promote_memory(&mid) { + Ok(node) => { + info!( + memory_id = %mid, + composite_score, + new_retention = node.retention_strength, + "Autopilot: auto-promoted memory with composite > {AUTO_PROMOTE_THRESHOLD}" + ); + let _ = event_tx.send(VestigeEvent::MemoryPromoted { + id: node.id, + new_retention: node.retention_strength, + timestamp: chrono::Utc::now(), + }); + } + Err(e) => { + warn!( + memory_id = %mid, + error = %e, + "Autopilot: auto-promote failed" + ); + } + } + } + } + + VestigeEvent::Heartbeat { memory_count, .. } => { + if memory_count <= DUPLICATES_THRESHOLD { + return; + } + // If a prior sweep is still running (possible on very large DBs + // where `find_duplicates` exceeds the 6h cooldown), skip this + // tick rather than spawn a concurrent second scan. + if dedup_state.is_running() { + debug!( + memory_count, + "Autopilot: dedup sweep already in flight — skipping Heartbeat tick" + ); + return; + } + let now = Instant::now(); + let cooldown_elapsed = dedup_state + .last_fired + .map(|t| now.duration_since(t).as_secs() >= DUPLICATES_SWEEP_COOLDOWN_SECS) + .unwrap_or(true); + if !cooldown_elapsed { + return; + } + dedup_state.last_fired = Some(now); + + // Fire the find_duplicates tool with conservative defaults. + // Running on the heartbeat task keeps this off the critical + // MCP-dispatch path. Result is logged only — the user's client + // can still call the tool explicitly for an interactive run. + let storage = storage.clone(); + let handle = tokio::spawn(async move { + let args = serde_json::json!({ + "similarity_threshold": 0.85, + "limit": 50, + }); + match crate::tools::dedup::execute(&storage, Some(args)).await { + Ok(result) => { + let clusters = result + .get("duplicate_clusters") + .and_then(|v| v.as_array()) + .map(|a| a.len()) + .unwrap_or(0); + if clusters > 0 { + info!( + memory_count, + clusters, + "Autopilot: Heartbeat-triggered find_duplicates surfaced clusters" + ); + } + } + Err(e) => { + warn!( + memory_count, + error = %e, + "Autopilot: Heartbeat-triggered find_duplicates failed" + ); + } + } + }); + dedup_state.in_flight = Some(handle); + } + + // Events that carry no autopilot work today. Explicit pass-through so + // adding a new event variant upstream produces a non_exhaustive_match + // compiler nudge here. + VestigeEvent::MemoryUpdated { .. } + | VestigeEvent::MemoryDeleted { .. } + | VestigeEvent::MemoryDemoted { .. } + | VestigeEvent::MemoryUnsuppressed { .. } + | VestigeEvent::Rac1CascadeSwept { .. } + | VestigeEvent::DeepReferenceCompleted { .. } + | VestigeEvent::HookVerdictRecorded { .. } + | VestigeEvent::DreamStarted { .. } + | VestigeEvent::DreamProgress { .. } + | VestigeEvent::DreamCompleted { .. } + | VestigeEvent::ConsolidationStarted { .. } + | VestigeEvent::ConsolidationCompleted { .. } + | VestigeEvent::RetentionDecayed { .. } + | VestigeEvent::ConnectionDiscovered { .. } + | VestigeEvent::ActivationSpread { .. } => {} + } +} + +/// Background task that polls `prospective_memory.check_triggers()` every +/// `PROSPECTIVE_POLL_SECS` seconds. Today triggers are logged at info! +/// level; v2.5 "Autonomic" upgrades this to fire MCP sampling/createMessage +/// notifications so the agent sees intentions mid-conversation. +async fn run_prospective_poller(cognitive: Arc>) { + // Short delay on startup so hydration + other init settles first. + tokio::time::sleep(Duration::from_secs(10)).await; + + let mut ticker = tokio::time::interval(Duration::from_secs(PROSPECTIVE_POLL_SECS)); + // Skip the immediate first tick that `interval` fires. + ticker.tick().await; + + loop { + ticker.tick().await; + + let context = ProspectiveContext { + timestamp: chrono::Utc::now(), + ..Default::default() + }; + + let triggered = { + let cog = cognitive.lock().await; + cog.prospective_memory.check_triggers(&context) + }; + + match triggered { + Ok(intentions) if !intentions.is_empty() => { + info!( + n_triggered = intentions.len(), + ids = ?intentions.iter().map(|i| i.id.as_str()).collect::>(), + "Autopilot: prospective memory triggered intentions" + ); + // v2.5 "Autonomic" will emit MCP sampling/createMessage here + // so the agent actually sees the intention mid-conversation. + } + Ok(_) => { + // No triggers — silent. This runs every 60s and the common + // case is no work to do. + } + Err(e) => { + warn!(error = %e, "Autopilot: prospective check_triggers failed"); + } + } + } +} diff --git a/crates/vestige-mcp/src/bin/cli.rs b/crates/vestige-mcp/src/bin/cli.rs index eeffe54..f555844 100644 --- a/crates/vestige-mcp/src/bin/cli.rs +++ b/crates/vestige-mcp/src/bin/cli.rs @@ -2,15 +2,20 @@ //! //! Command-line interface for managing cognitive memory system. +use std::collections::HashSet; +use std::env; +use std::fs; use std::io::{BufWriter, Write}; +use std::path::Path; use std::path::PathBuf; -use std::sync::Arc; +use std::process::Command; +use std::sync::{Arc, OnceLock}; +use anyhow::Context; use chrono::{NaiveDate, Utc}; -use clap::{Parser, Subcommand}; +use clap::{Args, Parser, Subcommand}; use colored::Colorize; -use directories::ProjectDirs; -use vestige_core::{IngestInput, Storage}; +use vestige_core::{IngestInput, PortableImportMode, Storage}; /// Vestige - Cognitive Memory System CLI #[derive(Parser)] @@ -22,10 +27,68 @@ use vestige_core::{IngestInput, Storage}; long_about = "Vestige is a cognitive memory system based on 130 years of memory research.\n\nIt implements FSRS-6, spreading activation, synaptic tagging, and more." )] struct Cli { + /// Use a specific Vestige data directory for this command. + #[arg(long, global = true, value_name = "DIR")] + data_dir: Option, + #[command(subcommand)] command: Commands, } +static CLI_DB_PATH: OnceLock = OnceLock::new(); + +#[derive(Debug, Clone, Default, Args)] +struct SandwichInstallOptions { + /// Overwrite existing staged Vestige hook and agent files. + #[arg(long)] + force: bool, + + /// Wire optional UserPromptSubmit preflight hooks. + #[arg(long)] + enable_preflight: bool, + + /// Wire both optional preflight hooks and the optional Sanhedrin verifier. + #[arg(long)] + enable_sandwich: bool, + + /// Wire optional Sanhedrin Stop hook. + #[arg(long)] + enable_sanhedrin: bool, + + /// On Apple Silicon, auto-start the local MLX Sanhedrin backend. + #[arg(long)] + with_launchd: bool, + + /// Also stage the large memory-loader hook file. + #[arg(long)] + include_memory_loader: bool, + + /// OpenAI-compatible chat completions endpoint for optional Sanhedrin. + #[arg(long, value_name = "URL")] + sanhedrin_endpoint: Option, + + /// Model name passed to the optional Sanhedrin endpoint. + #[arg(long, value_name = "MODEL")] + sanhedrin_model: Option, + + /// Use a local checkout/release root containing hooks/ and agents/. + #[arg(long, value_name = "DIR", hide = true)] + src: Option, +} + +#[derive(Subcommand)] +enum SandwichCommands { + /// Install/update Cognitive Sandwich companion files without enabling hooks by default. + Install { + /// Install files from a specific release tag instead of latest. + #[arg(long)] + version: Option, + + #[command(flatten)] + options: SandwichInstallOptions, + }, +} + #[derive(Subcommand)] enum Commands { /// Show memory statistics @@ -45,6 +108,38 @@ enum Commands { /// Run memory consolidation cycle Consolidate, + /// Update Vestige binaries from the latest GitHub release + Update { + /// Install a specific release tag instead of latest (example: v2.1.21) + #[arg(long)] + version: Option, + + /// Override install directory (defaults to the current vestige binary's directory) + #[arg(long)] + install_dir: Option, + + /// Print what would be updated without changing files + #[arg(long)] + dry_run: bool, + + /// Deprecated: companion updates are skipped by default. + #[arg(long)] + no_sandwich: bool, + + /// Also refresh optional Claude Code Cognitive Sandwich companion files. + #[arg(long)] + sandwich_companion: bool, + + #[command(flatten)] + sandwich: SandwichInstallOptions, + }, + + /// Manage optional Claude Code Cognitive Sandwich companion files. + Sandwich { + #[command(subcommand)] + command: SandwichCommands, + }, + /// Restore memories from backup file Restore { /// Path to backup JSON file @@ -72,6 +167,27 @@ enum Commands { since: Option, }, + /// Export an exact portable archive for Vestige-to-Vestige transfer + PortableExport { + /// Output archive path + output: PathBuf, + }, + + /// Import an exact portable archive + PortableImport { + /// Input archive path + input: PathBuf, + /// Merge into the current database instead of requiring an empty target + #[arg(long)] + merge: bool, + }, + + /// Two-way sync with a file-backed portable archive + Sync { + /// Sync archive path, often in Dropbox/iCloud/Syncthing/Git + archive: PathBuf, + }, + /// Garbage collect stale memories below retention threshold Gc { /// Minimum retention strength to keep (delete below this) @@ -130,10 +246,37 @@ enum Commands { fn main() -> anyhow::Result<()> { let cli = Cli::parse(); + if let Some(data_dir) = cli.data_dir { + let db_path = Storage::db_path_for_data_dir(data_dir)?; + CLI_DB_PATH + .set(db_path) + .map_err(|_| anyhow::anyhow!("data directory was initialized more than once"))?; + } + match cli.command { Commands::Stats { tagging, states } => run_stats(tagging, states), Commands::Health => run_health(), Commands::Consolidate => run_consolidate(), + Commands::Update { + version, + install_dir, + dry_run, + no_sandwich, + sandwich_companion, + sandwich, + } => run_update( + version, + install_dir, + dry_run, + no_sandwich, + sandwich_companion, + sandwich, + ), + Commands::Sandwich { command } => match command { + SandwichCommands::Install { version, options } => { + run_sandwich_install(version.as_deref(), &options) + } + }, Commands::Restore { file } => run_restore(file), Commands::Backup { output } => run_backup(output), Commands::Export { @@ -142,6 +285,9 @@ fn main() -> anyhow::Result<()> { tags, since, } => run_export(output, format, tags, since), + Commands::PortableExport { output } => run_portable_export(output), + Commands::PortableImport { input, merge } => run_portable_import(input, merge), + Commands::Sync { archive } => run_sync(archive), Commands::Gc { min_retention, max_age_days, @@ -163,9 +309,1005 @@ fn main() -> anyhow::Result<()> { } } +#[derive(Debug, Clone, Copy)] +struct ReleaseAsset { + target: &'static str, + archive_ext: &'static str, + binary_suffix: &'static str, +} + +struct UpdateTempDir { + path: PathBuf, +} + +impl UpdateTempDir { + fn create() -> anyhow::Result { + let path = env::temp_dir().join(format!( + "vestige-update-{}-{}", + std::process::id(), + Utc::now().timestamp_millis() + )); + fs::create_dir_all(&path) + .with_context(|| format!("failed to create temp directory {}", path.display()))?; + Ok(Self { path }) + } +} + +impl Drop for UpdateTempDir { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.path); + } +} + +fn release_asset_for(os: &str, arch: &str) -> anyhow::Result { + match (os, arch) { + ("macos", "aarch64") => Ok(ReleaseAsset { + target: "aarch64-apple-darwin", + archive_ext: "tar.gz", + binary_suffix: "", + }), + ("macos", "x86_64") => Ok(ReleaseAsset { + target: "x86_64-apple-darwin", + archive_ext: "tar.gz", + binary_suffix: "", + }), + ("linux", "x86_64") => Ok(ReleaseAsset { + target: "x86_64-unknown-linux-gnu", + archive_ext: "tar.gz", + binary_suffix: "", + }), + ("windows", "x86_64") => Ok(ReleaseAsset { + target: "x86_64-pc-windows-msvc", + archive_ext: "zip", + binary_suffix: ".exe", + }), + _ => anyhow::bail!( + "unsupported platform for vestige update: {}-{}. Download manually from https://github.com/samvallad33/vestige/releases", + os, + arch + ), + } +} + +fn current_release_asset() -> anyhow::Result { + release_asset_for(env::consts::OS, env::consts::ARCH) +} + +fn release_download_url(asset: ReleaseAsset, version: Option<&str>) -> String { + let archive_name = format!("vestige-mcp-{}.{}", asset.target, asset.archive_ext); + match version { + Some(version) => { + let tag = normalize_release_tag(version); + format!( + "https://github.com/samvallad33/vestige/releases/download/{}/{}", + tag, archive_name + ) + } + None => format!( + "https://github.com/samvallad33/vestige/releases/latest/download/{}", + archive_name + ), + } +} + +fn normalize_release_tag(version: &str) -> String { + if version.starts_with('v') { + version.to_string() + } else { + format!("v{}", version) + } +} + +fn source_archive_url(tag: &str) -> String { + format!( + "https://github.com/samvallad33/vestige/archive/refs/tags/{}.tar.gz", + tag + ) +} + +fn download_file(url: &str, output: &Path, action: &str) -> anyhow::Result<()> { + run_command( + Command::new("curl") + .arg("-fsSL") + .arg("-A") + .arg("vestige-cli") + .arg(url) + .arg("-o") + .arg(output), + action, + ) +} + +fn parse_sha256(text: &str) -> anyhow::Result { + let hash = text + .split_whitespace() + .next() + .ok_or_else(|| anyhow::anyhow!("checksum file is empty"))? + .to_ascii_lowercase(); + if hash.len() != 64 || !hash.chars().all(|ch| ch.is_ascii_hexdigit()) { + anyhow::bail!("checksum file does not contain a valid SHA-256 hash"); + } + Ok(hash) +} + +fn sha256_from_command(command: &mut Command) -> anyhow::Result> { + match command.output() { + Ok(output) if output.status.success() => { + let text = String::from_utf8_lossy(&output.stdout); + Ok(Some(parse_sha256(&text)?)) + } + Ok(_) => Ok(None), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err).context("failed to run checksum command"), + } +} + +fn compute_sha256(path: &Path) -> anyhow::Result { + #[cfg(windows)] + { + if let Some(hash) = sha256_from_command( + Command::new("powershell") + .arg("-NoProfile") + .arg("-Command") + .arg("(Get-FileHash -Algorithm SHA256 -LiteralPath $args[0]).Hash.ToLowerInvariant()") + .arg(path), + )? { + return Ok(hash); + } + } + + #[cfg(not(windows))] + { + if let Some(hash) = + sha256_from_command(Command::new("shasum").arg("-a").arg("256").arg(path))? + { + return Ok(hash); + } + if let Some(hash) = sha256_from_command(Command::new("sha256sum").arg(path))? { + return Ok(hash); + } + } + + anyhow::bail!("no SHA-256 command available to verify release archive"); +} + +fn verify_release_checksum(archive_path: &Path, checksum_path: &Path) -> anyhow::Result<()> { + let expected = parse_sha256(&fs::read_to_string(checksum_path).with_context(|| { + format!( + "failed to read release checksum file {}", + checksum_path.display() + ) + })?)?; + let actual = compute_sha256(archive_path)?; + if actual != expected { + anyhow::bail!( + "release archive checksum mismatch for {}", + archive_path.display() + ); + } + Ok(()) +} + +fn latest_release_tag() -> anyhow::Result { + let temp_dir = UpdateTempDir::create()?; + let metadata_path = temp_dir.path.join("latest-release.json"); + download_file( + "https://api.github.com/repos/samvallad33/vestige/releases/latest", + &metadata_path, + "checking latest Vestige release", + )?; + let file = fs::File::open(&metadata_path)?; + let metadata: serde_json::Value = + serde_json::from_reader(file).context("failed to parse latest Vestige release metadata")?; + metadata + .get("tag_name") + .and_then(|tag| tag.as_str()) + .map(|tag| tag.to_string()) + .ok_or_else(|| anyhow::anyhow!("latest Vestige release metadata did not include tag_name")) +} + +fn release_tag_for_source(version: Option<&str>) -> anyhow::Result { + match version { + Some(version) => Ok(normalize_release_tag(version)), + None => latest_release_tag(), + } +} + +fn find_sandwich_source_root(root: &Path) -> Option { + if root.join("hooks").is_dir() && root.join("agents").is_dir() { + return Some(root.to_path_buf()); + } + + let entries = fs::read_dir(root).ok()?; + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("hooks").is_dir() && path.join("agents").is_dir() { + return Some(path); + } + } + + None +} + +fn download_sandwich_source(version: Option<&str>, output_dir: &Path) -> anyhow::Result { + let tag = release_tag_for_source(version)?; + let archive_path = output_dir.join(format!("vestige-source-{}.tar.gz", tag)); + let url = source_archive_url(&tag); + + println!("{}: {}", "Sandwich source".white().bold(), tag); + download_file(&url, &archive_path, "downloading Vestige source archive")?; + extract_source_archive(&archive_path, output_dir)?; + find_sandwich_source_root(output_dir).ok_or_else(|| { + anyhow::anyhow!("Vestige source archive did not contain hooks/ and agents/ directories") + }) +} + +fn home_dir() -> anyhow::Result { + directories::BaseDirs::new() + .map(|dirs| dirs.home_dir().to_path_buf()) + .ok_or_else(|| anyhow::anyhow!("failed to locate home directory")) +} + +fn is_vestige_hook_command(command: &str) -> bool { + const NEEDLES: &[&str] = &[ + "synthesis-preflight.sh", + "cwd-state-injector.sh", + "vestige-pulse-daemon.sh", + "preflight-swarm.sh", + "load-all-memory.sh", + "veto-detector.sh", + "sanhedrin.sh", + "synthesis-stop-validator.sh", + "synthesis-gate.sh", + ]; + NEEDLES.iter().any(|needle| command.contains(needle)) +} + +fn scrub_vestige_hooks(settings: &mut serde_json::Value) { + let Some(hooks) = settings + .get_mut("hooks") + .and_then(|hooks| hooks.as_object_mut()) + else { + return; + }; + + for event_name in ["UserPromptSubmit", "Stop"] { + let Some(groups) = hooks + .get_mut(event_name) + .and_then(|groups| groups.as_array_mut()) + else { + continue; + }; + + for group in groups.iter_mut() { + if let Some(commands) = group + .get_mut("hooks") + .and_then(|hooks| hooks.as_array_mut()) + { + commands.retain(|hook| { + !hook + .get("command") + .and_then(|command| command.as_str()) + .is_some_and(is_vestige_hook_command) + }); + } + } + + groups.retain(|group| { + group + .get("hooks") + .and_then(|hooks| hooks.as_array()) + .is_some_and(|hooks| !hooks.is_empty()) + }); + } + + hooks.retain(|_, value| match value { + serde_json::Value::Array(items) => !items.is_empty(), + serde_json::Value::Object(items) => !items.is_empty(), + serde_json::Value::Null => false, + _ => true, + }); + + if hooks.is_empty() + && let Some(root) = settings.as_object_mut() + { + root.remove("hooks"); + } +} + +fn merge_json(base: &mut serde_json::Value, overlay: serde_json::Value) { + match (base, overlay) { + (serde_json::Value::Object(base), serde_json::Value::Object(overlay)) => { + for (key, value) in overlay { + match base.get_mut(&key) { + Some(existing) => merge_json(existing, value), + None => { + base.insert(key, value); + } + } + } + } + (base, overlay) => *base = overlay, + } +} + +fn merge_settings_fragment( + settings: &mut serde_json::Value, + fragment_path: &Path, +) -> anyhow::Result<()> { + let file = fs::File::open(fragment_path) + .with_context(|| format!("failed to open {}", fragment_path.display()))?; + let fragment: serde_json::Value = serde_json::from_reader(file) + .with_context(|| format!("failed to parse {}", fragment_path.display()))?; + merge_json(settings, fragment); + Ok(()) +} + +fn copy_companion_files( + source_dir: &Path, + destination_dir: &Path, + allowed_extensions: &[&str], + _mode: u32, + options: &SandwichInstallOptions, +) -> anyhow::Result<(usize, usize)> { + fs::create_dir_all(destination_dir)?; + let mut copied = 0; + let mut skipped = 0; + + for entry in fs::read_dir(source_dir) + .with_context(|| format!("failed to read {}", source_dir.display()))? + { + let entry = entry?; + let source = entry.path(); + if !source.is_file() { + continue; + } + + let extension = source + .extension() + .and_then(|ext| ext.to_str()) + .unwrap_or(""); + if !allowed_extensions.contains(&extension) { + continue; + } + + let Some(file_name) = source.file_name() else { + continue; + }; + if file_name.to_string_lossy() == "load-all-memory.sh" && !options.include_memory_loader { + continue; + } + + let destination = destination_dir.join(file_name); + if destination.exists() && !options.force { + skipped += 1; + continue; + } + + fs::copy(&source, &destination).with_context(|| { + format!( + "failed to copy {} to {}", + source.display(), + destination.display() + ) + })?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&destination)?.permissions(); + perms.set_mode(_mode); + fs::set_permissions(&destination, perms)?; + } + + copied += 1; + } + + Ok((copied, skipped)) +} + +fn quote_shell_env(value: &str) -> String { + format!("'{}'", value.replace('\'', "'\\''")) +} + +fn write_sanhedrin_env( + hooks_dir: &Path, + endpoint: &str, + model: &str, + dashboard_port: &str, +) -> anyhow::Result<()> { + let env_path = hooks_dir.join("vestige-sanhedrin.env"); + let contents = format!( + "VESTIGE_SANHEDRIN_ENABLED=1\nVESTIGE_SANHEDRIN_ENDPOINT={}\nVESTIGE_SANHEDRIN_MODEL={}\nVESTIGE_DASHBOARD_PORT={}\nVESTIGE_SANHEDRIN_CLAIM_MODE=1\nVESTIGE_SANHEDRIN_OUTPUT=json\n", + quote_shell_env(endpoint), + quote_shell_env(model), + quote_shell_env(dashboard_port) + ); + fs::write(&env_path, contents)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&env_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&env_path, perms)?; + } + + println!("{}: {}", "Sanhedrin env".white().bold(), env_path.display()); + Ok(()) +} + +fn install_launchd_job(source_root: &Path, home: &Path, model: &str) -> anyhow::Result<()> { + let launchd_dir = home.join("Library").join("LaunchAgents"); + fs::create_dir_all(&launchd_dir)?; + + let template_path = source_root + .join("launchd") + .join("com.vestige.mlx-server.plist.template"); + let template = fs::read_to_string(&template_path) + .with_context(|| format!("failed to read {}", template_path.display()))?; + let rendered = template + .replace("__HOME__", &home.display().to_string()) + .replace("__MODEL__", model); + + let plist = launchd_dir.join("com.vestige.mlx-server.plist"); + fs::write(&plist, rendered)?; + let _ = Command::new("launchctl").arg("unload").arg(&plist).status(); + run_command( + Command::new("launchctl").arg("load").arg(&plist), + "loading Vestige MLX launchd job", + )?; + println!("{}: {}", "launchd".white().bold(), plist.display()); + Ok(()) +} + +fn remove_legacy_launchd_job(home: &Path) { + if env::consts::OS != "macos" { + return; + } + + let plist = home + .join("Library") + .join("LaunchAgents") + .join("com.vestige.mlx-server.plist"); + if plist.exists() { + let _ = Command::new("launchctl").arg("unload").arg(&plist).status(); + if fs::remove_file(&plist).is_ok() { + println!( + "{}: removed old Sanhedrin launchd job", + "launchd".white().bold() + ); + } + } +} + +fn install_sandwich_from_source( + source_root: &Path, + options: &SandwichInstallOptions, +) -> anyhow::Result<()> { + let home = home_dir()?; + let claude_dir = home.join(".claude"); + let hooks_dir = claude_dir.join("hooks"); + let agents_dir = claude_dir.join("agents"); + let settings_path = claude_dir.join("settings.json"); + let source_root = + find_sandwich_source_root(source_root).unwrap_or_else(|| source_root.to_path_buf()); + + if !source_root.join("hooks").is_dir() || !source_root.join("agents").is_dir() { + anyhow::bail!( + "Cognitive Sandwich source missing hooks/ or agents/: {}", + source_root.display() + ); + } + + let enable_preflight = options.enable_preflight || options.enable_sandwich; + let mut enable_sanhedrin = + options.enable_sanhedrin || options.enable_sandwich || options.with_launchd; + let mut with_launchd = options.with_launchd; + + if with_launchd && (env::consts::OS != "macos" || env::consts::ARCH != "aarch64") { + println!( + "{}", + "--with-launchd is Apple Silicon only; using endpoint-backed Sanhedrin instead." + .yellow() + ); + with_launchd = false; + enable_sanhedrin = true; + } + + fs::create_dir_all(&claude_dir)?; + let (hooks_copied, hooks_skipped) = copy_companion_files( + &source_root.join("hooks"), + &hooks_dir, + &["sh", "py"], + 0o755, + options, + )?; + let (agents_copied, agents_skipped) = copy_companion_files( + &source_root.join("agents"), + &agents_dir, + &["md"], + 0o644, + options, + )?; + + println!( + "{}: {} installed, {} skipped", + "Hooks".white().bold(), + hooks_copied, + hooks_skipped + ); + println!( + "{}: {} installed, {} skipped", + "Agents".white().bold(), + agents_copied, + agents_skipped + ); + + if !with_launchd { + remove_legacy_launchd_job(&home); + } + + let dashboard_port = env::var("VESTIGE_DASHBOARD_PORT").unwrap_or_else(|_| "3927".to_string()); + let endpoint = options + .sanhedrin_endpoint + .clone() + .or_else(|| env::var("VESTIGE_SANHEDRIN_ENDPOINT").ok()) + .or_else(|| env::var("MLX_ENDPOINT").ok()) + .unwrap_or_else(|| "http://127.0.0.1:8080/v1/chat/completions".to_string()) + .trim_end_matches('/') + .to_string(); + let model = options + .sanhedrin_model + .clone() + .or_else(|| env::var("VESTIGE_SANHEDRIN_MODEL").ok()) + .or_else(|| env::var("VESTIGE_SANDWICH_MODEL").ok()) + .unwrap_or_else(|| "mlx-community/Qwen3.6-35B-A3B-4bit".to_string()); + + if enable_sanhedrin { + write_sanhedrin_env(&hooks_dir, &endpoint, &model, &dashboard_port)?; + } + if with_launchd { + install_launchd_job(&source_root, &home, &model)?; + } + + if !settings_path.exists() { + fs::write(&settings_path, "{}\n")?; + } + let backup_path = claude_dir.join("settings.json.bak.pre-sandwich"); + if !backup_path.exists() { + fs::copy(&settings_path, &backup_path)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&backup_path)?.permissions(); + perms.set_mode(0o600); + fs::set_permissions(&backup_path, perms)?; + } + } + + let settings_file = fs::File::open(&settings_path)?; + let mut settings: serde_json::Value = + serde_json::from_reader(settings_file).unwrap_or_else(|_| serde_json::json!({})); + scrub_vestige_hooks(&mut settings); + + if enable_preflight { + merge_settings_fragment( + &mut settings, + &source_root + .join("hooks") + .join("settings.preflight.fragment.json"), + )?; + } + if enable_sanhedrin { + merge_settings_fragment( + &mut settings, + &source_root + .join("hooks") + .join("settings.sanhedrin.fragment.json"), + )?; + } + + let mut settings_file = fs::File::create(&settings_path)?; + serde_json::to_writer_pretty(&mut settings_file, &settings)?; + writeln!(settings_file)?; + + if enable_preflight || enable_sanhedrin { + let mut layers = Vec::new(); + if enable_preflight { + layers.push("preflight"); + } + if enable_sanhedrin { + layers.push("sanhedrin"); + } + println!( + "{}: enabled optional layer(s): {}", + "Settings".white().bold(), + layers.join(", ") + ); + } else { + println!( + "{}: no Vestige Claude Code hooks enabled by default", + "Settings".white().bold() + ); + } + + Ok(()) +} + +fn run_sandwich_install( + version: Option<&str>, + options: &SandwichInstallOptions, +) -> anyhow::Result<()> { + println!( + "{}", + "=== Vestige Cognitive Sandwich Install ===".cyan().bold() + ); + println!(); + + if let Some(source_root) = &options.src { + install_sandwich_from_source(source_root, options)?; + } else { + let temp_dir = UpdateTempDir::create()?; + let source_root = download_sandwich_source(version, &temp_dir.path)?; + install_sandwich_from_source(&source_root, options)?; + } + + println!(); + let optional_layers_enabled = options.enable_preflight + || options.enable_sandwich + || options.enable_sanhedrin + || options.with_launchd; + let message = if optional_layers_enabled { + "Cognitive Sandwich files updated. Restart Claude Code to use enabled optional hooks." + } else { + "Cognitive Sandwich files updated. No hooks enabled; no automatic model calls." + }; + println!("{}", message.green().bold()); + Ok(()) +} + +fn run_command(command: &mut Command, action: &str) -> anyhow::Result<()> { + let status = command + .status() + .with_context(|| format!("failed to start {}", action))?; + if !status.success() { + anyhow::bail!("{} failed with status {}", action, status); + } + Ok(()) +} + +fn create_private_file(path: &Path) -> std::io::Result { + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path) + } + + #[cfg(not(unix))] + { + fs::File::create(path) + } +} + +fn command_output(command: &mut Command, action: &str) -> anyhow::Result { + let output = command + .output() + .with_context(|| format!("failed to start {}", action))?; + if !output.status.success() { + anyhow::bail!("{} failed with status {}", action, output.status); + } + Ok(String::from_utf8_lossy(&output.stdout).to_string()) +} + +fn powershell_quote(value: &Path) -> String { + format!("'{}'", value.display().to_string().replace('\'', "''")) +} + +fn normalize_archive_entry(entry: &str) -> anyhow::Result { + let normalized = entry.trim().replace('\\', "/"); + let normalized = normalized.strip_prefix("./").unwrap_or(&normalized); + if normalized.is_empty() + || normalized.starts_with('/') + || normalized.get(1..2) == Some(":") + || normalized + .split('/') + .any(|part| part.is_empty() || part == "..") + { + anyhow::bail!("archive contains unsafe entry: {}", entry); + } + Ok(normalized.to_string()) +} + +fn archive_listing(archive_path: &Path, archive_ext: &str) -> anyhow::Result { + let listing = match archive_ext { + "tar.gz" => command_output( + Command::new("tar").arg("-tzf").arg(archive_path), + "listing Vestige archive with tar", + )?, + "zip" => { + let script = format!( + "Add-Type -AssemblyName System.IO.Compression.FileSystem; \ + $zip = [System.IO.Compression.ZipFile]::OpenRead({}); \ + try {{ $zip.Entries | ForEach-Object {{ $_.FullName }} }} finally {{ $zip.Dispose() }}", + powershell_quote(archive_path) + ); + command_output( + Command::new("powershell") + .arg("-NoProfile") + .arg("-Command") + .arg(script), + "listing Vestige archive with PowerShell", + )? + } + other => anyhow::bail!("unsupported release archive extension: {}", other), + }; + Ok(listing) +} + +fn validate_archive_safety(archive_path: &Path, archive_ext: &str) -> anyhow::Result<()> { + let listing = archive_listing(archive_path, archive_ext)?; + for entry in listing.lines().filter(|line| !line.trim().is_empty()) { + normalize_archive_entry(entry)?; + } + Ok(()) +} + +fn validate_archive_entries( + archive_path: &Path, + archive_ext: &str, + expected_members: &[String], +) -> anyhow::Result<()> { + let listing = archive_listing(archive_path, archive_ext)?; + + let expected: HashSet<&str> = expected_members.iter().map(String::as_str).collect(); + for entry in listing.lines().filter(|line| !line.trim().is_empty()) { + let normalized = normalize_archive_entry(entry)?; + if !expected.contains(normalized.as_str()) { + anyhow::bail!("release archive contains unexpected entry: {}", entry); + } + } + Ok(()) +} + +fn extract_source_archive(archive_path: &Path, output_dir: &Path) -> anyhow::Result<()> { + validate_archive_safety(archive_path, "tar.gz")?; + run_command( + Command::new("tar") + .arg("-xzf") + .arg(archive_path) + .arg("-C") + .arg(output_dir), + "extracting Vestige source archive with tar", + ) +} + +fn extract_archive( + archive_path: &Path, + output_dir: &Path, + archive_ext: &str, + expected_members: &[String], +) -> anyhow::Result<()> { + validate_archive_entries(archive_path, archive_ext, expected_members)?; + match archive_ext { + "tar.gz" => run_command( + Command::new("tar") + .arg("-xzf") + .arg(archive_path) + .arg("-C") + .arg(output_dir), + "extracting Vestige release archive with tar", + ), + "zip" => run_command( + Command::new("powershell") + .arg("-NoProfile") + .arg("-Command") + .arg(format!( + "Expand-Archive -LiteralPath {} -DestinationPath {} -Force", + powershell_quote(archive_path), + powershell_quote(output_dir) + )), + "extracting Vestige release archive with PowerShell", + ), + other => anyhow::bail!("unsupported release archive extension: {}", other), + } +} + +fn replace_binary(source: &Path, destination: &Path) -> anyhow::Result<()> { + let file_name = destination + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow::anyhow!("invalid destination path {}", destination.display()))?; + let temp_destination = destination.with_file_name(format!( + ".{}.vestige-update-{}", + file_name, + std::process::id() + )); + + fs::copy(source, &temp_destination).with_context(|| { + format!( + "failed to stage {} for install at {}", + source.display(), + temp_destination.display() + ) + })?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = fs::metadata(&temp_destination)?.permissions(); + perms.set_mode(0o755); + fs::set_permissions(&temp_destination, perms)?; + } + + #[cfg(windows)] + if destination.exists() { + fs::remove_file(destination).with_context(|| { + format!( + "failed to replace {}. Close running Vestige processes and retry", + destination.display() + ) + })?; + } + + fs::rename(&temp_destination, destination).with_context(|| { + let _ = fs::remove_file(&temp_destination); + format!( + "failed to install {}. If this is a system directory, retry with: sudo vestige update", + destination.display() + ) + })?; + + Ok(()) +} + +fn run_update( + version: Option, + install_dir: Option, + dry_run: bool, + no_sandwich: bool, + sandwich_companion: bool, + sandwich: SandwichInstallOptions, +) -> anyhow::Result<()> { + println!("{}", "=== Vestige Update ===".cyan().bold()); + println!(); + + let asset = current_release_asset()?; + let current_exe = env::current_exe().context("failed to locate current vestige executable")?; + let install_dir = match install_dir { + Some(path) => path, + None => current_exe + .parent() + .ok_or_else(|| anyhow::anyhow!("current executable has no parent directory"))? + .to_path_buf(), + }; + + let url = release_download_url(asset, version.as_deref()); + let archive_name = format!("vestige-mcp-{}.{}", asset.target, asset.archive_ext); + + println!( + "{}: {}", + "Current version".white().bold(), + env!("CARGO_PKG_VERSION") + ); + println!( + "{}: {}", + "Release".white().bold(), + version.as_deref().unwrap_or("latest") + ); + println!("{}: {}", "Target".white().bold(), asset.target); + println!( + "{}: {}", + "Install dir".white().bold(), + install_dir.display() + ); + println!("{}: {}", "Download".white().bold(), url); + + if dry_run { + println!(); + println!("{}", "Dry run: no files changed.".yellow().bold()); + return Ok(()); + } + + fs::create_dir_all(&install_dir).with_context(|| { + format!( + "failed to create install directory {}", + install_dir.display() + ) + })?; + + let temp_dir = UpdateTempDir::create()?; + let archive_path = temp_dir.path.join(&archive_name); + let checksum_path = temp_dir.path.join(format!("{}.sha256", archive_name)); + + println!(); + println!("{}", "Downloading release archive...".cyan()); + download_file(&url, &archive_path, "downloading Vestige release archive")?; + download_file( + &format!("{}.sha256", url), + &checksum_path, + "downloading Vestige release checksum", + )?; + verify_release_checksum(&archive_path, &checksum_path)?; + + let binaries = ["vestige", "vestige-mcp", "vestige-restore"]; + let mut expected_members = binaries + .iter() + .map(|binary| format!("{}{}", binary, asset.binary_suffix)) + .collect::>(); + if asset.target == "x86_64-apple-darwin" { + expected_members.push("INSTALL-INTEL-MAC.md".to_string()); + } + + println!("{}", "Extracting release archive...".cyan()); + extract_archive( + &archive_path, + &temp_dir.path, + asset.archive_ext, + &expected_members, + )?; + + for binary in binaries { + let filename = format!("{}{}", binary, asset.binary_suffix); + let source = temp_dir.path.join(&filename); + if !source.exists() { + anyhow::bail!("release archive is missing expected binary: {}", filename); + } + + let destination = install_dir.join(&filename); + println!(" {} {}", "install".dimmed(), destination.display()); + replace_binary(&source, &destination)?; + } + + println!(); + let installed_mcp = install_dir.join(format!("vestige-mcp{}", asset.binary_suffix)); + if let Ok(output) = Command::new(&installed_mcp).arg("--version").output() + && output.status.success() + { + let version = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if !version.is_empty() { + println!("{}: {}", "Installed".white().bold(), version.green()); + } + } + + println!( + "{}", + "Binary update complete. Restart your MCP client to pick up the new binary." + .green() + .bold() + ); + + if sandwich_companion && !no_sandwich { + println!(); + println!( + "{}", + "Updating Cognitive Sandwich companion files...".cyan() + ); + run_sandwich_install(version.as_deref(), &sandwich)?; + } else if no_sandwich { + println!( + "{}", + "Skipped Cognitive Sandwich companion update (--no-sandwich).".yellow() + ); + } else { + println!( + "{}", + "Skipped Cognitive Sandwich companion update (default). Pass --sandwich-companion to refresh Claude Code companion files." + .yellow() + ); + } + + Ok(()) +} + /// Run stats command fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { - let storage = Storage::new(None)?; + let storage = open_storage()?; let stats = storage.get_stats()?; println!("{}", "=== Vestige Memory Statistics ===".cyan().bold()); @@ -199,8 +1341,20 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { stats.nodes_with_embeddings ); + if let Some(model) = &stats.active_embedding_model { + println!("{}: {}", "Active Embedding Model".white().bold(), model); + } + if let Some(model) = &stats.embedding_model { - println!("{}: {}", "Embedding Model".white().bold(), model); + println!("{}: {}", "Stored Embedding Model".white().bold(), model); + } + + if stats.nodes_with_mismatched_embeddings > 0 { + println!( + "{}: {}", + "Mismatched Embeddings".white().bold(), + stats.nodes_with_mismatched_embeddings + ); } if let Some(oldest) = stats.oldest_memory { @@ -220,13 +1374,13 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { // Embedding coverage let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; println!( "{}: {:.1}%", - "Embedding Coverage".white().bold(), + "Active Embedding Coverage".white().bold(), embedding_coverage ); @@ -351,7 +1505,7 @@ fn print_distribution_bar(label: &str, count: usize, total: usize, color: &str) /// Run health check fn run_health() -> anyhow::Result<()> { - let storage = Storage::new(None)?; + let storage = open_storage()?; let stats = storage.get_stats()?; println!("{}", "=== Vestige Health Check ===".cyan().bold()); @@ -390,13 +1544,13 @@ fn run_health() -> anyhow::Result<()> { // Embedding coverage let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; println!( "{}: {:.1}%", - "Embedding Coverage".white(), + "Active Embedding Coverage".white(), embedding_coverage ); println!( @@ -421,14 +1575,18 @@ fn run_health() -> anyhow::Result<()> { warnings.push("Many memories are due for review"); } - if stats.total_nodes > 0 && stats.nodes_with_embeddings == 0 { - warnings.push("No embeddings generated - semantic search unavailable"); + if stats.total_nodes > 0 && stats.nodes_with_active_embeddings == 0 { + warnings.push("No active-model embeddings generated - semantic search unavailable"); } if embedding_coverage < 50.0 && stats.total_nodes > 10 { warnings.push("Low embedding coverage - run consolidation to improve semantic search"); } + if stats.nodes_with_mismatched_embeddings > 0 { + warnings.push("Stored embeddings from another model are present - run consolidation after changing embedding models"); + } + if !warnings.is_empty() { println!(); println!("{}", "Warnings:".yellow().bold()); @@ -449,9 +1607,9 @@ fn run_health() -> anyhow::Result<()> { recommendations.push("Review due memories to strengthen retention."); } - if stats.nodes_with_embeddings < stats.total_nodes { + if stats.nodes_with_active_embeddings < stats.total_nodes { recommendations - .push("Run 'vestige consolidate' to generate embeddings for better semantic search."); + .push("Run 'vestige consolidate' to generate active-model embeddings for better semantic search."); } if stats.total_nodes > 100 && stats.average_retention < 0.7 { @@ -488,7 +1646,7 @@ fn run_consolidate() -> anyhow::Result<()> { println!("Running memory consolidation cycle..."); println!(); - let storage = Storage::new(None)?; + let storage = open_storage()?; let result = storage.run_consolidation()?; println!( @@ -534,7 +1692,49 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { println!("Loading backup from: {}", backup_path.display()); // Read and parse backup - let backup_content = std::fs::read_to_string(&backup_path)?; + let backup_bytes = std::fs::read(&backup_path)?; + if backup_bytes.starts_with(b"SQLite format 3\0") { + anyhow::bail!( + "{} is a raw SQLite database backup, not a JSON restore file. Use portable-export/portable-import for cross-device transfer, or replace the database file manually while Vestige is stopped.", + backup_path.display() + ); + } + let backup_content = String::from_utf8(backup_bytes) + .with_context(|| format!("{} is not UTF-8 JSON", backup_path.display()))?; + + if let Ok(archive) = serde_json::from_str::(&backup_content) + && archive.archive_format == vestige_core::PORTABLE_ARCHIVE_FORMAT + { + println!("Detected portable archive."); + println!("{}: {}", "Format".white().bold(), archive.archive_format); + println!("{}: {}", "Schema".white().bold(), archive.schema_version); + println!("{}: {}", "Tables".white().bold(), archive.tables.len()); + println!("{}: {}", "Rows".white().bold(), archive.total_rows()); + println!(); + + let storage = open_storage()?; + let report = storage.import_portable_archive(&archive, PortableImportMode::EmptyOnly)?; + + println!( + "{}: {}", + "Tables imported".white().bold(), + report.tables_imported + ); + println!( + "{}: {}", + "Rows imported".white().bold(), + report.rows_imported + ); + println!( + "{}: {}", + "Tables skipped".white().bold(), + report.tables_skipped + ); + println!("{}: {}", "FTS rebuilt".white().bold(), report.fts_rebuilt); + println!(); + println!("{}", "Portable restore complete.".green().bold()); + return Ok(()); + } #[derive(serde::Deserialize)] struct BackupWrapper { @@ -557,16 +1757,27 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { source: Option, } - let wrapper: Vec = serde_json::from_str(&backup_content)?; - let recall_result: RecallResult = serde_json::from_str(&wrapper[0].text)?; - let memories = recall_result.results; + let memories = if let Ok(wrapper) = serde_json::from_str::>(&backup_content) + { + let first = wrapper.first().context("backup wrapper is empty")?; + let recall_result: RecallResult = serde_json::from_str(&first.text)?; + recall_result.results + } else if let Ok(recall_result) = serde_json::from_str::(&backup_content) { + recall_result.results + } else if let Ok(memories) = serde_json::from_str::>(&backup_content) { + memories + } else { + anyhow::bail!( + "Unrecognized backup format. Expected portable archive, MCP wrapper, RecallResult, or array of memories." + ); + }; println!("Found {} memories to restore", memories.len()); println!(); // Initialize storage println!("Initializing storage..."); - let storage = Storage::new(None)?; + let storage = open_storage()?; println!("Generating embeddings and ingesting memories..."); println!(); @@ -616,8 +1827,8 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { println!("{}: {}", "Total Nodes".white(), stats.total_nodes); println!( "{}: {}", - "With Embeddings".white(), - stats.nodes_with_embeddings + "Active Embeddings".white(), + stats.nodes_with_active_embeddings ); Ok(()) @@ -625,9 +1836,20 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { /// Get the default database path fn get_default_db_path() -> anyhow::Result { - let proj_dirs = ProjectDirs::from("com", "vestige", "core") - .ok_or_else(|| anyhow::anyhow!("Could not determine project directories"))?; - Ok(proj_dirs.data_dir().join("vestige.db")) + if let Some(path) = CLI_DB_PATH.get() { + Ok(path.clone()) + } else { + Ok(Storage::default_db_path()?) + } +} + +/// Open storage using the CLI-selected data directory, if one was provided. +fn open_storage() -> anyhow::Result { + if let Some(path) = CLI_DB_PATH.get() { + Ok(Storage::new(Some(path.clone()))?) + } else { + Ok(Storage::new(None)?) + } } /// Fetch all nodes from storage using pagination @@ -663,7 +1885,7 @@ fn run_backup(output: PathBuf) -> anyhow::Result<()> { // Open storage to flush WAL before copying println!("Flushing WAL checkpoint..."); { - let storage = Storage::new(None)?; + let storage = open_storage()?; // get_stats triggers a read so the connection is active, then drop flushes let _ = storage.get_stats()?; } @@ -687,6 +1909,13 @@ fn run_backup(output: PathBuf) -> anyhow::Result<()> { println!(" {} {}", "To:".dimmed(), output.display()); std::fs::copy(&db_path, &output)?; + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut perms = std::fs::metadata(&output)?.permissions(); + perms.set_mode(0o600); + std::fs::set_permissions(&output, perms)?; + } let file_size = std::fs::metadata(&output)?.len(); let size_display = if file_size >= 1024 * 1024 { @@ -750,7 +1979,7 @@ fn run_export( }) .unwrap_or_default(); - let storage = Storage::new(None)?; + let storage = open_storage()?; let all_nodes = fetch_all_nodes(&storage)?; // Apply filters @@ -797,7 +2026,7 @@ fn run_export( std::fs::create_dir_all(parent)?; } - let file = std::fs::File::create(&output)?; + let file = create_private_file(&output)?; let mut writer = BufWriter::new(file); match format.as_str() { @@ -841,6 +2070,145 @@ fn run_export( Ok(()) } +/// Run exact portable archive export. +fn run_portable_export(output: PathBuf) -> anyhow::Result<()> { + println!("{}", "=== Vestige Portable Export ===".cyan().bold()); + println!(); + + if let Some(parent) = output.parent() + && !parent.exists() + { + std::fs::create_dir_all(parent)?; + } + + let storage = open_storage()?; + let archive = storage.export_portable_archive_to_path(&output)?; + + let file_size = std::fs::metadata(&output)?.len(); + let size_display = if file_size >= 1024 * 1024 { + format!("{:.2} MB", file_size as f64 / (1024.0 * 1024.0)) + } else if file_size >= 1024 { + format!("{:.1} KB", file_size as f64 / 1024.0) + } else { + format!("{} bytes", file_size) + }; + + println!("{}: {}", "Archive".white().bold(), output.display()); + println!("{}: {}", "Format".white().bold(), archive.archive_format); + println!("{}: {}", "Schema".white().bold(), archive.schema_version); + println!("{}: {}", "Tables".white().bold(), archive.tables.len()); + println!("{}: {}", "Rows".white().bold(), archive.total_rows()); + println!(); + println!( + "{}", + format!( + "Portable export complete: {} ({})", + output.display(), + size_display + ) + .green() + .bold() + ); + + Ok(()) +} + +/// Run exact portable archive import. +fn run_portable_import(input: PathBuf, merge: bool) -> anyhow::Result<()> { + println!("{}", "=== Vestige Portable Import ===".cyan().bold()); + println!(); + println!("{}: {}", "Archive".white().bold(), input.display()); + let mode = if merge { + PortableImportMode::Merge + } else { + PortableImportMode::EmptyOnly + }; + println!( + "{}", + if merge { + "Mode: merge into existing database".yellow() + } else { + "Mode: empty database only".yellow() + } + ); + println!(); + + let storage = open_storage()?; + let report = storage.import_portable_archive_from_path(&input, mode)?; + + println!( + "{}: {}", + "Tables imported".white().bold(), + report.tables_imported + ); + println!( + "{}: {}", + "Rows imported".white().bold(), + report.rows_imported + ); + println!( + "{}: {}", + "Tables skipped".white().bold(), + report.tables_skipped + ); + println!("{}: {}", "FTS rebuilt".white().bold(), report.fts_rebuilt); + if merge { + println!( + "{}: {} inserted, {} updated, {} deleted, {} skipped, {} kept local", + "Merge".white().bold(), + report.rows_inserted, + report.rows_updated, + report.rows_deleted, + report.rows_skipped, + report.conflicts_kept_local + ); + } + println!(); + println!("{}", "Portable import complete.".green().bold()); + + Ok(()) +} + +/// Run file-backed two-way sync. +fn run_sync(archive: PathBuf) -> anyhow::Result<()> { + println!("{}", "=== Vestige File Sync ===".cyan().bold()); + println!(); + println!("{}: {}", "Archive".white().bold(), archive.display()); + + let storage = open_storage()?; + let report = storage.sync_portable_archive_file(&archive)?; + + if let Some(pull) = &report.pull { + println!("{}", "Pull: merged remote archive".yellow()); + println!( + " {} inserted, {} updated, {} deleted, {} skipped, {} kept local", + pull.rows_inserted, + pull.rows_updated, + pull.rows_deleted, + pull.rows_skipped, + pull.conflicts_kept_local + ); + } else { + println!( + "{}", + "Pull: archive does not exist yet; creating it".yellow() + ); + } + + println!("{}", "Push: wrote merged local state".yellow()); + println!( + "{}", + format!( + "Sync complete: {} tables, {} rows", + report.pushed_tables, report.pushed_rows + ) + .green() + .bold() + ); + + Ok(()) +} + /// Run garbage collection command fn run_gc( min_retention: f64, @@ -851,7 +2219,7 @@ fn run_gc( println!("{}", "=== Vestige Garbage Collection ===".cyan().bold()); println!(); - let storage = Storage::new(None)?; + let storage = open_storage()?; let all_nodes = fetch_all_nodes(&storage)?; let now = Utc::now(); @@ -1027,7 +2395,7 @@ fn run_ingest( valid_until: None, }; - let storage = Storage::new(None)?; + let storage = open_storage()?; // Try smart_ingest (PE Gating) if available, otherwise regular ingest #[cfg(all(feature = "embeddings", feature = "vector-search"))] @@ -1081,7 +2449,7 @@ fn run_dashboard(port: u16, open_browser: bool) -> anyhow::Result<()> { format!("http://127.0.0.1:{}", port).cyan() ); - let storage = Storage::new(None)?; + let storage = open_storage()?; // Try to initialize embeddings for search support #[cfg(feature = "embeddings")] @@ -1112,7 +2480,7 @@ fn run_serve(port: u16, with_dashboard: bool, dashboard_port: u16) -> anyhow::Re println!("{}", "=== Vestige HTTP Server ===".cyan().bold()); println!(); - let storage = Storage::new(None)?; + let storage = open_storage()?; #[cfg(feature = "embeddings")] { @@ -1173,7 +2541,9 @@ fn run_serve(port: u16, with_dashboard: bool, dashboard_port: u16) -> anyhow::Re bind, port ); - println!(" {} Auth token: {}...", ">".cyan(), &token[..8]); + if let Ok(path) = vestige_mcp::protocol::auth::token_path() { + println!(" {} Auth token file: {}", ">".cyan(), path.display()); + } println!(); println!("{}", "Press Ctrl+C to stop.".dimmed()); @@ -1207,3 +2577,86 @@ fn truncate(s: &str, max_chars: usize) -> String { format!("{}...", truncated) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn update_asset_mapping_matches_release_names() { + let mac_arm = release_asset_for("macos", "aarch64").unwrap(); + assert_eq!(mac_arm.target, "aarch64-apple-darwin"); + assert_eq!(mac_arm.archive_ext, "tar.gz"); + assert_eq!(mac_arm.binary_suffix, ""); + + let linux = release_asset_for("linux", "x86_64").unwrap(); + assert_eq!(linux.target, "x86_64-unknown-linux-gnu"); + assert_eq!(linux.archive_ext, "tar.gz"); + + let windows = release_asset_for("windows", "x86_64").unwrap(); + assert_eq!(windows.target, "x86_64-pc-windows-msvc"); + assert_eq!(windows.archive_ext, "zip"); + assert_eq!(windows.binary_suffix, ".exe"); + } + + #[test] + fn update_url_uses_latest_or_normalized_tag() { + let asset = release_asset_for("macos", "aarch64").unwrap(); + assert_eq!( + release_download_url(asset, None), + "https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz" + ); + assert_eq!( + release_download_url(asset, Some("2.1.0")), + "https://github.com/samvallad33/vestige/releases/download/v2.1.0/vestige-mcp-aarch64-apple-darwin.tar.gz" + ); + assert_eq!( + release_download_url(asset, Some("v2.1.0")), + "https://github.com/samvallad33/vestige/releases/download/v2.1.0/vestige-mcp-aarch64-apple-darwin.tar.gz" + ); + } + + #[test] + fn source_archive_url_uses_normalized_tag() { + assert_eq!(normalize_release_tag("2.1.1"), "v2.1.1"); + assert_eq!(normalize_release_tag("v2.1.1"), "v2.1.1"); + assert_eq!( + source_archive_url("v2.1.1"), + "https://github.com/samvallad33/vestige/archive/refs/tags/v2.1.1.tar.gz" + ); + } + + #[test] + fn scrub_vestige_hooks_removes_only_vestige_commands() { + let mut settings = serde_json::json!({ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { "type": "command", "command": "/tmp/synthesis-preflight.sh" }, + { "type": "command", "command": "/tmp/custom-user-hook.sh" } + ] + } + ], + "Stop": [ + { + "hooks": [ + { "type": "command", "command": "/tmp/sanhedrin.sh" } + ] + } + ] + }, + "other": true + }); + + scrub_vestige_hooks(&mut settings); + + let user_hooks = settings["hooks"]["UserPromptSubmit"][0]["hooks"] + .as_array() + .unwrap(); + assert_eq!(user_hooks.len(), 1); + assert_eq!(user_hooks[0]["command"], "/tmp/custom-user-hook.sh"); + assert!(settings["hooks"].get("Stop").is_none()); + assert_eq!(settings["other"], true); + } +} diff --git a/crates/vestige-mcp/src/cognitive.rs b/crates/vestige-mcp/src/cognitive.rs index 86aadcf..3d106ff 100644 --- a/crates/vestige-mcp/src/cognitive.rs +++ b/crates/vestige-mcp/src/cognitive.rs @@ -6,6 +6,7 @@ use vestige_core::neuroscience::predictive_retrieval::PredictiveMemory; use vestige_core::neuroscience::prospective_memory::{IntentionParser, ProspectiveMemory}; +#[cfg(feature = "vector-search")] use vestige_core::search::TemporalSearcher; use vestige_core::{ AccessibilityCalculator, @@ -31,9 +32,6 @@ use vestige_core::{ MemoryDreamer, NoveltySignal, ReconsolidationManager, - // Search modules - Reranker, - RerankerConfig, RewardSignal, SpeculativeRetriever, StateUpdateService, @@ -41,6 +39,8 @@ use vestige_core::{ Storage, SynapticTaggingSystem, }; +#[cfg(feature = "vector-search")] +use vestige_core::{Reranker, RerankerConfig}; /// Stateful cognitive engine holding all neuroscience modules. /// @@ -80,7 +80,9 @@ pub struct CognitiveEngine { pub consolidation_scheduler: ConsolidationScheduler, // -- Search -- + #[cfg(feature = "vector-search")] pub reranker: Reranker, + #[cfg(feature = "vector-search")] pub temporal_searcher: TemporalSearcher, } @@ -161,7 +163,9 @@ impl CognitiveEngine { consolidation_scheduler: ConsolidationScheduler::new(), // Search + #[cfg(feature = "vector-search")] reranker: Reranker::new(RerankerConfig::default()), + #[cfg(feature = "vector-search")] temporal_searcher: TemporalSearcher::new(), } } diff --git a/crates/vestige-mcp/src/dashboard/events.rs b/crates/vestige-mcp/src/dashboard/events.rs index be875b0..8edb238 100644 --- a/crates/vestige-mcp/src/dashboard/events.rs +++ b/crates/vestige-mcp/src/dashboard/events.rs @@ -66,6 +66,35 @@ pub enum VestigeEvent { timestamp: DateTime, }, + // -- Reasoning (v2.0.4+ Reasoning Theater) -- + // Emitted after a dashboard /api/deep_reference call completes. Carries + // the memory IDs the 3D graph should light up: primary evidence (camera + // glide target + brightest pulse), supporting evidence (softer pulses), + // and contradiction pairs (render geodesic arcs between these pairs). + DeepReferenceCompleted { + query: String, + intent: String, + status: String, + confidence: f64, + primary_id: Option, + supporting_ids: Vec, + contradicting_ids: Vec, + contradiction_pairs: Vec<(String, String)>, + memories_analyzed: usize, + duration_ms: u64, + timestamp: DateTime, + }, + + // -- Hook verdicts -- + HookVerdictRecorded { + hook: String, + verdict: String, + phase: String, + reason: String, + receipt_id: Option, + timestamp: DateTime, + }, + // -- Dream -- DreamStarted { memory_count: usize, @@ -123,6 +152,12 @@ pub enum VestigeEvent { // -- Importance -- ImportanceScored { + /// v2.0.9: memory the score refers to, if the score was computed for a + /// stored memory (None when scoring arbitrary content via importance tool). + /// Required so the Autopilot event-subscriber can auto-promote on + /// composite_score > 0.85 without having to re-query by content. + #[serde(default)] + memory_id: Option, content_preview: String, composite_score: f64, novelty: f64, diff --git a/crates/vestige-mcp/src/dashboard/handlers.rs b/crates/vestige-mcp/src/dashboard/handlers.rs index 28d4431..df3fc35 100644 --- a/crates/vestige-mcp/src/dashboard/handlers.rs +++ b/crates/vestige-mcp/src/dashboard/handlers.rs @@ -2,10 +2,15 @@ //! //! v2.0: Adds cognitive operation endpoints (dream, explore, predict, importance, consolidation) +use std::cmp::Reverse; +use std::fs::{self, OpenOptions}; +use std::io::Write; +use std::path::{Path as FsPath, PathBuf}; + use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::response::{Json, Redirect}; -use chrono::{Duration, Utc}; +use chrono::{DateTime, Duration, Utc}; use serde::Deserialize; use serde_json::Value; @@ -340,6 +345,328 @@ pub async fn unsuppress_memory( }))) } +#[derive(Debug, Deserialize)] +pub struct SanhedrinAppealRequest { + pub reason: String, + pub note: Option, + #[serde(rename = "receiptId")] + pub receipt_id: Option, + #[serde(rename = "claimId")] + pub claim_id: Option, +} + +/// Return the latest Sanhedrin receipt written by the Stop-hook bridge. +pub async fn get_sanhedrin_latest() -> Result, StatusCode> { + let state_dir = sanhedrin_state_dir(); + let latest_path = state_dir.join("latest.json"); + if !latest_path.exists() { + return Ok(Json(serde_json::json!({ + "receipt": null, + "stateDir": state_dir, + }))); + } + + let raw = fs::read_to_string(&latest_path).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let receipt: Value = + serde_json::from_str(&raw).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + Ok(Json(serde_json::json!({ + "receipt": receipt, + "stateDir": state_dir, + "receiptPath": latest_path, + "htmlPath": state_dir.join("latest.html"), + }))) +} + +/// Record feedback that a Sanhedrin veto was stale, wrong, or too strict. +/// +/// This intentionally does not promote, demote, suppress, edit, or delete any +/// memory. The hook reads this ledger and suppresses future same-fingerprint +/// vetoes, which keeps appeal training scoped to Sanhedrin behavior. +pub async fn appeal_sanhedrin( + State(state): State, + Json(req): Json, +) -> Result, StatusCode> { + let reason = req.reason.trim().to_ascii_lowercase(); + if !matches!(reason.as_str(), "stale" | "wrong" | "too_strict") { + return Err(StatusCode::BAD_REQUEST); + } + + let state_dir = sanhedrin_state_dir(); + let latest_path = state_dir.join("latest.json"); + let raw = match fs::read_to_string(&latest_path) { + Ok(raw) => raw, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(StatusCode::NOT_FOUND); + } + Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR), + }; + let mut receipt: Value = serde_json::from_str(&raw).map_err(|_| StatusCode::BAD_REQUEST)?; + let original_receipt = receipt.clone(); + let note = req.note.unwrap_or_default(); + let receipt_id = receipt + .get("id") + .and_then(Value::as_str) + .map(ToOwned::to_owned); + let receipt_id_ref = receipt_id.as_deref().ok_or(StatusCode::BAD_REQUEST)?; + let _ = sanitize_receipt_id(receipt_id_ref)?; + let expected_receipt_id = req.receipt_id.as_deref().ok_or(StatusCode::BAD_REQUEST)?; + if expected_receipt_id != receipt_id_ref { + return Err(StatusCode::CONFLICT); + } + if receipt + .get("verdictBar") + .and_then(Value::as_str) + .map(|v| v != "VETO") + .unwrap_or(true) + { + return Err(StatusCode::CONFLICT); + } + let claim = mark_sanhedrin_claim(&mut receipt, &reason, ¬e, req.claim_id.as_deref())?; + + let appeal = serde_json::json!({ + "timestamp": Utc::now().to_rfc3339(), + "receiptId": receipt_id.as_deref(), + "claimId": claim.get("id").and_then(Value::as_str), + "claimFingerprint": claim.get("fingerprint").and_then(Value::as_str), + "claim": claim.get("text").and_then(Value::as_str), + "reason": &reason, + "note": ¬e, + "status": "active", + }); + + set_json_field(&mut receipt, "overall", "appealed"); + set_json_field(&mut receipt, "verdictBar", "APPEALED"); + set_json_field(&mut receipt, "summary", &format!("Appealed as {}.", reason)); + save_sanhedrin_receipt(&state_dir, &receipt)?; + if let Err(err) = append_sanhedrin_appeal(&state_dir, &appeal) { + let _ = save_sanhedrin_receipt(&state_dir, &original_receipt); + return Err(err); + } + + state.emit(VestigeEvent::HookVerdictRecorded { + hook: "sanhedrin".to_string(), + verdict: "APPEALED".to_string(), + phase: "appeal".to_string(), + reason: reason.clone(), + receipt_id: receipt_id.clone(), + timestamp: Utc::now(), + }); + + Ok(Json(serde_json::json!({ + "appeal": appeal, + "receipt": receipt, + }))) +} + +fn sanhedrin_state_dir() -> PathBuf { + std::env::var_os("VESTIGE_SANHEDRIN_STATE_DIR") + .map(PathBuf::from) + .or_else(|| { + std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".vestige/sanhedrin")) + }) + .unwrap_or_else(|| PathBuf::from(".vestige/sanhedrin")) +} + +fn ensure_sanhedrin_dirs(state_dir: &FsPath) -> Result<(), StatusCode> { + fs::create_dir_all(state_dir.join("receipts")).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) +} + +fn mark_sanhedrin_claim( + receipt: &mut Value, + reason: &str, + note: &str, + claim_id: Option<&str>, +) -> Result { + let claim_id = claim_id.ok_or(StatusCode::BAD_REQUEST)?; + let claims = receipt + .get_mut("claims") + .and_then(Value::as_array_mut) + .ok_or(StatusCode::BAD_REQUEST)?; + + if claims.is_empty() { + return Err(StatusCode::BAD_REQUEST); + } + + let selected = claims + .iter() + .position(|claim| claim.get("id").and_then(Value::as_str) == Some(claim_id)) + .ok_or(StatusCode::NOT_FOUND)?; + + if claims + .get(selected) + .and_then(|claim| claim.get("decision")) + .and_then(Value::as_str) + != Some("veto") + { + return Err(StatusCode::CONFLICT); + } + + let claim = claims + .get_mut(selected) + .and_then(Value::as_object_mut) + .ok_or(StatusCode::BAD_REQUEST)?; + + claim.insert( + "decision".to_string(), + Value::String("appealed".to_string()), + ); + claim.insert( + "evidence_state".to_string(), + Value::String("appealed".to_string()), + ); + claim.insert( + "appeal".to_string(), + serde_json::json!({ + "status": "appealed", + "lastReason": reason, + "note": note, + "actions": ["stale", "wrong", "too_strict"], + }), + ); + + Ok(Value::Object(claim.clone())) +} + +fn set_json_field(receipt: &mut Value, key: &str, value: &str) { + if let Some(obj) = receipt.as_object_mut() { + obj.insert(key.to_string(), Value::String(value.to_string())); + } +} + +fn save_sanhedrin_receipt(state_dir: &FsPath, receipt: &Value) -> Result<(), StatusCode> { + ensure_sanhedrin_dirs(state_dir)?; + let rendered = render_sanhedrin_receipt_html(receipt); + let pretty = serde_json::to_string_pretty(receipt).map_err(|_| StatusCode::BAD_REQUEST)?; + let safe_id = receipt + .get("id") + .and_then(Value::as_str) + .map(sanitize_receipt_id) + .transpose()?; + + if let Some(safe_id) = safe_id { + write_atomic( + &state_dir.join("receipts").join(format!("{}.json", safe_id)), + pretty.as_bytes(), + )?; + write_atomic( + &state_dir.join("receipts").join(format!("{}.html", safe_id)), + rendered.as_bytes(), + )?; + } + + write_atomic(&state_dir.join("latest.json"), pretty.as_bytes())?; + write_atomic(&state_dir.join("latest.html"), rendered.as_bytes())?; + Ok(()) +} + +fn append_sanhedrin_appeal(state_dir: &FsPath, appeal: &Value) -> Result<(), StatusCode> { + ensure_sanhedrin_dirs(state_dir)?; + let mut appeals = OpenOptions::new() + .create(true) + .append(true) + .open(state_dir.join("appeals.jsonl")) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + writeln!(appeals, "{}", appeal).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) +} + +fn sanitize_receipt_id(id: &str) -> Result<&str, StatusCode> { + if !id.is_empty() + && id + .bytes() + .all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-')) + { + Ok(id) + } else { + Err(StatusCode::BAD_REQUEST) + } +} + +fn write_atomic(path: &FsPath, bytes: &[u8]) -> Result<(), StatusCode> { + let parent = path.parent().ok_or(StatusCode::INTERNAL_SERVER_ERROR)?; + fs::create_dir_all(parent).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let tmp = path.with_extension(format!( + "{}.tmp", + Utc::now().timestamp_nanos_opt().unwrap_or_default() + )); + fs::write(&tmp, bytes).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + fs::rename(&tmp, path).map_err(|_| { + let _ = fs::remove_file(&tmp); + StatusCode::INTERNAL_SERVER_ERROR + }) +} + +fn render_sanhedrin_receipt_html(receipt: &Value) -> String { + let verdict = escape_html( + receipt + .get("verdictBar") + .and_then(Value::as_str) + .unwrap_or("PASS"), + ); + let summary = escape_html(receipt.get("summary").and_then(Value::as_str).unwrap_or("")); + let mut claims_html = String::new(); + + if let Some(claims) = receipt.get("claims").and_then(Value::as_array) { + for claim in claims { + let text = escape_html(claim.get("text").and_then(Value::as_str).unwrap_or("")); + let decision = escape_html(claim.get("decision").and_then(Value::as_str).unwrap_or("")); + let evidence_state = escape_html( + claim + .get("evidence_state") + .and_then(Value::as_str) + .unwrap_or(""), + ); + let fix = escape_html( + claim + .get("fix") + .and_then(Value::as_str) + .filter(|s| !s.is_empty()) + .unwrap_or("No change required."), + ); + let mut precedents = String::new(); + if let Some(items) = claim.get("precedent").and_then(Value::as_array) { + for item in items { + let summary = item + .get("summary") + .and_then(Value::as_str) + .unwrap_or("Precedent recorded."); + precedents.push_str(&format!("
        • {}
        • ", escape_html(summary))); + } + } + claims_html.push_str(&format!( + "
          {} / {}

          {}

          Fix: {}

          Appeal: stale | wrong | too_strict

            {}
          ", + decision, evidence_state, text, fix, precedents + )); + } + } + + format!( + r#" +Vestige Veto Receipt + +
          Verdict{}
          +

          Veto Receipt

          {}

          {} +"#, + verdict, summary, claims_html + ) +} + +fn escape_html(value: &str) -> String { + value + .replace('&', "&") + .replace('<', "<") + .replace('>', ">") + .replace('"', """) + .replace('\'', "'") +} + /// Get system stats pub async fn get_stats(State(state): State) -> Result, StatusCode> { let stats = state @@ -373,6 +700,13 @@ pub struct TimelineParams { pub limit: Option, } +#[derive(Debug, Deserialize)] +pub struct ChangelogParams { + pub start: Option, + pub end: Option, + pub limit: Option, +} + /// Get timeline data pub async fn get_timeline( State(state): State, @@ -428,6 +762,108 @@ pub async fn get_timeline( }))) } +/// Recent cognitive events in the same envelope used by the WebSocket event +/// stream. The pulse hook polls this endpoint once per Claude wake, so keep it +/// cheap, bounded, and tolerant of empty history. +pub async fn get_changelog( + State(state): State, + Query(params): Query, +) -> Result, StatusCode> { + let limit = params.limit.unwrap_or(50).clamp(1, 100); + let start = parse_changelog_bound(params.start.as_deref())?; + let end = parse_changelog_bound(params.end.as_deref())?; + let fetch_limit = if start.is_some() || end.is_some() { + limit.saturating_mul(4) + } else { + limit + }; + + let mut events: Vec<(DateTime, Value)> = Vec::new(); + + let dreams = state + .storage + .get_dream_history(fetch_limit) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + for dream in dreams { + if changelog_window_contains(dream.dreamed_at, start.as_ref(), end.as_ref()) { + events.push((dream.dreamed_at, dream_changelog_event(&dream))); + } + } + + // Connections are currently persisted as graph edges rather than as audit + // rows, so filter by created_at from the connection table. + let connections = state + .storage + .get_all_connections() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + for conn in connections { + if changelog_window_contains(conn.created_at, start.as_ref(), end.as_ref()) { + events.push((conn.created_at, connection_changelog_event(&conn))); + } + } + + events.sort_by_key(|event| Reverse(event.0)); + events.truncate(limit as usize); + let formatted_events: Vec = events.into_iter().map(|(_, event)| event).collect(); + let total_events = formatted_events.len(); + + Ok(Json(serde_json::json!({ + "events": formatted_events, + "totalEvents": total_events, + "filter": { + "start": start.as_ref().map(DateTime::to_rfc3339), + "end": end.as_ref().map(DateTime::to_rfc3339), + "limit": limit, + }, + }))) +} + +fn parse_changelog_bound(raw: Option<&str>) -> Result>, StatusCode> { + match raw { + Some(value) if !value.trim().is_empty() => DateTime::parse_from_rfc3339(value) + .map(|dt| Some(dt.with_timezone(&Utc))) + .map_err(|_| StatusCode::BAD_REQUEST), + _ => Ok(None), + } +} + +fn changelog_window_contains( + ts: DateTime, + start: Option<&DateTime>, + end: Option<&DateTime>, +) -> bool { + start.is_none_or(|s| ts >= *s) && end.is_none_or(|e| ts <= *e) +} + +fn dream_changelog_event(dream: &vestige_core::DreamHistoryRecord) -> Value { + serde_json::json!({ + "type": "DreamCompleted", + "timestamp": dream.dreamed_at.to_rfc3339(), + "data": { + "memories_replayed": dream.memories_replayed, + "connections_found": dream.connections_found, + "connections_persisted": dream.connections_found, + "insights_generated": dream.insights_generated, + "duration_ms": dream.duration_ms, + "timestamp": dream.dreamed_at.to_rfc3339(), + }, + }) +} + +fn connection_changelog_event(conn: &vestige_core::ConnectionRecord) -> Value { + serde_json::json!({ + "type": "ConnectionDiscovered", + "timestamp": conn.created_at.to_rfc3339(), + "data": { + "source_id": &conn.source_id, + "target_id": &conn.target_id, + "connection_type": &conn.link_type, + "weight": conn.strength, + "timestamp": conn.created_at.to_rfc3339(), + }, + }) +} + /// Health check pub async fn health_check(State(state): State) -> Result, StatusCode> { let stats = state @@ -468,6 +904,30 @@ pub struct GraphParams { pub center_id: Option, pub depth: Option, pub max_nodes: Option, + /// How to choose the default center when neither `query` nor `center_id` + /// is provided. "recent" (default) uses the newest memory — matches + /// what users actually expect ("show me my recent stuff"). "connected" + /// uses the most-connected memory for a richer initial subgraph; used + /// to be the default but ended up clustering on historical hotspots + /// and hiding fresh memories that hadn't accumulated edges yet. + /// Unknown values fall back to "recent". + pub sort: Option, +} + +/// Which memory to center the default subgraph on. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GraphSort { + Recent, + Connected, +} + +impl GraphSort { + fn parse(raw: Option<&str>) -> Self { + match raw.map(str::to_ascii_lowercase).as_deref() { + Some("connected") => Self::Connected, + _ => Self::Recent, + } + } } /// Get memory graph data (nodes + edges with layout positions) @@ -477,8 +937,10 @@ pub async fn get_graph( ) -> Result, StatusCode> { let depth = params.depth.unwrap_or(2).clamp(1, 3); let max_nodes = params.max_nodes.unwrap_or(50).clamp(1, 200); + let sort = GraphSort::parse(params.sort.as_deref()); // Determine center node + let explicit_center = params.center_id.is_some() || params.query.is_some(); let center_id = if let Some(ref id) = params.center_id { id.clone() } else if let Some(ref query) = params.query { @@ -491,32 +953,36 @@ pub async fn get_graph( .map(|n| n.id.clone()) .ok_or(StatusCode::NOT_FOUND)? } else { - // Default: most connected memory (for a rich initial graph) - let most_connected = state - .storage - .get_most_connected_memory() - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - if let Some(id) = most_connected { - id - } else { - // Fallback: most recent memory - let recent = state - .storage - .get_all_nodes(1, 0) - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - recent - .first() - .map(|n| n.id.clone()) - .ok_or(StatusCode::NOT_FOUND)? - } + default_center_id(&state.storage, sort)? }; // Get subgraph - let (nodes, edges) = state + let (mut nodes, mut edges) = state .storage .get_memory_subgraph(¢er_id, depth, max_nodes) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + // Default-load fallback: if the newest memory is isolated (1 node, 0 edges), + // silently re-resolve via Connected so the user sees the densest cluster + // instead of a lonely orb. Explicit query/center_id requests are honored + // as-is — the user asked for that specific subgraph. + let mut center_id = center_id; + if !explicit_center + && sort == GraphSort::Recent + && nodes.len() <= 1 + && edges.is_empty() + && let Ok(fallback) = default_center_id(&state.storage, GraphSort::Connected) + && fallback != center_id + && let Ok((n2, e2)) = state + .storage + .get_memory_subgraph(&fallback, depth, max_nodes) + && n2.len() > nodes.len() + { + center_id = fallback; + nodes = n2; + edges = e2; + } + if nodes.is_empty() { return Err(StatusCode::NOT_FOUND); } @@ -565,6 +1031,46 @@ pub async fn get_graph( }))) } +/// Pick the default subgraph center when neither `query` nor `center_id` +/// was provided. Factored out so both the route handler and unit tests can +/// exercise the same branching (recent vs connected + empty-db fallback) +/// without spinning up a full axum server. +fn default_center_id( + storage: &std::sync::Arc, + sort: GraphSort, +) -> Result { + match sort { + GraphSort::Recent => { + let recent = storage + .get_all_nodes(1, 0) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + recent + .first() + .map(|n| n.id.clone()) + .ok_or(StatusCode::NOT_FOUND) + } + GraphSort::Connected => { + let most_connected = storage + .get_most_connected_memory() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + if let Some(id) = most_connected { + Ok(id) + } else { + // Nothing connected yet (fresh DB, or every node is isolated) — + // fall through to the newest memory so the user still sees + // SOMETHING rather than a 404. + let recent = storage + .get_all_nodes(1, 0) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + recent + .first() + .map(|n| n.id.clone()) + .ok_or(StatusCode::NOT_FOUND) + } + } + } +} + // ============================================================================ // SEARCH (dedicated endpoint) // ============================================================================ @@ -677,23 +1183,14 @@ pub async fn trigger_dream(State(state): State) -> Result, // Run dream through CognitiveEngine let cog = cognitive.lock().await; - // Capture start time before the dream — composite-score eviction in store_connections - // reorders the buffer, making positional slicing (pre_dream_count..) unreliable. - let dream_start = Utc::now(); - let dream_result = cog.dreamer.dream(&dream_memories).await; + let (dream_result, new_connections) = cog.dreamer.dream_with_connections(&dream_memories).await; let insights = cog.dreamer.synthesize_insights(&dream_memories); - let all_connections = cog.dreamer.get_connections(); drop(cog); // Persist new connections - // Filter by timestamp — same approach as dream.rs to avoid positional index issues. - let new_connections: Vec<&vestige_core::DiscoveredConnection> = all_connections - .iter() - .filter(|c| c.discovered_at >= dream_start) - .collect(); let mut connections_persisted = 0u64; let now = Utc::now(); - for conn in new_connections.iter() { + for conn in &new_connections { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -725,14 +1222,38 @@ pub async fn trigger_dream(State(state): State) -> Result, } let duration_ms = start.elapsed().as_millis() as u64; + let completed_at = Utc::now(); + let insights_generated = insights.len(); + + if let Err(e) = state + .storage + .save_dream_history(&vestige_core::DreamHistoryRecord { + dreamed_at: completed_at, + duration_ms: duration_ms as i64, + memories_replayed: dream_memories.len() as i32, + connections_found: connections_persisted as i32, + insights_generated: insights_generated as i32, + memories_strengthened: dream_result.memories_strengthened as i32, + memories_compressed: dream_result.memories_compressed as i32, + phase_nrem1_ms: None, + phase_nrem3_ms: None, + phase_rem_ms: None, + phase_integration_ms: None, + summaries_generated: None, + emotional_memories_processed: None, + creative_connections_found: None, + }) + { + tracing::warn!("Failed to persist dashboard dream history: {}", e); + } // Emit completion event state.emit(VestigeEvent::DreamCompleted { memories_replayed: dream_memories.len(), connections_found: connections_persisted as usize, - insights_generated: insights.len(), + insights_generated, duration_ms, - timestamp: Utc::now(), + timestamp: completed_at, }); Ok(Json(serde_json::json!({ @@ -904,6 +1425,7 @@ pub async fn score_importance( let attention = score.attention; state.emit(VestigeEvent::ImportanceScored { + memory_id: None, // /api/importance scores arbitrary content, not a stored memory content_preview: req.content.chars().take(80).collect(), composite_score: composite, novelty, @@ -1088,3 +1610,305 @@ pub async fn list_intentions( "filter": status_filter, }))) } + +// ============================================================================ +// DEEP REFERENCE (Reasoning Theater, v2.0.8) +// ============================================================================ + +#[derive(Debug, Deserialize)] +pub struct DeepReferenceBody { + pub query: String, + pub depth: Option, +} + +/// Run the 8-stage deep_reference cognitive pipeline over HTTP. +/// +/// Wraps the existing `crate::tools::cross_reference::execute` tool so the +/// dashboard can surface the same reasoning chain the MCP clients see. Emits +/// a `DeepReferenceCompleted` event with primary + supporting + contradicting +/// memory IDs so Graph3D can camera-glide, pulse evidence nodes, and draw +/// contradiction arcs in the 3D scene. +pub async fn deep_reference_query( + State(state): State, + Json(body): Json, +) -> Result, StatusCode> { + let cognitive = state + .cognitive + .as_ref() + .ok_or(StatusCode::SERVICE_UNAVAILABLE)?; + + if body.query.trim().is_empty() { + return Err(StatusCode::BAD_REQUEST); + } + + let args = serde_json::json!({ + "query": body.query.clone(), + "depth": body.depth.unwrap_or(20).clamp(5, 50), + }); + + let start = std::time::Instant::now(); + let response = crate::tools::cross_reference::execute(&state.storage, cognitive, Some(args)) + .await + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let duration_ms = start.elapsed().as_millis() as u64; + + // Pull evidence IDs out for the WebSocket event so Graph3D can glide, + // pulse, and arc. We intentionally read from the serialized JSON rather + // than re-running the pipeline — whatever the tool decided is what the + // Theater visualizes. + let primary_id = response + .get("recommended") + .and_then(|r| r.get("memory_id")) + .and_then(|v| v.as_str()) + .map(String::from); + + let supporting_ids: Vec = response + .get("evidence") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|e| { + let role = e.get("role").and_then(|r| r.as_str()).unwrap_or(""); + if role == "supporting" || role == "primary" { + e.get("id").and_then(|v| v.as_str()).map(String::from) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default(); + + let contradicting_ids: Vec = response + .get("evidence") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|e| { + let role = e.get("role").and_then(|r| r.as_str()).unwrap_or(""); + if role == "contradicting" { + e.get("id").and_then(|v| v.as_str()).map(String::from) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default(); + + let contradiction_pairs: Vec<(String, String)> = response + .get("contradictions") + .and_then(|v| v.as_array()) + .map(|arr| { + arr.iter() + .filter_map(|c| { + let a = c.get("a_id").and_then(|v| v.as_str())?.to_string(); + let b = c.get("b_id").and_then(|v| v.as_str())?.to_string(); + Some((a, b)) + }) + .collect() + }) + .unwrap_or_default(); + + let memories_analyzed = response + .get("memoriesAnalyzed") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + + let confidence = response + .get("confidence") + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + + let intent = response + .get("intent") + .and_then(|v| v.as_str()) + .unwrap_or("Synthesis") + .to_string(); + + let status = response + .get("status") + .and_then(|v| v.as_str()) + .unwrap_or("unknown") + .to_string(); + + state.emit(VestigeEvent::DeepReferenceCompleted { + query: body.query, + intent, + status, + confidence, + primary_id, + supporting_ids, + contradicting_ids, + contradiction_pairs, + memories_analyzed, + duration_ms, + timestamp: Utc::now(), + }); + + Ok(Json(response)) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::Utc; + use std::sync::Arc; + use tempfile::tempdir; + use vestige_core::memory::IngestInput; + use vestige_core::{ConnectionRecord, DreamHistoryRecord, Storage}; + + #[test] + fn graph_sort_parse_defaults_to_recent() { + assert_eq!(GraphSort::parse(None), GraphSort::Recent); + assert_eq!(GraphSort::parse(Some("")), GraphSort::Recent); + assert_eq!(GraphSort::parse(Some("recent")), GraphSort::Recent); + assert_eq!(GraphSort::parse(Some("RECENT")), GraphSort::Recent); + assert_eq!(GraphSort::parse(Some("Recent")), GraphSort::Recent); + assert_eq!(GraphSort::parse(Some("garbage")), GraphSort::Recent); + } + + #[test] + fn graph_sort_parse_accepts_connected_case_insensitive() { + assert_eq!(GraphSort::parse(Some("connected")), GraphSort::Connected); + assert_eq!(GraphSort::parse(Some("CONNECTED")), GraphSort::Connected); + assert_eq!(GraphSort::parse(Some("Connected")), GraphSort::Connected); + } + + #[test] + fn changelog_dream_event_uses_pulse_compatible_shape() { + let now = Utc::now(); + let event = dream_changelog_event(&DreamHistoryRecord { + dreamed_at: now, + duration_ms: 1234, + memories_replayed: 12, + connections_found: 3, + insights_generated: 2, + memories_strengthened: 0, + memories_compressed: 0, + phase_nrem1_ms: None, + phase_nrem3_ms: None, + phase_rem_ms: None, + phase_integration_ms: None, + summaries_generated: None, + emotional_memories_processed: None, + creative_connections_found: None, + }); + + assert_eq!(event["type"], "DreamCompleted"); + assert_eq!(event["data"]["insights_generated"], 2); + assert_eq!(event["data"]["connections_persisted"], 3); + assert_eq!(event["data"]["timestamp"], now.to_rfc3339()); + } + + #[test] + fn changelog_connection_event_uses_pulse_compatible_shape() { + let now = Utc::now(); + let event = connection_changelog_event(&ConnectionRecord { + source_id: "source-memory".to_string(), + target_id: "target-memory".to_string(), + strength: 0.82, + link_type: "semantic".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }); + + assert_eq!(event["type"], "ConnectionDiscovered"); + assert_eq!(event["data"]["source_id"], "source-memory"); + assert_eq!(event["data"]["target_id"], "target-memory"); + assert_eq!(event["data"]["connection_type"], "semantic"); + } + + fn seed_storage() -> (tempfile::TempDir, Arc) { + let dir = tempdir().unwrap(); + let db_path = dir.path().join("test.db"); + let storage = Arc::new(Storage::new(Some(db_path)).unwrap()); + (dir, storage) + } + + fn ingest(storage: &Storage, content: &str) -> String { + let node = storage + .ingest(IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + node.id + } + + #[test] + fn default_center_id_recent_returns_newest_node() { + let (_dir, storage) = seed_storage(); + ingest(&storage, "first"); + ingest(&storage, "second"); + let newest = ingest(&storage, "third"); + + let center = default_center_id(&storage, GraphSort::Recent).unwrap(); + assert_eq!( + center, newest, + "Recent mode should pick the newest ingested memory" + ); + } + + fn link(storage: &Storage, source: &str, target: &str) { + let now = Utc::now(); + storage + .save_connection(&ConnectionRecord { + source_id: source.to_string(), + target_id: target.to_string(), + strength: 0.9, + link_type: "semantic".to_string(), + created_at: now, + last_activated: now, + activation_count: 0, + }) + .unwrap(); + } + + #[test] + fn default_center_id_connected_prefers_hub_over_newest() { + let (_dir, storage) = seed_storage(); + let hub = ingest(&storage, "hub node"); + let spoke_a = ingest(&storage, "spoke A"); + let spoke_b = ingest(&storage, "spoke B"); + let spoke_c = ingest(&storage, "spoke C"); + // Wire the spokes into `hub` so it has the most connections. Leave + // the final `lonely` node unconnected — it's the newest by + // insertion order and would win in Recent mode. + for spoke in [&spoke_a, &spoke_b, &spoke_c] { + link(&storage, &hub, spoke); + } + let _lonely = ingest(&storage, "lonely newcomer"); + + let center = default_center_id(&storage, GraphSort::Connected).unwrap(); + assert_eq!( + center, hub, + "Connected mode should pick the densest node, not the newest" + ); + } + + #[test] + fn default_center_id_connected_falls_back_to_recent_when_no_edges() { + let (_dir, storage) = seed_storage(); + ingest(&storage, "alpha"); + let newest = ingest(&storage, "beta"); + + // No connections exist — Connected mode should degrade to Recent + // rather than returning 404. + let center = default_center_id(&storage, GraphSort::Connected).unwrap(); + assert_eq!(center, newest); + } + + #[test] + fn default_center_id_returns_not_found_on_empty_db() { + let (_dir, storage) = seed_storage(); + + let recent_err = default_center_id(&storage, GraphSort::Recent).unwrap_err(); + assert_eq!(recent_err, StatusCode::NOT_FOUND); + + let connected_err = default_center_id(&storage, GraphSort::Connected).unwrap_err(); + assert_eq!(connected_err, StatusCode::NOT_FOUND); + } +} diff --git a/crates/vestige-mcp/src/dashboard/mod.rs b/crates/vestige-mcp/src/dashboard/mod.rs index e45b673..2a3b5c9 100644 --- a/crates/vestige-mcp/src/dashboard/mod.rs +++ b/crates/vestige-mcp/src/dashboard/mod.rs @@ -142,7 +142,10 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { // since v2.0.5 despite having full graph event handlers; this closes // the gap so dashboard users can trigger inhibition without dropping // to the MCP layer. - .route("/api/memories/{id}/suppress", post(handlers::suppress_memory)) + .route( + "/api/memories/{id}/suppress", + post(handlers::suppress_memory), + ) .route( "/api/memories/{id}/unsuppress", post(handlers::unsuppress_memory), @@ -154,6 +157,7 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { .route("/api/health", get(handlers::health_check)) // Timeline .route("/api/timeline", get(handlers::get_timeline)) + .route("/api/changelog", get(handlers::get_changelog)) // Graph .route("/api/graph", get(handlers::get_graph)) // Cognitive operations (v2.0) @@ -168,6 +172,13 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { ) // Intentions (v2.0) .route("/api/intentions", get(handlers::list_intentions)) + // Reasoning Theater (v2.0.8) — 8-stage cognitive pipeline surface. + // Wraps crate::tools::cross_reference::execute. Emits + // DeepReferenceCompleted so Graph3D can glide, pulse, and arc. + .route("/api/deep_reference", post(handlers::deep_reference_query)) + // Sanhedrin receipts (v2.1.22): latest local hook verdict + appeal training. + .route("/api/sanhedrin/latest", get(handlers::get_sanhedrin_latest)) + .route("/api/sanhedrin/appeal", post(handlers::appeal_sanhedrin)) .layer( ServiceBuilder::new() .concurrency_limit(50) diff --git a/crates/vestige-mcp/src/lib.rs b/crates/vestige-mcp/src/lib.rs index b5a2c3e..8784409 100644 --- a/crates/vestige-mcp/src/lib.rs +++ b/crates/vestige-mcp/src/lib.rs @@ -2,6 +2,7 @@ //! //! Shared modules accessible to all binaries in the crate. +pub mod autopilot; pub mod cognitive; pub mod dashboard; pub mod protocol; diff --git a/crates/vestige-mcp/src/main.rs b/crates/vestige-mcp/src/main.rs index 32681ed..523f3ea 100644 --- a/crates/vestige-mcp/src/main.rs +++ b/crates/vestige-mcp/src/main.rs @@ -1,4 +1,4 @@ -//! Vestige MCP Server v1.0 - Cognitive Memory for Claude +//! Vestige MCP Server - local cognitive memory for MCP agents. //! //! A bleeding-edge Rust MCP (Model Context Protocol) server that provides //! Claude and other AI assistants with long-term memory capabilities @@ -31,8 +31,11 @@ use vestige_mcp::cognitive; use vestige_mcp::protocol; use vestige_mcp::server; +use directories::BaseDirs; +use std::ffi::OsString; +use std::fs; use std::io; -use std::path::PathBuf; +use std::path::{Component, PathBuf}; use std::sync::Arc; use tokio::sync::Mutex; use tracing::{Level, error, info, warn}; @@ -44,29 +47,50 @@ use vestige_core::Storage; use protocol::stdio::StdioTransport; use server::McpServer; +const DATA_DIR_ENV: &str = "VESTIGE_DATA_DIR"; +const DATABASE_FILE: &str = "vestige.db"; + /// Parsed CLI configuration. struct Config { data_dir: Option, http_port: u16, + http_enabled: bool, dashboard_enabled: bool, } +fn data_dir_from_env() -> Option { + std::env::var_os(DATA_DIR_ENV).and_then(|value| { + if value.as_os_str().is_empty() { + None + } else { + Some(PathBuf::from(value)) + } + }) +} + /// Parse command-line arguments into a `Config`. /// Exits the process if `--help` or `--version` is requested. fn parse_args() -> Config { - let args: Vec = std::env::args().collect(); - let mut data_dir: Option = None; + parse_args_from(std::env::args_os().collect(), data_dir_from_env()) +} + +fn parse_args_from(args: Vec, env_data_dir: Option) -> Config { + let mut data_dir = env_data_dir; let mut http_port: u16 = std::env::var("VESTIGE_HTTP_PORT") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(3928); + let mut http_enabled = std::env::var("VESTIGE_HTTP_ENABLED") + .map(|v| v.eq_ignore_ascii_case("true") || v == "1") + .unwrap_or(false); let dashboard_enabled = std::env::var("VESTIGE_DASHBOARD_ENABLED") .map(|v| v.eq_ignore_ascii_case("true") || v == "1") .unwrap_or(false); let mut i = 1; while i < args.len() { - match args[i].as_str() { + let arg = args[i].to_string_lossy(); + match arg.as_ref() { "--help" | "-h" => { println!("Vestige MCP Server v{}", env!("CARGO_PKG_VERSION")); println!(); @@ -78,18 +102,29 @@ fn parse_args() -> Config { println!("OPTIONS:"); println!(" -h, --help Print help information"); println!(" -V, --version Print version information"); - println!(" --data-dir Custom data directory"); - println!(" --http-port HTTP transport port (default: 3928)"); + println!( + " --data-dir Custom data directory (overrides VESTIGE_DATA_DIR)" + ); + println!(" --http Enable Streamable HTTP transport"); + println!(" --no-http Disable Streamable HTTP transport"); + println!(" --http-port HTTP transport port (also enables HTTP)"); println!(); println!("ENVIRONMENT:"); + println!( + " VESTIGE_DATA_DIR Data directory fallback (stores vestige.db inside)" + ); println!( " RUST_LOG Log level filter (e.g., debug, info, warn, error)" ); println!( - " VESTIGE_AUTH_TOKEN Override the bearer token for HTTP transport" + " VESTIGE_AUTH_TOKEN Override the bearer token for HTTP transport" ); - println!(" VESTIGE_HTTP_PORT HTTP transport port (default: 3928)"); - println!(" VESTIGE_DASHBOARD_ENABLED Enable dashboard (default: disabled)"); + println!(" VESTIGE_HTTP_ENABLED Enable HTTP transport (default: false)"); + println!(" VESTIGE_HTTP_PORT HTTP transport port (default: 3928)"); + println!( + " VESTIGE_HTTP_ALLOWED_ORIGINS Comma-separated browser origins allowed for HTTP" + ); + println!(" VESTIGE_DASHBOARD_ENABLED Enable dashboard (default: disabled)"); println!(" VESTIGE_DASHBOARD_PORT Dashboard port (default: 3927)"); println!( " VESTIGE_SYSTEM_PROMPT_MODE Inject the full composition mandate into every MCP session (minimal|full, default: minimal)" @@ -98,7 +133,8 @@ fn parse_args() -> Config { println!("EXAMPLES:"); println!(" vestige-mcp"); println!(" vestige-mcp --data-dir /custom/path"); - println!(" vestige-mcp --http-port 8080"); + println!(" VESTIGE_DATA_DIR=~/.vestige vestige-mcp"); + println!(" vestige-mcp --http --http-port 8080"); println!(" RUST_LOG=debug vestige-mcp"); std::process::exit(0); } @@ -113,6 +149,11 @@ fn parse_args() -> Config { eprintln!("Usage: vestige-mcp --data-dir "); std::process::exit(1); } + if args[i].as_os_str().is_empty() { + eprintln!("error: --data-dir requires a non-empty path argument"); + eprintln!("Usage: vestige-mcp --data-dir "); + std::process::exit(1); + } data_dir = Some(PathBuf::from(&args[i])); } arg if arg.starts_with("--data-dir=") => { @@ -125,22 +166,31 @@ fn parse_args() -> Config { } data_dir = Some(PathBuf::from(path)); } + "--http" => { + http_enabled = true; + } + "--no-http" => { + http_enabled = false; + } "--http-port" => { + http_enabled = true; i += 1; if i >= args.len() { eprintln!("error: --http-port requires a port number"); eprintln!("Usage: vestige-mcp --http-port "); std::process::exit(1); } - http_port = match args[i].parse() { + let port = args[i].to_string_lossy(); + http_port = match port.parse() { Ok(p) => p, Err(_) => { - eprintln!("error: invalid port number '{}'", args[i]); + eprintln!("error: invalid port number '{}'", port); std::process::exit(1); } }; } arg if arg.starts_with("--http-port=") => { + http_enabled = true; let val = arg.strip_prefix("--http-port=").unwrap_or(""); http_port = match val.parse() { Ok(p) => p, @@ -163,10 +213,47 @@ fn parse_args() -> Config { Config { data_dir, http_port, + http_enabled, dashboard_enabled, } } +fn expand_tilde(path: PathBuf) -> PathBuf { + let rest = { + let mut components = path.components(); + match components.next() { + Some(Component::Normal(first)) if first == "~" => { + Some(components.as_path().to_path_buf()) + } + _ => None, + } + }; + + match rest { + Some(rest) => BaseDirs::new() + .map(|dirs| dirs.home_dir().join(rest)) + .unwrap_or(path), + None => path, + } +} + +fn prepare_storage_path(data_dir: Option) -> io::Result> { + let Some(data_dir) = data_dir else { + return Ok(None); + }; + + let data_dir = expand_tilde(data_dir); + fs::create_dir_all(&data_dir)?; + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = fs::set_permissions(&data_dir, fs::Permissions::from_mode(0o700)); + } + + Ok(Some(data_dir.join(DATABASE_FILE))) +} + #[tokio::main] async fn main() { // Parse CLI arguments first (before logging init, so --help/--version work cleanly) @@ -185,8 +272,17 @@ async fn main() { env!("CARGO_PKG_VERSION") ); - // Initialize storage with optional custom data directory - let storage = match Storage::new(config.data_dir) { + let storage_path = match prepare_storage_path(config.data_dir) { + Ok(path) => path, + Err(e) => { + error!("Failed to prepare storage data directory: {}", e); + std::process::exit(1); + } + }; + + // Initialize storage with optional custom data directory. + // Storage::new(Some(...)) expects a DB file path, so map data dirs to vestige.db here. + let storage = match Storage::new(storage_path) { Ok(s) => { info!("Storage initialized successfully"); @@ -309,6 +405,20 @@ async fn main() { let (event_tx, _) = tokio::sync::broadcast::channel::(1024); + // v2.0.9 "Autopilot" — spawn the backend event-subscriber that routes + // every live WebSocket event into the cognitive modules that already + // have trigger methods implemented. Without this, the 20 event types + // terminate at the dashboard and the cognitive engine is a passive + // library that only responds to MCP tool queries. + // + // See `crates/vestige-mcp/src/autopilot.rs` for the routing table and + // `docs/VESTIGE_STATE_AND_PLAN.md` §15 for the architectural rationale. + vestige_mcp::autopilot::spawn( + Arc::clone(&cognitive), + Arc::clone(&storage), + event_tx.clone(), + ); + // Spawn dashboard HTTP server alongside MCP server (now with CognitiveEngine access) if config.dashboard_enabled { let dashboard_port = std::env::var("VESTIGE_DASHBOARD_PORT") @@ -339,8 +449,8 @@ async fn main() { info!("Dashboard disabled by VESTIGE_DASHBOARD_ENABLED=false"); } - // Start HTTP MCP transport (Streamable HTTP for Claude.ai / remote clients) - { + // Start optional HTTP MCP transport for clients that need Streamable HTTP. + if config.http_enabled { let http_storage = Arc::clone(&storage); let http_cognitive = Arc::clone(&cognitive); let http_event_tx = event_tx.clone(); @@ -351,7 +461,9 @@ async fn main() { let bind = std::env::var("VESTIGE_HTTP_BIND").unwrap_or_else(|_| "127.0.0.1".to_string()); eprintln!("Vestige HTTP transport: http://{}:{}/mcp", bind, http_port); - eprintln!("Auth token: {}...", &token[..token.len().min(8)]); + if let Ok(path) = protocol::auth::token_path() { + eprintln!("Auth token file: {}", path.display()); + } tokio::spawn(async move { if let Err(e) = protocol::http::start_http_transport( http_storage, @@ -373,10 +485,12 @@ async fn main() { ); } } + } else { + info!("HTTP MCP transport disabled; set VESTIGE_HTTP_ENABLED=1 or pass --http to enable"); } // Load cross-encoder reranker in the background (downloads ~150MB on first run) - #[cfg(feature = "embeddings")] + #[cfg(feature = "vector-search")] { let cog_clone = Arc::clone(&cognitive); tokio::spawn(async move { @@ -403,3 +517,79 @@ async fn main() { info!("Vestige MCP Server shutting down"); } + +#[cfg(test)] +mod tests { + use super::*; + + fn os_args(args: &[&str]) -> Vec { + args.iter().map(OsString::from).collect() + } + + #[test] + fn vestige_data_dir_env_is_used_when_cli_data_dir_is_absent() { + let config = parse_args_from( + os_args(&["vestige-mcp"]), + Some(PathBuf::from("/tmp/vestige-env")), + ); + + assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-env"))); + assert!(!config.http_enabled); + } + + #[test] + fn cli_data_dir_takes_precedence_over_env_data_dir() { + let config = parse_args_from( + os_args(&["vestige-mcp", "--data-dir", "/tmp/vestige-cli"]), + Some(PathBuf::from("/tmp/vestige-env")), + ); + + assert_eq!(config.data_dir, Some(PathBuf::from("/tmp/vestige-cli"))); + } + + #[test] + fn http_is_opt_in_and_port_flag_enables_it() { + let disabled = parse_args_from(os_args(&["vestige-mcp"]), None); + assert!(!disabled.http_enabled); + + let enabled = parse_args_from(os_args(&["vestige-mcp", "--http-port", "8080"]), None); + assert!(enabled.http_enabled); + assert_eq!(enabled.http_port, 8080); + } + + #[test] + fn prepare_storage_path_creates_dir_and_points_to_vestige_db() { + let temp = tempfile::tempdir().unwrap(); + let data_dir = temp.path().join("nested").join("vestige"); + + let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap(); + + assert!(data_dir.is_dir()); + assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE))); + } + + #[test] + fn prepare_storage_path_reuses_existing_data_dir() { + let temp = tempfile::tempdir().unwrap(); + let data_dir = temp.path().join("existing"); + fs::create_dir_all(&data_dir).unwrap(); + + let db_path = prepare_storage_path(Some(data_dir.clone())).unwrap(); + + assert_eq!(db_path, Some(data_dir.join(DATABASE_FILE))); + } + + #[test] + fn expand_tilde_expands_current_users_home_only() { + let home = BaseDirs::new().unwrap().home_dir().to_path_buf(); + + assert_eq!( + expand_tilde(PathBuf::from("~/vestige")), + home.join("vestige") + ); + assert_eq!( + expand_tilde(PathBuf::from("~other/vestige")), + PathBuf::from("~other/vestige") + ); + } +} diff --git a/crates/vestige-mcp/src/protocol/auth.rs b/crates/vestige-mcp/src/protocol/auth.rs index dce821b..955336b 100644 --- a/crates/vestige-mcp/src/protocol/auth.rs +++ b/crates/vestige-mcp/src/protocol/auth.rs @@ -19,7 +19,7 @@ use tracing::{info, warn}; const MIN_TOKEN_LENGTH: usize = 32; /// Return the auth token file path inside the Vestige data directory. -fn token_path() -> Result> { +pub fn token_path() -> Result> { let dirs = ProjectDirs::from("com", "vestige", "core") .ok_or("could not determine project directories")?; Ok(dirs.data_dir().join("auth_token")) diff --git a/crates/vestige-mcp/src/protocol/http.rs b/crates/vestige-mcp/src/protocol/http.rs index e90c313..8bb788c 100644 --- a/crates/vestige-mcp/src/protocol/http.rs +++ b/crates/vestige-mcp/src/protocol/http.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use axum::extract::{DefaultBodyLimit, State}; -use axum::http::{HeaderMap, StatusCode}; +use axum::http::{HeaderMap, HeaderValue, StatusCode, header}; use axum::response::IntoResponse; use axum::routing::{delete, post}; use axum::{Json, Router}; @@ -48,6 +48,7 @@ const MAX_BODY_SIZE: usize = 256 * 1024; struct Session { server: McpServer, last_active: Instant, + protocol_version: String, } /// Shared state cloned into every axum handler. @@ -58,6 +59,7 @@ pub struct HttpTransportState { cognitive: Arc>, event_tx: broadcast::Sender, auth_token: String, + allowed_origins: Arc>, } /// Start the HTTP MCP transport on `127.0.0.1:`. @@ -76,6 +78,7 @@ pub async fn start_http_transport( cognitive, event_tx, auth_token, + allowed_origins: Arc::new(allowed_origins(port)), }; // Spawn session reaper @@ -105,6 +108,12 @@ pub async fn start_http_transport( }); } + let cors_origins = state + .allowed_origins + .iter() + .filter_map(|origin| origin.parse().ok()) + .collect::>(); + let app = Router::new() .route("/mcp", post(post_mcp)) .route("/mcp", delete(delete_mcp)) @@ -114,15 +123,7 @@ pub async fn start_http_transport( .layer(ConcurrencyLimitLayer::new(CONCURRENCY_LIMIT)) .layer( CorsLayer::new() - .allow_origin( - [ - format!("http://127.0.0.1:{}", port), - format!("http://localhost:{}", port), - ] - .into_iter() - .filter_map(|s| s.parse().ok()) - .collect::>(), - ) + .allow_origin(cors_origins) .allow_methods([ axum::http::Method::POST, axum::http::Method::DELETE, @@ -131,6 +132,12 @@ pub async fn start_http_transport( .allow_headers([ axum::http::header::CONTENT_TYPE, axum::http::header::AUTHORIZATION, + axum::http::HeaderName::from_static("mcp-protocol-version"), + axum::http::HeaderName::from_static("mcp-session-id"), + ]) + .expose_headers([ + axum::http::HeaderName::from_static("mcp-protocol-version"), + axum::http::HeaderName::from_static("mcp-session-id"), ]), ), ) @@ -187,6 +194,105 @@ fn validate_auth(headers: &HeaderMap, expected: &str) -> Result<(), (StatusCode, Ok(()) } +fn allowed_origins(port: u16) -> Vec { + if let Ok(configured) = std::env::var("VESTIGE_HTTP_ALLOWED_ORIGINS") { + let origins: Vec = configured + .split(',') + .map(str::trim) + .filter(|origin| !origin.is_empty()) + .map(ToOwned::to_owned) + .collect(); + if !origins.is_empty() { + return origins; + } + } + + vec![ + format!("http://127.0.0.1:{}", port), + format!("http://localhost:{}", port), + ] +} + +fn validate_origin( + headers: &HeaderMap, + allowed_origins: &[String], +) -> Result<(), (StatusCode, &'static str)> { + let Some(origin) = headers.get(header::ORIGIN).and_then(|v| v.to_str().ok()) else { + return Ok(()); + }; + + if allowed_origins.iter().any(|allowed| allowed == origin) { + Ok(()) + } else { + Err((StatusCode::FORBIDDEN, "Origin not allowed")) + } +} + +fn validate_accept(headers: &HeaderMap) -> Result<(), (StatusCode, &'static str)> { + let Some(accept) = headers.get(header::ACCEPT).and_then(|v| v.to_str().ok()) else { + return Err(( + StatusCode::NOT_ACCEPTABLE, + "Accept must include application/json and text/event-stream", + )); + }; + + let mut accepts_json = false; + let mut accepts_sse = false; + for mime in accept + .split(',') + .map(|part| part.trim().split(';').next().unwrap_or("").trim()) + { + accepts_json |= mime == "application/json"; + accepts_sse |= mime == "text/event-stream"; + } + + if accepts_json && accepts_sse { + Ok(()) + } else { + Err(( + StatusCode::NOT_ACCEPTABLE, + "Accept must include application/json and text/event-stream", + )) + } +} + +fn protocol_version_from_headers(headers: &HeaderMap) -> Option<&str> { + headers + .get("mcp-protocol-version") + .and_then(|v| v.to_str().ok()) +} + +fn validate_protocol_version( + headers: &HeaderMap, + expected: &str, +) -> Result<(), (StatusCode, &'static str)> { + let Some(version) = protocol_version_from_headers(headers) else { + return Err(( + StatusCode::BAD_REQUEST, + "MCP-Protocol-Version header required", + )); + }; + + if version == expected { + Ok(()) + } else { + Err((StatusCode::BAD_REQUEST, "MCP-Protocol-Version mismatch")) + } +} + +fn response_protocol_version(response: &crate::protocol::types::JsonRpcResponse) -> Option { + if response.error.is_some() { + return None; + } + + response + .result + .as_ref() + .and_then(|result| result.get("protocolVersion")) + .and_then(|value| value.as_str()) + .map(ToOwned::to_owned) +} + /// Extract and validate the `Mcp-Session-Id` header value. /// /// Only accepts valid UUID v4 format (8-4-4-4-12 hex) to prevent header @@ -209,6 +315,13 @@ async fn post_mcp( headers: HeaderMap, Json(request): Json, ) -> impl IntoResponse { + if let Err((status, msg)) = validate_origin(&headers, &state.allowed_origins) { + return (status, HeaderMap::new(), msg.to_string()).into_response(); + } + if let Err((status, msg)) = validate_accept(&headers) { + return (status, HeaderMap::new(), msg.to_string()).into_response(); + } + // Auth check if let Err((status, msg)) = validate_auth(&headers, &state.auth_token) { return (status, HeaderMap::new(), msg.to_string()).into_response(); @@ -235,6 +348,7 @@ async fn post_mcp( let session = Arc::new(Mutex::new(Session { server, last_active: Instant::now(), + protocol_version: crate::protocol::types::MCP_VERSION.to_string(), })); // Handle the initialize request @@ -243,12 +357,18 @@ async fn post_mcp( sess.server.handle_request(request).await }; - // Insert session while still holding write lock — atomic check-and-insert - sessions.insert(session_id.clone(), session); - drop(sessions); - match response { Some(resp) => { + let Some(protocol_version) = response_protocol_version(&resp) else { + drop(sessions); + return (StatusCode::OK, HeaderMap::new(), Json(resp)).into_response(); + }; + { + let mut sess = session.lock().await; + sess.protocol_version = protocol_version.clone(); + } + sessions.insert(session_id.clone(), session); + drop(sessions); let mut resp_headers = HeaderMap::new(); resp_headers.insert( "mcp-session-id", @@ -256,18 +376,14 @@ async fn post_mcp( .parse() .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), ); + if let Ok(value) = HeaderValue::from_str(&protocol_version) { + resp_headers.insert("mcp-protocol-version", value); + } (StatusCode::OK, resp_headers, Json(resp)).into_response() } None => { - // Notifications return 202 - let mut resp_headers = HeaderMap::new(); - resp_headers.insert( - "mcp-session-id", - session_id - .parse() - .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), - ); - (StatusCode::ACCEPTED, resp_headers).into_response() + drop(sessions); + (StatusCode::ACCEPTED, HeaderMap::new()).into_response() } } } else { @@ -295,8 +411,14 @@ async fn post_mcp( } }; + let session_protocol_version; let response = { let mut sess = session.lock().await; + if let Err((status, msg)) = validate_protocol_version(&headers, &sess.protocol_version) + { + return (status, msg).into_response(); + } + session_protocol_version = sess.protocol_version.clone(); sess.last_active = Instant::now(); sess.server.handle_request(request).await }; @@ -308,6 +430,9 @@ async fn post_mcp( .parse() .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), ); + if let Ok(value) = HeaderValue::from_str(&session_protocol_version) { + resp_headers.insert("mcp-protocol-version", value); + } match response { Some(resp) => (StatusCode::OK, resp_headers, Json(resp)).into_response(), @@ -321,6 +446,9 @@ async fn delete_mcp( State(state): State, headers: HeaderMap, ) -> impl IntoResponse { + if let Err((status, msg)) = validate_origin(&headers, &state.allowed_origins) { + return (status, msg).into_response(); + } if let Err((status, msg)) = validate_auth(&headers, &state.auth_token) { return (status, msg).into_response(); } @@ -336,6 +464,22 @@ async fn delete_mcp( } }; + let session = { + let sessions = state.sessions.read().await; + sessions.get(&session_id).cloned() + }; + let Some(session) = session else { + return (StatusCode::NOT_FOUND, "Session not found").into_response(); + }; + + let protocol_version = { + let sess = session.lock().await; + sess.protocol_version.clone() + }; + if let Err((status, msg)) = validate_protocol_version(&headers, &protocol_version) { + return (status, msg).into_response(); + } + let mut sessions = state.sessions.write().await; if sessions.remove(&session_id).is_some() { info!("Session {} deleted via DELETE /mcp", &session_id[..8]); @@ -344,3 +488,79 @@ async fn delete_mcp( (StatusCode::NOT_FOUND, "Session not found").into_response() } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn origin_validation_allows_absent_and_configured_origin() { + let allowed = vec!["http://127.0.0.1:3928".to_string()]; + let mut headers = HeaderMap::new(); + assert!(validate_origin(&headers, &allowed).is_ok()); + + headers.insert( + header::ORIGIN, + HeaderValue::from_static("http://127.0.0.1:3928"), + ); + assert!(validate_origin(&headers, &allowed).is_ok()); + + headers.insert( + header::ORIGIN, + HeaderValue::from_static("http://evil.example"), + ); + assert_eq!( + validate_origin(&headers, &allowed).unwrap_err().0, + StatusCode::FORBIDDEN + ); + } + + #[test] + fn accept_validation_rejects_incompatible_clients() { + let mut headers = HeaderMap::new(); + assert_eq!( + validate_accept(&headers).unwrap_err().0, + StatusCode::NOT_ACCEPTABLE + ); + + headers.insert( + header::ACCEPT, + HeaderValue::from_static("application/json, text/event-stream"), + ); + assert!(validate_accept(&headers).is_ok()); + + headers.insert(header::ACCEPT, HeaderValue::from_static("application/json")); + assert_eq!( + validate_accept(&headers).unwrap_err().0, + StatusCode::NOT_ACCEPTABLE + ); + } + + #[test] + fn protocol_header_must_match_session_when_present() { + let mut headers = HeaderMap::new(); + assert_eq!( + validate_protocol_version(&headers, "2025-11-25") + .unwrap_err() + .0, + StatusCode::BAD_REQUEST + ); + + headers.insert( + "mcp-protocol-version", + HeaderValue::from_static("2025-11-25"), + ); + assert!(validate_protocol_version(&headers, "2025-11-25").is_ok()); + + headers.insert( + "mcp-protocol-version", + HeaderValue::from_static("2024-11-05"), + ); + assert_eq!( + validate_protocol_version(&headers, "2025-11-25") + .unwrap_err() + .0, + StatusCode::BAD_REQUEST + ); + } +} diff --git a/crates/vestige-mcp/src/protocol/messages.rs b/crates/vestige-mcp/src/protocol/messages.rs index b4d6fe1..8f7e459 100644 --- a/crates/vestige-mcp/src/protocol/messages.rs +++ b/crates/vestige-mcp/src/protocol/messages.rs @@ -82,13 +82,25 @@ pub struct ServerCapabilities { // ============================================================================ /// Tool description for tools/list -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ToolDescription { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub input_schema: Value, + /// Per-tool `_meta` annotations from the MCP wire spec. + /// + /// Notable keys recognized by Claude Code (v2.1.91+): + /// - `anthropic/maxResultSizeChars` (integer, up to 500_000): + /// per-tool override of the 50K default `CallToolResult` truncation + /// ceiling. Pinned on the Tool definition; applies to every invocation. + /// + /// Free-form `serde_json::Value` (typically an object) so additional + /// vendor-specific `_meta` keys can be added without further schema + /// changes. + #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] + pub meta: Option, } /// Result of tools/list @@ -113,6 +125,8 @@ pub struct CallToolRequest { pub struct CallToolResult { pub content: Vec, #[serde(skip_serializing_if = "Option::is_none")] + pub structured_content: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub is_error: Option, } diff --git a/crates/vestige-mcp/src/protocol/stdio.rs b/crates/vestige-mcp/src/protocol/stdio.rs index 3d36b3e..12d4706 100644 --- a/crates/vestige-mcp/src/protocol/stdio.rs +++ b/crates/vestige-mcp/src/protocol/stdio.rs @@ -1,10 +1,9 @@ //! stdio Transport for MCP //! //! Handles JSON-RPC communication over stdin/stdout. -//! v1.9.2: Async tokio I/O with heartbeat and error resilience. +//! v1.9.2: Async tokio I/O with error resilience. use std::io; -use std::time::Duration; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tracing::{debug, error, info, warn}; @@ -14,9 +13,6 @@ use crate::server::McpServer; /// Maximum consecutive I/O errors before giving up const MAX_CONSECUTIVE_ERRORS: u32 = 5; -/// Heartbeat interval — sends a ping notification to keep the connection alive -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(30); - /// stdio Transport for MCP server pub struct StdioTransport; @@ -25,7 +21,7 @@ impl StdioTransport { Self } - /// Run the MCP server over stdio with heartbeat and error resilience + /// Run the MCP server over stdio with error resilience. pub async fn run(self, mut server: McpServer) -> Result<(), io::Error> { let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); @@ -111,25 +107,10 @@ impl StdioTransport { break; } // Brief pause before retrying - tokio::time::sleep(Duration::from_millis(100)).await; + tokio::time::sleep(std::time::Duration::from_millis(100)).await; } } } - _ = tokio::time::sleep(HEARTBEAT_INTERVAL) => { - // Send a heartbeat ping notification to keep the connection alive - let ping = "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/ping\"}\n"; - if let Err(e) = stdout.write_all(ping.as_bytes()).await { - warn!("Failed to send heartbeat ping: {}", e); - consecutive_errors += 1; - if consecutive_errors >= MAX_CONSECUTIVE_ERRORS { - error!("Too many consecutive errors, shutting down"); - break; - } - } else { - let _ = stdout.flush().await; - debug!("Heartbeat ping sent"); - } - } } } diff --git a/crates/vestige-mcp/src/protocol/types.rs b/crates/vestige-mcp/src/protocol/types.rs index 57f5a10..fa489dc 100644 --- a/crates/vestige-mcp/src/protocol/types.rs +++ b/crates/vestige-mcp/src/protocol/types.rs @@ -114,6 +114,10 @@ impl JsonRpcError { Self::new(ErrorCode::MethodNotFound, message) } + pub fn invalid_request(message: &str) -> Self { + Self::new(ErrorCode::InvalidRequest, message) + } + pub fn invalid_params(message: &str) -> Self { Self::new(ErrorCode::InvalidParams, message) } diff --git a/crates/vestige-mcp/src/server.rs b/crates/vestige-mcp/src/server.rs index 711b37a..a409ff5 100644 --- a/crates/vestige-mcp/src/server.rs +++ b/crates/vestige-mcp/src/server.rs @@ -31,11 +31,9 @@ use vestige_core::Storage; /// Vestige without imposing one maintainer's workflow on strangers. /// /// The "full" variant is the composition mandate that enforces the -/// Composing / Never-composed / Recommendation response shape, names the -/// AIMO3 36/50 case study as the origin, and includes the "Vestige is -/// blocking this:" refusal phrase. It is load-bearing for Sam's own -/// decision-adjacent work but would misfire on trivial retrievals for a -/// general audience, so it is opt-in via `VESTIGE_SYSTEM_PROMPT_MODE=full`. +/// Composing / Never-composed / Recommendation response shape. It can misfire +/// on trivial retrievals for a general audience, so it is opt-in via +/// `VESTIGE_SYSTEM_PROMPT_MODE=full`. /// /// Anything other than `full` falls back to minimal. fn build_instructions() -> String { @@ -45,7 +43,7 @@ fn build_instructions() -> String { Every retrieval MUST be composed into a recommendation, never summarized.\ \n\nCOMPOSITION MANDATE: When you receive memories from search, deep_reference, \ cross_reference, or explore_connections, your response MUST follow this shape. \ - (a) Composing: [memory IDs], followed by your composition logic (your chain-of-thought \ + (a) Composing: [memory IDs], followed by a brief composition rationale \ about how the memories relate, NOT a restatement of their contents). \ (b) Never-composed detected: list combinations of retrieved memories that share \ tags/topics but have never been referenced together, or write 'None.' \ @@ -66,6 +64,10 @@ fn build_instructions() -> String { } } +fn supported_protocol_versions() -> &'static [&'static str] { + &["2024-11-05", "2025-03-26", "2025-06-18", MCP_VERSION] +} + /// MCP Server implementation pub struct McpServer { storage: Arc, @@ -115,6 +117,13 @@ impl McpServer { pub async fn handle_request(&mut self, request: JsonRpcRequest) -> Option { debug!("Handling request: {}", request.method); + if request.id.is_none() { + if request.method != "notifications/initialized" { + debug!("Dropping JSON-RPC notification '{}'", request.method); + } + return None; + } + // Check initialization for non-initialize requests if !self.initialized && request.method != "initialize" @@ -132,10 +141,9 @@ impl McpServer { let result = match request.method.as_str() { "initialize" => self.handle_initialize(request.params).await, - "notifications/initialized" => { - // Notification, no response needed - return None; - } + "notifications/initialized" => Err(JsonRpcError::invalid_request( + "notifications/initialized must be sent without an id", + )), "tools/list" => self.handle_tools_list().await, "tools/call" => self.handle_tools_call(request.params).await, "resources/list" => self.handle_resources_list().await, @@ -161,20 +169,27 @@ impl McpServer { let request: InitializeRequest = match params { Some(p) => serde_json::from_value(p) .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, - None => InitializeRequest::default(), + None => { + return Err(JsonRpcError::invalid_params( + "initialize params are required", + )); + } }; - // Version negotiation: use client's version if older than server's - // Claude Desktop rejects servers with newer protocol versions - let negotiated_version = if request.protocol_version.as_str() < MCP_VERSION { - info!( - "Client requested older protocol version {}, using it", - request.protocol_version - ); - request.protocol_version.clone() - } else { - MCP_VERSION.to_string() - }; + let negotiated_version = + if supported_protocol_versions().contains(&request.protocol_version.as_str()) { + info!( + "Client requested supported protocol version {}, using it", + request.protocol_version + ); + request.protocol_version.clone() + } else { + info!( + "Client requested unsupported protocol version {}, using {}", + request.protocol_version, MCP_VERSION + ); + MCP_VERSION.to_string() + }; self.initialized = true; info!( @@ -209,10 +224,10 @@ impl McpServer { /// Handle tools/list request async fn handle_tools_list(&self) -> Result { - // v2.0.5+: 24 tools (verified by the `tools.len() == 24` assertion in the + // v2.1.21: 25 tools (verified by the `tools.len() == 25` assertion in the // handle_tools_list test below — the `suppress` tool landed in v2.0.5). // Deprecated tools still work via redirects in handle_tools_call. - let tools = vec![ + let mut tools = vec![ // ================================================================ // UNIFIED TOOLS (v1.1+) // ================================================================ @@ -220,21 +235,25 @@ impl McpServer { name: "search".to_string(), description: Some("Unified search tool. Uses hybrid search (keyword + semantic + convex combination fusion) internally. Auto-strengthens memories on access (Testing Effect).".to_string()), input_schema: tools::search_unified::schema(), + ..Default::default() }, ToolDescription { name: "memory".to_string(), - description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'delete' (remove memory), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()), + description: Some("Unified memory management tool. Actions: 'get' (retrieve full node), 'purge' (irreversibly remove content/embeddings with confirm=true), 'delete' (legacy alias for purge), 'state' (get accessibility state), 'promote' (thumbs up — increases retrieval strength), 'demote' (thumbs down — decreases retrieval strength, does NOT delete), 'edit' (update content in-place, preserves FSRS state).".to_string()), input_schema: tools::memory_unified::schema(), + ..Default::default() }, ToolDescription { name: "codebase".to_string(), description: Some("Unified codebase tool. Actions: 'remember_pattern' (store code pattern), 'remember_decision' (store architectural decision), 'get_context' (retrieve patterns and decisions).".to_string()), input_schema: tools::codebase_unified::schema(), + ..Default::default() }, ToolDescription { name: "intention".to_string(), description: Some("Unified intention management tool. Actions: 'set' (create), 'check' (find triggered), 'update' (complete/snooze/cancel), 'list' (show intentions).".to_string()), input_schema: tools::intention_unified::schema(), + ..Default::default() }, // ================================================================ // CORE MEMORY (v1.7: smart_ingest absorbs ingest + checkpoint) @@ -243,6 +262,7 @@ impl McpServer { name: "smart_ingest".to_string(), description: Some("INTELLIGENT memory ingestion with Prediction Error Gating. Single mode: provide 'content' to auto-decide CREATE/UPDATE/SUPERSEDE. Batch mode: provide 'items' array (max 20) for session-end saves — each item runs the full cognitive pipeline (importance scoring, intent detection, synaptic tagging).".to_string()), input_schema: tools::smart_ingest::schema(), + ..Default::default() }, // ================================================================ // TEMPORAL TOOLS (v1.2+) @@ -251,11 +271,13 @@ impl McpServer { name: "memory_timeline".to_string(), description: Some("Browse memories chronologically. Returns memories in a time range, grouped by day. Defaults to last 7 days.".to_string()), input_schema: tools::timeline::schema(), + ..Default::default() }, ToolDescription { name: "memory_changelog".to_string(), description: Some("View audit trail of memory changes. Per-memory: state transitions. System-wide: consolidations + recent state changes.".to_string()), input_schema: tools::changelog::schema(), + ..Default::default() }, // ================================================================ // MAINTENANCE TOOLS (v1.7: system_status replaces health_check + stats) @@ -264,26 +286,31 @@ impl McpServer { name: "system_status".to_string(), description: Some("Combined system health and statistics. Returns status (healthy/degraded/critical/empty), full stats, FSRS preview, cognitive module health, state distribution, warnings, and recommendations.".to_string()), input_schema: tools::maintenance::system_status_schema(), + ..Default::default() }, ToolDescription { name: "consolidate".to_string(), description: Some("Run FSRS-6 memory consolidation cycle. Applies decay, generates embeddings, and performs maintenance. Use when memories seem stale.".to_string()), input_schema: tools::maintenance::consolidate_schema(), + ..Default::default() }, ToolDescription { name: "backup".to_string(), description: Some("Create a SQLite database backup. Returns the backup file path.".to_string()), input_schema: tools::maintenance::backup_schema(), + ..Default::default() }, ToolDescription { name: "export".to_string(), description: Some("Export memories as JSON or JSONL. Supports tag and date filters.".to_string()), input_schema: tools::maintenance::export_schema(), + ..Default::default() }, ToolDescription { name: "gc".to_string(), description: Some("Garbage collect stale memories below retention threshold. Defaults to dry_run=true for safety.".to_string()), input_schema: tools::maintenance::gc_schema(), + ..Default::default() }, // ================================================================ // AUTO-SAVE & DEDUP TOOLS (v1.3+) @@ -292,11 +319,13 @@ impl McpServer { name: "importance_score".to_string(), description: Some("Score content importance using 4-channel neuroscience model (novelty/arousal/reward/attention). Returns composite score, channel breakdown, encoding boost, and explanations.".to_string()), input_schema: tools::importance::schema(), + ..Default::default() }, ToolDescription { name: "find_duplicates".to_string(), description: Some("Find duplicate and near-duplicate memory clusters using cosine similarity on embeddings. Returns clusters with suggested actions (merge/review). Use to clean up redundant memories.".to_string()), input_schema: tools::dedup::schema(), + ..Default::default() }, // ================================================================ // COGNITIVE TOOLS (v1.5+) @@ -305,16 +334,19 @@ impl McpServer { name: "dream".to_string(), description: Some("Trigger memory dreaming — replays recent memories to discover hidden connections, synthesize insights, and strengthen important patterns. Returns insights, connections, and dream stats.".to_string()), input_schema: tools::dream::schema(), + ..Default::default() }, ToolDescription { name: "explore_connections".to_string(), description: Some("Graph exploration tool for memory connections. Actions: 'chain' (build reasoning path between memories), 'associations' (find related memories via spreading activation + hippocampal index), 'bridges' (find connecting memories between two nodes).".to_string()), input_schema: tools::explore::schema(), + ..Default::default() }, ToolDescription { name: "predict".to_string(), description: Some("Proactive memory prediction — predicts what memories you'll need next based on context, recent activity, and learned patterns. Returns predictions, suggestions, and speculative retrievals.".to_string()), input_schema: tools::predict::schema(), + ..Default::default() }, // ================================================================ // RESTORE TOOL (v1.5+) @@ -323,6 +355,7 @@ impl McpServer { name: "restore".to_string(), description: Some("Restore memories from a JSON backup file. Supports MCP wrapper format, RecallResult format, and direct memory array format.".to_string()), input_schema: tools::restore::schema(), + ..Default::default() }, // ================================================================ // CONTEXT PACKETS (v1.8+) @@ -331,6 +364,7 @@ impl McpServer { name: "session_context".to_string(), description: Some("One-call session initialization. Combines search, intentions, status, predictions, and codebase context into a single token-budgeted response. Replaces 5 separate calls at session start.".to_string()), input_schema: tools::session_context::schema(), + ..Default::default() }, // ================================================================ // AUTONOMIC TOOLS (v1.9+) @@ -339,11 +373,13 @@ impl McpServer { name: "memory_health".to_string(), description: Some("Retention dashboard. Returns avg retention, retention distribution (buckets: 0-20%, 20-40%, etc.), trend (improving/declining/stable), and recommendation. Lightweight alternative to full system_status focused on memory quality.".to_string()), input_schema: tools::health::schema(), + ..Default::default() }, ToolDescription { name: "memory_graph".to_string(), description: Some("Subgraph export for visualization. Input: center_id or query, depth (1-3), max_nodes. Returns nodes with force-directed layout positions and edges with weights. Powers memory graph visualization.".to_string()), input_schema: tools::graph::schema(), + ..Default::default() }, // ================================================================ // DEEP REFERENCE (v2.0.4+) — replaces cross_reference @@ -352,11 +388,19 @@ impl McpServer { name: "deep_reference".to_string(), description: Some("Deep cognitive reasoning across memories. Combines FSRS-6 trust scoring, spreading activation, temporal supersession, dream insights, and contradiction analysis to build a complete understanding of a topic. Returns trust-scored evidence, fact evolution timeline, and a recommended answer. Use this when accuracy matters.".to_string()), input_schema: tools::cross_reference::schema(), + ..Default::default() }, ToolDescription { name: "cross_reference".to_string(), description: Some("Alias for deep_reference. Connect the dots across memories with cognitive reasoning.".to_string()), input_schema: tools::cross_reference::schema(), + ..Default::default() + }, + ToolDescription { + name: "contradictions".to_string(), + description: Some("Inspect memory disagreements directly. Scans a topic or recent memories for trust-weighted contradiction pairs using the same local logic as deep_reference.".to_string()), + input_schema: tools::contradictions::schema(), + ..Default::default() }, // ================================================================ // ACTIVE FORGETTING (v2.0.5) — top-down suppression @@ -366,9 +410,47 @@ impl McpServer { name: "suppress".to_string(), description: Some("Actively suppress a memory via top-down inhibitory control (Anderson 2025 SIF + Davis Rac1). Distinct from delete: the memory persists but is inhibited from retrieval and actively decays. Each call compounds. A background Rac1 worker cascades decay to co-activated neighbors. Reversible within 24 hours via reverse=true.".to_string()), input_schema: tools::suppress::schema(), + ..Default::default() }, ]; + // Per-tool result-size annotation `_meta["anthropic/maxResultSizeChars"]`. + // + // Claude Code v2.1.91+ honors this annotation to override its 50K default + // `CallToolResult` truncation. Without it, large Vestige payloads + // (`search` with `detail_level="full"` at `limit=20` has been observed + // at ~135K chars; `memory_timeline` at `limit=30` at ~84K chars) are + // silently truncated and spilled to disk, forcing the parent agent to + // chunk-read them. + // + // Per-tool caps below are sized at ~2× observed peak with growth + // headroom; max permitted by Anthropic is 500_000. Only the four + // empirically-measured high-payload tools carry the annotation today; + // the remaining 21 tools deliberately do NOT (cargo-cult prevention — + // annotating a small-payload tool dilutes the signal). + // + // Other tools that COULD plausibly grow into the annotated set with + // future workload (`deep_reference`, `cross_reference`, `memory_graph`, + // `explore_connections`, `session_context`) are left unannotated until + // empirical measurement shows truncation under realistic use. + for tool in tools.iter_mut() { + let max_chars: Option = match tool.name.as_str() { + "search" => Some(300_000), + "memory_timeline" => Some(200_000), + "memory" => Some(100_000), + "codebase" => Some(100_000), + _ => None, + }; + if let Some(n) = max_chars { + let mut meta = serde_json::Map::new(); + meta.insert( + "anthropic/maxResultSizeChars".to_string(), + serde_json::Value::from(n), + ); + tool.meta = Some(serde_json::Value::Object(meta)); + } + } + let result = ListToolsResult { tools }; serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(&e.to_string())) } @@ -383,6 +465,13 @@ impl McpServer { .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, None => return Err(JsonRpcError::invalid_params("Missing tool call parameters")), }; + if let Some(arguments) = &request.arguments + && !arguments.is_object() + { + return Err(JsonRpcError::invalid_params( + "tools/call arguments must be an object", + )); + } // Record activity on every tool call (non-blocking) if let Ok(mut cog) = self.cognitive.try_lock() { @@ -551,14 +640,19 @@ impl McpServer { } "delete_knowledge" => { warn!( - "Tool 'delete_knowledge' is deprecated. Use 'memory' with action='delete' instead." + "Tool 'delete_knowledge' is deprecated. Use 'memory' with action='purge', confirm=true instead." ); let unified_args = match request.arguments { Some(ref args) => { let id = args.get("id").cloned().unwrap_or(serde_json::Value::Null); + let confirm = args + .get("confirm") + .cloned() + .unwrap_or(serde_json::Value::Bool(false)); Some(serde_json::json!({ "action": "delete", - "id": id + "id": id, + "confirm": confirm })) } None => None, @@ -832,6 +926,9 @@ impl McpServer { tools::cross_reference::execute(&self.storage, &self.cognitive, request.arguments) .await } + "contradictions" => { + tools::contradictions::execute(&self.storage, request.arguments).await + } // ================================================================ // ACTIVE FORGETTING (v2.0.5) — top-down suppression @@ -839,7 +936,7 @@ impl McpServer { "suppress" => tools::suppress::execute(&self.storage, request.arguments).await, name => { - return Err(JsonRpcError::method_not_found_with_message(&format!( + return Err(JsonRpcError::invalid_params(&format!( "Unknown tool: {}", name ))); @@ -862,17 +959,20 @@ impl McpServer { text: serde_json::to_string_pretty(&content) .unwrap_or_else(|_| content.to_string()), }], + structured_content: Some(content), is_error: Some(false), }; serde_json::to_value(call_result) .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } Err(e) => { + let error_content = serde_json::json!({ "error": e }); let call_result = CallToolResult { content: vec![crate::protocol::messages::ToolResultContent { content_type: "text".to_string(), - text: serde_json::json!({ "error": e }).to_string(), + text: error_content.to_string(), }], + structured_content: Some(error_content), is_error: Some(true), }; serde_json::to_value(call_result) @@ -1037,7 +1137,15 @@ impl McpServer { serde_json::to_value(result) .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } - Err(e) => Err(JsonRpcError::internal_error(&e)), + Err(e) => { + if e.to_ascii_lowercase().contains("unknown") + || e.to_ascii_lowercase().contains("not found") + { + Err(JsonRpcError::resource_not_found(uri)) + } else { + Err(JsonRpcError::internal_error(&e)) + } + } } } @@ -1164,8 +1272,21 @@ impl McpServer { .unwrap_or("") .to_string(); match action { - "delete" => { - self.emit(VestigeEvent::MemoryDeleted { id, timestamp: now }); + "delete" | "purge" + if result + .get("success") + .and_then(|value| value.as_bool()) + .unwrap_or(false) => + { + let node_id = result + .get("nodeId") + .and_then(|value| value.as_str()) + .unwrap_or(&id) + .to_string(); + self.emit(VestigeEvent::MemoryDeleted { + id: node_id, + timestamp: now, + }); } "promote" => { let retention = result @@ -1328,6 +1449,7 @@ impl McpServer { .and_then(|v| v.as_f64()) .unwrap_or(0.0); self.emit(VestigeEvent::ImportanceScored { + memory_id: None, // importance_score tool runs on arbitrary content content_preview: preview, composite_score: composite, novelty, @@ -1378,6 +1500,26 @@ mod tests { } } + fn make_notification(method: &str, params: Option) -> JsonRpcRequest { + JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id: None, + method: method.to_string(), + params, + } + } + + fn init_params() -> serde_json::Value { + serde_json::json!({ + "protocolVersion": MCP_VERSION, + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + }) + } + // ======================================================================== // INITIALIZATION TESTS // ======================================================================== @@ -1429,13 +1571,31 @@ mod tests { } #[tokio::test] - async fn test_initialize_with_default_params() { + async fn test_initialize_unsupported_protocol_falls_back_to_latest() { + let (mut server, _dir) = test_server().await; + let params = serde_json::json!({ + "protocolVersion": "1.0.0", + "capabilities": {}, + "clientInfo": { "name": "test", "version": "1.0" } + }); + let request = make_request("initialize", Some(params)); + + let response = server.handle_request(request).await.unwrap(); + let result = response.result.unwrap(); + + assert_eq!(result["protocolVersion"], MCP_VERSION); + } + + #[tokio::test] + async fn test_initialize_missing_params_returns_error() { let (mut server, _dir) = test_server().await; let request = make_request("initialize", None); let response = server.handle_request(request).await.unwrap(); - assert!(response.result.is_some()); - assert!(response.error.is_none()); + assert!(response.result.is_none()); + assert!(response.error.is_some()); + assert_eq!(response.error.unwrap().code, -32602); + assert!(!server.initialized); } // ======================================================================== @@ -1475,17 +1635,39 @@ mod tests { let (mut server, _dir) = test_server().await; // First initialize - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; // Send initialized notification - let notification = make_request("notifications/initialized", None); + let notification = make_notification("notifications/initialized", None); let response = server.handle_request(notification).await; // Notifications should return None assert!(response.is_none()); } + #[tokio::test] + async fn test_initialized_notification_with_id_returns_invalid_request() { + let (mut server, _dir) = test_server().await; + + let request = make_request("notifications/initialized", None); + let response = server.handle_request(request).await.unwrap(); + + assert!(response.error.is_some()); + assert_eq!(response.error.unwrap().code, -32600); + } + + #[tokio::test] + async fn test_notification_does_not_emit_response_or_side_effect() { + let (mut server, _dir) = test_server().await; + + let notification = make_notification("initialize", None); + let response = server.handle_request(notification).await; + + assert!(response.is_none()); + assert!(!server.initialized); + } + // ======================================================================== // TOOLS/LIST TESTS // ======================================================================== @@ -1495,7 +1677,7 @@ mod tests { let (mut server, _dir) = test_server().await; // Initialize first - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("tools/list", None); @@ -1504,8 +1686,8 @@ mod tests { let result = response.result.unwrap(); let tools = result["tools"].as_array().unwrap(); - // v2.0.5: 24 tools (4 unified + 1 core + 2 temporal + 5 maintenance + 2 auto-save + 3 cognitive + 1 restore + 1 session_context + 2 autonomic + 1 deep_reference + 1 cross_reference alias + 1 suppress) - assert_eq!(tools.len(), 24, "Expected exactly 24 tools in v2.0.5+"); + // v2.1.21: 25 tools (includes first-class contradictions surface) + assert_eq!(tools.len(), 25, "Expected exactly 25 tools in v2.1.21"); let tool_names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); @@ -1575,6 +1757,7 @@ mod tests { // Deep reference + cross_reference alias (v2.0.4) assert!(tool_names.contains(&"deep_reference")); assert!(tool_names.contains(&"cross_reference")); + assert!(tool_names.contains(&"contradictions")); // Active forgetting (v2.0.5) — Anderson 2025 + Davis Rac1 assert!(tool_names.contains(&"suppress")); @@ -1584,7 +1767,7 @@ mod tests { async fn test_tools_have_descriptions_and_schemas() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("tools/list", None); @@ -1614,7 +1797,7 @@ mod tests { async fn test_resources_list_returns_all_resources() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("resources/list", None); @@ -1643,7 +1826,7 @@ mod tests { async fn test_resources_have_descriptions() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("resources/list", None); @@ -1671,7 +1854,7 @@ mod tests { let (mut server, _dir) = test_server().await; // Initialize first - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("unknown/method", None); @@ -1687,7 +1870,7 @@ mod tests { async fn test_unknown_tool_returns_error() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request( @@ -1700,7 +1883,7 @@ mod tests { let response = server.handle_request(request).await.unwrap(); assert!(response.error.is_some()); - assert_eq!(response.error.unwrap().code, -32601); + assert_eq!(response.error.unwrap().code, -32602); } // ======================================================================== @@ -1711,7 +1894,7 @@ mod tests { async fn test_ping_returns_empty_object() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("ping", None); @@ -1730,7 +1913,7 @@ mod tests { async fn test_tools_call_missing_params_returns_error() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request("tools/call", None); @@ -1744,7 +1927,7 @@ mod tests { async fn test_tools_call_invalid_params_returns_error() { let (mut server, _dir) = test_server().await; - let init_request = make_request("initialize", None); + let init_request = make_request("initialize", Some(init_params())); server.handle_request(init_request).await; let request = make_request( @@ -1758,4 +1941,162 @@ mod tests { assert!(response.error.is_some()); assert_eq!(response.error.unwrap().code, -32602); } + + #[tokio::test] + async fn test_tools_call_rejects_non_object_arguments() { + let (mut server, _dir) = test_server().await; + + let init_request = make_request("initialize", Some(init_params())); + server.handle_request(init_request).await; + + let request = make_request( + "tools/call", + Some(serde_json::json!({ + "name": "search", + "arguments": "not-an-object" + })), + ); + + let response = server.handle_request(request).await.unwrap(); + assert!(response.error.is_some()); + assert_eq!(response.error.unwrap().code, -32602); + } + + // ======================================================================== + // Per-tool result-size annotation tests + // (`_meta["anthropic/maxResultSizeChars"]`, CC v2.1.91+) + // + // The annotation lives on the Tool definition in `tools/list`, so CC reads + // it once when the MCP session opens and applies the override to every + // invocation of that tool. These tests pin the wire-form so a future + // refactor of `ToolDescription` cannot silently drop the annotation. + // ======================================================================== + + /// Expected per-tool caps. Returns `Some(cap)` for tools the discipline + /// annotates, `None` for tools that MUST NOT carry the annotation + /// (cargo-cult prevention). + fn expected_max_result_size(name: &str) -> Option { + match name { + "search" => Some(300_000), + "memory_timeline" => Some(200_000), + "memory" => Some(100_000), + "codebase" => Some(100_000), + _ => None, + } + } + + #[tokio::test] + async fn test_high_payload_tools_have_max_result_size_annotation() { + let (mut server, _dir) = test_server().await; + let init_request = make_request("initialize", Some(init_params())); + server.handle_request(init_request).await; + + let request = make_request("tools/list", None); + let response = server.handle_request(request).await.unwrap(); + let result = response.result.unwrap(); + let tools = result["tools"].as_array().unwrap(); + + for name in ["search", "memory_timeline", "memory", "codebase"] { + let tool = tools + .iter() + .find(|t| t["name"].as_str() == Some(name)) + .unwrap_or_else(|| panic!("Tool '{}' missing from tools/list", name)); + + let expected = expected_max_result_size(name).unwrap(); + let meta = tool.get("_meta").unwrap_or_else(|| { + panic!("Tool '{}' is missing the `_meta` field on the wire", name) + }); + let actual = meta + .get("anthropic/maxResultSizeChars") + .and_then(|v| v.as_u64()) + .unwrap_or_else(|| { + panic!( + "Tool '{}' _meta lacks integer 'anthropic/maxResultSizeChars'", + name + ) + }); + assert_eq!( + actual, expected, + "Tool '{}' cap drift: expected {} got {}", + name, expected, actual + ); + assert!( + actual <= 500_000, + "Tool '{}' cap {} exceeds Anthropic 500K ceiling", + name, + actual + ); + } + } + + #[tokio::test] + async fn test_other_tools_do_not_carry_max_result_size_annotation() { + // Cargo-cult prevention. Dynamically derived from tools/list so this + // test is robust to new tools being added: any tool that is NOT in + // the discipline-prescribed set MUST NOT carry the annotation. + // Adding the annotation to a small-payload tool dilutes the signal + // and trains future maintainers that the value is arbitrary. + let (mut server, _dir) = test_server().await; + let init_request = make_request("initialize", Some(init_params())); + server.handle_request(init_request).await; + + let request = make_request("tools/list", None); + let response = server.handle_request(request).await.unwrap(); + let result = response.result.unwrap(); + let tools = result["tools"].as_array().unwrap(); + + for tool in tools { + let name = tool["name"].as_str().unwrap(); + if expected_max_result_size(name).is_some() { + continue; // covered by the annotated-tools test + } + + // Either the `_meta` key is absent OR it is an object without the + // anthropic key — both are acceptable. The forbidden case is the + // anthropic key present on this tool. + let has_max_size = tool + .get("_meta") + .and_then(|m| m.get("anthropic/maxResultSizeChars")) + .is_some(); + assert!( + !has_max_size, + "Tool '{}' should NOT carry maxResultSizeChars annotation \ + (not in the discipline-prescribed set: search, memory_timeline, \ + memory, codebase). If this tool's realistic max-payload now \ + routinely exceeds 50K, update expected_max_result_size() + the \ + annotation loop in handle_tools_list together.", + name + ); + } + } + + #[tokio::test] + async fn test_meta_wire_shape_uses_underscore_meta_field() { + // Anthropic's MCP spec is explicit: the field on the wire is `_meta`, + // NOT `meta`. The Rust struct uses `meta: Option` with + // `#[serde(rename = "_meta")]` — assert the rename actually fired. + let (mut server, _dir) = test_server().await; + let init_request = make_request("initialize", Some(init_params())); + server.handle_request(init_request).await; + + let request = make_request("tools/list", None); + let response = server.handle_request(request).await.unwrap(); + let result = response.result.unwrap(); + let tools = result["tools"].as_array().unwrap(); + + let search_tool = tools + .iter() + .find(|t| t["name"].as_str() == Some("search")) + .expect("'search' tool present"); + + // Wire-form: `_meta` must exist; `meta` (un-renamed) must NOT exist. + assert!( + search_tool.get("_meta").is_some(), + "search tool missing `_meta` key (serde rename to _meta did not apply)" + ); + assert!( + search_tool.get("meta").is_none(), + "search tool has un-renamed `meta` key (regression — serde rename broke)" + ); + } } diff --git a/crates/vestige-mcp/src/tools/checkpoint.rs b/crates/vestige-mcp/src/tools/checkpoint.rs deleted file mode 100644 index 69cab08..0000000 --- a/crates/vestige-mcp/src/tools/checkpoint.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Session Checkpoint Tool -//! -//! Batch smart_ingest for session-end saves. Accepts up to 20 items -//! in a single call, routing each through Prediction Error Gating. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{IngestInput, Storage}; - -/// Input schema for session_checkpoint tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "items": { - "type": "array", - "description": "Array of items to save (max 20). Each goes through Prediction Error Gating.", - "maxItems": 20, - "items": { - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "The content to remember" - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags for categorization" - }, - "node_type": { - "type": "string", - "description": "Type: fact, concept, event, person, place, note, pattern, decision", - "default": "fact" - }, - "source": { - "type": "string", - "description": "Source reference" - } - }, - "required": ["content"] - } - } - }, - "required": ["items"] - }) -} - -#[derive(Debug, Deserialize)] -struct CheckpointArgs { - items: Vec, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CheckpointItem { - content: String, - tags: Option>, - node_type: Option, - source: Option, -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: CheckpointArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.items.is_empty() { - return Err("Items array cannot be empty".to_string()); - } - - if args.items.len() > 20 { - return Err("Maximum 20 items per checkpoint".to_string()); - } - - let mut results = Vec::new(); - let mut created = 0u32; - let mut updated = 0u32; - let mut skipped = 0u32; - let mut errors = 0u32; - - for (i, item) in args.items.into_iter().enumerate() { - if item.content.trim().is_empty() { - results.push(serde_json::json!({ - "index": i, - "status": "skipped", - "reason": "Empty content" - })); - skipped += 1; - continue; - } - - let input = IngestInput { - content: item.content, - node_type: item.node_type.unwrap_or_else(|| "fact".to_string()), - source: item.source, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: item.tags.unwrap_or_default(), - valid_from: None, - valid_until: None, - }; - - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - match storage.smart_ingest(input) { - Ok(result) => { - match result.decision.as_str() { - "create" | "supersede" | "replace" => created += 1, - "update" | "reinforce" | "merge" | "add_context" => updated += 1, - _ => created += 1, - } - results.push(serde_json::json!({ - "index": i, - "status": "saved", - "decision": result.decision, - "nodeId": result.node.id, - "similarity": result.similarity, - "reason": result.reason - })); - } - Err(e) => { - errors += 1; - results.push(serde_json::json!({ - "index": i, - "status": "error", - "reason": e.to_string() - })); - } - } - } - - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - match storage.ingest(input) { - Ok(node) => { - created += 1; - results.push(serde_json::json!({ - "index": i, - "status": "saved", - "decision": "create", - "nodeId": node.id, - "reason": "Embeddings not available - used regular ingest" - })); - } - Err(e) => { - errors += 1; - results.push(serde_json::json!({ - "index": i, - "status": "error", - "reason": e.to_string() - })); - } - } - } - } - - Ok(serde_json::json!({ - "success": errors == 0, - "summary": { - "total": results.len(), - "created": created, - "updated": updated, - "skipped": skipped, - "errors": errors - }, - "results": results - })) -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - #[test] - fn test_schema_has_required_fields() { - let schema = schema(); - assert_eq!(schema["type"], "object"); - assert!(schema["properties"]["items"].is_object()); - } - - #[tokio::test] - async fn test_empty_items_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, Some(serde_json::json!({ "items": [] }))).await; - assert!(result.is_err()); - } - - #[tokio::test] - async fn test_batch_ingest() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "First checkpoint item", "tags": ["test"] }, - { "content": "Second checkpoint item", "tags": ["test"] } - ] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["total"], 2); - } - - #[tokio::test] - async fn test_skips_empty_content() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "Valid item" }, - { "content": "" }, - { "content": "Another valid item" } - ] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["skipped"], 1); - } - - #[tokio::test] - async fn test_missing_args_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_exceeds_20_items_fails() { - let (storage, _dir) = test_storage().await; - let items: Vec = (0..21) - .map(|i| serde_json::json!({ "content": format!("Item {}", i) })) - .collect(); - let result = execute(&storage, Some(serde_json::json!({ "items": items }))).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Maximum 20 items")); - } - - #[tokio::test] - async fn test_exactly_20_items_succeeds() { - let (storage, _dir) = test_storage().await; - let items: Vec = (0..20) - .map(|i| serde_json::json!({ "content": format!("Item {}", i) })) - .collect(); - let result = execute(&storage, Some(serde_json::json!({ "items": items }))).await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["total"], 20); - } - - #[tokio::test] - async fn test_skips_whitespace_only_content() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": " \t\n " }, - { "content": "Valid content" } - ] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["skipped"], 1); - assert_eq!(value["summary"]["created"], 1); - } - - #[tokio::test] - async fn test_single_item_succeeds() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [{ "content": "Single item" }] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["total"], 1); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_items_with_all_fields() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [{ - "content": "Full fields item", - "tags": ["test", "checkpoint"], - "node_type": "decision", - "source": "test-suite" - }] - })), - ) - .await; - assert!(result.is_ok()); - let value = result.unwrap(); - assert_eq!(value["summary"]["created"], 1); - } - - #[tokio::test] - async fn test_results_array_matches_items() { - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "First" }, - { "content": "" }, - { "content": "Third" } - ] - })), - ) - .await; - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert_eq!(results.len(), 3); - assert_eq!(results[0]["index"], 0); - assert_eq!(results[1]["index"], 1); - assert_eq!(results[1]["status"], "skipped"); - assert_eq!(results[2]["index"], 2); - } - - #[tokio::test] - async fn test_success_false_when_errors() { - // All items empty = all skipped = 0 errors = success true - let (storage, _dir) = test_storage().await; - let result = execute( - &storage, - Some(serde_json::json!({ - "items": [ - { "content": "" }, - { "content": " " } - ] - })), - ) - .await; - let value = result.unwrap(); - assert_eq!(value["success"], true); // skipped ≠ errors - assert_eq!(value["summary"]["errors"], 0); - assert_eq!(value["summary"]["skipped"], 2); - } -} diff --git a/crates/vestige-mcp/src/tools/codebase.rs b/crates/vestige-mcp/src/tools/codebase.rs deleted file mode 100644 index 3d51a4b..0000000 --- a/crates/vestige-mcp/src/tools/codebase.rs +++ /dev/null @@ -1,298 +0,0 @@ -//! Codebase Tools (Deprecated - use codebase_unified instead) -//! -//! Remember patterns, decisions, and context about codebases. -//! This is a differentiating feature for AI-assisted development. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{IngestInput, Storage}; - -/// Input schema for remember_pattern tool -pub fn pattern_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name/title for this pattern" - }, - "description": { - "type": "string", - "description": "Detailed description of the pattern" - }, - "files": { - "type": "array", - "items": { "type": "string" }, - "description": "Files where this pattern is used" - }, - "codebase": { - "type": "string", - "description": "Codebase/project identifier (e.g., 'vestige-tauri')" - } - }, - "required": ["name", "description"] - }) -} - -/// Input schema for remember_decision tool -pub fn decision_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "decision": { - "type": "string", - "description": "The architectural or design decision made" - }, - "rationale": { - "type": "string", - "description": "Why this decision was made" - }, - "alternatives": { - "type": "array", - "items": { "type": "string" }, - "description": "Alternatives that were considered" - }, - "files": { - "type": "array", - "items": { "type": "string" }, - "description": "Files affected by this decision" - }, - "codebase": { - "type": "string", - "description": "Codebase/project identifier" - } - }, - "required": ["decision", "rationale"] - }) -} - -/// Input schema for get_codebase_context tool -pub fn context_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "codebase": { - "type": "string", - "description": "Codebase/project identifier to get context for" - }, - "limit": { - "type": "integer", - "description": "Maximum items per category (default: 10)", - "default": 10 - } - }, - "required": [] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct PatternArgs { - name: String, - description: String, - files: Option>, - codebase: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DecisionArgs { - decision: String, - rationale: String, - alternatives: Option>, - files: Option>, - codebase: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ContextArgs { - codebase: Option, - limit: Option, -} - -pub async fn execute_pattern(storage: &Arc, args: Option) -> Result { - let args: PatternArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.name.trim().is_empty() { - return Err("Pattern name cannot be empty".to_string()); - } - - // Build content with structured format - let mut content = format!("# Code Pattern: {}\n\n{}", args.name, args.description); - - if let Some(ref files) = args.files - && !files.is_empty() - { - content.push_str("\n\n## Files:\n"); - for f in files { - content.push_str(&format!("- {}\n", f)); - } - } - - // Build tags - let mut tags = vec!["pattern".to_string(), "codebase".to_string()]; - if let Some(ref codebase) = args.codebase { - tags.push(format!("codebase:{}", codebase)); - } - - let input = IngestInput { - content, - node_type: "pattern".to_string(), - source: args.codebase.clone(), - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags, - valid_from: None, - valid_until: None, - }; - - let node = storage.ingest(input).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "nodeId": node.id, - "patternName": args.name, - "message": format!("Pattern '{}' remembered successfully", args.name), - })) -} - -pub async fn execute_decision( - storage: &Arc, - args: Option, -) -> Result { - let args: DecisionArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.decision.trim().is_empty() { - return Err("Decision cannot be empty".to_string()); - } - - // Build content with structured format (ADR-like) - let mut content = format!( - "# Decision: {}\n\n## Context\n\n{}\n\n## Decision\n\n{}", - &args.decision[..args.decision.len().min(50)], - args.rationale, - args.decision - ); - - if let Some(ref alternatives) = args.alternatives - && !alternatives.is_empty() - { - content.push_str("\n\n## Alternatives Considered:\n"); - for alt in alternatives { - content.push_str(&format!("- {}\n", alt)); - } - } - - if let Some(ref files) = args.files - && !files.is_empty() - { - content.push_str("\n\n## Affected Files:\n"); - for f in files { - content.push_str(&format!("- {}\n", f)); - } - } - - // Build tags - let mut tags = vec![ - "decision".to_string(), - "architecture".to_string(), - "codebase".to_string(), - ]; - if let Some(ref codebase) = args.codebase { - tags.push(format!("codebase:{}", codebase)); - } - - let input = IngestInput { - content, - node_type: "decision".to_string(), - source: args.codebase.clone(), - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags, - valid_from: None, - valid_until: None, - }; - - let node = storage.ingest(input).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "nodeId": node.id, - "message": "Architectural decision remembered successfully", - })) -} - -pub async fn execute_context(storage: &Arc, args: Option) -> Result { - let args: ContextArgs = args - .map(serde_json::from_value) - .transpose() - .map_err(|e| format!("Invalid arguments: {}", e))? - .unwrap_or(ContextArgs { - codebase: None, - limit: Some(10), - }); - - let limit = args.limit.unwrap_or(10).clamp(1, 50); - - // Build tag filter for codebase - // Tags are stored as: ["pattern", "codebase", "codebase:vestige"] - // We search for the "codebase:{name}" tag - let tag_filter = args.codebase.as_ref().map(|cb| format!("codebase:{}", cb)); - - // Query patterns by node_type and tag - let patterns = storage - .get_nodes_by_type_and_tag("pattern", tag_filter.as_deref(), limit) - .unwrap_or_default(); - - // Query decisions by node_type and tag - let decisions = storage - .get_nodes_by_type_and_tag("decision", tag_filter.as_deref(), limit) - .unwrap_or_default(); - - let formatted_patterns: Vec = patterns - .iter() - .map(|n| { - serde_json::json!({ - "id": n.id, - "content": n.content, - "tags": n.tags, - "retentionStrength": n.retention_strength, - "createdAt": n.created_at.to_rfc3339(), - }) - }) - .collect(); - - let formatted_decisions: Vec = decisions - .iter() - .map(|n| { - serde_json::json!({ - "id": n.id, - "content": n.content, - "tags": n.tags, - "retentionStrength": n.retention_strength, - "createdAt": n.created_at.to_rfc3339(), - }) - }) - .collect(); - - Ok(serde_json::json!({ - "codebase": args.codebase, - "patterns": { - "count": formatted_patterns.len(), - "items": formatted_patterns, - }, - "decisions": { - "count": formatted_decisions.len(), - "items": formatted_decisions, - }, - })) -} diff --git a/crates/vestige-mcp/src/tools/consolidate.rs b/crates/vestige-mcp/src/tools/consolidate.rs deleted file mode 100644 index ab9e22b..0000000 --- a/crates/vestige-mcp/src/tools/consolidate.rs +++ /dev/null @@ -1,36 +0,0 @@ -//! Consolidation Tool (Deprecated) -//! -//! Run memory consolidation cycle with FSRS decay and embedding generation. - -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::Storage; - -/// Input schema for run_consolidation tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {}, - }) -} - -pub async fn execute(storage: &Arc) -> Result { - let result = storage.run_consolidation().map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "nodesProcessed": result.nodes_processed, - "nodesPromoted": result.nodes_promoted, - "nodesPruned": result.nodes_pruned, - "decayApplied": result.decay_applied, - "embeddingsGenerated": result.embeddings_generated, - "durationMs": result.duration_ms, - "message": format!( - "Consolidation complete: {} nodes processed, {} embeddings generated, {}ms", - result.nodes_processed, - result.embeddings_generated, - result.duration_ms - ), - })) -} diff --git a/crates/vestige-mcp/src/tools/contradictions.rs b/crates/vestige-mcp/src/tools/contradictions.rs new file mode 100644 index 0000000..33c0038 --- /dev/null +++ b/crates/vestige-mcp/src/tools/contradictions.rs @@ -0,0 +1,213 @@ +//! First-class contradiction surface. +//! +//! `deep_reference` already computes trust-weighted contradiction pairs while +//! answering a specific question. This tool exposes the same local logic as an +//! inspectable memory-health operation. + +use chrono::{DateTime, Utc}; +use serde::Deserialize; +use serde_json::Value; +use std::sync::Arc; + +use crate::tools::cross_reference::{appears_contradictory, compute_trust, topic_overlap}; +use vestige_core::{KnowledgeNode, Storage}; + +pub fn schema() -> Value { + serde_json::json!({ + "type": "object", + "properties": { + "topic": { + "type": "string", + "description": "Optional topic/query to scope contradiction detection. If omitted, scans recent memories." + }, + "since": { + "type": "string", + "description": "Optional RFC3339 timestamp; only memories updated after this time are considered." + }, + "min_trust": { + "type": "number", + "description": "Minimum trust score for both sides of a contradiction.", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.3 + }, + "limit": { + "type": "integer", + "description": "Maximum memories to analyze before pairwise contradiction detection.", + "minimum": 2, + "maximum": 200, + "default": 50 + } + } + }) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct ContradictionArgs { + topic: Option, + since: Option, + #[serde(alias = "min_trust")] + min_trust: Option, + limit: Option, +} + +pub async fn execute(storage: &Arc, args: Option) -> Result { + let args: ContradictionArgs = match args { + Some(value) => { + serde_json::from_value(value).map_err(|e| format!("Invalid arguments: {}", e))? + } + None => ContradictionArgs { + topic: None, + since: None, + min_trust: None, + limit: None, + }, + }; + + let limit = args.limit.unwrap_or(50).clamp(2, 200); + let min_trust = args.min_trust.unwrap_or(0.3).clamp(0.0, 1.0); + let since = match args.since.as_deref() { + Some(raw) => Some( + DateTime::parse_from_rfc3339(raw) + .map_err(|e| format!("Invalid since timestamp: {}", e))? + .with_timezone(&Utc), + ), + None => None, + }; + + let mut memories = if let Some(topic) = args.topic.as_deref().filter(|s| !s.trim().is_empty()) { + storage + .hybrid_search(topic, limit, 0.3, 0.7) + .map_err(|e| e.to_string())? + .into_iter() + .map(|result| result.node) + .collect::>() + } else { + storage.get_all_nodes(limit, 0).map_err(|e| e.to_string())? + }; + + if let Some(since) = since { + memories.retain(|memory| memory.updated_at >= since); + } + + let contradictions = find_contradictions(&memories, min_trust); + + Ok(serde_json::json!({ + "topic": args.topic, + "memoriesAnalyzed": memories.len(), + "minTrust": min_trust, + "contradictionsFound": contradictions.len(), + "contradictions": contradictions, + })) +} + +fn find_contradictions(memories: &[KnowledgeNode], min_trust: f64) -> Vec { + let mut contradictions = Vec::new(); + + for i in 0..memories.len() { + for j in (i + 1)..memories.len() { + let a = &memories[i]; + let b = &memories[j]; + let overlap = topic_overlap(&a.content, &b.content); + if overlap < 0.4 || !appears_contradictory(&a.content, &b.content) { + continue; + } + + let a_trust = trust_for(a); + let b_trust = trust_for(b); + if a_trust.min(b_trust) < min_trust { + continue; + } + + let (stronger, stronger_trust, weaker, weaker_trust) = if a_trust >= b_trust { + (a, a_trust, b, b_trust) + } else { + (b, b_trust, a, a_trust) + }; + + contradictions.push(serde_json::json!({ + "stronger": memory_card(stronger, stronger_trust), + "weaker": memory_card(weaker, weaker_trust), + "topicOverlap": overlap, + })); + } + } + + contradictions.sort_by(|a, b| { + let a_overlap = a["topicOverlap"].as_f64().unwrap_or(0.0); + let b_overlap = b["topicOverlap"].as_f64().unwrap_or(0.0); + b_overlap + .partial_cmp(&a_overlap) + .unwrap_or(std::cmp::Ordering::Equal) + }); + contradictions +} + +fn trust_for(memory: &KnowledgeNode) -> f64 { + compute_trust( + memory.retention_strength, + memory.stability, + memory.reps, + memory.lapses, + ) +} + +fn memory_card(memory: &KnowledgeNode, trust: f64) -> Value { + serde_json::json!({ + "id": memory.id.clone(), + "preview": memory.content.chars().take(200).collect::(), + "trust": (trust * 100.0).round() / 100.0, + "updatedAt": memory.updated_at.to_rfc3339(), + "tags": memory.tags.clone(), + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use vestige_core::IngestInput; + + async fn test_storage() -> (Arc, TempDir) { + let dir = TempDir::new().unwrap(); + let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); + (Arc::new(storage), dir) + } + + #[tokio::test] + async fn test_contradictions_reports_conflicting_memories() { + let (storage, _dir) = test_storage().await; + storage + .ingest(IngestInput { + content: + "For the release workflow we always run cargo test before publishing Vestige" + .to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + storage + .ingest(IngestInput { + content: + "Correction: for the release workflow we never run cargo test before publishing Vestige" + .to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let result = execute( + &storage, + Some(serde_json::json!({ + "topic": "release workflow cargo test Vestige", + "min_trust": 0.0 + })), + ) + .await + .unwrap(); + + assert_eq!(result["contradictionsFound"], 1); + assert!(result["contradictions"][0]["stronger"]["id"].is_string()); + } +} diff --git a/crates/vestige-mcp/src/tools/cross_reference.rs b/crates/vestige-mcp/src/tools/cross_reference.rs index 6a56da6..e1a9128 100644 --- a/crates/vestige-mcp/src/tools/cross_reference.rs +++ b/crates/vestige-mcp/src/tools/cross_reference.rs @@ -58,7 +58,7 @@ struct DeepRefArgs { /// Compute trust score from FSRS-6 memory state. /// Higher = more trustworthy (frequently accessed, high retention, stable, few lapses). -fn compute_trust(retention: f64, stability: f64, reps: i32, lapses: i32) -> f64 { +pub(crate) fn compute_trust(retention: f64, stability: f64, reps: i32, lapses: i32) -> f64 { let retention_factor = retention * 0.4; let stability_factor = (stability / 30.0).min(1.0) * 0.2; let reps_factor = (reps as f64 / 10.0).min(1.0) * 0.2; @@ -204,8 +204,13 @@ fn assess_relation( }; } - // Contradiction: same topic + correction signals detected - if has_correction && topic_sim > 0.15 { + // Contradiction: same topic + correction signals detected. + // Require HIGH similarity (>= 0.55). Previous 0.15 threshold was a keyword- + // coincidence floor, not a shared-topic floor — it fired on any two memories + // sharing 2+ words where either one happened to contain "fix" / "updated". + // A real contradiction needs the two memories to be *about the same thing*, + // not merely in the same domain. + if has_correction && topic_sim > 0.55 { return RelationAssessment { relation: Relation::Contradicts, confidence: topic_sim as f64 * 0.8, @@ -345,6 +350,11 @@ fn generate_reasoning_chain( // Contradiction Detection (enhanced with relation assessment) // ============================================================================ +// Each pair is ("negative form", "positive form"). A contradiction requires +// one memory to contain the negative AND the other to contain the positive +// (or vice versa). Previously we had wildcard entries like ("not ", "") that +// fired on any asymmetric presence of "not " — matched millions of innocent +// sentences ("FSRS-6 is not yet..." vs anything without the word "not"). const NEGATION_PAIRS: &[(&str, &str)] = &[ ("don't", "do"), ("never", "always"), @@ -355,8 +365,6 @@ const NEGATION_PAIRS: &[(&str, &str)] = &[ ("outdated", "current"), ("removed", "added"), ("disabled", "enabled"), - ("not ", ""), - ("no longer", ""), ]; const CORRECTION_SIGNALS: &[&str] = &[ @@ -376,7 +384,7 @@ const CORRECTION_SIGNALS: &[&str] = &[ "migrated to", ]; -fn appears_contradictory(a: &str, b: &str) -> bool { +pub(crate) fn appears_contradictory(a: &str, b: &str) -> bool { let a_lower = a.to_lowercase(); let b_lower = b.to_lowercase(); @@ -386,26 +394,48 @@ fn appears_contradictory(a: &str, b: &str) -> bool { b_lower.split_whitespace().filter(|w| w.len() > 3).collect(); let shared_words = a_words.intersection(&b_words).count(); - if shared_words < 2 { + // Require ≥4 substantive shared words — two memories must be about the + // same thing, not merely brushing the same domain. Previous floor of 2 + // flagged "FSRS-6 upgrade research sources" and "ARC-AGI-3 FSRS-6 v11 + // fixes" as contradictions of "Vestige uses FSRS-6 with 21 parameters" + // because they all mention "FSRS-6" — different applications, same word. + if shared_words < 4 { return false; } - for (neg, _) in NEGATION_PAIRS { + // Negation: one memory carries a negative stance ("don't", "never", + // "avoid", etc.) and the other doesn't. Combined with the shared_words + // ≥ 4 gate above, this means "same subject, opposite position." The + // wildcard `("not ", "")` and `("no longer", "")` entries were dropped + // from NEGATION_PAIRS specifically because "not" / "no longer" are too + // common in natural prose to indicate a stance flip without other + // signals. + for (neg, _opp) in NEGATION_PAIRS { if (a_lower.contains(neg) && !b_lower.contains(neg)) || (b_lower.contains(neg) && !a_lower.contains(neg)) { return true; } } - for signal in CORRECTION_SIGNALS { - if a_lower.contains(signal) || b_lower.contains(signal) { - return true; + // Correction signal: require ≥6 shared substantive words so we know the + // two memories are on the SAME subject, and require the signal to appear + // in exactly one of them (asymmetric — the memory with the correction + // marker is the one superseding the other). Previously fired on ANY + // signal in EITHER memory, which caught every bug-fix memory against + // every related memory as a pairwise contradiction. + if shared_words >= 6 { + for signal in CORRECTION_SIGNALS { + let in_a = a_lower.contains(signal); + let in_b = b_lower.contains(signal); + if in_a != in_b { + return true; + } } } false } -fn topic_overlap(a: &str, b: &str) -> f32 { +pub(crate) fn topic_overlap(a: &str, b: &str) -> f32 { let a_lower = a.to_lowercase(); let b_lower = b.to_lowercase(); let a_words: std::collections::HashSet<&str> = @@ -483,6 +513,7 @@ pub async fn execute( } let mut ranked = results; + #[cfg(feature = "vector-search")] if let Ok(mut cog) = cognitive.try_lock() { let candidates: Vec<_> = ranked .iter() @@ -590,7 +621,10 @@ pub async fn execute( let a = &scored[i]; let b = &scored[j]; let overlap = topic_overlap(&a.content, &b.content); - if overlap < 0.15 { + // Raised from 0.15 to 0.4: STAGE 5 contradiction penalties must + // reflect genuine same-topic conflicts. Domain-keyword overlap + // (e.g. two memories both mentioning "Vestige") shouldn't count. + if overlap < 0.4 { continue; } @@ -647,19 +681,98 @@ pub async fn execute( } } + // ==================================================================== + // Primary Selection (shared by STAGE 7's chain + STAGE 8's recommended) + // ==================================================================== + // Extract the substantive "topic terms" from the query — tokens ≥ 5 chars + // that aren't question words or filler. A memory cannot be primary unless + // it contains at least one of these terms. This catches the class of bug + // where a high-trust, semantically-adjacent memory from an unrelated + // domain beats the actual topic memory because the cross-encoder reranker + // over-weights token-level similarity (e.g. an unrelated security memory + // about "true positives + conservative thresholds" winning an "FSRS-6 trust + // scoring" query because "trust" + "scoring" + "threshold" cluster in + // embedding space, even though the winning memory contains neither + // "FSRS-6" nor anything about spaced repetition). + const TOPIC_STOPWORDS: &[&str] = &[ + "how", "what", "when", "where", "why", "who", "which", "does", "did", "is", "are", "was", + "were", "will", "the", "and", "for", "with", "this", "that", "work", "works", "use", + "uses", "used", "using", "about", "from", "into", "than", "then", + ]; + let topic_terms: Vec = args + .query + .to_lowercase() + .split(|c: char| !c.is_alphanumeric() && c != '-') + .filter(|w| w.len() >= 5 && !TOPIC_STOPWORDS.contains(w)) + .map(|w| w.to_string()) + .collect(); + let has_topic_match = |s: &ScoredMemory| -> bool { + if topic_terms.is_empty() { + return true; // no substantive terms → can't filter, allow all + } + let content_lower = s.content.to_lowercase(); + topic_terms.iter().any(|t| content_lower.contains(t)) + }; + + // Composite score. 50% query relevance (combined_score from hybrid_search + // + reranker), 20% FSRS-6 trust, 30% topic-term match fraction (how many + // of the query's substantive terms appear in the memory). Term match is + // the tie-breaker that promotes on-topic memories within the same trust + // band — trust alone let high-trust off-topic memories win. + let term_presence = |s: &ScoredMemory| -> f64 { + if topic_terms.is_empty() { + return 0.0; + } + let content_lower = s.content.to_lowercase(); + let matches = topic_terms + .iter() + .filter(|t| content_lower.contains(*t)) + .count(); + matches as f64 / topic_terms.len() as f64 + }; + let composite = + |s: &ScoredMemory| s.combined_score as f64 * 0.5 + s.trust * 0.2 + term_presence(s) * 0.3; + + // Build candidate pools. Strictest wins: + // 1. Non-superseded AND has ≥1 query-topic term AND combined_score ≥ 0.25 + // 2. Fall back to non-superseded + has ≥1 query-topic term + // 3. Fall back to all non-superseded (tiny corpus or weak query) + // This way on-topic memories always beat off-topic high-trust ones, and + // we never return "no primary" when evidence exists. + let non_superseded_all: Vec<&ScoredMemory> = scored + .iter() + .filter(|s| !superseded_ids.contains(&s.id)) + .collect(); + let on_topic_relevant: Vec<&ScoredMemory> = non_superseded_all + .iter() + .copied() + .filter(|s| has_topic_match(s) && s.combined_score as f64 >= 0.25) + .collect(); + let on_topic_any: Vec<&ScoredMemory> = non_superseded_all + .iter() + .copied() + .filter(|s| has_topic_match(s)) + .collect(); + let primary_pool: &[&ScoredMemory] = if !on_topic_relevant.is_empty() { + &on_topic_relevant + } else if !on_topic_any.is_empty() { + &on_topic_any + } else { + &non_superseded_all + }; + + let recommended = primary_pool.iter().copied().max_by(|a, b| { + composite(a) + .partial_cmp(&composite(b)) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| a.updated_at.cmp(&b.updated_at)) + }); + // ==================================================================== // STAGE 7: Relation Assessment (per-pair, using trust + temporal + similarity) // ==================================================================== let mut pair_relations: Vec<(String, f64, RelationAssessment)> = Vec::new(); - if let Some(primary) = scored - .iter() - .filter(|s| !superseded_ids.contains(&s.id)) - .max_by(|a, b| { - a.trust - .partial_cmp(&b.trust) - .unwrap_or(std::cmp::Ordering::Equal) - }) - { + if let Some(primary) = recommended { for other in scored.iter().filter(|s| s.id != primary.id).take(15) { // Use combined_score as a proxy for semantic similarity (already reranked) // Fall back to topic_overlap for keyword-level comparison @@ -687,28 +800,12 @@ pub async fn execute( // ==================================================================== // STAGE 8: Synthesis + Reasoning Chain Generation // ==================================================================== - // Composite score: half query relevance (combined_score from - // hybrid_search + reranker) and half FSRS-6 trust. Both signals belong - // in the recommended pick — relevance picks the right *topic*, trust - // picks the most reliable variant within that topic. - let composite = |s: &ScoredMemory| s.combined_score as f64 * 0.5 + s.trust * 0.5; - - // Find the recommended answer: highest composite, not superseded, most recent - let recommended = scored - .iter() - .filter(|s| !superseded_ids.contains(&s.id)) - .max_by(|a, b| { - composite(a) - .partial_cmp(&composite(b)) - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.updated_at.cmp(&b.updated_at)) - }); + // `composite` and `recommended` were computed above (shared with STAGE 7 + // so the chain's PRIMARY FINDING and the citation card's Primary Source + // are always the same memory). // Build evidence list (top memories by composite, not superseded) - let mut non_superseded: Vec<&ScoredMemory> = scored - .iter() - .filter(|s| !superseded_ids.contains(&s.id)) - .collect(); + let mut non_superseded: Vec<&ScoredMemory> = non_superseded_all.clone(); non_superseded.sort_by(|a, b| { composite(b) .partial_cmp(&composite(a)) @@ -1065,7 +1162,7 @@ mod tests { QueryIntent::Timeline ); assert_eq!( - classify_intent("How has the AIMO3 score evolved over time?"), + classify_intent("How has the benchmark score evolved over time?"), QueryIntent::Timeline ); } @@ -1097,7 +1194,7 @@ mod tests { #[test] fn test_intent_synthesis_default() { assert_eq!( - classify_intent("Tell me about Sam's projects"), + classify_intent("Tell me about the user's projects"), QueryIntent::Synthesis ); assert_eq!(classify_intent("What is Vestige?"), QueryIntent::Synthesis); diff --git a/crates/vestige-mcp/src/tools/dedup.rs b/crates/vestige-mcp/src/tools/dedup.rs index 8456629..ea3da25 100644 --- a/crates/vestige-mcp/src/tools/dedup.rs +++ b/crates/vestige-mcp/src/tools/dedup.rs @@ -4,8 +4,10 @@ //! cosine similarity on stored embeddings. Uses union-find for //! efficient clustering. +#[cfg(all(feature = "embeddings", feature = "vector-search"))] use serde::Deserialize; use serde_json::Value; +#[cfg(all(feature = "embeddings", feature = "vector-search"))] use std::collections::HashMap; use std::sync::Arc; @@ -43,6 +45,7 @@ pub fn schema() -> Value { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] +#[cfg(all(feature = "embeddings", feature = "vector-search"))] struct DedupArgs { #[serde(alias = "similarity_threshold")] similarity_threshold: Option, @@ -51,11 +54,13 @@ struct DedupArgs { } /// Simple union-find for clustering +#[cfg(all(feature = "embeddings", feature = "vector-search"))] struct UnionFind { parent: Vec, rank: Vec, } +#[cfg(all(feature = "embeddings", feature = "vector-search"))] impl UnionFind { fn new(n: usize) -> Self { Self { @@ -89,21 +94,22 @@ impl UnionFind { } pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: DedupArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => DedupArgs { - similarity_threshold: None, - limit: None, - tags: None, - }, - }; - - let threshold = args.similarity_threshold.unwrap_or(0.80) as f32; - let limit = args.limit.unwrap_or(20); - let tag_filter = args.tags.unwrap_or_default(); - #[cfg(all(feature = "embeddings", feature = "vector-search"))] { + let args: DedupArgs = match args { + Some(v) => { + serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))? + } + None => DedupArgs { + similarity_threshold: None, + limit: None, + tags: None, + }, + }; + let threshold = args.similarity_threshold.unwrap_or(0.80) as f32; + let limit = args.limit.unwrap_or(20); + let tag_filter = args.tags.unwrap_or_default(); + // Load all embeddings let all_embeddings = storage .get_all_embeddings() @@ -261,6 +267,8 @@ pub async fn execute(storage: &Arc, args: Option) -> Result serde_json::Value { "type": "integer", "description": "Number of recent memories to dream about (default: 50)", "default": 50 + }, + "min_similarity": { + "type": "number", + "description": "Minimum similarity for connection discovery (0.0-1.0, default: 0.5)", + "minimum": 0.0, + "maximum": 1.0, + "default": 0.5 } } }) @@ -32,6 +39,11 @@ pub async fn execute( .and_then(|v| v.as_u64()) .unwrap_or(50) .min(500) as usize; // Cap at 500 to prevent O(N^2) hang + let min_similarity = args + .as_ref() + .and_then(|a| a.get("min_similarity")) + .and_then(|v| v.as_f64()) + .map(|v| v.clamp(0.0, 1.0)); // v1.9.0: Waking SWR tagging — preferential replay of tagged memories (70/30 split) let tagged_nodes = storage @@ -95,15 +107,18 @@ pub async fn execute( .collect(); let cog = cognitive.lock().await; - // Capture start time before the dream so we can identify newly discovered - // connections by timestamp rather than by buffer position. This is robust - // against the composite-score eviction sort in store_connections, which - // reorders the buffer and makes positional slicing (pre_dream_count..) - // unreliable. - let dream_start = Utc::now(); - let dream_result = cog.dreamer.dream(&dream_memories).await; + let (dream_result, new_connections) = if let Some(min_similarity) = min_similarity { + let config = vestige_core::DreamConfig { + min_similarity, + ..vestige_core::DreamConfig::default() + }; + cog.dreamer + .dream_with_config_and_connections(&dream_memories, config) + .await + } else { + cog.dreamer.dream_with_connections(&dream_memories).await + }; let insights = cog.dreamer.synthesize_insights(&dream_memories); - let all_connections = cog.dreamer.get_connections(); drop(cog); // v2.1.0: Persist dream insights to database (Bug #4 fix) @@ -126,17 +141,10 @@ pub async fn execute( } } - // Identify new connections from this dream by timestamp rather than buffer - // position — positional slicing is broken after composite-score eviction - // reorders the buffer. - let new_connections: Vec<&vestige_core::DiscoveredConnection> = all_connections - .iter() - .filter(|c| c.discovered_at >= dream_start) - .collect(); let mut connections_persisted = 0u64; { let now = Utc::now(); - for conn in new_connections.iter() { + for conn in &new_connections { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -178,7 +186,7 @@ pub async fn execute( // Hydrate live cognitive engine with newly persisted connections if connections_persisted > 0 { let mut cog = cognitive.lock().await; - for conn in new_connections.iter() { + for conn in &new_connections { let link_type_enum = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => LinkType::Semantic, vestige_core::DiscoveredConnectionType::SharedConcept => LinkType::Semantic, @@ -286,6 +294,9 @@ mod tests { assert_eq!(s["type"], "object"); assert!(s["properties"]["memory_count"].is_object()); assert_eq!(s["properties"]["memory_count"]["default"], 50); + assert!(s["properties"]["min_similarity"].is_object()); + assert_eq!(s["properties"]["min_similarity"]["minimum"], 0.0); + assert_eq!(s["properties"]["min_similarity"]["maximum"], 1.0); } #[tokio::test] @@ -616,6 +627,42 @@ mod tests { ); } + #[tokio::test] + async fn test_dream_persists_dense_connection_set_above_legacy_buffer_cap() { + let (storage, _dir) = test_storage().await; + ingest_n_memories(&storage, 50).await; + + let result = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ + "memory_count": 50, + "min_similarity": 0.1 + })), + ) + .await + .unwrap(); + + assert_eq!(result["status"], "dreamed"); + let found = result["stats"]["new_connections_found"] + .as_u64() + .unwrap_or(0); + let persisted = result["connectionsPersisted"].as_u64().unwrap_or(0); + + assert!( + found > 1_000, + "test setup should discover more than the legacy 1,000 connection cap" + ); + assert_eq!( + persisted, found, + "dense dreams should persist every connection discovered in the run" + ); + assert_eq!( + storage.get_all_connections().unwrap().len(), + persisted as usize + ); + } + #[tokio::test] async fn test_dream_persists_insights() { let (storage, _dir) = test_storage().await; diff --git a/crates/vestige-mcp/src/tools/ingest.rs b/crates/vestige-mcp/src/tools/ingest.rs deleted file mode 100644 index d724445..0000000 --- a/crates/vestige-mcp/src/tools/ingest.rs +++ /dev/null @@ -1,456 +0,0 @@ -//! Ingest Tool -//! -//! Add new knowledge to memory. -//! -//! v1.5.0: Enhanced with same cognitive pipeline as smart_ingest: -//! Pre-ingest: importance scoring + intent detection -//! Post-ingest: synaptic tagging + novelty model update + hippocampal indexing - -use chrono::Utc; -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; -use tokio::sync::Mutex; - -use crate::cognitive::CognitiveEngine; -use vestige_core::{ - ContentType, ImportanceContext, ImportanceEvent, ImportanceEventType, IngestInput, Storage, -}; - -/// Input schema for ingest tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "content": { - "type": "string", - "description": "The content to remember" - }, - "node_type": { - "type": "string", - "description": "Type of knowledge: fact, concept, event, person, place, note, pattern, decision", - "default": "fact" - }, - "tags": { - "type": "array", - "items": { "type": "string" }, - "description": "Tags for categorization" - }, - "source": { - "type": "string", - "description": "Source or reference for this knowledge" - } - }, - "required": ["content"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct IngestArgs { - content: String, - node_type: Option, - tags: Option>, - source: Option, -} - -pub async fn execute( - storage: &Arc, - cognitive: &Arc>, - args: Option, -) -> Result { - let args: IngestArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - // Validate content - if args.content.trim().is_empty() { - return Err("Content cannot be empty".to_string()); - } - - if args.content.len() > 1_000_000 { - return Err("Content too large (max 1MB)".to_string()); - } - - // ==================================================================== - // COGNITIVE PRE-INGEST: importance scoring + intent detection - // ==================================================================== - let mut importance_composite = 0.0_f64; - let mut tags = args.tags.unwrap_or_default(); - let mut is_novel = false; - let mut embedding_strategy = String::new(); - - if let Ok(cog) = cognitive.try_lock() { - // Full 4-channel importance scoring - let context = ImportanceContext::current(); - let importance = cog - .importance_signals - .compute_importance(&args.content, &context); - importance_composite = importance.composite; - - // Standalone novelty check (dopaminergic signal) - let novelty_ctx = vestige_core::neuroscience::importance_signals::Context::default(); - is_novel = cog.novelty_signal.is_novel(&args.content, &novelty_ctx); - - // Intent detection → auto-tag - let intent_result = cog.intent_detector.detect_intent(); - if intent_result.confidence > 0.5 { - let intent_tag = format!("intent:{:?}", intent_result.primary_intent); - let intent_tag = if intent_tag.len() > 50 { - format!("{}...", &intent_tag[..intent_tag.floor_char_boundary(47)]) - } else { - intent_tag - }; - tags.push(intent_tag); - } - - // Detect content type → select adaptive embedding strategy - let content_type = ContentType::detect(&args.content); - let strategy = cog.adaptive_embedder.select_strategy(&content_type); - embedding_strategy = format!("{:?}", strategy); - } - - let input = IngestInput { - content: args.content.clone(), - node_type: args.node_type.unwrap_or_else(|| "fact".to_string()), - source: args.source, - sentiment_score: 0.0, - sentiment_magnitude: importance_composite, - tags, - valid_from: None, - valid_until: None, - }; - - // ==================================================================== - // INGEST (storage lock) - // ==================================================================== - - // Route through smart_ingest when embeddings are available to prevent duplicates. - // Falls back to raw ingest only when embeddings aren't ready. - #[cfg(all(feature = "embeddings", feature = "vector-search"))] - { - let fallback_input = input.clone(); - match storage.smart_ingest(input) { - Ok(result) => { - let node_id = result.node.id.clone(); - let node_content = result.node.content.clone(); - let node_type = result.node.node_type.clone(); - let has_embedding = result.node.has_embedding.unwrap_or(false); - - run_post_ingest( - cognitive, - &node_id, - &node_content, - &node_type, - importance_composite, - ); - - Ok(serde_json::json!({ - "success": true, - "nodeId": node_id, - "decision": result.decision, - "message": format!("Knowledge ingested successfully. Node ID: {} ({})", node_id, result.decision), - "hasEmbedding": has_embedding, - "similarity": result.similarity, - "reason": result.reason, - "isNovel": is_novel, - "embeddingStrategy": embedding_strategy, - })) - } - Err(_) => { - let node = storage.ingest(fallback_input).map_err(|e| e.to_string())?; - let node_id = node.id.clone(); - let node_content = node.content.clone(); - let node_type = node.node_type.clone(); - let has_embedding = node.has_embedding.unwrap_or(false); - - run_post_ingest( - cognitive, - &node_id, - &node_content, - &node_type, - importance_composite, - ); - - Ok(serde_json::json!({ - "success": true, - "nodeId": node_id, - "decision": "create", - "message": format!("Knowledge ingested successfully. Node ID: {}", node_id), - "hasEmbedding": has_embedding, - "isNovel": is_novel, - "embeddingStrategy": embedding_strategy, - })) - } - } - } - - // Fallback for builds without embedding features - #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] - { - let node = storage.ingest(input).map_err(|e| e.to_string())?; - let node_id = node.id.clone(); - let node_content = node.content.clone(); - let node_type = node.node_type.clone(); - let has_embedding = node.has_embedding.unwrap_or(false); - - run_post_ingest( - cognitive, - &node_id, - &node_content, - &node_type, - importance_composite, - ); - - Ok(serde_json::json!({ - "success": true, - "nodeId": node_id, - "decision": "create", - "message": format!("Knowledge ingested successfully. Node ID: {}", node_id), - "hasEmbedding": has_embedding, - "isNovel": is_novel, - "embeddingStrategy": embedding_strategy, - })) - } -} - -/// Cognitive post-ingest side effects: synaptic tagging, novelty update, hippocampal indexing. -fn run_post_ingest( - cognitive: &Arc>, - node_id: &str, - content: &str, - node_type: &str, - importance_composite: f64, -) { - if let Ok(mut cog) = cognitive.try_lock() { - // Synaptic tagging for retroactive capture - if importance_composite > 0.3 { - cog.synaptic_tagging.tag_memory(node_id); - if importance_composite > 0.7 { - let event = ImportanceEvent::for_memory(node_id, ImportanceEventType::NoveltySpike); - let _capture = cog.synaptic_tagging.trigger_prp(event); - } - } - - // Update novelty model - cog.importance_signals.learn_content(content); - - // Record in hippocampal index - let _ = cog - .hippocampal_index - .index_memory(node_id, content, node_type, Utc::now(), None); - - // Cross-project pattern recording - cog.cross_project - .record_project_memory(node_id, "default", None); - } -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use crate::cognitive::CognitiveEngine; - use tempfile::TempDir; - - fn test_cognitive() -> Arc> { - Arc::new(Mutex::new(CognitiveEngine::new())) - } - - /// Create a test storage instance with a temporary database - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - // ======================================================================== - // INPUT VALIDATION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_empty_content_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "content": "" }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_ingest_whitespace_only_content_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "content": " \n\t " }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_ingest_missing_arguments_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_ingest_missing_content_field_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "node_type": "fact" }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Invalid arguments")); - } - - // ======================================================================== - // LARGE CONTENT TESTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_large_content_fails() { - let (storage, _dir) = test_storage().await; - // Create content larger than 1MB - let large_content = "x".repeat(1_000_001); - let args = serde_json::json!({ "content": large_content }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("too large")); - } - - #[tokio::test] - async fn test_ingest_exactly_1mb_succeeds() { - let (storage, _dir) = test_storage().await; - // Create content exactly 1MB - let exact_content = "x".repeat(1_000_000); - let args = serde_json::json!({ "content": exact_content }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - } - - // ======================================================================== - // SUCCESSFUL INGEST TESTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_basic_content_succeeds() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "This is a test fact to remember." - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["nodeId"].is_string()); - assert!(value["message"].as_str().unwrap().contains("successfully")); - } - - #[tokio::test] - async fn test_ingest_with_node_type() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "Error handling should use Result pattern.", - "node_type": "pattern" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_ingest_with_tags() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "The Rust programming language emphasizes safety.", - "tags": ["rust", "programming", "safety"] - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_ingest_with_source() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "MCP protocol version 2024-11-05 is the current standard.", - "source": "https://modelcontextprotocol.io/spec" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - } - - #[tokio::test] - async fn test_ingest_with_all_optional_fields() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "Complex memory with all metadata.", - "node_type": "decision", - "tags": ["architecture", "design"], - "source": "team meeting notes" - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["nodeId"].is_string()); - } - - // ======================================================================== - // NODE TYPE DEFAULTS - // ======================================================================== - - #[tokio::test] - async fn test_ingest_default_node_type_is_fact() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "content": "Default type test content." - }); - let result = execute(&storage, &test_cognitive(), Some(args)).await; - assert!(result.is_ok()); - - // Verify node was created - the default type is "fact" - let node_id = result.unwrap()["nodeId"].as_str().unwrap().to_string(); - let node = storage.get_node(&node_id).unwrap().unwrap(); - assert_eq!(node.node_type, "fact"); - } - - // ======================================================================== - // SCHEMA TESTS - // ======================================================================== - - #[test] - fn test_schema_has_required_fields() { - let schema_value = schema(); - assert_eq!(schema_value["type"], "object"); - assert!(schema_value["properties"]["content"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("content")) - ); - } - - #[test] - fn test_schema_has_optional_fields() { - let schema_value = schema(); - assert!(schema_value["properties"]["node_type"].is_object()); - assert!(schema_value["properties"]["tags"].is_object()); - assert!(schema_value["properties"]["source"].is_object()); - } -} diff --git a/crates/vestige-mcp/src/tools/intentions.rs b/crates/vestige-mcp/src/tools/intentions.rs deleted file mode 100644 index 169ac43..0000000 --- a/crates/vestige-mcp/src/tools/intentions.rs +++ /dev/null @@ -1,1093 +0,0 @@ -//! Intentions Tools (Deprecated - use intention_unified instead) -//! -//! Prospective memory tools for setting and checking future intentions. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use chrono::{DateTime, Duration, Utc}; -use uuid::Uuid; - -use vestige_core::{IntentionRecord, Storage}; - -/// Schema for set_intention tool -pub fn set_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "What to remember to do" - }, - "trigger": { - "type": "object", - "description": "When to trigger this intention", - "properties": { - "type": { - "type": "string", - "enum": ["time", "context", "event"], - "description": "Trigger type: time-based, context-based, or event-based" - }, - "at": { - "type": "string", - "description": "ISO timestamp for time-based triggers" - }, - "in_minutes": { - "type": "integer", - "description": "Minutes from now for duration-based triggers" - }, - "codebase": { - "type": "string", - "description": "Trigger when working in this codebase" - }, - "file_pattern": { - "type": "string", - "description": "Trigger when editing files matching this pattern" - }, - "topic": { - "type": "string", - "description": "Trigger when discussing this topic" - }, - "condition": { - "type": "string", - "description": "Natural language condition for event triggers" - } - } - }, - "priority": { - "type": "string", - "enum": ["low", "normal", "high", "critical"], - "default": "normal", - "description": "Priority level" - }, - "deadline": { - "type": "string", - "description": "Optional deadline (ISO timestamp)" - } - }, - "required": ["description"] - }) -} - -/// Schema for check_intentions tool -pub fn check_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "context": { - "type": "object", - "description": "Current context for matching intentions", - "properties": { - "current_time": { - "type": "string", - "description": "Current ISO timestamp (defaults to now)" - }, - "codebase": { - "type": "string", - "description": "Current codebase/project name" - }, - "file": { - "type": "string", - "description": "Current file path" - }, - "topics": { - "type": "array", - "items": { "type": "string" }, - "description": "Current discussion topics" - } - } - }, - "include_snoozed": { - "type": "boolean", - "default": false, - "description": "Include snoozed intentions" - } - } - }) -} - -/// Schema for complete_intention tool -pub fn complete_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "intentionId": { - "type": "string", - "description": "ID of the intention to mark as complete" - } - }, - "required": ["intentionId"] - }) -} - -/// Schema for snooze_intention tool -pub fn snooze_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "intentionId": { - "type": "string", - "description": "ID of the intention to snooze" - }, - "minutes": { - "type": "integer", - "description": "Minutes to snooze for", - "default": 30 - } - }, - "required": ["intentionId"] - }) -} - -/// Schema for list_intentions tool -pub fn list_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": ["active", "fulfilled", "cancelled", "snoozed", "all"], - "default": "active", - "description": "Filter by status" - }, - "limit": { - "type": "integer", - "default": 20, - "description": "Maximum number to return" - } - } - }) -} - -#[derive(Debug, Deserialize, serde::Serialize)] -#[serde(rename_all = "camelCase")] -struct TriggerSpec { - #[serde(rename = "type")] - trigger_type: Option, - at: Option, - in_minutes: Option, - codebase: Option, - file_pattern: Option, - topic: Option, - condition: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SetIntentionArgs { - description: String, - trigger: Option, - priority: Option, - deadline: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ContextSpec { - #[allow(dead_code)] // Deserialized from JSON but not yet used in context matching - current_time: Option, - codebase: Option, - file: Option, - topics: Option>, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct CheckIntentionsArgs { - context: Option, - #[allow(dead_code)] // Deserialized from JSON for future snoozed intentions filter - include_snoozed: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct IntentionIdArgs { - intention_id: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SnoozeArgs { - intention_id: String, - minutes: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct ListArgs { - status: Option, - limit: Option, -} - -/// Execute set_intention tool -pub async fn execute_set(storage: &Arc, args: Option) -> Result { - let args: SetIntentionArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.description.trim().is_empty() { - return Err("Description cannot be empty".to_string()); - } - - let now = Utc::now(); - let id = Uuid::new_v4().to_string(); - - // Determine trigger type and data - let (trigger_type, trigger_data) = if let Some(trigger) = &args.trigger { - let t_type = trigger - .trigger_type - .clone() - .unwrap_or_else(|| "time".to_string()); - let data = serde_json::to_string(trigger).unwrap_or_else(|_| "{}".to_string()); - (t_type, data) - } else { - ("manual".to_string(), "{}".to_string()) - }; - - // Parse priority - let priority = match args.priority.as_deref() { - Some("low") => 1, - Some("high") => 3, - Some("critical") => 4, - _ => 2, // normal - }; - - // Parse deadline - let deadline = args.deadline.and_then(|s| { - DateTime::parse_from_rfc3339(&s) - .ok() - .map(|dt| dt.with_timezone(&Utc)) - }); - - // Calculate trigger time if specified - let trigger_at = if let Some(trigger) = &args.trigger { - if let Some(at) = &trigger.at { - DateTime::parse_from_rfc3339(at) - .ok() - .map(|dt| dt.with_timezone(&Utc)) - } else { - trigger.in_minutes.map(|mins| now + Duration::minutes(mins)) - } - } else { - None - }; - - let record = IntentionRecord { - id: id.clone(), - content: args.description.clone(), - trigger_type, - trigger_data, - priority, - status: "active".to_string(), - created_at: now, - deadline, - fulfilled_at: None, - reminder_count: 0, - last_reminded_at: None, - notes: None, - tags: vec![], - related_memories: vec![], - snoozed_until: None, - source_type: "mcp".to_string(), - source_data: None, - }; - - storage.save_intention(&record).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": true, - "intentionId": id, - "message": format!("Intention created: {}", args.description), - "priority": priority, - "triggerAt": trigger_at.map(|dt| dt.to_rfc3339()), - "deadline": deadline.map(|dt| dt.to_rfc3339()), - })) -} - -/// Execute check_intentions tool -pub async fn execute_check(storage: &Arc, args: Option) -> Result { - let args: CheckIntentionsArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => CheckIntentionsArgs { - context: None, - include_snoozed: None, - }, - }; - - let now = Utc::now(); - - // Get active intentions - let intentions = storage.get_active_intentions().map_err(|e| e.to_string())?; - - let mut triggered = Vec::new(); - let mut pending = Vec::new(); - - for intention in intentions { - // Parse trigger data - let trigger: Option = serde_json::from_str(&intention.trigger_data).ok(); - - // Check if triggered - let is_triggered = if let Some(t) = &trigger { - match t.trigger_type.as_deref() { - Some("time") => { - if let Some(at) = &t.at { - if let Ok(trigger_time) = DateTime::parse_from_rfc3339(at) { - trigger_time.with_timezone(&Utc) <= now - } else { - false - } - } else if let Some(mins) = t.in_minutes { - let trigger_time = intention.created_at + Duration::minutes(mins); - trigger_time <= now - } else { - false - } - } - Some("context") => { - if let Some(ctx) = &args.context { - // Check codebase match - if let (Some(trigger_codebase), Some(current_codebase)) = - (&t.codebase, &ctx.codebase) - { - current_codebase - .to_lowercase() - .contains(&trigger_codebase.to_lowercase()) - // Check file pattern match - } else if let (Some(pattern), Some(file)) = (&t.file_pattern, &ctx.file) { - file.contains(pattern) - // Check topic match - } else if let (Some(topic), Some(topics)) = (&t.topic, &ctx.topics) { - topics - .iter() - .any(|t| t.to_lowercase().contains(&topic.to_lowercase())) - } else { - false - } - } else { - false - } - } - _ => false, - } - } else { - false - }; - - // Check if overdue - let is_overdue = intention.deadline.map(|d| d < now).unwrap_or(false); - - let item = serde_json::json!({ - "id": intention.id, - "description": intention.content, - "priority": match intention.priority { - 1 => "low", - 3 => "high", - 4 => "critical", - _ => "normal", - }, - "createdAt": intention.created_at.to_rfc3339(), - "deadline": intention.deadline.map(|d| d.to_rfc3339()), - "isOverdue": is_overdue, - }); - - if is_triggered || is_overdue { - triggered.push(item); - } else { - pending.push(item); - } - } - - Ok(serde_json::json!({ - "triggered": triggered, - "pending": pending, - "checkedAt": now.to_rfc3339(), - })) -} - -/// Execute complete_intention tool -pub async fn execute_complete( - storage: &Arc, - args: Option, -) -> Result { - let args: IntentionIdArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing intention_id".to_string()), - }; - - let updated = storage - .update_intention_status(&args.intention_id, "fulfilled") - .map_err(|e| e.to_string())?; - - if updated { - Ok(serde_json::json!({ - "success": true, - "message": "Intention marked as complete", - "intentionId": args.intention_id, - })) - } else { - Err(format!("Intention not found: {}", args.intention_id)) - } -} - -/// Execute snooze_intention tool -pub async fn execute_snooze(storage: &Arc, args: Option) -> Result { - let args: SnoozeArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing intention_id".to_string()), - }; - - let minutes = args.minutes.unwrap_or(30); - let snooze_until = Utc::now() + Duration::minutes(minutes); - - let updated = storage - .snooze_intention(&args.intention_id, snooze_until) - .map_err(|e| e.to_string())?; - - if updated { - Ok(serde_json::json!({ - "success": true, - "message": format!("Intention snoozed for {} minutes", minutes), - "intentionId": args.intention_id, - "snoozedUntil": snooze_until.to_rfc3339(), - })) - } else { - Err(format!("Intention not found: {}", args.intention_id)) - } -} - -/// Execute list_intentions tool -pub async fn execute_list(storage: &Arc, args: Option) -> Result { - let args: ListArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => ListArgs { - status: None, - limit: None, - }, - }; - - let status = args.status.as_deref().unwrap_or("active"); - - let intentions = if status == "all" { - // Get all by combining different statuses - let mut all = storage.get_active_intentions().map_err(|e| e.to_string())?; - all.extend( - storage - .get_intentions_by_status("fulfilled") - .map_err(|e| e.to_string())?, - ); - all.extend( - storage - .get_intentions_by_status("cancelled") - .map_err(|e| e.to_string())?, - ); - all.extend( - storage - .get_intentions_by_status("snoozed") - .map_err(|e| e.to_string())?, - ); - all - } else if status == "active" { - // Use get_active_intentions for proper priority ordering - storage.get_active_intentions().map_err(|e| e.to_string())? - } else { - storage - .get_intentions_by_status(status) - .map_err(|e| e.to_string())? - }; - - let limit = args.limit.unwrap_or(20) as usize; - let now = Utc::now(); - - let items: Vec = intentions - .into_iter() - .take(limit) - .map(|i| { - let is_overdue = i.deadline.map(|d| d < now).unwrap_or(false); - serde_json::json!({ - "id": i.id, - "description": i.content, - "status": i.status, - "priority": match i.priority { - 1 => "low", - 3 => "high", - 4 => "critical", - _ => "normal", - }, - "createdAt": i.created_at.to_rfc3339(), - "deadline": i.deadline.map(|d| d.to_rfc3339()), - "isOverdue": is_overdue, - "snoozedUntil": i.snoozed_until.map(|d| d.to_rfc3339()), - }) - }) - .collect(); - - Ok(serde_json::json!({ - "intentions": items, - "total": items.len(), - "status": status, - })) -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - /// Create a test storage instance with a temporary database - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - /// Helper to create an intention and return its ID - async fn create_test_intention(storage: &Arc, description: &str) -> String { - let args = serde_json::json!({ - "description": description - }); - let result = execute_set(storage, Some(args)).await.unwrap(); - result["intentionId"].as_str().unwrap().to_string() - } - - // ======================================================================== - // SET_INTENTION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_set_intention_empty_description_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "description": "" }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_set_intention_whitespace_only_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "description": " \t\n " }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_set_intention_missing_arguments_fails() { - let (storage, _dir) = test_storage().await; - let result = execute_set(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_set_intention_basic_succeeds() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Remember to write unit tests" - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["intentionId"].is_string()); - assert!( - value["message"] - .as_str() - .unwrap() - .contains("Intention created") - ); - } - - #[tokio::test] - async fn test_set_intention_with_priority() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Critical bug fix needed", - "priority": "critical" - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["priority"], 4); // critical = 4 - } - - #[tokio::test] - async fn test_set_intention_with_time_trigger() { - let (storage, _dir) = test_storage().await; - let future_time = (Utc::now() + Duration::hours(1)).to_rfc3339(); - let args = serde_json::json!({ - "description": "Meeting reminder", - "trigger": { - "type": "time", - "at": future_time - } - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["triggerAt"].is_string()); - } - - #[tokio::test] - async fn test_set_intention_with_duration_trigger() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Check build status", - "trigger": { - "type": "time", - "inMinutes": 30 - } - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["triggerAt"].is_string()); - } - - #[tokio::test] - async fn test_set_intention_with_context_trigger() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ - "description": "Review error handling", - "trigger": { - "type": "context", - "codebase": "payments" - } - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_set_intention_with_deadline() { - let (storage, _dir) = test_storage().await; - let deadline = (Utc::now() + Duration::days(7)).to_rfc3339(); - let args = serde_json::json!({ - "description": "Complete feature by end of week", - "deadline": deadline - }); - let result = execute_set(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["deadline"].is_string()); - } - - // ======================================================================== - // CHECK_INTENTIONS TESTS - // ======================================================================== - - #[tokio::test] - async fn test_check_intentions_empty_succeeds() { - let (storage, _dir) = test_storage().await; - let result = execute_check(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["triggered"].is_array()); - assert!(value["pending"].is_array()); - assert!(value["checkedAt"].is_string()); - } - - #[tokio::test] - async fn test_check_intentions_returns_pending() { - let (storage, _dir) = test_storage().await; - // Create an intention without immediate trigger - create_test_intention(&storage, "Future task").await; - - let result = execute_check(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let pending = value["pending"].as_array().unwrap(); - assert!(!pending.is_empty()); - } - - #[tokio::test] - async fn test_check_intentions_with_context() { - let (storage, _dir) = test_storage().await; - - // Create context-triggered intention - let args = serde_json::json!({ - "description": "Check tests in payments", - "trigger": { - "type": "context", - "codebase": "payments" - } - }); - execute_set(&storage, Some(args)).await.unwrap(); - - // Check with matching context - let check_args = serde_json::json!({ - "context": { - "codebase": "payments-service" - } - }); - let result = execute_check(&storage, Some(check_args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let triggered = value["triggered"].as_array().unwrap(); - assert!(!triggered.is_empty()); - } - - #[tokio::test] - async fn test_check_intentions_time_triggered() { - let (storage, _dir) = test_storage().await; - - // Create time-triggered intention in the past - let past_time = (Utc::now() - Duration::hours(1)).to_rfc3339(); - let args = serde_json::json!({ - "description": "Past due task", - "trigger": { - "type": "time", - "at": past_time - } - }); - execute_set(&storage, Some(args)).await.unwrap(); - - let result = execute_check(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let triggered = value["triggered"].as_array().unwrap(); - assert!(!triggered.is_empty()); - } - - // ======================================================================== - // COMPLETE_INTENTION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_complete_intention_succeeds() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to complete").await; - - let args = serde_json::json!({ - "intentionId": intention_id - }); - let result = execute_complete(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["message"].as_str().unwrap().contains("complete")); - } - - #[tokio::test] - async fn test_complete_intention_nonexistent_fails() { - let (storage, _dir) = test_storage().await; - let fake_id = Uuid::new_v4().to_string(); - - let args = serde_json::json!({ - "intentionId": fake_id - }); - let result = execute_complete(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not found")); - } - - #[tokio::test] - async fn test_complete_intention_missing_id_fails() { - let (storage, _dir) = test_storage().await; - let result = execute_complete(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing intention_id")); - } - - #[tokio::test] - async fn test_completed_intention_not_in_active_list() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to hide").await; - - // Complete it - let args = serde_json::json!({ "intentionId": intention_id }); - execute_complete(&storage, Some(args)).await.unwrap(); - - // Check active intentions - should not include completed - let list_args = serde_json::json!({ "status": "active" }); - let result = execute_list(&storage, Some(list_args)).await.unwrap(); - let intentions = result["intentions"].as_array().unwrap(); - - let ids: Vec<&str> = intentions - .iter() - .map(|i| i["id"].as_str().unwrap()) - .collect(); - assert!(!ids.contains(&intention_id.as_str())); - } - - // ======================================================================== - // SNOOZE_INTENTION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_snooze_intention_succeeds() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to snooze").await; - - let args = serde_json::json!({ - "intentionId": intention_id, - "minutes": 30 - }); - let result = execute_snooze(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["success"], true); - assert!(value["snoozedUntil"].is_string()); - assert!(value["message"].as_str().unwrap().contains("snoozed")); - } - - #[tokio::test] - async fn test_snooze_intention_default_minutes() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task with default snooze").await; - - let args = serde_json::json!({ - "intentionId": intention_id - }); - let result = execute_snooze(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["message"].as_str().unwrap().contains("30 minutes")); - } - - #[tokio::test] - async fn test_snooze_intention_nonexistent_fails() { - let (storage, _dir) = test_storage().await; - let fake_id = Uuid::new_v4().to_string(); - - let args = serde_json::json!({ - "intentionId": fake_id, - "minutes": 15 - }); - let result = execute_snooze(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("not found")); - } - - #[tokio::test] - async fn test_snooze_intention_missing_id_fails() { - let (storage, _dir) = test_storage().await; - let result = execute_snooze(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing intention_id")); - } - - // ======================================================================== - // LIST_INTENTIONS TESTS - // ======================================================================== - - #[tokio::test] - async fn test_list_intentions_empty_succeeds() { - let (storage, _dir) = test_storage().await; - let result = execute_list(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert!(value["intentions"].is_array()); - assert_eq!(value["total"], 0); - assert_eq!(value["status"], "active"); - } - - #[tokio::test] - async fn test_list_intentions_returns_created() { - let (storage, _dir) = test_storage().await; - create_test_intention(&storage, "First task").await; - create_test_intention(&storage, "Second task").await; - - let result = execute_list(&storage, None).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["total"], 2); - } - - #[tokio::test] - async fn test_list_intentions_filter_by_status() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to complete").await; - - // Complete one - let args = serde_json::json!({ "intentionId": intention_id }); - execute_complete(&storage, Some(args)).await.unwrap(); - - // Create another active one - create_test_intention(&storage, "Active task").await; - - // List fulfilled - let list_args = serde_json::json!({ "status": "fulfilled" }); - let result = execute_list(&storage, Some(list_args)).await.unwrap(); - assert_eq!(result["total"], 1); - assert_eq!(result["status"], "fulfilled"); - } - - #[tokio::test] - async fn test_list_intentions_with_limit() { - let (storage, _dir) = test_storage().await; - for i in 0..5 { - create_test_intention(&storage, &format!("Task {}", i)).await; - } - - let args = serde_json::json!({ "limit": 3 }); - let result = execute_list(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let intentions = value["intentions"].as_array().unwrap(); - assert!(intentions.len() <= 3); - } - - #[tokio::test] - async fn test_list_intentions_all_status() { - let (storage, _dir) = test_storage().await; - let intention_id = create_test_intention(&storage, "Task to complete").await; - create_test_intention(&storage, "Active task").await; - - // Complete one - let args = serde_json::json!({ "intentionId": intention_id }); - execute_complete(&storage, Some(args)).await.unwrap(); - - // List all - let list_args = serde_json::json!({ "status": "all" }); - let result = execute_list(&storage, Some(list_args)).await.unwrap(); - assert_eq!(result["total"], 2); - } - - // ======================================================================== - // INTENTION LIFECYCLE TESTS - // ======================================================================== - - #[tokio::test] - async fn test_intention_full_lifecycle() { - let (storage, _dir) = test_storage().await; - - // 1. Create intention - let intention_id = create_test_intention(&storage, "Full lifecycle test").await; - - // 2. Verify it appears in list - let list_result = execute_list(&storage, None).await.unwrap(); - assert_eq!(list_result["total"], 1); - - // 3. Snooze it - let snooze_args = serde_json::json!({ - "intentionId": intention_id, - "minutes": 5 - }); - let snooze_result = execute_snooze(&storage, Some(snooze_args)).await; - assert!(snooze_result.is_ok()); - - // 4. Complete it - let complete_args = serde_json::json!({ "intentionId": intention_id }); - let complete_result = execute_complete(&storage, Some(complete_args)).await; - assert!(complete_result.is_ok()); - - // 5. Verify it's no longer active - let final_list = execute_list(&storage, None).await.unwrap(); - assert_eq!(final_list["total"], 0); - - // 6. Verify it's in fulfilled list - let fulfilled_args = serde_json::json!({ "status": "fulfilled" }); - let fulfilled_list = execute_list(&storage, Some(fulfilled_args)).await.unwrap(); - assert_eq!(fulfilled_list["total"], 1); - } - - #[tokio::test] - async fn test_intention_priority_ordering() { - let (storage, _dir) = test_storage().await; - - // Create intentions with different priorities - let args_low = serde_json::json!({ - "description": "Low priority task", - "priority": "low" - }); - execute_set(&storage, Some(args_low)).await.unwrap(); - - let args_critical = serde_json::json!({ - "description": "Critical task", - "priority": "critical" - }); - execute_set(&storage, Some(args_critical)).await.unwrap(); - - let args_normal = serde_json::json!({ - "description": "Normal task", - "priority": "normal" - }); - execute_set(&storage, Some(args_normal)).await.unwrap(); - - // List and verify ordering (critical should be first due to priority DESC ordering) - let list_result = execute_list(&storage, None).await.unwrap(); - let intentions = list_result["intentions"].as_array().unwrap(); - - assert!(intentions.len() >= 3); - // Critical (4) should come before normal (2) and low (1) - let first_priority = intentions[0]["priority"].as_str().unwrap(); - assert_eq!(first_priority, "critical"); - } - - // ======================================================================== - // SCHEMA TESTS - // ======================================================================== - - #[test] - fn test_set_schema_has_required_fields() { - let schema_value = set_schema(); - assert_eq!(schema_value["type"], "object"); - assert!(schema_value["properties"]["description"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("description")) - ); - } - - #[test] - fn test_complete_schema_has_required_fields() { - let schema_value = complete_schema(); - assert!(schema_value["properties"]["intentionId"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("intentionId")) - ); - } - - #[test] - fn test_snooze_schema_has_required_fields() { - let schema_value = snooze_schema(); - assert!(schema_value["properties"]["intentionId"].is_object()); - assert!(schema_value["properties"]["minutes"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("intentionId")) - ); - } - - #[test] - fn test_list_schema_has_optional_fields() { - let schema_value = list_schema(); - assert!(schema_value["properties"]["status"].is_object()); - assert!(schema_value["properties"]["limit"].is_object()); - } - - #[test] - fn test_check_schema_has_context_field() { - let schema_value = check_schema(); - assert!(schema_value["properties"]["context"].is_object()); - } -} diff --git a/crates/vestige-mcp/src/tools/knowledge.rs b/crates/vestige-mcp/src/tools/knowledge.rs deleted file mode 100644 index f26646d..0000000 --- a/crates/vestige-mcp/src/tools/knowledge.rs +++ /dev/null @@ -1,106 +0,0 @@ -//! Knowledge Tools (Deprecated - use memory_unified instead) -//! -//! Get and delete specific knowledge nodes. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::Storage; - -/// Input schema for get_knowledge tool -pub fn get_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the knowledge node to retrieve" - } - }, - "required": ["id"] - }) -} - -/// Input schema for delete_knowledge tool -pub fn delete_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The ID of the knowledge node to delete" - } - }, - "required": ["id"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct KnowledgeArgs { - id: String, -} - -pub async fn execute_get(storage: &Arc, args: Option) -> Result { - let args: KnowledgeArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - // Validate UUID - uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?; - - let node = storage.get_node(&args.id).map_err(|e| e.to_string())?; - - match node { - Some(n) => Ok(serde_json::json!({ - "found": true, - "node": { - "id": n.id, - "content": n.content, - "nodeType": n.node_type, - "createdAt": n.created_at.to_rfc3339(), - "updatedAt": n.updated_at.to_rfc3339(), - "lastAccessed": n.last_accessed.to_rfc3339(), - "stability": n.stability, - "difficulty": n.difficulty, - "reps": n.reps, - "lapses": n.lapses, - "storageStrength": n.storage_strength, - "retrievalStrength": n.retrieval_strength, - "retentionStrength": n.retention_strength, - "sentimentScore": n.sentiment_score, - "sentimentMagnitude": n.sentiment_magnitude, - "nextReview": n.next_review.map(|d| d.to_rfc3339()), - "source": n.source, - "tags": n.tags, - "hasEmbedding": n.has_embedding, - "embeddingModel": n.embedding_model, - } - })), - None => Ok(serde_json::json!({ - "found": false, - "nodeId": args.id, - "message": "Node not found", - })), - } -} - -pub async fn execute_delete(storage: &Arc, args: Option) -> Result { - let args: KnowledgeArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - // Validate UUID - uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?; - - let deleted = storage.delete_node(&args.id).map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "success": deleted, - "nodeId": args.id, - "message": if deleted { "Node deleted successfully" } else { "Node not found" }, - })) -} diff --git a/crates/vestige-mcp/src/tools/maintenance.rs b/crates/vestige-mcp/src/tools/maintenance.rs index 892cf0a..82f5374 100644 --- a/crates/vestige-mcp/src/tools/maintenance.rs +++ b/crates/vestige-mcp/src/tools/maintenance.rs @@ -6,12 +6,31 @@ use chrono::{NaiveDate, Utc}; use serde::Deserialize; use serde_json::Value; +use std::path::Path; use std::sync::Arc; use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; use vestige_core::{FSRSScheduler, Storage}; +fn create_private_file(path: &Path) -> std::io::Result { + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(path) + } + + #[cfg(not(unix))] + { + std::fs::File::create(path) + } +} + // ============================================================================ // SCHEMAS // ============================================================================ @@ -36,8 +55,8 @@ pub fn export_schema() -> Value { "properties": { "format": { "type": "string", - "description": "Export format: 'json' (default) or 'jsonl'", - "enum": ["json", "jsonl"], + "description": "Export format: 'json' (default), 'jsonl', or 'portable' for exact Vestige-to-Vestige transfer", + "enum": ["json", "jsonl", "portable"], "default": "json" }, "tags": { @@ -51,7 +70,7 @@ pub fn export_schema() -> Value { }, "path": { "type": "string", - "description": "Custom filename (not path). File is saved in ~/.vestige/exports/. Default: memories-{timestamp}.{format}" + "description": "Custom filename (not path). File is saved in the active Vestige data directory's exports/ folder. Default: memories-{timestamp}.{format}" } } }) @@ -117,7 +136,7 @@ pub async fn execute_system_status( }; let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; @@ -131,12 +150,17 @@ pub async fn execute_system_status( if stats.nodes_due_for_review > 10 { warnings.push("Many memories are due for review"); } - if stats.total_nodes > 0 && stats.nodes_with_embeddings == 0 { - warnings.push("No embeddings generated - semantic search unavailable"); + if stats.total_nodes > 0 && stats.nodes_with_active_embeddings == 0 { + warnings.push("No active-model embeddings generated - semantic search unavailable"); } if embedding_coverage < 50.0 && stats.total_nodes > 10 { warnings.push("Low embedding coverage - run consolidate to improve semantic search"); } + if stats.nodes_with_mismatched_embeddings > 0 { + warnings.push( + "Stored embeddings from another model are present - run consolidate after changing embedding models", + ); + } let mut recommendations = Vec::new(); if status == "critical" { @@ -146,8 +170,8 @@ pub async fn execute_system_status( if stats.nodes_due_for_review > 5 { recommendations.push("Review due memories to strengthen retention."); } - if stats.nodes_with_embeddings < stats.total_nodes { - recommendations.push("Run 'consolidate' to generate missing embeddings."); + if stats.nodes_with_active_embeddings < stats.total_nodes { + recommendations.push("Run 'consolidate' to generate active-model embeddings."); } if stats.total_nodes > 100 && stats.average_retention < 0.7 { recommendations.push("Consider running periodic consolidation."); @@ -233,7 +257,7 @@ pub async fn execute_system_status( Some(dt) => storage.count_memories_since(*dt).unwrap_or(0), None => stats.total_nodes, }; - let last_backup = Storage::get_last_backup_timestamp(); + let last_backup = storage.last_backup_timestamp(); Ok(serde_json::json!({ "tool": "system_status", @@ -249,8 +273,11 @@ pub async fn execute_system_status( "averageStorageStrength": stats.average_storage_strength, "averageRetrievalStrength": stats.average_retrieval_strength, "withEmbeddings": stats.nodes_with_embeddings, + "withActiveEmbeddings": stats.nodes_with_active_embeddings, + "mismatchedEmbeddings": stats.nodes_with_mismatched_embeddings, "embeddingCoverage": format!("{:.1}%", embedding_coverage), "embeddingModel": stats.embedding_model, + "activeEmbeddingModel": stats.active_embedding_model, "oldestMemory": stats.oldest_memory.map(|dt| dt.to_rfc3339()), "newestMemory": stats.newest_memory.map(|dt| dt.to_rfc3339()), // Distribution @@ -299,13 +326,7 @@ pub async fn execute_consolidate( /// Backup tool pub async fn execute_backup(storage: &Arc, _args: Option) -> Result { // Determine backup path - let vestige_dir = directories::ProjectDirs::from("com", "vestige", "core") - .ok_or("Could not determine data directory")?; - let backup_dir = vestige_dir - .data_dir() - .parent() - .unwrap_or(vestige_dir.data_dir()) - .join("backups"); + let backup_dir = storage.sidecar_dir("backups"); std::fs::create_dir_all(&backup_dir) .map_err(|e| format!("Failed to create backup directory: {}", e))?; @@ -354,13 +375,60 @@ pub async fn execute_export(storage: &Arc, args: Option) -> Resu }; let format = args.format.unwrap_or_else(|| "json".to_string()); - if format != "json" && format != "jsonl" { + if format != "json" && format != "jsonl" && format != "portable" { return Err(format!( - "Invalid format '{}'. Must be 'json' or 'jsonl'.", + "Invalid format '{}'. Must be 'json', 'jsonl', or 'portable'.", format )); } + if format == "portable" { + if args.tags.as_ref().is_some_and(|tags| !tags.is_empty()) || args.since.is_some() { + return Err( + "Portable export is exact and does not support tags or since filters.".to_string(), + ); + } + + let export_dir = storage.sidecar_dir("exports"); + std::fs::create_dir_all(&export_dir) + .map_err(|e| format!("Failed to create export directory: {}", e))?; + + let export_path = match args.path { + Some(ref p) => { + let filename = std::path::Path::new(p) + .file_name() + .ok_or("Invalid export filename: must be a simple filename, not a path")?; + let name_str = filename.to_str().ok_or("Invalid filename encoding")?; + if name_str.contains("..") { + return Err("Invalid export filename: '..' not allowed".to_string()); + } + export_dir.join(filename) + } + None => { + let timestamp = Utc::now().format("%Y%m%d-%H%M%S"); + export_dir.join(format!("vestige-portable-{}.json", timestamp)) + } + }; + + let archive = storage + .export_portable_archive_to_path(&export_path) + .map_err(|e| e.to_string())?; + let file_size = std::fs::metadata(&export_path) + .map(|m| m.len()) + .unwrap_or(0); + + return Ok(serde_json::json!({ + "tool": "export", + "path": export_path.display().to_string(), + "format": "portable", + "archiveFormat": archive.archive_format, + "schemaVersion": archive.schema_version, + "tablesExported": archive.tables.len(), + "rowsExported": archive.total_rows(), + "sizeBytes": file_size, + })); + } + // Parse since date let since_date = match &args.since { Some(date_str) => { @@ -412,13 +480,7 @@ pub async fn execute_export(storage: &Arc, args: Option) -> Resu .collect(); // Determine export path — always constrained to vestige exports directory - let vestige_dir = directories::ProjectDirs::from("com", "vestige", "core") - .ok_or("Could not determine data directory")?; - let export_dir = vestige_dir - .data_dir() - .parent() - .unwrap_or(vestige_dir.data_dir()) - .join("exports"); + let export_dir = storage.sidecar_dir("exports"); std::fs::create_dir_all(&export_dir) .map_err(|e| format!("Failed to create export directory: {}", e))?; @@ -441,7 +503,7 @@ pub async fn execute_export(storage: &Arc, args: Option) -> Resu }; // Write export - let file = std::fs::File::create(&export_path) + let file = create_private_file(&export_path) .map_err(|e| format!("Failed to create export file: {}", e))?; let mut writer = std::io::BufWriter::new(file); @@ -729,4 +791,41 @@ mod tests { assert_eq!(triggers["savesSinceLastDream"], 3); assert!(triggers["lastDreamTimestamp"].is_null()); } + + #[tokio::test] + async fn test_portable_export_writes_archive_to_storage_exports_dir() { + let (storage, _dir) = test_storage().await; + storage + .ingest(vestige_core::IngestInput { + content: "Portable MCP export test memory".to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["portable".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); + + let result = execute_export( + &storage, + Some(serde_json::json!({ + "format": "portable", + "path": "portable-test.json" + })), + ) + .await + .unwrap(); + + let path = result["path"].as_str().unwrap(); + assert_eq!(result["format"], "portable"); + assert!(path.ends_with("exports/portable-test.json")); + assert!(std::path::Path::new(path).exists()); + assert_eq!( + result["archiveFormat"], + vestige_core::PORTABLE_ARCHIVE_FORMAT + ); + assert!(result["rowsExported"].as_u64().unwrap() > 0); + } } diff --git a/crates/vestige-mcp/src/tools/memory_unified.rs b/crates/vestige-mcp/src/tools/memory_unified.rs index 2d73b6b..8a9ddcf 100644 --- a/crates/vestige-mcp/src/tools/memory_unified.rs +++ b/crates/vestige-mcp/src/tools/memory_unified.rs @@ -43,8 +43,8 @@ pub fn schema() -> Value { "properties": { "action": { "type": "string", - "enum": ["get", "get_batch", "delete", "state", "promote", "demote", "edit"], - "description": "Action to perform: 'get' retrieves full memory node, 'get_batch' retrieves multiple memories by IDs (use 'ids' array), 'delete' removes memory, 'state' returns accessibility state, 'promote' increases retrieval strength (thumbs up), 'demote' decreases retrieval strength (thumbs down), 'edit' updates content in-place (preserves FSRS state)" + "enum": ["get", "get_batch", "delete", "purge", "state", "promote", "demote", "edit"], + "description": "Action to perform: 'get' retrieves full memory node, 'get_batch' retrieves multiple memories by IDs (use 'ids' array), 'purge' permanently removes memory content and embeddings after confirm=true, 'delete' is a backwards-compatible alias for purge and also requires confirm=true, 'state' returns accessibility state, 'promote' increases retrieval strength (thumbs up), 'demote' decreases retrieval strength (thumbs down), 'edit' updates content in-place (preserves FSRS state)" }, "id": { "type": "string", @@ -57,7 +57,12 @@ pub fn schema() -> Value { }, "reason": { "type": "string", - "description": "Why this memory is being promoted/demoted (optional, for logging). Only used with promote/demote actions." + "description": "Why this memory is being promoted/demoted/purged (optional, for logging)." + }, + "confirm": { + "type": "boolean", + "description": "Required for action='purge' and action='delete'. Purge/delete permanently removes memory content and embeddings; only a non-content tombstone remains.", + "default": false }, "content": { "type": "string", @@ -75,6 +80,7 @@ struct MemoryArgs { id: Option, ids: Option>, reason: Option, + confirm: Option, content: Option, } @@ -110,13 +116,32 @@ pub async fn execute( match args.action.as_str() { "get" => execute_get(storage, &id).await, - "delete" => execute_delete(storage, &id).await, + "delete" => { + execute_purge( + storage, + &id, + args.reason, + args.confirm.unwrap_or(false), + "delete", + ) + .await + } + "purge" => { + execute_purge( + storage, + &id, + args.reason, + args.confirm.unwrap_or(false), + "purge", + ) + .await + } "state" => execute_state(storage, &id).await, "promote" => execute_promote(storage, cognitive, &id, args.reason).await, "demote" => execute_demote(storage, cognitive, &id, args.reason).await, "edit" => execute_edit(storage, &id, args.content).await, _ => Err(format!( - "Invalid action '{}'. Must be one of: get, get_batch, delete, state, promote, demote, edit", + "Invalid action '{}'. Must be one of: get, get_batch, delete, purge, state, promote, demote, edit", args.action )), } @@ -205,15 +230,39 @@ async fn execute_get_batch(storage: &Arc, ids: &[String]) -> Result, id: &str) -> Result { - let deleted = storage.delete_node(id).map_err(|e| e.to_string())?; +/// Permanently purge a memory and return cleanup details. +async fn execute_purge( + storage: &Arc, + id: &str, + reason: Option, + confirm: bool, + action: &str, +) -> Result { + if !confirm { + return Err( + "Purge is irreversible. Pass confirm=true to permanently remove memory content and embeddings." + .to_string(), + ); + } + + let report = storage + .purge_node(id, reason.as_deref()) + .map_err(|e| e.to_string())?; Ok(serde_json::json!({ - "action": "delete", - "success": deleted, + "action": action, + "success": report.deleted, "nodeId": id, - "message": if deleted { "Memory deleted successfully" } else { "Memory not found" }, + "message": if report.deleted { + "Memory purged permanently; content and embeddings removed. Non-content tombstone retained for sync/audit." + } else { + "Memory not found" + }, + "deletedAt": report.deleted_at.to_rfc3339(), + "edgesPruned": report.edges_pruned, + "insightsRewritten": report.insights_rewritten, + "insightsDeleted": report.insights_deleted, + "childrenOrphaned": report.children_orphaned, })) } @@ -469,13 +518,15 @@ mod tests { assert!(schema["properties"]["reason"].is_object()); assert_eq!(schema["required"], serde_json::json!(["action"])); assert!(schema["properties"]["ids"].is_object()); // get_batch support - // Verify all 7 actions are in enum + // Verify all 8 actions are in enum let actions = schema["properties"]["action"]["enum"].as_array().unwrap(); - assert_eq!(actions.len(), 7); + assert_eq!(actions.len(), 8); assert!(actions.contains(&serde_json::json!("get_batch"))); + assert!(actions.contains(&serde_json::json!("purge"))); assert!(actions.contains(&serde_json::json!("edit"))); assert!(actions.contains(&serde_json::json!("promote"))); assert!(actions.contains(&serde_json::json!("demote"))); + assert!(schema["properties"]["confirm"].is_object()); } // === INTEGRATION TESTS === @@ -562,10 +613,21 @@ mod tests { } #[tokio::test] - async fn test_delete_existing_memory() { + async fn test_delete_requires_confirm() { let (storage, _dir) = test_storage().await; let id = ingest_memory(&storage).await; - let args = serde_json::json!({ "action": "delete", "id": id }); + let args = serde_json::json!({ "action": "delete", "id": id.clone() }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("confirm=true")); + assert!(storage.get_node(&id).unwrap().is_some()); + } + + #[tokio::test] + async fn test_delete_existing_memory_with_confirm() { + let (storage, _dir) = test_storage().await; + let id = ingest_memory(&storage).await; + let args = serde_json::json!({ "action": "delete", "id": id, "confirm": true }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -586,8 +648,11 @@ mod tests { .unwrap() .id; let _ = storage.delete_node(&warmup_id); - let args = - serde_json::json!({ "action": "delete", "id": "00000000-0000-0000-0000-000000000000" }); + let args = serde_json::json!({ + "action": "delete", + "id": "00000000-0000-0000-0000-000000000000", + "confirm": true + }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -599,7 +664,7 @@ mod tests { async fn test_delete_then_get_returns_not_found() { let (storage, _dir) = test_storage().await; let id = ingest_memory(&storage).await; - let del_args = serde_json::json!({ "action": "delete", "id": id }); + let del_args = serde_json::json!({ "action": "delete", "id": id, "confirm": true }); execute(&storage, &test_cognitive(), Some(del_args)) .await .unwrap(); @@ -609,6 +674,42 @@ mod tests { assert_eq!(value["found"], false); } + #[tokio::test] + async fn test_purge_requires_confirm() { + let (storage, _dir) = test_storage().await; + let id = ingest_memory(&storage).await; + let args = serde_json::json!({ "action": "purge", "id": id.clone() }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("confirm=true")); + assert!(storage.get_node(&id).unwrap().is_some()); + } + + #[tokio::test] + async fn test_purge_existing_memory() { + let (storage, _dir) = test_storage().await; + let id = ingest_memory(&storage).await; + let args = serde_json::json!({ + "action": "purge", + "id": id, + "confirm": true, + "reason": "test cleanup" + }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value["action"], "purge"); + assert_eq!(value["success"], true); + assert!( + value["message"] + .as_str() + .unwrap() + .contains("purged permanently") + ); + assert_eq!(value["edgesPruned"], 0); + assert!(storage.get_node(&id).unwrap().is_none()); + } + #[tokio::test] async fn test_state_existing_memory() { let (storage, _dir) = test_storage().await; diff --git a/crates/vestige-mcp/src/tools/mod.rs b/crates/vestige-mcp/src/tools/mod.rs index 6d2d674..260869e 100644 --- a/crates/vestige-mcp/src/tools/mod.rs +++ b/crates/vestige-mcp/src/tools/mod.rs @@ -38,40 +38,29 @@ pub mod graph; pub mod health; // v2.1: Cross-reference (connect the dots) +pub mod contradictions; pub mod cross_reference; // v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1 pub mod suppress; -// Deprecated/internal tools — not advertised in the public MCP tools/list, -// but some functions are actively dispatched for backwards compatibility -// and internal cognitive operations. #[allow(dead_code)] suppresses warnings -// for the unused schema/struct items within these modules. -#[allow(dead_code)] -pub mod checkpoint; -#[allow(dead_code)] -pub mod codebase; -#[allow(dead_code)] -pub mod consolidate; +// Internal/backwards-compat tools still dispatched by server.rs for specific +// tool names. Each module below has live callers via string dispatch in +// `server.rs` (match arms on request.name). The #[allow(dead_code)] +// suppresses warnings for the per-module schema/struct items that aren't +// yet consumed. +// +// The nine legacy siblings here pre-v2.0.8 (checkpoint, codebase, consolidate, +// ingest, intentions, knowledge, recall, search, stats) were removed in the +// post-v2.0.8 dead-code sweep — all nine had zero callers after the +// unification work landed `*_unified` + `maintenance::*` replacements. #[allow(dead_code)] pub mod context; #[allow(dead_code)] pub mod feedback; #[allow(dead_code)] -pub mod ingest; -#[allow(dead_code)] -pub mod intentions; -#[allow(dead_code)] -pub mod knowledge; -#[allow(dead_code)] pub mod memory_states; #[allow(dead_code)] -pub mod recall; -#[allow(dead_code)] pub mod review; #[allow(dead_code)] -pub mod search; -#[allow(dead_code)] -pub mod stats; -#[allow(dead_code)] pub mod tagging; diff --git a/crates/vestige-mcp/src/tools/predict.rs b/crates/vestige-mcp/src/tools/predict.rs index 32a52b2..c3eb25a 100644 --- a/crates/vestige-mcp/src/tools/predict.rs +++ b/crates/vestige-mcp/src/tools/predict.rs @@ -114,15 +114,18 @@ pub async fn execute( degraded = true; Vec::new() }); - let accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or_else(|e| { - tracing::warn!( - target: "vestige::predict", - error = %e, - "prediction_accuracy failed; returning 0.0" - ); - degraded = true; - 0.0 - }); + let accuracy = cog + .predictive_memory + .prediction_accuracy() + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "prediction_accuracy failed; returning 0.0" + ); + degraded = true; + 0.0 + }); // Build speculative context let speculative_context = vestige_core::PredictionContext { diff --git a/crates/vestige-mcp/src/tools/recall.rs b/crates/vestige-mcp/src/tools/recall.rs deleted file mode 100644 index 28ff8c4..0000000 --- a/crates/vestige-mcp/src/tools/recall.rs +++ /dev/null @@ -1,403 +0,0 @@ -//! Recall Tool (Deprecated - use search_unified instead) -//! -//! Search and retrieve knowledge from memory. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{RecallInput, SearchMode, Storage}; - -/// Input schema for recall tool -pub fn schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - "minimum": 1, - "maximum": 100 - }, - "min_retention": { - "type": "number", - "description": "Minimum retention strength (0.0-1.0, default: 0.0)", - "default": 0.0, - "minimum": 0.0, - "maximum": 1.0 - } - }, - "required": ["query"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct RecallArgs { - query: String, - limit: Option, - min_retention: Option, -} - -pub async fn execute(storage: &Arc, args: Option) -> Result { - let args: RecallArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.query.trim().is_empty() { - return Err("Query cannot be empty".to_string()); - } - - let input = RecallInput { - query: args.query.clone(), - limit: args.limit.unwrap_or(10).clamp(1, 100), - min_retention: args.min_retention.unwrap_or(0.0).clamp(0.0, 1.0), - search_mode: SearchMode::Hybrid, - valid_at: None, - }; - - let nodes = storage.recall(input).map_err(|e| e.to_string())?; - - let results: Vec = nodes - .iter() - .map(|n| { - serde_json::json!({ - "id": n.id, - "content": n.content, - "nodeType": n.node_type, - "retentionStrength": n.retention_strength, - "stability": n.stability, - "difficulty": n.difficulty, - "reps": n.reps, - "tags": n.tags, - "source": n.source, - "createdAt": n.created_at.to_rfc3339(), - "lastAccessed": n.last_accessed.to_rfc3339(), - "nextReview": n.next_review.map(|d| d.to_rfc3339()), - }) - }) - .collect(); - - Ok(serde_json::json!({ - "query": args.query, - "total": results.len(), - "results": results, - })) -} - -// ============================================================================ -// TESTS -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - use vestige_core::IngestInput; - - /// Create a test storage instance with a temporary database - async fn test_storage() -> (Arc, TempDir) { - let dir = TempDir::new().unwrap(); - let storage = Storage::new(Some(dir.path().join("test.db"))).unwrap(); - (Arc::new(storage), dir) - } - - /// Helper to ingest test content - async fn ingest_test_content(storage: &Arc, content: &str) -> String { - let input = IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec![], - valid_from: None, - valid_until: None, - }; - let node = storage.ingest(input).unwrap(); - node.id - } - - // ======================================================================== - // QUERY VALIDATION TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_empty_query_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "query": "" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_recall_whitespace_only_query_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "query": " \t\n " }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("empty")); - } - - #[tokio::test] - async fn test_recall_missing_arguments_fails() { - let (storage, _dir) = test_storage().await; - let result = execute(&storage, None).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Missing arguments")); - } - - #[tokio::test] - async fn test_recall_missing_query_field_fails() { - let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "limit": 10 }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_err()); - assert!(result.unwrap_err().contains("Invalid arguments")); - } - - // ======================================================================== - // LIMIT CLAMPING TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_limit_clamped_to_minimum() { - let (storage, _dir) = test_storage().await; - // Ingest some content first - ingest_test_content(&storage, "Test content for limit clamping").await; - - // Try with limit 0 - should clamp to 1 - let args = serde_json::json!({ - "query": "test", - "limit": 0 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_recall_limit_clamped_to_maximum() { - let (storage, _dir) = test_storage().await; - // Ingest some content first - ingest_test_content(&storage, "Test content for max limit").await; - - // Try with limit 1000 - should clamp to 100 - let args = serde_json::json!({ - "query": "test", - "limit": 1000 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_recall_negative_limit_clamped() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Test content for negative limit").await; - - let args = serde_json::json!({ - "query": "test", - "limit": -5 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - // ======================================================================== - // MIN_RETENTION CLAMPING TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_min_retention_clamped_to_zero() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Test content for retention clamping").await; - - let args = serde_json::json!({ - "query": "test", - "min_retention": -0.5 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - } - - #[tokio::test] - async fn test_recall_min_retention_clamped_to_one() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Test content for max retention").await; - - let args = serde_json::json!({ - "query": "test", - "min_retention": 1.5 - }); - let result = execute(&storage, Some(args)).await; - // Should succeed but return no results (retention > 1.0 clamped to 1.0) - assert!(result.is_ok()); - } - - // ======================================================================== - // SUCCESSFUL RECALL TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_basic_query_succeeds() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "The Rust programming language is memory safe.").await; - - let args = serde_json::json!({ "query": "rust" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["query"], "rust"); - assert!(value["total"].is_number()); - assert!(value["results"].is_array()); - } - - #[tokio::test] - async fn test_recall_returns_matching_content() { - let (storage, _dir) = test_storage().await; - let node_id = - ingest_test_content(&storage, "Python is a dynamic programming language.").await; - - let args = serde_json::json!({ "query": "python" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert!(!results.is_empty()); - assert_eq!(results[0]["id"], node_id); - } - - #[tokio::test] - async fn test_recall_with_limit() { - let (storage, _dir) = test_storage().await; - // Ingest multiple items - ingest_test_content(&storage, "Testing content one").await; - ingest_test_content(&storage, "Testing content two").await; - ingest_test_content(&storage, "Testing content three").await; - - let args = serde_json::json!({ - "query": "testing", - "limit": 2 - }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert!(results.len() <= 2); - } - - #[tokio::test] - async fn test_recall_empty_database_returns_empty_array() { - // With hybrid search (keyword + semantic), any query against content - // may return low-similarity matches. The true "no matches" case - // is an empty database. - let (storage, _dir) = test_storage().await; - // Don't ingest anything - database is empty - - let args = serde_json::json!({ "query": "anything" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - assert_eq!(value["total"], 0); - assert!(value["results"].as_array().unwrap().is_empty()); - } - - #[tokio::test] - async fn test_recall_result_contains_expected_fields() { - let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Testing field presence in recall results.").await; - - let args = serde_json::json!({ "query": "testing" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - if !results.is_empty() { - let first = &results[0]; - assert!(first["id"].is_string()); - assert!(first["content"].is_string()); - assert!(first["nodeType"].is_string()); - assert!(first["retentionStrength"].is_number()); - assert!(first["stability"].is_number()); - assert!(first["difficulty"].is_number()); - assert!(first["reps"].is_number()); - assert!(first["createdAt"].is_string()); - assert!(first["lastAccessed"].is_string()); - } - } - - // ======================================================================== - // DEFAULT VALUES TESTS - // ======================================================================== - - #[tokio::test] - async fn test_recall_default_limit_is_10() { - let (storage, _dir) = test_storage().await; - // Ingest more than 10 items - for i in 0..15 { - ingest_test_content(&storage, &format!("Item number {}", i)).await; - } - - let args = serde_json::json!({ "query": "item" }); - let result = execute(&storage, Some(args)).await; - assert!(result.is_ok()); - - let value = result.unwrap(); - let results = value["results"].as_array().unwrap(); - assert!(results.len() <= 10); - } - - // ======================================================================== - // SCHEMA TESTS - // ======================================================================== - - #[test] - fn test_schema_has_required_fields() { - let schema_value = schema(); - assert_eq!(schema_value["type"], "object"); - assert!(schema_value["properties"]["query"].is_object()); - assert!( - schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("query")) - ); - } - - #[test] - fn test_schema_has_optional_fields() { - let schema_value = schema(); - assert!(schema_value["properties"]["limit"].is_object()); - assert!(schema_value["properties"]["min_retention"].is_object()); - } - - #[test] - fn test_schema_limit_has_bounds() { - let schema_value = schema(); - let limit_schema = &schema_value["properties"]["limit"]; - assert_eq!(limit_schema["minimum"], 1); - assert_eq!(limit_schema["maximum"], 100); - assert_eq!(limit_schema["default"], 10); - } - - #[test] - fn test_schema_min_retention_has_bounds() { - let schema_value = schema(); - let retention_schema = &schema_value["properties"]["min_retention"]; - assert_eq!(retention_schema["minimum"], 0.0); - assert_eq!(retention_schema["maximum"], 1.0); - assert_eq!(retention_schema["default"], 0.0); - } -} diff --git a/crates/vestige-mcp/src/tools/restore.rs b/crates/vestige-mcp/src/tools/restore.rs index 4d77252..ac7184c 100644 --- a/crates/vestige-mcp/src/tools/restore.rs +++ b/crates/vestige-mcp/src/tools/restore.rs @@ -6,9 +6,10 @@ use serde::Deserialize; use serde_json::Value; +use std::path::Path; use std::sync::Arc; -use vestige_core::{IngestInput, Storage}; +use vestige_core::{IngestInput, PortableArchive, PortableImportMode, Storage}; /// Input schema for restore tool pub fn schema() -> Value { @@ -18,6 +19,16 @@ pub fn schema() -> Value { "path": { "type": "string", "description": "Path to the backup JSON file to restore from" + }, + "allowAnyPath": { + "type": "boolean", + "description": "Allow restoring from a file outside the active Vestige backups/ or exports/ directories. Only set true for trusted local files.", + "default": false + }, + "merge": { + "type": "boolean", + "description": "For portable archives, merge into the current database instead of requiring an empty target. Applies sync tombstones and keeps newer local memory rows on conflict.", + "default": false } }, "required": ["path"] @@ -25,8 +36,13 @@ pub fn schema() -> Value { } #[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] struct RestoreArgs { path: String, + #[serde(default)] + allow_any_path: bool, + #[serde(default)] + merge: bool, } #[derive(Deserialize)] @@ -56,37 +72,79 @@ pub async fn execute(storage: &Arc, args: Option) -> Result return Err("Missing arguments".to_string()), }; - let path = std::path::Path::new(&args.path); + let path = Path::new(&args.path); if !path.exists() { return Err(format!("Backup file not found: {}", args.path)); } + if !args.allow_any_path { + ensure_restore_path_allowed(storage, path)?; + } + // Read and parse backup - let backup_content = - std::fs::read_to_string(path).map_err(|e| format!("Failed to read backup: {}", e))?; + let backup_bytes = std::fs::read(path).map_err(|e| format!("Failed to read backup: {}", e))?; + if backup_bytes.starts_with(b"SQLite format 3\0") { + return Err( + "Restore expected JSON, but this file is a raw SQLite database backup. Use portable export/import for cross-device transfer, or replace the database file manually while Vestige is stopped." + .to_string(), + ); + } + let backup_content = String::from_utf8(backup_bytes) + .map_err(|_| "Restore file is not UTF-8 JSON".to_string())?; + + if let Ok(archive) = serde_json::from_str::(&backup_content) + && archive.archive_format == vestige_core::PORTABLE_ARCHIVE_FORMAT + { + let rows = archive.total_rows(); + let tables = archive.tables.len(); + let mode = if args.merge { + PortableImportMode::Merge + } else { + PortableImportMode::EmptyOnly + }; + let report = storage + .import_portable_archive(&archive, mode) + .map_err(|e| e.to_string())?; + return Ok(serde_json::json!({ + "tool": "restore", + "success": true, + "mode": if args.merge { "portable-merge" } else { "portable" }, + "tables": tables, + "rows": rows, + "tablesImported": report.tables_imported, + "rowsImported": report.rows_imported, + "tablesSkipped": report.tables_skipped, + "rowsInserted": report.rows_inserted, + "rowsUpdated": report.rows_updated, + "rowsDeleted": report.rows_deleted, + "rowsSkipped": report.rows_skipped, + "conflictsKeptLocal": report.conflicts_kept_local, + "ftsRebuilt": report.fts_rebuilt, + "message": format!("Imported {} rows from portable archive.", report.rows_imported), + })); + } // Try parsing as wrapped format first (MCP response wrapper), // then fall back to direct RecallResult - let memories: Vec = if let Ok(wrapper) = - serde_json::from_str::>(&backup_content) - { - if let Some(first) = wrapper.first() { - let recall: RecallResult = serde_json::from_str(&first.text) - .map_err(|e| format!("Failed to parse backup contents: {}", e))?; + let memories: Vec = + if let Ok(wrapper) = serde_json::from_str::>(&backup_content) { + if let Some(first) = wrapper.first() { + let recall: RecallResult = serde_json::from_str(&first.text) + .map_err(|e| format!("Failed to parse backup contents: {}", e))?; + recall.results + } else { + return Err("Empty backup file".to_string()); + } + } else if let Ok(recall) = serde_json::from_str::(&backup_content) { recall.results + } else if let Ok(nodes) = serde_json::from_str::>(&backup_content) { + nodes } else { - return Err("Empty backup file".to_string()); - } - } else if let Ok(recall) = serde_json::from_str::(&backup_content) { - recall.results - } else if let Ok(nodes) = serde_json::from_str::>(&backup_content) { - nodes - } else { - return Err( + return Err( "Unrecognized backup format. Expected MCP wrapper, RecallResult, or array of memories." .to_string(), ); - }; + }; let total = memories.len(); if total == 0 { @@ -133,6 +191,33 @@ pub async fn execute(storage: &Arc, args: Option) -> Result Result<(), String> { + let canonical_path = path + .canonicalize() + .map_err(|e| format!("Failed to resolve restore path: {}", e))?; + + for dir in [ + storage.sidecar_dir("exports"), + storage.sidecar_dir("backups"), + ] { + if !dir.exists() { + continue; + } + let canonical_dir = dir + .canonicalize() + .map_err(|e| format!("Failed to resolve allowed restore directory: {}", e))?; + if canonical_path.starts_with(&canonical_dir) { + return Ok(()); + } + } + + Err(format!( + "MCP restore is restricted to {} and {} by default. Pass allowAnyPath=true only for trusted local files.", + storage.sidecar_dir("exports").display(), + storage.sidecar_dir("backups").display() + )) +} + #[cfg(test)] mod tests { use super::*; @@ -157,6 +242,7 @@ mod tests { let s = schema(); assert_eq!(s["type"], "object"); assert!(s["properties"]["path"].is_object()); + assert!(s["properties"]["allowAnyPath"].is_object()); assert!( s["required"] .as_array() @@ -194,12 +280,22 @@ mod tests { async fn test_malformed_json_fails() { let (storage, dir) = test_storage().await; let path = write_temp_file(&dir, "bad.json", "this is not json {{{"); - let args = serde_json::json!({ "path": path }); + let args = serde_json::json!({ "path": path, "allowAnyPath": true }); let result = execute(&storage, Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Unrecognized backup format")); } + #[tokio::test] + async fn test_restore_rejects_arbitrary_path_by_default() { + let (storage, dir) = test_storage().await; + let path = write_temp_file(&dir, "outside_exports.json", "[]"); + let args = serde_json::json!({ "path": path }); + let result = execute(&storage, Some(args)).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("restricted")); + } + #[tokio::test] async fn test_restore_direct_array_format() { let (storage, dir) = test_storage().await; @@ -208,7 +304,7 @@ mod tests { { "content": "Memory two", "nodeType": "concept" } ]); let path = write_temp_file(&dir, "backup.json", &backup.to_string()); - let args = serde_json::json!({ "path": path }); + let args = serde_json::json!({ "path": path, "allowAnyPath": true }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -230,7 +326,7 @@ mod tests { ] }); let path = write_temp_file(&dir, "recall.json", &backup.to_string()); - let args = serde_json::json!({ "path": path }); + let args = serde_json::json!({ "path": path, "allowAnyPath": true }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -238,12 +334,47 @@ mod tests { assert_eq!(value["total"], 3); } + #[tokio::test] + async fn test_restore_portable_archive_from_allowed_exports_dir() { + let (source, _source_dir) = test_storage().await; + let (target, _target_dir) = test_storage().await; + + source + .ingest(vestige_core::IngestInput { + content: "Portable MCP restore test memory".to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["portable".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); + + let export_dir = target.sidecar_dir("exports"); + std::fs::create_dir_all(&export_dir).unwrap(); + let archive_path = export_dir.join("portable-restore.json"); + source + .export_portable_archive_to_path(&archive_path) + .unwrap(); + + let args = serde_json::json!({ "path": archive_path }); + let result = execute(&target, Some(args)).await.unwrap(); + + assert_eq!(result["tool"], "restore"); + assert_eq!(result["success"], true); + assert_eq!(result["mode"], "portable"); + assert!(result["rowsImported"].as_u64().unwrap() > 0); + assert_eq!(target.get_stats().unwrap().total_nodes, 1); + } + #[tokio::test] async fn test_restore_empty_results_array() { let (storage, dir) = test_storage().await; let backup = serde_json::json!({ "results": [] }); let path = write_temp_file(&dir, "empty.json", &backup.to_string()); - let args = serde_json::json!({ "path": path }); + let args = serde_json::json!({ "path": path, "allowAnyPath": true }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -256,7 +387,7 @@ mod tests { // Empty [] parses as Vec first, which has no items → "Empty backup file" let (storage, dir) = test_storage().await; let path = write_temp_file(&dir, "empty_arr.json", "[]"); - let args = serde_json::json!({ "path": path }); + let args = serde_json::json!({ "path": path, "allowAnyPath": true }); let result = execute(&storage, Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Empty backup file")); @@ -267,7 +398,7 @@ mod tests { let (storage, dir) = test_storage().await; let backup = serde_json::json!([{ "content": "No type specified" }]); let path = write_temp_file(&dir, "notype.json", &backup.to_string()); - let args = serde_json::json!({ "path": path }); + let args = serde_json::json!({ "path": path, "allowAnyPath": true }); let result = execute(&storage, Some(args)).await; assert!(result.is_ok()); assert_eq!(result.unwrap()["restored"], 1); diff --git a/crates/vestige-mcp/src/tools/search.rs b/crates/vestige-mcp/src/tools/search.rs deleted file mode 100644 index 7653cec..0000000 --- a/crates/vestige-mcp/src/tools/search.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Search Tools (Deprecated - use search_unified instead) -//! -//! Semantic and hybrid search implementations. - -use serde::Deserialize; -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::Storage; - -/// Input schema for semantic_search tool -pub fn semantic_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query for semantic similarity" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - "minimum": 1, - "maximum": 50 - }, - "min_similarity": { - "type": "number", - "description": "Minimum similarity threshold (0.0-1.0, default: 0.5)", - "default": 0.5, - "minimum": 0.0, - "maximum": 1.0 - } - }, - "required": ["query"] - }) -} - -/// Input schema for hybrid_search tool -pub fn hybrid_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Search query" - }, - "limit": { - "type": "integer", - "description": "Maximum number of results (default: 10)", - "default": 10, - "minimum": 1, - "maximum": 50 - }, - "keyword_weight": { - "type": "number", - "description": "Weight for keyword search (0.0-1.0, default: 0.5)", - "default": 0.5, - "minimum": 0.0, - "maximum": 1.0 - }, - "semantic_weight": { - "type": "number", - "description": "Weight for semantic search (0.0-1.0, default: 0.5)", - "default": 0.5, - "minimum": 0.0, - "maximum": 1.0 - } - }, - "required": ["query"] - }) -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct SemanticSearchArgs { - query: String, - limit: Option, - min_similarity: Option, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct HybridSearchArgs { - query: String, - limit: Option, - keyword_weight: Option, - semantic_weight: Option, -} - -pub async fn execute_semantic( - storage: &Arc, - args: Option, -) -> Result { - let args: SemanticSearchArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.query.trim().is_empty() { - return Err("Query cannot be empty".to_string()); - } - - // Check if embeddings are ready - if !storage.is_embedding_ready() { - return Ok(serde_json::json!({ - "error": "Embedding service not ready", - "hint": "Run consolidation first to initialize embeddings, or the model may still be loading.", - })); - } - - let results = storage - .semantic_search( - &args.query, - args.limit.unwrap_or(10).clamp(1, 50), - args.min_similarity.unwrap_or(0.5).clamp(0.0, 1.0), - ) - .map_err(|e| e.to_string())?; - - let formatted: Vec = results - .iter() - .map(|r| { - serde_json::json!({ - "id": r.node.id, - "content": r.node.content, - "similarity": r.similarity, - "nodeType": r.node.node_type, - "tags": r.node.tags, - "retentionStrength": r.node.retention_strength, - }) - }) - .collect(); - - Ok(serde_json::json!({ - "query": args.query, - "method": "semantic", - "total": formatted.len(), - "results": formatted, - })) -} - -pub async fn execute_hybrid(storage: &Arc, args: Option) -> Result { - let args: HybridSearchArgs = match args { - Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, - None => return Err("Missing arguments".to_string()), - }; - - if args.query.trim().is_empty() { - return Err("Query cannot be empty".to_string()); - } - - let results = storage - .hybrid_search( - &args.query, - args.limit.unwrap_or(10).clamp(1, 50), - args.keyword_weight.unwrap_or(0.3).clamp(0.0, 1.0), - args.semantic_weight.unwrap_or(0.7).clamp(0.0, 1.0), - ) - .map_err(|e| e.to_string())?; - - let formatted: Vec = results - .iter() - .map(|r| { - serde_json::json!({ - "id": r.node.id, - "content": r.node.content, - "combinedScore": r.combined_score, - "keywordScore": r.keyword_score, - "semanticScore": r.semantic_score, - "matchType": format!("{:?}", r.match_type), - "nodeType": r.node.node_type, - "tags": r.node.tags, - "retentionStrength": r.node.retention_strength, - }) - }) - .collect(); - - Ok(serde_json::json!({ - "query": args.query, - "method": "hybrid", - "total": formatted.len(), - "results": formatted, - })) -} diff --git a/crates/vestige-mcp/src/tools/search_unified.rs b/crates/vestige-mcp/src/tools/search_unified.rs index bfe419e..9faf961 100644 --- a/crates/vestige-mcp/src/tools/search_unified.rs +++ b/crates/vestige-mcp/src/tools/search_unified.rs @@ -87,6 +87,11 @@ pub fn schema() -> Value { "description": "precise: top results only (fast, token-efficient, skips activation/competition). balanced: full 7-stage cognitive pipeline (default). exhaustive: maximum recall with 5x overfetch, deep graph traversal, no competition suppression.", "enum": ["precise", "balanced", "exhaustive"], "default": "balanced" + }, + "concrete": { + "type": "boolean", + "description": "Force literal/concrete search. Skips semantic expansion, FSRS reweighting, spreading activation, and cognitive side effects. Auto-enabled for quoted strings, env vars, UUIDs, paths, and code identifiers.", + "default": false } }, "required": ["query"] @@ -114,6 +119,7 @@ struct SearchArgs { token_budget: Option, #[serde(alias = "retrieval_mode")] retrieval_mode: Option, + concrete: Option, } /// Execute unified search with 7-stage cognitive pipeline. @@ -173,6 +179,78 @@ pub async fn execute( } }; + let concrete = args + .concrete + .unwrap_or_else(|| is_literal_query(&args.query)); + if concrete { + let results = storage + .concrete_search_filtered( + &args.query, + limit, + args.include_types.as_deref(), + args.exclude_types.as_deref(), + ) + .map_err(|e| e.to_string())?; + + let ids: Vec<&str> = results.iter().map(|r| r.node.id.as_str()).collect(); + let _ = storage.strengthen_batch_on_access(&ids); + + let mut formatted: Vec = results + .iter() + .filter(|r| r.node.retention_strength >= min_retention) + .map(|r| format_search_result(r, detail_level)) + .collect(); + + let mut budget_expandable: Vec = Vec::new(); + let mut budget_tokens_used: Option = None; + if let Some(budget) = args.token_budget { + let budget = budget.clamp(100, 100000) as usize; + let budget_chars = budget * 4; + let mut used = 0; + let mut budgeted = Vec::new(); + + for result in &formatted { + let size = serde_json::to_string(result).unwrap_or_default().len(); + if used + size > budget_chars { + if let Some(id) = result.get("id").and_then(|v| v.as_str()) { + budget_expandable.push(id.to_string()); + } + continue; + } + used += size; + budgeted.push(result.clone()); + } + + budget_tokens_used = Some(used / 4); + formatted = budgeted; + } + + let mut response = serde_json::json!({ + "query": args.query, + "method": "concrete", + "retrievalMode": retrieval_mode, + "concrete": true, + "detailLevel": detail_level, + "total": formatted.len(), + "results": formatted, + }); + + if formatted.is_empty() { + response["hint"] = serde_json::json!( + "No concrete matches found. Try concrete=false or a broader natural-language query." + ); + } + if !budget_expandable.is_empty() { + response["expandable"] = serde_json::json!(budget_expandable); + } + if let Some(tokens) = budget_tokens_used { + response["tokenBudgetUsed"] = serde_json::json!(tokens); + response["tokenBudgetLimit"] = serde_json::json!(args.token_budget.unwrap()); + } + + return Ok(response); + } + // Favor semantic search — research shows 0.3/0.7 outperforms equal weights let keyword_weight = 0.3_f32; let semantic_weight = 0.7_f32; @@ -305,7 +383,8 @@ pub async fn execute( .unwrap_or(std::cmp::Ordering::Equal) }); - // Rerank the remaining candidates + // Rerank the remaining candidates when the vector-search search stack is enabled. + #[cfg(feature = "vector-search")] let reranked_results: Vec = if rerank_candidates.is_empty() { Vec::new() } else if let Ok(mut cog) = cognitive.try_lock() { @@ -330,6 +409,11 @@ pub async fn execute( .cloned() .collect() }; + #[cfg(not(feature = "vector-search"))] + let reranked_results: Vec = rerank_candidates + .into_iter() + .map(|(result, _)| result) + .collect(); // Merge: bypass first, then reranked, trim to limit filtered_results = bypass_results; @@ -340,6 +424,7 @@ pub async fn execute( // ==================================================================== // STAGE 3: Temporal boosting (recency + validity windows) // ==================================================================== + #[cfg(feature = "vector-search")] if let Ok(cog) = cognitive.try_lock() { for result in &mut filtered_results { let recency = cog.temporal_searcher.recency_boost(result.node.created_at); @@ -661,6 +746,41 @@ pub async fn execute( Ok(response) } +fn is_literal_query(query: &str) -> bool { + let trimmed = query.trim(); + if trimmed.len() >= 2 { + let bytes = trimmed.as_bytes(); + if (bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"') + || (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\'') + { + return true; + } + } + + if uuid::Uuid::parse_str(trimmed).is_ok() { + return true; + } + + let token_count = trimmed.split_whitespace().count(); + if token_count != 1 { + return false; + } + + let has_identifier_punctuation = trimmed + .chars() + .any(|c| matches!(c, '_' | '-' | '/' | '\\' | '.' | ':' | '=' | '@')); + if has_identifier_punctuation { + return true; + } + + let has_alpha = trimmed.chars().any(|c| c.is_ascii_alphabetic()); + has_alpha + && trimmed.contains('_') + && trimmed + .chars() + .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_') +} + /// Format a search result based on the requested detail level. fn format_search_result(r: &vestige_core::SearchResult, detail_level: &str) -> Value { match detail_level { @@ -833,6 +953,97 @@ mod tests { assert!(result.unwrap_err().contains("Invalid arguments")); } + #[test] + fn test_literal_query_detection() { + assert!(is_literal_query("\"exact phrase\"")); + assert!(is_literal_query("OPENAI_API_KEY")); + assert!(is_literal_query("mlx_lm.server")); + assert!(is_literal_query("src/main.rs")); + assert!(is_literal_query("4da778e2-1111-4444-8888-123456789abc")); + assert!(!is_literal_query("how should memory search work")); + } + + #[tokio::test] + async fn test_concrete_search_env_var_lands_first() { + let (storage, _dir) = test_storage().await; + ingest_test_content( + &storage, + "General OpenAI setup and API key rotation guidance", + ) + .await; + let target = ingest_test_content( + &storage, + "Release smoke test requires OPENAI_API_KEY to be set in the shell", + ) + .await; + ingest_test_content( + &storage, + "Credentials should be stored outside the repository", + ) + .await; + + let args = serde_json::json!({ + "query": "OPENAI_API_KEY", + "limit": 5 + }); + let result = execute(&storage, &test_cognitive(), Some(args)) + .await + .unwrap(); + + assert_eq!(result["method"], "concrete"); + assert_eq!(result["concrete"], true); + assert_eq!(result["results"][0]["id"], target); + } + + #[tokio::test] + async fn test_concrete_search_uuid_lands_first() { + let (storage, _dir) = test_storage().await; + let uuid = "4da778e2-1111-4444-8888-123456789abc"; + ingest_test_content(&storage, "Several memories mention release identifiers").await; + let target = ingest_test_content( + &storage, + &format!("The migration bug is tracked by exact id {}", uuid), + ) + .await; + + let args = serde_json::json!({ + "query": uuid, + "limit": 5 + }); + let result = execute(&storage, &test_cognitive(), Some(args)) + .await + .unwrap(); + + assert_eq!(result["method"], "concrete"); + assert_eq!(result["results"][0]["id"], target); + } + + #[tokio::test] + async fn test_concrete_search_process_name_lands_first() { + let (storage, _dir) = test_storage().await; + ingest_test_content( + &storage, + "The local MLX server can expose an OpenAI-compatible endpoint", + ) + .await; + let target = ingest_test_content( + &storage, + "If mlx_lm.server is already running, do not start a second Sanhedrin backend", + ) + .await; + + let args = serde_json::json!({ + "query": "mlx_lm.server", + "limit": 5 + }); + let result = execute(&storage, &test_cognitive(), Some(args)) + .await + .unwrap(); + + assert_eq!(result["method"], "concrete"); + assert_eq!(result["results"][0]["id"], target); + } + // ======================================================================== // LIMIT CLAMPING TESTS // ======================================================================== diff --git a/crates/vestige-mcp/src/tools/session_context.rs b/crates/vestige-mcp/src/tools/session_context.rs index e7414eb..0a32ce4 100644 --- a/crates/vestige-mcp/src/tools/session_context.rs +++ b/crates/vestige-mcp/src/tools/session_context.rs @@ -223,7 +223,7 @@ pub async fn execute( Some(dt) => storage.count_memories_since(*dt).unwrap_or(0), None => stats.total_nodes, }; - let last_backup = Storage::get_last_backup_timestamp(); + let last_backup = storage.last_backup_timestamp(); let now = Utc::now(); let needs_dream = last_dream @@ -236,7 +236,7 @@ pub async fn execute( if include_status { let embedding_pct = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 + (stats.nodes_with_active_embeddings as f64 / stats.total_nodes as f64) * 100.0 } else { 0.0 }; @@ -546,13 +546,13 @@ mod tests { let (storage, _dir) = test_storage().await; ingest_test_content( &storage, - "Sam prefers Rust and TypeScript for all projects.", + "The user prefers Rust and TypeScript for all projects.", vec![], ) .await; let args = serde_json::json!({ - "queries": ["Sam preferences", "project context"] + "queries": ["user preferences", "project context"] }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); diff --git a/crates/vestige-mcp/src/tools/smart_ingest.rs b/crates/vestige-mcp/src/tools/smart_ingest.rs index 72dc3a6..f5c60cd 100644 --- a/crates/vestige-mcp/src/tools/smart_ingest.rs +++ b/crates/vestige-mcp/src/tools/smart_ingest.rs @@ -315,7 +315,10 @@ async fn execute_batch( let mut results = Vec::new(); let mut created = 0u32; + #[cfg(all(feature = "embeddings", feature = "vector-search"))] let mut updated = 0u32; + #[cfg(not(all(feature = "embeddings", feature = "vector-search")))] + let updated = 0u32; let mut skipped = 0u32; let mut errors = 0u32; diff --git a/crates/vestige-mcp/src/tools/stats.rs b/crates/vestige-mcp/src/tools/stats.rs deleted file mode 100644 index f9f31aa..0000000 --- a/crates/vestige-mcp/src/tools/stats.rs +++ /dev/null @@ -1,132 +0,0 @@ -//! Stats Tools (Deprecated - use memory_unified instead) -//! -//! Memory statistics and health check. - -use serde_json::Value; -use std::sync::Arc; - -use vestige_core::{MemoryStats, Storage}; - -/// Input schema for get_stats tool -pub fn stats_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {}, - }) -} - -/// Input schema for health_check tool -pub fn health_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {}, - }) -} - -pub async fn execute_stats(storage: &Arc) -> Result { - let stats = storage.get_stats().map_err(|e| e.to_string())?; - - Ok(serde_json::json!({ - "totalNodes": stats.total_nodes, - "nodesDueForReview": stats.nodes_due_for_review, - "averageRetention": stats.average_retention, - "averageStorageStrength": stats.average_storage_strength, - "averageRetrievalStrength": stats.average_retrieval_strength, - "oldestMemory": stats.oldest_memory.map(|d| d.to_rfc3339()), - "newestMemory": stats.newest_memory.map(|d| d.to_rfc3339()), - "nodesWithEmbeddings": stats.nodes_with_embeddings, - "embeddingModel": stats.embedding_model, - "embeddingServiceReady": storage.is_embedding_ready(), - })) -} - -pub async fn execute_health(storage: &Arc) -> Result { - let stats = storage.get_stats().map_err(|e| e.to_string())?; - - // Determine health status - let status = if stats.total_nodes == 0 { - "empty" - } else if stats.average_retention < 0.3 { - "critical" - } else if stats.average_retention < 0.5 { - "degraded" - } else { - "healthy" - }; - - let mut warnings = Vec::new(); - - if stats.average_retention < 0.5 && stats.total_nodes > 0 { - warnings.push( - "Low average retention - consider running consolidation or reviewing memories" - .to_string(), - ); - } - - if stats.nodes_due_for_review > 10 { - warnings.push(format!( - "{} memories are due for review", - stats.nodes_due_for_review - )); - } - - if stats.total_nodes > 0 && stats.nodes_with_embeddings == 0 { - warnings.push( - "No embeddings generated - semantic search unavailable. Run consolidation.".to_string(), - ); - } - - let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 - } else { - 0.0 - }; - - if embedding_coverage < 50.0 && stats.total_nodes > 10 { - warnings.push(format!( - "Only {:.1}% of memories have embeddings", - embedding_coverage - )); - } - - Ok(serde_json::json!({ - "status": status, - "totalNodes": stats.total_nodes, - "nodesDueForReview": stats.nodes_due_for_review, - "averageRetention": stats.average_retention, - "embeddingCoverage": format!("{:.1}%", embedding_coverage), - "embeddingServiceReady": storage.is_embedding_ready(), - "warnings": warnings, - "recommendations": get_recommendations(&stats, status), - })) -} - -fn get_recommendations(stats: &MemoryStats, status: &str) -> Vec { - let mut recommendations = Vec::new(); - - if status == "critical" { - recommendations.push("CRITICAL: Many memories have very low retention. Review important memories with 'mark_reviewed'.".to_string()); - } - - if stats.nodes_due_for_review > 5 { - recommendations.push("Review due memories to strengthen retention.".to_string()); - } - - if stats.nodes_with_embeddings < stats.total_nodes { - recommendations.push( - "Run 'run_consolidation' to generate embeddings for better semantic search." - .to_string(), - ); - } - - if stats.total_nodes > 100 && stats.average_retention < 0.7 { - recommendations - .push("Consider running periodic consolidation to maintain memory health.".to_string()); - } - - if recommendations.is_empty() { - recommendations.push("Memory system is healthy!".to_string()); - } - - recommendations -} diff --git a/crates/vestige-mcp/src/tools/timeline.rs b/crates/vestige-mcp/src/tools/timeline.rs index f73983a..14e58bf 100644 --- a/crates/vestige-mcp/src/tools/timeline.rs +++ b/crates/vestige-mcp/src/tools/timeline.rs @@ -207,12 +207,7 @@ mod tests { /// Ingest with explicit node_type and tags. Used by the sparse-filter /// regression tests so the dominant and sparse sets can be told apart. - async fn ingest_typed( - storage: &Arc, - content: &str, - node_type: &str, - tags: &[&str], - ) { + async fn ingest_typed(storage: &Arc, content: &str, node_type: &str, tags: &[&str]) { storage .ingest(vestige_core::IngestInput { content: content.to_string(), @@ -392,11 +387,23 @@ mod tests { // Dominant set: 10 facts for i in 0..10 { - ingest_typed(&storage, &format!("Dominant memory {}", i), "fact", &["alpha"]).await; + ingest_typed( + &storage, + &format!("Dominant memory {}", i), + "fact", + &["alpha"], + ) + .await; } // Sparse set: 2 concepts for i in 0..2 { - ingest_typed(&storage, &format!("Sparse memory {}", i), "concept", &["beta"]).await; + ingest_typed( + &storage, + &format!("Sparse memory {}", i), + "concept", + &["beta"], + ) + .await; } // Limit 5 against 12 total — before the fix, `retain` on `concept` @@ -426,7 +433,13 @@ mod tests { // Dominant set: 10 memories with tag "common" for i in 0..10 { - ingest_typed(&storage, &format!("Common memory {}", i), "fact", &["common"]).await; + ingest_typed( + &storage, + &format!("Common memory {}", i), + "fact", + &["common"], + ) + .await; } // Sparse set: 2 memories with tag "rare" for i in 0..2 { diff --git a/docs/AGENT-MEMORY-PROTOCOL.md b/docs/AGENT-MEMORY-PROTOCOL.md new file mode 100644 index 0000000..367ca4b --- /dev/null +++ b/docs/AGENT-MEMORY-PROTOCOL.md @@ -0,0 +1,81 @@ +# Agent Memory Protocol + +> Minimal instructions for any MCP-compatible agent using Vestige. + +Vestige is an MCP server, not a Claude-specific workflow. Register `vestige-mcp` +with your client, then give the agent a short instruction that makes memory part +of its normal reasoning loop. + +## Register Vestige + +Use your client's MCP server configuration format. The command is the same: + +```json +{ + "mcpServers": { + "vestige": { + "command": "vestige-mcp" + } + } +} +``` + +Examples: + +```bash +claude mcp add vestige vestige-mcp -s user +codex mcp add vestige -- vestige-mcp +``` + +## Agent Instruction + +Add this to the agent's global or project instruction file: + +```text +Use Vestige as durable local memory. + +At the start of a new session, call `session_context` with the current user, +project, and task context. If `session_context` is unavailable or too broad, call +`search` with a concrete query matching the current task. + +When accuracy or prior decisions matter, call `deep_reference`. When memories may +conflict, call `contradictions` before answering. Compose retrieved evidence into +the answer; do not merely paste memory summaries. + +Save durable preferences, project decisions, recurring corrections, stable facts, +and reusable code patterns with `smart_ingest`. Do not store secrets, credentials, +one-off logs, speculation, or transient command output. + +When the user says a memory was useful, call `memory` with `action="promote"`. +When the user says a memory was wrong or unhelpful, call `memory` with +`action="demote"`. When the user explicitly asks to erase a memory permanently, +call `memory` with `action="purge"` and `confirm=true`. +``` + +## Practical Tool Choices + +| Situation | Tool | +|-----------|------| +| Start of session | `session_context` | +| Find exact identifiers, paths, env vars, names | `search` | +| Answer from prior decisions or evolving facts | `deep_reference` | +| Inspect disagreements before answering | `contradictions` | +| Save a preference, decision, correction, or code pattern | `smart_ingest` | +| Retrieve, promote, demote, edit, or purge one memory | `memory` | +| Create a future reminder | `intention` | +| Check health or maintenance state | `system_status` | + +## What Not To Store + +- API keys, tokens, passwords, private keys, or session cookies. +- Raw logs or command output unless the durable lesson is extracted first. +- Guesswork the agent has not verified. +- Temporary plans that will be obsolete after the current session. +- User data the user asked not to retain. + +## Portability Notes + +The same protocol applies to Claude Code, Codex, Cursor, VS Code, Xcode, +JetBrains, Windsurf, and any other client that can run a stdio MCP server. Claude +Code's Cognitive Sandwich hooks are optional companion files; they are not +required for normal Vestige memory. diff --git a/docs/COGNITIVE_SANDWICH.md b/docs/COGNITIVE_SANDWICH.md new file mode 100644 index 0000000..62ee9aa --- /dev/null +++ b/docs/COGNITIVE_SANDWICH.md @@ -0,0 +1,233 @@ +# Cognitive Sandwich + +**Vestige's defense-in-depth safety architecture for Claude Code.** + +The default Cognitive Sandwich installer only stages files and removes old v2.1.0 hook wiring. It activates no Claude Code hooks and makes no automatic model calls. Both the preflight layer and the Stop-hook layer are explicit opt-ins: + +``` +┌────────────────────────────────────────────────┐ +│ 🥪 TOP BREAD — UserPromptSubmit hooks │ +│ • Vestige memory graph injection │ +│ • CWD / git / CI state injection │ +│ • Synthesis-protocol gate (decision-adjacent) │ +│ • Lateral-thinker subconscious swarm │ +│ • Pulse daemon (background dream insights) │ +├────────────────────────────────────────────────┤ +│ 🥩 MEAT — Claude Code reasons │ +├────────────────────────────────────────────────┤ +│ 🥪 OPTIONAL BOTTOM BREAD — Stop hooks │ +│ • Veto-detector / synthesis validator │ +│ • Sanhedrin Executioner verifier │ +└────────────────────────────────────────────────┘ +``` + +Sanhedrin, preflight, and all Vestige Claude Code hooks are optional. The default installer wires none of them; it does not call Claude, start MLX, require a 19 GB model download, or require 20+ GB of RAM. Users who want preflight context can opt in with `--enable-preflight`. Users who want the post-response verifier can opt in with `--enable-sanhedrin` and point it at any OpenAI-compatible `/v1/chat/completions` endpoint. On Apple Silicon, an additional `--with-launchd` flag can auto-start the local MLX Qwen backend. + +--- + +## How a single response flows through the Sandwich + +1. **You type a prompt in Claude Code.** +2. **If explicitly enabled, UserPromptSubmit hooks fire in parallel** (none can block — all fail-open): + - `load-all-memory.sh` (opt-in) — dumps every memory MD into context + - `synthesis-preflight.sh` — POSTs your prompt to `vestige-mcp` `/api/deep_reference`, injects the trust-scored reasoning chain + - `cwd-state-injector.sh` — captures git status, branch, open PRs/issues, modified files + - `vestige-pulse-daemon.sh` — injects fresh Vestige dream insights from the past 20 min into the next prompt context + - `preflight-swarm.sh` — spawns the `lateral-thinker` subagent in fresh context to surface cross-disciplinary structural parallels +3. **Claude reads the assembled context and generates a draft.** +4. **By default, no Vestige Stop hooks are installed.** If explicitly enabled, Stop hooks fire serially (any can VETO with `exit 2`, forcing a rewrite): + - `veto-detector.sh` — fast regex against `veto`-tagged Vestige memories (~50ms) + - `sanhedrin.sh` → `sanhedrin-local.py` — optional Sanhedrin verifier + - `synthesis-stop-validator.sh` — regex against forbidden patterns (hedging, summary-instead-of-composition) +5. **If all enabled Stop hooks return `exit 0`, the response is delivered.** + +--- + +## The Sanhedrin Executioner protocol + +Sanhedrin has two execution modes: + +- **Legacy mode** (`VESTIGE_SANHEDRIN_CLAIM_MODE=0`) keeps the original broad draft-level semantic check for technical-looking responses. +- **Claim mode** (`VESTIGE_SANHEDRIN_CLAIM_MODE=1`) extracts check-worthy claims, retrieves Vestige evidence per claim, and aggregates structured verdicts before the Stop hook allows delivery. + +The claim-mode Executioner extracts atomic claims from Claude's draft across these classes: + +`TECHNICAL` · `BIOGRAPHICAL` · `FINANCIAL` · `ACHIEVEMENT` · `TIMELINE` · `QUANTITATIVE` · `ATTRIBUTION` · `CAUSAL` · `COMPARATIVE` · `EXISTENTIAL` · plus v2.1.0 additions: `VAGUE-QUANTIFIER` · `UNVERIFIED-POSITIVE` + +For each check-worthy claim, claim mode calls Vestige's `/api/deep_reference` and judges the claim against high-trust durable evidence plus any optional staged evidence overlay. Decision rules: + +| Class | Rule | +|---|---| +| TECHNICAL / EXISTENTIAL / CAUSAL / COMPARATIVE | VETO only on same-subject durable contradiction; missing memory is `NEI` | +| BIOGRAPHICAL / FINANCIAL / ACHIEVEMENT / TIMELINE / QUANTITATIVE / ATTRIBUTION / VAGUE-QUANTIFIER about the user | zero high-trust durable evidence is `REFUTED_BY_ABSENCE` and blocks | +| **VAGUE-QUANTIFIER** | VETO on vague achievement or financial claims without durable enumeration | +| **UNVERIFIED-POSITIVE** | VETO on specific named institutions/dates/employers not in evidence | + +Structured verdicts: + +| Verdict | Meaning | +|---|---| +| `SUPPORTED` | High-trust evidence supports or does not contradict the claim | +| `REFUTED` | High-trust durable evidence directly contradicts the same-subject claim | +| `REFUTED_BY_ABSENCE` | User-critical claim has no high-trust durable Vestige evidence | +| `NEI` | Not enough information; allow unless another claim blocks | + +The bridge still prints legacy one-line `yes` / `no - ...` by default for Stop-hook compatibility. With `VESTIGE_SANHEDRIN_OUTPUT=json`, it emits structured JSON containing `decision`, `reason`, and per-claim verdicts. `sanhedrin.sh` can parse either format. + +### Staged evidence overlay + +`VESTIGE_SANHEDRIN_STAGE_FILE` may point to a JSON array of current-turn evidence candidates. Sanhedrin can read this staged evidence as context, but staged evidence is deliberately non-durable: + +- it never calls `smart_ingest` +- it cannot promote, demote, merge, suppress, or supersede durable memories +- it does not satisfy the durable-evidence requirement for `REFUTED_BY_ABSENCE` +- durable memory writes remain a separate commit-after-pass step + +False-positive guards (added v2.1.0 after dogfood): +- Subject-equality gate (memory about Vestige codebase ≠ contradiction with external tools) +- Version-discriminator rule (M3 Max ≠ M5 Max; Qwen3.5 ≠ Qwen3.6) +- Agreement-is-not-contradiction (memory that AGREES with draft → PASS) +- Architecture-vs-component (overall architecture memory doesn't contradict component-level draft) +- Inference-verb ban (no `implies` / `suggests` / `must mean` in veto reasons) + +--- + +## Installation + +### From an installed Vestige CLI + +```bash +vestige sandwich install +``` + +`vestige update` updates binaries only by default. To refresh these optional +Claude Code companion files during an update, run +`vestige update --sandwich-companion`. The companion installer does not activate +any Claude Code hook unless you pass an explicit opt-in flag. It removes old +v2.1.0 Vestige hook wiring from `~/.claude/settings.json` while preserving +unrelated user hooks. + +### From a checkout + +```bash +git clone https://github.com/samvallad33/vestige +cd vestige +./scripts/install-sandwich.sh # add --force to overwrite existing hooks +./scripts/check-sandwich-prereqs.sh # verify no Vestige hooks are wired by default +``` + +### Optional Preflight + +Preflight is a separate opt-in layer. It includes `preflight-swarm.sh`, which uses `claude -p --model claude-haiku-4-5-20251001`; it is not wired by default. + +```bash +vestige sandwich install --enable-preflight +scripts/check-sandwich-prereqs.sh --preflight +``` + +### Optional Sanhedrin + +Sanhedrin is a separate opt-in layer. + +```bash +# Wire the Sanhedrin Stop hook, using the default OpenAI-compatible endpoint. +vestige sandwich install --enable-sanhedrin + +# Apple Silicon only, and only if the machine has enough memory: +vestige sandwich install --enable-sanhedrin --with-launchd + +# x86 / Linux / Intel Mac: use any OpenAI-compatible endpoint. +vestige sandwich install \ + --enable-sanhedrin \ + --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions \ + --sanhedrin-model=qwen2.5:14b +``` + +### Prerequisites + +| Tool | Install | +|---|---| +| Python 3.10+ | typically preinstalled | +| `jq` | `brew install jq` | +| `vestige-mcp` | `npm install -g vestige-mcp-server` | +| Claude Code | https://claude.ai/code | + +Optional Apple Silicon local Sanhedrin backend: + +| Tool | Install | +|---|---| +| macOS Apple Silicon (M1+) | required for MLX launchd only | +| `uv` | `brew install uv` | +| `mlx-lm` | `uv tool install mlx-lm` | +| `huggingface_hub[cli]` | `uv tool install 'huggingface_hub[cli]'` | +| Qwen3.6-35B-A3B-4bit | `hf download mlx-community/Qwen3.6-35B-A3B-4bit` (~19 GB) | + +### What the installer does + +1. Verifies prereqs (warnings for missing tools, fatal only on jq/python3). +2. Copies hooks to `~/.claude/hooks/`, agents to `~/.claude/agents/`. +3. Backs up existing `~/.claude/settings.json` to `.bak.pre-sandwich`, then removes old Vestige hook wiring from previous v2.1.0 installs. +4. With `--enable-preflight`, merges the UserPromptSubmit hooks block. +5. With `--enable-sanhedrin`, writes `~/.claude/hooks/vestige-sanhedrin.env` and merges a Sanhedrin-enabled Stop hooks block. +6. With `--enable-sanhedrin --with-launchd` on Apple Silicon, renders and loads `launchd/com.vestige.mlx-server.plist.template`. + +### Uninstall + +```bash +launchctl unload ~/Library/LaunchAgents/com.vestige.mlx-server.plist +rm ~/Library/LaunchAgents/com.vestige.mlx-server.plist +cp ~/.claude/settings.json.bak.pre-sandwich ~/.claude/settings.json +# Hook files in ~/.claude/hooks/ can be deleted manually. +``` + +--- + +## Performance notes + +Optional local MLX backend on M3 Max 16-core (400 GB/s memory bandwidth): +- Legacy Sanhedrin verdict: 5–15 seconds end-to-end (single deep_reference + single Qwen call) +- Claim mode: one `/api/deep_reference` call per extracted check-worthy claim, capped by `VESTIGE_SANHEDRIN_MAX_CLAIMS` +- mlx_lm.server token generation: ~82 tok/s +- mlx_lm.server peak resident memory: ~19.7 GB +- Cold model load: ~5 seconds + +On M3 Max 14-core or M2/M1 Max: closer to 3–7s prompt processing, ~50–60 tok/s generation. + +--- + +## Configuration + +| Env var | Default | Effect | +|---|---|---| +| `VESTIGE_SANHEDRIN_ENABLED` | `0` | Set to `1` to enable the optional Sanhedrin Stop hook | +| `VESTIGE_SWARM_ENABLED` | `1` | Set to `0` to disable preflight lateral-thinker swarm | +| `VESTIGE_DASHBOARD_PORT` | `3927` | Vestige MCP HTTP API port used by hooks | +| `VESTIGE_SANHEDRIN_ENDPOINT` | `http://127.0.0.1:8080/v1/chat/completions` | OpenAI-compatible chat completions endpoint for Sanhedrin | +| `VESTIGE_SANHEDRIN_MODEL` | `mlx-community/Qwen3.6-35B-A3B-4bit` | Model name sent to the Sanhedrin endpoint | +| `VESTIGE_SANHEDRIN_CLAIM_MODE` | `1` when installed with `--enable-sanhedrin` | Enables per-claim retrieval and fail-closed user-critical lanes | +| `VESTIGE_SANHEDRIN_OUTPUT` | `json` when installed with `--enable-sanhedrin` | Emits structured JSON from the bridge; shell hook also accepts legacy text | +| `VESTIGE_SANHEDRIN_STAGE_FILE` | unset | Optional JSON-array staged evidence overlay, read-only and non-durable | +| `VESTIGE_SANHEDRIN_MAX_CLAIMS` | `8` | Max check-worthy claims adjudicated per draft | +| `VESTIGE_SANHEDRIN_PYTHON` | `python3` from `PATH` | Optional Python interpreter override for the Stop hook bridge | +| `MLX_ENDPOINT` / `VESTIGE_SANDWICH_MODEL` | legacy aliases | Backward-compatible names still read by the bridge | +| `VESTIGE_MEMORY_DIR` | (auto) | Override per-user Claude memory dir | + +--- + +## Architecture provenance + +The Cognitive Sandwich originated April 2026 as a defense against a dogfood failure mode: Claude retrieved relevant memories but summarized them instead of composing them into a recommendation. The pre-cognitive layer enforces composition; the post-cognitive layer catches contradictions before they ship. + +Full architecture memory: search Vestige for `god-tier-plan` or `cognitive-sandwich` tags after install. + +--- + +## Linux / Intel Mac / x86 + +The base hook harness runs on x86. The launchd MLX helper is macOS-arm64-only. + +On Linux, Windows under WSL, or Intel Mac: +- Run `scripts/install-sandwich.sh` normally to stage files and remove old Vestige hook wiring. No hooks are activated. +- If you want Sanhedrin, run an OpenAI-compatible endpoint such as vLLM, Ollama, llama.cpp server, or a remote MLX/vLLM box. +- Install with `--enable-sanhedrin --sanhedrin-endpoint= --sanhedrin-model=`. +- If the endpoint is unreachable, Sanhedrin fails open and does not block Claude Code. diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 52c6b08..1205f70 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -6,7 +6,7 @@ ## First-Run Network Requirement -Vestige downloads the **Nomic Embed Text v1.5** model (~130MB) from Hugging Face on first use. +Vestige downloads the **Nomic Embed Text v1.5** model (~130MB) from Hugging Face on first use. Qwen3 embeddings are opt-in and download their own Hugging Face model when selected. **All subsequent runs are fully offline.** @@ -16,7 +16,7 @@ The embedding model is cached in platform-specific directories: | Platform | Cache Location | |----------|----------------| -| macOS | `~/Library/Caches/com.vestige.core/fastembed` | +| macOS | `~/Library/Caches/vestige/fastembed` | | Linux | `~/.cache/vestige/fastembed` | | Windows | `%LOCALAPPDATA%\vestige\cache\fastembed` | @@ -25,16 +25,28 @@ Override with environment variable: export FASTEMBED_CACHE_PATH="/custom/path" ``` +Qwen3 currently uses Hugging Face Hub's Candle loader directly, so use the standard Hugging Face cache environment such as `HF_HOME` if you need to relocate that larger model cache. + --- ## Environment Variables | Variable | Default | Description | |----------|---------|-------------| -| `VESTIGE_DATA_DIR` | Platform default | Custom database location | -| `VESTIGE_LOG_LEVEL` | `info` | Logging verbosity | -| `RUST_LOG` | - | Detailed tracing output | -| `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location | +| `VESTIGE_DATA_DIR` | OS per-user data directory | Storage directory fallback; overridden by `--data-dir`; database lives at `/vestige.db` | +| `VESTIGE_EMBEDDING_MODEL` | `nomic-v1.5` | Embedding backend selector. Use `qwen3-0.6b` with a build that enables `qwen3-embeddings` | +| `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering | +| `FASTEMBED_CACHE_PATH` | Platform cache directory; `./.fastembed_cache` fallback | Embedding model cache location | +| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port | +| `VESTIGE_HTTP_ENABLED` | `false` | Set `true` or `1` to enable optional MCP-over-HTTP | +| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port; `--http-port` also enables HTTP | +| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address | +| `VESTIGE_HTTP_ALLOWED_ORIGINS` | localhost origins for the HTTP port | Comma-separated browser origins allowed to call MCP-over-HTTP | +| `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard + MCP HTTP bearer auth | +| `VESTIGE_DASHBOARD_ENABLED` | `false` | Set `true` or `1` to enable the web dashboard | +| `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS-6 decay cycle cadence | + +> **Storage location precedence:** `--data-dir ` wins over `VESTIGE_DATA_DIR`; if neither is set, Vestige uses your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/core/` on Linux, `%APPDATA%\vestige\core\` on Windows. Custom paths are directories, are created if missing, expand a leading `~`, and store the database at `/vestige.db`. --- @@ -42,6 +54,8 @@ export FASTEMBED_CACHE_PATH="/custom/path" ```bash vestige-mcp --data-dir /custom/path # Custom storage location +VESTIGE_DATA_DIR=~/.vestige vestige-mcp # Env fallback storage location +VESTIGE_DATA_DIR=./.vestige vestige stats # Point the CLI at the same custom DB vestige-mcp --help # Show all options ``` @@ -58,6 +72,10 @@ vestige stats --states # Cognitive state distribution vestige health # System health check vestige consolidate # Run memory maintenance vestige restore # Restore from backup +vestige portable-export # Exact Vestige-to-Vestige archive +vestige portable-import # Import exact archive into an empty database +vestige portable-import --merge # Merge exact archive into this database +vestige sync # Pull/merge/push through a file backend ``` --- @@ -140,6 +158,14 @@ For per-project or custom storage: } ``` +For a shell-level default: + +```bash +export VESTIGE_DATA_DIR="/path/to/custom/dir" +``` + +`--data-dir` takes precedence over `VESTIGE_DATA_DIR`, so you can keep a global env default and still isolate one client or project with an explicit CLI argument. + See [Storage Modes](STORAGE.md) for more options. --- @@ -148,16 +174,27 @@ See [Storage Modes](STORAGE.md) for more options. **Latest version:** ```bash -cd vestige -git pull -cargo build --release -sudo cp target/release/vestige-mcp /usr/local/bin/ +vestige update +``` + +This updates `vestige`, `vestige-mcp`, and `vestige-restore`. It does not mutate +Claude Code Cognitive Sandwich companion files unless you explicitly request it. + +**Also refresh optional Claude Code companion files:** +```bash +vestige update --sandwich-companion ``` **Pin to specific version:** ```bash -git checkout v1.1.2 -cargo build --release +vestige update --version v2.1.21 +``` + +**Manage the optional Cognitive Sandwich layer without updating binaries:** +```bash +vestige sandwich install +vestige sandwich install --enable-preflight +vestige sandwich install --enable-sanhedrin --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions ``` **Check your version:** diff --git a/docs/FAQ.md b/docs/FAQ.md index a5a8162..93005b8 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -22,13 +22,13 @@ ## Getting Started
          -"Can Vestige support a two-Claude household?" +"Can Vestige support multiple agents or MCP clients?" -**Yes!** See [Storage Modes](STORAGE.md#option-3-multi-claude-household). You can either: -- **Share memories**: Both Claudes point to the same `--data-dir` -- **Separate identities**: Each Claude gets its own data directory +**Yes.** See [Storage Modes](STORAGE.md#option-3-multi-agent-household). You can either: +- **Share memories**: Multiple agents point to the same `--data-dir` +- **Separate identities**: Each agent gets its own data directory -For two Claudes with distinct personas (e.g., "Domovoi" and "Storm") sharing the same human, use separate directories but consider a shared "household" memory for common knowledge. +For two agents with distinct roles sharing the same human, use separate directories but consider a shared "household" memory for common knowledge.
          @@ -38,28 +38,28 @@ For two Claudes with distinct personas (e.g., "Domovoi" and "Storm") sharing the **For non-technical users:** 1. Have a technical friend do the 5-minute install -2. Add the CLAUDE.md instructions -3. Just talk to Claude normally—it handles the memory calls +2. Add the [agent memory protocol](AGENT-MEMORY-PROTOCOL.md) to your MCP client's instruction file +3. Just talk normally; the agent handles the memory calls -**The magic**: Once set up, you never think about it. Claude just... remembers. +**The magic**: Once set up, you never think about it. Your agent just remembers.
          "What input do you feed it? How does it create memories?" -Claude creates memories via MCP tool calls. Three ways: +Your agent creates memories via MCP tool calls. Three ways: -1. **Explicit**: You say "Remember that I prefer dark mode" → Claude calls `smart_ingest` -2. **Automatic**: Claude notices something important → calls `smart_ingest` proactively -3. **Codebase**: Claude detects patterns/decisions → calls `remember_pattern` or `remember_decision` +1. **Explicit**: You say "Remember that I prefer dark mode" -> the agent calls `smart_ingest` +2. **Automatic**: The agent notices something important -> calls `smart_ingest` proactively +3. **Codebase**: The agent detects patterns/decisions -> calls `codebase(action="remember_pattern")` or `codebase(action="remember_decision")` -The CLAUDE.md instructions tell Claude when to create memories proactively. +The agent memory protocol tells the client when to create memories proactively.
          "Can it be filled with a conversation stream in realtime?" -Not currently. Vestige is **tool-based**, not stream-based. Claude decides what's worth remembering, not everything gets saved. +Not currently. Vestige is **tool-based**, not stream-based. The agent decides what's worth remembering, not everything gets saved. This is intentional—saving everything would: - Bloat the knowledge base @@ -160,6 +160,8 @@ Accessibility is calculated as: `0.5 × retention + 0.3 × retrieval_strength + Memories are never deleted automatically. They fade from relevance but can be revived if accessed again (like human memory—"oh, I forgot about that!"). +If you explicitly want content gone, use `memory(action="purge", confirm=true)`. Purge permanently removes the memory content and embeddings, scrubs internal references, and keeps only a non-content tombstone so sync/audit can prove the deletion happened. + **To configure decay**: The FSRS-6 algorithm auto-tunes based on your usage patterns. Memories you access stay strong; memories you ignore fade. No manual tuning needed.
          @@ -209,11 +211,9 @@ In Vestige's current implementation: In Vestige's implementation: ``` -importance( - memory_id="the-important-one", - event_type="user_flag", # or "emotional", "novelty", "repeated_access", "cross_reference" - hours_back=9, # Look back 9 hours (configurable) - hours_forward=2 # Capture next 2 hours too +importance_score( + content="the-important content", + context_topics=["release", "memory"] ) ``` @@ -328,9 +328,9 @@ The unified `search` always uses hybrid, which gives you the best of both worlds Three approaches: -1. **Mark as important**: `importance(memory_id="xxx", event_type="user_flag")` +1. **Mark as important**: `importance_score(content="...", event_type="user_flag")` 2. **Access regularly**: The Testing Effect strengthens memories each time you retrieve them -3. **Promote explicitly**: `promote_memory(id="xxx")` after it proves valuable +3. **Promote explicitly**: `memory(action="promote", id="xxx")` after it proves valuable For truly critical information, consider also: - Using specific tags like `["critical", "never-forget"]` @@ -547,13 +547,13 @@ Common issues: | Feature | Notes App | Vestige | |---------|-----------|---------| -| Retrieval | You search manually | Claude searches contextually | +| Retrieval | You search manually | The agent searches contextually | | Decay | Everything stays forever | Unused knowledge fades naturally | | Duplicates | You manage manually | Prediction Error Gating auto-merges | | Context | Static text | Active part of AI reasoning | | Strengthening | Manual review | Automatic via Testing Effect | -The key difference: **Vestige is part of Claude's cognitive loop.** Notes are external reference—Vestige is internal memory. +The key difference: **Vestige is part of the agent's cognitive loop.** Notes are external reference; Vestige is active working memory.
          @@ -617,7 +617,7 @@ Why Nomic: - No API costs or rate limits - Fast enough for real-time search -The model is cached at `~/.cache/huggingface/` after first run. +The model is cached in the platform user cache directory first, with `./.fastembed_cache` as a fallback. Set `FASTEMBED_CACHE_PATH` to choose a specific cache path.
          @@ -815,11 +815,11 @@ See [CLAUDE-SETUP.md](CLAUDE-SETUP.md) for the full template. The key elements: **During Work**: - Notice a pattern? `codebase(action="remember_pattern")` - Made a decision? `codebase(action="remember_decision")` with rationale -- Something important? `importance()` to strengthen recent memories +- Something important? `importance_score(content="...")` to score it before saving or promoting **Memory Hygiene**: -- When a memory helps: `promote_memory` -- When a memory misleads: `demote_memory` +- When a memory helps: `memory(action="promote", id="...")` +- When a memory misleads: `memory(action="demote", id="...")`
          --- diff --git a/docs/INSTALL-INTEL-MAC.md b/docs/INSTALL-INTEL-MAC.md new file mode 100644 index 0000000..3ec02e0 --- /dev/null +++ b/docs/INSTALL-INTEL-MAC.md @@ -0,0 +1,72 @@ +# Intel Mac Installation + +The Intel Mac (`x86_64-apple-darwin`) binary links dynamically against a system +ONNX Runtime instead of a prebuilt ort-sys library. Microsoft is discontinuing +x86_64 macOS prebuilts after ONNX Runtime v1.23.0, so we use the +`ort-dynamic` feature to runtime-link against the version you install locally. +This keeps Vestige working on Intel Mac without waiting for a dead upstream. + +## Prerequisite + +Install ONNX Runtime via Homebrew: + +```bash +brew install onnxruntime +``` + +## Install + +```bash +# 1. Install the binary +npm install -g vestige-mcp-server@latest + +# 2. Point the binary at Homebrew's libonnxruntime +echo 'export ORT_DYLIB_PATH="'"$(brew --prefix onnxruntime)"'/lib/libonnxruntime.dylib"' >> ~/.zshrc +source ~/.zshrc + +# 3. Verify +vestige-mcp --version + +# 4. Connect to Claude Code +claude mcp add vestige vestige-mcp -s user +``` + +`ORT_DYLIB_PATH` is how the `ort` crate's `load-dynamic` feature finds the +shared library at runtime. Without it the binary starts but fails on the first +embedding call with a "could not find libonnxruntime" error. + +## Building from source + +```bash +brew install onnxruntime +git clone https://github.com/samvallad33/vestige && cd vestige +cargo build --release -p vestige-mcp \ + --no-default-features \ + --features ort-dynamic,vector-search +export ORT_DYLIB_PATH="$(brew --prefix onnxruntime)/lib/libonnxruntime.dylib" +./target/release/vestige-mcp --version +``` + +## Troubleshooting + +**`dyld: Library not loaded: libonnxruntime.dylib`** — `ORT_DYLIB_PATH` is not +set for the shell that spawned `vestige-mcp`. Claude Code / Codex inherits the +env vars from whatever launched it; export `ORT_DYLIB_PATH` in `~/.zshrc` or +`~/.bashrc` and restart the client. + +**`error: ort-sys does not provide prebuilt binaries for the target +x86_64-apple-darwin`** — you hit this only if you ran `cargo build` without the +`--no-default-features --features ort-dynamic,vector-search` flags. The default +feature set still tries to download a non-existent prebuilt. Add the flags and +rebuild. + +**Homebrew installed `onnxruntime` but `brew --prefix onnxruntime` prints +nothing** — upgrade brew (`brew update`) and retry. Older brew formulae used +`onnx-runtime` (hyphenated). If your brew still has the hyphenated formula, +substitute accordingly in the commands above. + +## Long-term + +Intel Mac will move to a fully pure-Rust backend (`ort-candle`) in Vestige +v2.1, removing the Homebrew prerequisite entirely. Track progress at +[issue #41](https://github.com/samvallad33/vestige/issues/41). diff --git a/docs/SCIENCE.md b/docs/SCIENCE.md index 795708d..fad2ca0 100644 --- a/docs/SCIENCE.md +++ b/docs/SCIENCE.md @@ -126,11 +126,9 @@ In Vestige's implementation: In Vestige: ``` -importance( - memory_id="the-important-one", - event_type="user_flag", - hours_back=9, - hours_forward=2 +importance_score( + content="the-important content", + context_topics=["release", "memory"] ) ``` @@ -183,7 +181,7 @@ This gives you exact keyword matching AND semantic understanding in one search. - Runs 100% local (after first download) - Competitive with OpenAI's ada-002 -The model is cached at `~/.cache/huggingface/` after first run. +The model is cached in the platform user cache directory after first run, with `./.fastembed_cache` as a fallback. Set `FASTEMBED_CACHE_PATH` to choose a specific cache path. --- diff --git a/docs/STORAGE.md b/docs/STORAGE.md index cefca33..1a82687 100644 --- a/docs/STORAGE.md +++ b/docs/STORAGE.md @@ -1,6 +1,6 @@ # Storage Configuration -> Global, per-project, and multi-Claude setups +> Global, per-project, and multi-agent setups --- @@ -14,6 +14,52 @@ All memories are stored in a **single local SQLite file**: | Linux | `~/.local/share/vestige/core/vestige.db` | | Windows | `%APPDATA%\vestige\core\vestige.db` | +Override precedence: + +1. `vestige-mcp --data-dir ` +2. `VESTIGE_DATA_DIR=` +3. OS default shown above + +`--data-dir` and `VESTIGE_DATA_DIR` both point to a **directory**, not the database file itself. Vestige creates the directory if it does not exist, expands a leading `~`, and stores the database at `/vestige.db`. + +--- + +## Moving Memories Between Devices + +For device-to-device migration, use a portable archive instead of the normal JSON export: + +```bash +# On the source machine +vestige portable-export ~/Desktop/vestige-portable.json + +# On the destination machine, before adding memories +vestige portable-import ~/Desktop/vestige-portable.json +``` + +Portable archives preserve raw Vestige storage rows: memory IDs, FSRS state, graph connections, suppression state, timestamps, audit history, and embedding blobs. + +For one-time migration, keep the conservative empty-database import: + +```bash +vestige portable-import ~/Desktop/vestige-portable.json +``` + +For cross-device sync, use merge mode or the file-backed sync command: + +```bash +# Merge a portable archive into an existing database. +vestige portable-import ~/Dropbox/vestige/portable.json --merge + +# Pull, merge, and push through a shared archive file. +vestige sync ~/Dropbox/vestige/portable.json +``` + +`vestige sync` uses the same pluggable portable-sync backend interface as the core library. v2.1.1 ships a file backend, which works with Dropbox, iCloud Drive, Syncthing, Git, network shares, or any folder-sync system. The merge algorithm applies delete tombstones, keeps newer local memories on timestamp conflicts, preserves stable IDs, rebuilds FTS after import, and writes the pushed archive atomically when the filesystem supports rename. v2.1.2 also carries non-content purge tombstones so a hard purge can sync without retaining the deleted memory text. + +When using the MCP `export` tool with `format: "portable"`, Vestige writes the archive under the active data directory's `exports/` folder. The MCP `restore` tool only reads from that `exports/` or `backups/` folder by default; pass `allowAnyPath: true` only for a trusted local file you selected manually. + +The regular `vestige export` / `vestige restore` path remains useful for human-readable backups, partial exports, and older files, but it re-ingests memory content and does not preserve every storage-level relationship. + --- ## Storage Modes @@ -30,6 +76,12 @@ One shared memory for all projects. Good for: claude mcp add vestige vestige-mcp -s user ``` +To set a global override for all MCP launches that inherit your shell environment: + +```bash +export VESTIGE_DATA_DIR="~/.vestige" +``` + ### Option 2: Per-Project Memory Separate memory per codebase. Good for: @@ -37,9 +89,9 @@ Separate memory per codebase. Good for: - Different coding styles per project - Team environments -**Claude Code Setup:** +**MCP Client Setup:** -Add to your project's `.claude/settings.local.json`: +Add an MCP server entry to your client or project config: ```json { "mcpServers": { @@ -53,6 +105,15 @@ Add to your project's `.claude/settings.local.json`: This creates `.vestige/vestige.db` in your project root. Add `.vestige/` to `.gitignore`. +If both `VESTIGE_DATA_DIR` and `--data-dir` are set, the CLI flag wins. Use the env var for a machine-wide default and the CLI flag for per-client or per-project overrides. + +The `vestige` CLI also honors `VESTIGE_DATA_DIR`, so use the same directory when inspecting or exporting a custom MCP instance: + +```bash +VESTIGE_DATA_DIR=./.vestige vestige stats +VESTIGE_DATA_DIR=./.vestige vestige portable-export ./vestige-portable.json +``` + **Multiple Named Instances:** For power users who want both global AND project memory: @@ -70,11 +131,11 @@ For power users who want both global AND project memory: } ``` -### Option 3: Multi-Claude Household +### Option 3: Multi-Agent Household -For setups with multiple Claude instances (e.g., Claude Desktop + Claude Code, or two personas): +For setups with multiple MCP clients or agent personas: -**Shared Memory (Both Claudes share memories):** +**Shared Memory (all clients share memories):** ```json { "mcpServers": { @@ -86,27 +147,27 @@ For setups with multiple Claude instances (e.g., Claude Desktop + Claude Code, o } ``` -**Separate Identities (Each Claude has own memory):** +**Separate Identities (each agent has its own memory):** -Claude Desktop config - for "Domovoi": +Client config for "Research": ```json { "mcpServers": { "vestige": { "command": "vestige-mcp", - "args": ["--data-dir", "~/vestige-domovoi"] + "args": ["--data-dir", "~/vestige-research"] } } } ``` -Claude Code config - for "Storm": +Client config for "Builder": ```json { "mcpServers": { "vestige": { "command": "vestige-mcp", - "args": ["--data-dir", "~/vestige-storm"] + "args": ["--data-dir", "~/vestige-builder"] } } } @@ -116,7 +177,7 @@ Claude Code config - for "Storm": ## Data Safety -**Important:** Vestige stores data locally with no cloud sync, redundancy, or automatic backup. +**Important:** Vestige stores data locally. v2.1.1 adds user-controlled file-backed sync through `vestige sync`, but Vestige does not run a hosted cloud service, background replication daemon, or automatic backup for you. | Use Case | Risk Level | Recommendation | |----------|------------|----------------| @@ -169,3 +230,75 @@ SELECT COUNT(*) FROM knowledge_nodes WHERE retention_strength < 0.1; ``` **Caution**: Don't modify the database while Vestige is running. + +--- + +## Multi-Process Safety + +Vestige's SQLite configuration is tuned for **safe concurrent reads alongside a single writer**. Multiple `vestige-mcp` processes pointed at the same database file is a supported *read-heavy* pattern; concurrent heavy writes from multiple processes is **experimental** and documented here honestly. + +### What's shipped + +Every `Storage::new()` call executes these pragmas on both the reader and writer connection (`crates/vestige-core/src/storage/sqlite.rs`): + +```sql +PRAGMA journal_mode = WAL; -- readers don't block writers, writers don't block readers +PRAGMA synchronous = NORMAL; -- durable across app crashes, not across OS crashes +PRAGMA cache_size = -64000; -- 64 MiB page cache per connection +PRAGMA temp_store = MEMORY; +PRAGMA foreign_keys = ON; +PRAGMA busy_timeout = 5000; -- wait 5s on SQLITE_BUSY before surfacing the error +PRAGMA mmap_size = 268435456; -- 256 MiB memory-mapped I/O window +PRAGMA journal_size_limit = 67108864; +PRAGMA optimize = 0x10002; +``` + +Internally the `Storage` type holds **separate reader and writer connections**, each guarded by its own `Mutex`. Within a single process this means: + +- Any number of concurrent readers share the read connection lock. +- Writers serialize on the writer connection lock. +- WAL lets readers continue while a writer commits — they don't block each other at the SQLite level. + +### What works today + +| Pattern | Status | Notes | +|---------|--------|-------| +| One `vestige-mcp` + one MCP client | **Supported** | The default case. Zero contention. | +| Multiple MCP clients, separate `--data-dir` | **Supported** | Each process owns its own DB file. No shared state. | +| Multiple MCP clients, **shared** `--data-dir`, **one** `vestige-mcp` | **Supported** | Clients talk to a single MCP process that owns the DB. Recommended for multi-agent setups. | +| CLI (`vestige` binary) reading while `vestige-mcp` runs | **Supported** | WAL makes this safe — queries see a consistent snapshot. | +| Time Machine / `rsync` backup during writes | **Supported** | WAL journal gets copied with the main file; recovery handles it. | + +### What's experimental + +| Pattern | Status | Notes | +|---------|--------|-------| +| **Two `vestige-mcp` processes** writing the same DB concurrently | **Experimental** | SQLite serializes writers via a lock; if contention exceeds the 5s `busy_timeout`, writes surface `SQLITE_BUSY`. No exponential backoff or inter-process coordination layer beyond the pragma. | +| External writers (another SQLite client holding a write transaction open) | **Experimental** | Same concern as above — the 5s window is the only safety net. | +| Corrupted WAL recovery after hard-kill | **Supported by SQLite** | WAL is designed for crash recovery, but we do not explicitly test the `PRAGMA wal_checkpoint(RESTART)` path under load. | + +If you hit `database is locked` errors: + +```bash +# Identify the holder +lsof ~/Library/Application\ Support/com.vestige.core/vestige.db + +# Clean shutdown of all vestige processes +pkill -INT vestige-mcp +``` + +### Why the "Stigmergic Swarm" story is honest + +Multi-agent coordination through a shared memory graph — where agents alter the graph and other agents later *sense* those changes rather than passing explicit messages — is a first-class pattern on the **shared `--data-dir` + one `vestige-mcp`** setup above. In that configuration, every write flows through a single MCP process: WAL gives readers (agents querying state) a consistent view while the writer commits atomically, and the broadcast channel in `dashboard/events.rs` surfaces each cognitive event (dream, consolidation, promotion, suppression, Rac1 cascade) to every connected client in real time. No inter-process write coordination is required because there is one writer. + +Running two or more `vestige-mcp` processes against the same file is where "experimental" kicks in. For the swarm narrative, point every agent at one MCP instance — that's the shipping pattern. + +### Roadmap + +Things we haven't shipped yet, tracked for a future release: + +1. **File-based advisory lock** (`fs2` / `fcntl`) to detect and refuse startup when another `vestige-mcp` already owns the DB, instead of failing later with a lock error. +2. **Retry with jitter on `SQLITE_BUSY`** in addition to the pragma's blocking wait. +3. **Load test**: two `vestige-mcp` instances hammering the same file with mixed read/write traffic, verifying zero corruption and bounded write latency. + +Until those land, treat "two writer processes on one file" as experimental. For everything else on this page, WAL + the 5s busy timeout is the shipping story. diff --git a/docs/VESTIGE_STATE_AND_PLAN.md b/docs/VESTIGE_STATE_AND_PLAN.md new file mode 100644 index 0000000..3e3a001 --- /dev/null +++ b/docs/VESTIGE_STATE_AND_PLAN.md @@ -0,0 +1,102 @@ +# Vestige State And Plan + +This document is a public, sanitized replacement for an older internal planning +snapshot. It intentionally omits private local paths, personal operating +context, unpublished roadmap notes, and private repository locations. + +For current user-facing release information, use: + +- `README.md` +- `CHANGELOG.md` +- `docs/STORAGE.md` +- `docs/COGNITIVE_SANDWICH.md` +- `docs/AGENT-MEMORY-PROTOCOL.md` +- `docs/CLAUDE-SETUP.md` + +## Current Release Shape + +Vestige v2.1.21 is the "Agent-Neutral Hardening" release. Its public scope is: + +- stdio MCP as the default agent transport, with HTTP MCP opt-in only +- binary-only `vestige update` by default +- delete and purge confirmation parity for destructive memory removal +- portable sync fixes for purge tombstones, UPSERT merge, and vector index + reloads +- safer release packaging with dashboard freshness checks and checksums +- agent-neutral memory instructions for any MCP-compatible client + +The release keeps the local-first baseline intact. Heavy model hooks, local +verifier models, and preflight automation remain optional. + +## Release Gates + +Before tagging a release, run: + +```sh +cargo test --workspace --no-fail-fast +cargo clippy --workspace -- -D warnings +pnpm --filter @vestige/dashboard check +pnpm --filter @vestige/dashboard build +git diff --check +``` + +For dashboard route changes, rebuild and stage `apps/dashboard/build/` so the +embedded static assets match `apps/dashboard/src/`. + +## Product Principles + +- Exact things should stay exact. Literal identifiers should not lose to + semantic expansion. +- Forgetting should be honest. A hard purge should remove content, embeddings, + graph edges, and derived references while retaining only non-content proof + that deletion happened. +- Contradictions should be visible. Trust-weighted disagreement should be + inspectable directly instead of hidden inside a broader reasoning tool. +- Installation should remain boring. Users should not need a large local model + or background hook system just to use memory. +- Pro features should add managed convenience without weakening local-first + ownership. + +## Public Architecture Summary + +Vestige is organized as: + +- `crates/vestige-core`: storage, search, embeddings, memory lifecycle, FSRS, + graph, dream, and cognitive modules +- `crates/vestige-mcp`: MCP server, CLI, dashboard backend, tools, update flow +- `apps/dashboard`: SvelteKit dashboard source +- `packages/vestige-mcp-npm`: npm wrapper for the MCP binary +- `packages/vestige-init`: installer helper +- `docs`: user and integration documentation + +## v2.1.21 Implementation Notes + +HTTP MCP is disabled unless the user passes `--http`, passes `--http-port`, or +sets `VESTIGE_HTTP_ENABLED=1`. The stdio MCP server remains the portable default +for Claude Code, Codex, Cursor, VS Code, Xcode, JetBrains, Windsurf, and other +clients. + +Purge is implemented transactionally in storage and surfaced through the MCP +`memory` tool. `memory(action="purge", confirm=true)` is the explicit hard +delete path. `delete` remains a backwards-compatible alias but also requires +`confirm=true`. + +Portable merge imports preserve both sync tombstones and non-content deletion +tombstones. Keyed table writes use UPSERT rather than `INSERT OR REPLACE` so +related rows are not accidentally cascaded away. + +Claude Code Cognitive Sandwich files are optional companion files, not the +default Vestige setup path. Use `vestige update --sandwich-companion` or +`vestige sandwich install` only when that hook layer is wanted. + +## 15. Autopilot Rationale + +The backend event bus exists so dashboard and MCP activity can be observed by +the cognitive engine without making user-facing agent hooks mandatory. Any +autonomous behavior should be conservative, rate-limited, and local-first. + +Autopilot-style routing should never require a remote model, a heavy local +model, or a Claude hook to make normal memory useful. It should only connect +already-emitted Vestige events to existing cognitive modules when that improves +maintenance, retrieval quality, or dashboard fidelity without surprising the +user. diff --git a/docs/blog/xcode-memory.md b/docs/blog/xcode-memory.md index 4277f09..8036181 100644 --- a/docs/blog/xcode-memory.md +++ b/docs/blog/xcode-memory.md @@ -20,8 +20,7 @@ It speaks MCP (Model Context Protocol), the same protocol Xcode 26.3 uses for to **Step 1:** Install Vestige ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server ``` **Step 2:** Drop one file in your project root @@ -110,8 +109,7 @@ The full setup takes 30 seconds: ```bash # Install Vestige -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server # Add to your project (run from project root) cat > .mcp.json << 'EOF' diff --git a/docs/integrations/codex-intelligent-memory.md b/docs/integrations/codex-intelligent-memory.md new file mode 100644 index 0000000..b4ab0a5 --- /dev/null +++ b/docs/integrations/codex-intelligent-memory.md @@ -0,0 +1,72 @@ +# Codex Intelligent Memory Protocol + +Codex can connect to Vestige through MCP, but MCP registration alone only makes +the tools available. It does not make Codex automatically reason with memory. + +Use this protocol when configuring a Codex workspace that should behave like it +has long-term cognitive memory. + +## 1. Register Vestige MCP + +```toml +[mcp_servers.vestige] +command = "/absolute/path/to/vestige-mcp" +``` + +Restart Codex after changing MCP configuration. + +## 2. Add An `AGENTS.md` Trigger + +Codex reads `AGENTS.md` files as workspace instructions. Put a file at the repo +root, or a higher workspace root, with a rule like: + +```markdown +Before answering substantive prompts, consult Vestige using the current prompt +plus project and user context. Use `session_context` for broad context, `search` +for quick memory checks, and `deep_reference` for decisions, contradictions, or +accuracy-sensitive questions. Compose memories into actions; do not summarize +retrievals. +``` + +This is the Codex equivalent of the lightweight top-bread memory trigger. + +## 3. Use A Query Router + +Use the smallest call that can change the answer: + +- `session_context`: start of a topic or project switch. +- `search`: identity, preference, exact memory, or quick project context. +- `deep_reference` / `cross_reference`: decision history, contradictions, + timelines, or root-cause analysis. +- `memory(get_batch)`: expand specific load-bearing memories. +- `smart_ingest`: save durable corrections, decisions, or new preferences. + +## 4. Compose, Do Not Summarize + +Retrieved memory is evidence, not the final answer. + +Use this mental transform: + +```text +memory fact -> implication -> action +``` + +If memory does not change the action, do not mention it. If it does, make the +changed recommendation clear. + +## 5. Know The Limit + +Claude Code's Cognitive Sandwich can use `UserPromptSubmit` and `Stop` hooks to +wrap every response. Codex may expose different hook events depending on version. +Do not assume Claude's hook chain is active in Codex just because Vestige MCP is +registered. + +For Codex, the reliable portable layer is: + +1. MCP server configured. +2. `AGENTS.md` instruction trigger. +3. Local Codex rule docs. +4. Explicit agent discipline: call Vestige before substantive answers. + +If a future Codex version supports a stable pre-prompt hook, wire that hook to +inject a short Vestige reminder or context packet before the model answers. diff --git a/docs/integrations/codex.md b/docs/integrations/codex.md index 71f21bd..7d9b17b 100644 --- a/docs/integrations/codex.md +++ b/docs/integrations/codex.md @@ -89,6 +89,27 @@ args = ["--data-dir", "/Users/you/projects/my-app/.vestige"] --- +## Intelligent Memory Protocol + +MCP registration makes Vestige tools available to Codex. It does not, by itself, +force Codex to call those tools before answering. + +For workspaces where Codex should behave like it has persistent cognitive +memory, add an `AGENTS.md` file at the workspace or repo root: + +```markdown +Before answering substantive prompts, consult Vestige using the current prompt +plus project and user context. Use `session_context` for broad context, `search` +for quick memory checks, and `deep_reference` for decisions, contradictions, or +accuracy-sensitive questions. Compose memories into actions; do not summarize +retrievals. +``` + +Then use the full protocol in +[`codex-intelligent-memory.md`](./codex-intelligent-memory.md). + +--- + ## Troubleshooting
          diff --git a/docs/integrations/windsurf.md b/docs/integrations/windsurf.md index 8fd0c7f..3a0aca1 100644 --- a/docs/integrations/windsurf.md +++ b/docs/integrations/windsurf.md @@ -115,7 +115,7 @@ It remembers. ## Important: Tool Limit -Windsurf has a **hard cap of 100 tools** across all MCP servers. Vestige uses 24 tools, leaving plenty of room for other servers. +Windsurf has a **hard cap of 100 tools** across all MCP servers. Vestige uses 25 tools, leaving plenty of room for other servers. --- diff --git a/docs/integrations/xcode.md b/docs/integrations/xcode.md index fb22dcf..3856a5e 100644 --- a/docs/integrations/xcode.md +++ b/docs/integrations/xcode.md @@ -13,8 +13,7 @@ Xcode 26.3 supports [agentic coding](https://developer.apple.com/documentation/x ### 1. Install Vestige ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server@latest ``` ### 2. Add to your Xcode project @@ -27,7 +26,7 @@ cat > /path/to/your/project/.mcp.json << 'EOF' "mcpServers": { "vestige": { "type": "stdio", - "command": "/usr/local/bin/vestige-mcp", + "command": "vestige-mcp", "args": [], "env": { "PATH": "/usr/local/bin:/usr/bin:/bin" @@ -51,7 +50,7 @@ Quit Xcode completely (Cmd+Q) and reopen your project. ### 4. Verify -Type `/context` in the Agent panel. You should see `vestige` listed with 24 tools. +Type `/context` in the Agent panel. You should see `vestige` listed with 25 tools. --- @@ -166,7 +165,7 @@ See [CLAUDE.md templates](../CLAUDE-SETUP.md) for a full setup. The first time Vestige runs, it downloads the embedding model (~130MB). In Xcode's sandboxed environment, the cache location is: ``` -~/Library/Caches/com.vestige.core/fastembed +~/Library/Caches/vestige/fastembed ``` If the download fails behind a corporate proxy, pre-download by running `vestige-mcp` once from your terminal. @@ -231,7 +230,7 @@ Xcode 26.3 has a feature gate (`claudeai-mcp`) that may block custom MCP servers The first run downloads ~130MB. If Xcode's sandbox blocks the download: 1. Run `vestige-mcp` once from your terminal to cache the model -2. The cache at `~/Library/Caches/com.vestige.core/fastembed` will be available to the sandboxed instance +2. The cache at `~/Library/Caches/vestige/fastembed` will be available to the sandboxed instance Behind a proxy: ```bash diff --git a/docs/launch/blog-post.md b/docs/launch/blog-post.md index 558ef24..886bb5a 100644 --- a/docs/launch/blog-post.md +++ b/docs/launch/blog-post.md @@ -318,7 +318,7 @@ SQLite is the most deployed database in the world for a reason. WAL mode gives u ### fastembed (Nomic Embed v1.5) -All embeddings run locally. The Nomic Embed v1.5 model produces 768-dimensional vectors, runs via ONNX Runtime, and is competitive with OpenAI's ada-002. The model is cached at `~/.cache/huggingface/` after first download (~130MB). No API keys. No network calls during operation. Your memories never leave your machine. +All embeddings run locally. The Nomic Embed v1.5 model produces 768-dimensional vectors, runs via ONNX Runtime, and is competitive with OpenAI's ada-002. The model is cached in the platform user cache directory after first download (~130MB), with `./.fastembed_cache` as a fallback. No API keys. No network calls during operation. Your memories never leave your machine. ### Performance diff --git a/docs/launch/demo-script.md b/docs/launch/demo-script.md index f5444b2..4740dc4 100644 --- a/docs/launch/demo-script.md +++ b/docs/launch/demo-script.md @@ -194,10 +194,10 @@ wc -l $(find /path/to/vestige/crates -name "*.rs") | tail -1 # → 77,840 total ``` -> Seventy-eight thousand lines of Rust. Seven hundred thirty-four tests. Twenty-two megabyte binary. Ships with the dashboard embedded. Install is one curl command: +> Seventy-eight thousand lines of Rust. Seven hundred thirty-four tests. Twenty-two megabyte binary. Ships with the dashboard embedded. Install is one npm command: ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +npm install -g vestige-mcp-server claude mcp add vestige vestige-mcp -s user ``` @@ -241,8 +241,7 @@ claude mcp add vestige vestige-mcp -s user ```bash # Install (macOS Apple Silicon) -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server ``` > Three binaries. The MCP server, the CLI admin tool, and a restore utility. Twenty-two megabytes total. No Docker. No Python. No node_modules. No cloud API key. @@ -389,7 +388,7 @@ vestige-mcp --version # <300ns cosine similarity (benchmarked with Criterion) # Zero cloud dependencies # Zero API keys required -# One curl command to install +# One command to install ``` > This is what I've been building for the past three months. I'm one person, I'm twenty-one years old, and I believe this is how AI memory should work — grounded in real science, running locally, open source. @@ -420,7 +419,7 @@ vestige-mcp --version > Yes. It speaks MCP — the Model Context Protocol. One config change and it works with Claude Desktop, Cursor, VS Code Copilot, JetBrains, Windsurf, Xcode 26.3. Anything that speaks MCP. **Q: What about multi-user or team memory?** -> That's the v3.0 roadmap — "Hivemind." Ed25519 identity, CRDT-based sync, transactive directory (Wegner's "who knows what" routing), federated retrieval with differential privacy. The open source version is single-user, local-first. Team and cloud features will be proprietary. +> The current Pro plan is more pragmatic: prove portable sync/storage first, then ship Solo and Team workflows around managed sync, backups, onboarding, and support. The open-source core stays local-first; paid team features should stay in the separate Pro/commercial boundary. **Q: How does Prediction Error Gating prevent duplicate memories?** > When you ingest a new memory, it computes embedding similarity against all existing memories. If similarity is above 0.92, it reinforces the existing memory (bumps FSRS stability). Between 0.75 and 0.92, it updates/merges. Below 0.75, it creates a new memory. The thresholds come from computational neuroscience research on prediction error signals — the brain stores what's surprising, reinforces what's familiar, and updates what's partially known. Same principle. @@ -479,7 +478,7 @@ vestige-mcp --version - **Start from the dashboard.** The 3D graph is the hook. It's visual, it's unusual, it makes people lean in. - **Don't rush the dream sequence.** The purple wash and sequential node pulses are the most visually impressive moment. Let it breathe for 3-4 seconds. - **Say the scientists' names.** "Ebbinghaus," "Bjork," "Frey and Morris" — this signals that you've done the reading. The MCP Dev Summit audience respects depth. -- **Make eye contact during the punchline.** "One curl command. Your AI now has a brain." Look at the audience, not the screen. +- **Make eye contact during the punchline.** "One command. Your AI now has a brain." Look at the audience, not the screen. - **Own your age.** Twenty-one, solo developer, zero funding. This is an asset, not a liability. You built something that the well-funded competitors haven't. - **The dashboard is your co-presenter.** Every time Claude does something, the dashboard should be showing the corresponding event. Practice the terminal-to-browser switch until it's seamless. - **Don't apologize.** Not for bugs, not for the AGPL, not for being solo. Confident but not arrogant. The work speaks. diff --git a/docs/launch/reddit-cross-reference.md b/docs/launch/reddit-cross-reference.md index e6e918a..eae7aaf 100644 --- a/docs/launch/reddit-cross-reference.md +++ b/docs/launch/reddit-cross-reference.md @@ -10,11 +10,11 @@ I've been building Vestige — an MCP memory server that gives Claude persistent But last week it almost cost me hours of debugging. -Claude confidently told me my AIMO3 competition notebook should use `--enable-prefix-caching` with vLLM. I trusted it. The notebook crashed. Scored 0/50. Burned a daily submission. +Claude confidently told me a benchmark notebook should use `--enable-prefix-caching` with vLLM. I trusted it. The notebook crashed. Burned a daily submission. The problem? I had TWO memories: - **January**: "prefix caching crashes with our vLLM build" -- **March**: "prefix caching works with the new animsamuelk wheels" +- **March**: "prefix caching works with the newer vLLM build" Claude found both. Picked the wrong one. Gave me a confident wrong answer based on the January memory. The March memory was correct — but Claude had no way to know they conflicted. @@ -38,11 +38,11 @@ And gets back: { "newer": { "date": "2026-03-18", - "preview": "Switched to animsamuelk wheels which support --enable-prefix-caching..." + "preview": "Switched to a newer vLLM build that supports --enable-prefix-caching..." }, "older": { "date": "2026-01-15", - "preview": "prefix caching crashed with our samvalladares vLLM build..." + "preview": "prefix caching crashed with our older local vLLM build..." }, "recommendation": "Trust the newer memory. Consider demoting the older one." } @@ -88,7 +88,7 @@ Memory systems need to be SMARTER, not just bigger. That's what Vestige does — ### Install (30 seconds): ```bash # macOS Apple Silicon -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +npm install -g vestige-mcp-server sudo mv vestige-mcp /usr/local/bin/ claude mcp add vestige vestige-mcp -s user ``` @@ -162,7 +162,7 @@ The AI sees the conflict. Picks the right one. Every time. **100% local. Your data never leaves your machine.** ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz +npm install -g vestige-mcp-server sudo mv vestige-mcp /usr/local/bin/ claude mcp add vestige vestige-mcp -s user ``` diff --git a/docs/launch/show-hn.md b/docs/launch/show-hn.md index 03034e0..8cc5a95 100644 --- a/docs/launch/show-hn.md +++ b/docs/launch/show-hn.md @@ -401,8 +401,7 @@ locally on your machine. **Setup (2 minutes):** ```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +npm install -g vestige-mcp-server claude mcp add vestige vestige-mcp -s user ``` diff --git a/hooks/cwd-state-injector.sh b/hooks/cwd-state-injector.sh new file mode 100755 index 0000000..50c0bd9 --- /dev/null +++ b/hooks/cwd-state-injector.sh @@ -0,0 +1,116 @@ +#!/bin/bash +# cwd-state-injector.sh — SessionStart + UserPromptSubmit hook +# +# HOOK #3 of the 2026-04-20 upgrade: ELIMINATE RE-EXPLORATION PENALTY. +# +# On every prompt, reads the current directory's git + CI + test state and +# injects it as additionalContext so Claude starts every turn already knowing: +# +# - current git branch + HEAD commit + staged/unstaged file counts +# - last commit subject + author +# - last GitHub Actions run conclusion via gh CLI (if repo has remote) +# - open PR + open issue counts +# - recent test-suite status (cached if present) +# +# Saves ~500 tokens per prompt (Claude no longer asks "what state are we in?") +# and prevents stale-state reasoning errors. +# +# Cached in /tmp/cwd-state-{hash}.json for 60s to keep hook fast. +# Fails open: if gh or git unavailable, emits partial context. + +set -u + +INPUT="$(cat)" +CWD="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("cwd",""))' 2>/dev/null || printf '')" + +# Fallback to PWD if cwd not in input +if [ -z "$CWD" ] || [ ! -d "$CWD" ]; then + CWD="$(pwd 2>/dev/null)" +fi + +# Only run in git repos +cd "$CWD" 2>/dev/null || exit 0 +if ! /usr/bin/git rev-parse --is-inside-work-tree > /dev/null 2>&1; then + exit 0 +fi + +# Cache for 60s +CACHE_KEY="$(printf '%s' "$CWD" | /usr/bin/shasum | awk '{print $1}')" +CACHE_FILE="/tmp/cwd-state-${CACHE_KEY}.json" +if [ -f "$CACHE_FILE" ]; then + MTIME=$(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) + NOW=$(date +%s) + AGE=$((NOW - MTIME)) + if [ "$AGE" -lt 60 ] && [ -s "$CACHE_FILE" ]; then + cat "$CACHE_FILE" + exit 0 + fi +fi + +# Gather state +BRANCH="$(/usr/bin/git rev-parse --abbrev-ref HEAD 2>/dev/null)" +HEAD_SHA="$(/usr/bin/git rev-parse --short HEAD 2>/dev/null)" +HEAD_SUBJECT="$(/usr/bin/git log -1 --format='%s' 2>/dev/null | head -c 100)" +HEAD_AUTHOR="$(/usr/bin/git log -1 --format='%an' 2>/dev/null)" +STAGED_COUNT="$(/usr/bin/git diff --cached --name-only 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')" +UNSTAGED_COUNT="$(/usr/bin/git diff --name-only 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')" +UNTRACKED_COUNT="$(/usr/bin/git ls-files --others --exclude-standard 2>/dev/null | /usr/bin/wc -l | awk '{print $1}')" +AHEAD_BEHIND="$(/usr/bin/git rev-list --left-right --count HEAD...@{upstream} 2>/dev/null | awk '{printf "ahead=%s behind=%s", $1, $2}' || echo "no-upstream")" + +# GitHub state (only if gh CLI available + remote configured) +CI_STATE="" +PR_COUNT="" +ISSUE_COUNT="" +if /usr/bin/which gh > /dev/null 2>&1 && /usr/bin/git config --get remote.origin.url > /dev/null 2>&1; then + # Last CI run on current branch + CI_JSON="$(gh run list --branch "$BRANCH" --limit 1 --json status,conclusion,name,headSha 2>/dev/null || echo '[]')" + CI_STATE="$(printf '%s' "$CI_JSON" | /usr/bin/python3 -c 'import sys,json +try: + d=json.load(sys.stdin) + if d: r=d[0]; print(f"{r.get(\"name\",\"?\")}:{r.get(\"status\",\"?\")}:{r.get(\"conclusion\") or \"...\"}") +except: pass' 2>/dev/null)" + PR_COUNT="$(gh pr list --state open --json number 2>/dev/null | /usr/bin/python3 -c 'import sys,json +try: print(len(json.load(sys.stdin))) +except: print("?")' 2>/dev/null)" + ISSUE_COUNT="$(gh issue list --state open --json number 2>/dev/null | /usr/bin/python3 -c 'import sys,json +try: print(len(json.load(sys.stdin))) +except: print("?")' 2>/dev/null)" +fi + +# Build context block +REPO_NAME="$(/usr/bin/basename "$CWD")" +CONTEXT_LINES=() +CONTEXT_LINES+=("[CWD STATE — auto-injected, 60s cache]") +CONTEXT_LINES+=(" repo: $REPO_NAME branch: $BRANCH HEAD: $HEAD_SHA") +if [ -n "$HEAD_SUBJECT" ]; then + CONTEXT_LINES+=(" last commit: \"$HEAD_SUBJECT\" by $HEAD_AUTHOR") +fi +CONTEXT_LINES+=(" working tree: staged=$STAGED_COUNT unstaged=$UNSTAGED_COUNT untracked=$UNTRACKED_COUNT") +if [ "$AHEAD_BEHIND" != "no-upstream" ]; then + CONTEXT_LINES+=(" vs upstream: $AHEAD_BEHIND") +fi +if [ -n "$CI_STATE" ]; then + CONTEXT_LINES+=(" last CI run: $CI_STATE") +fi +if [ -n "$PR_COUNT" ] && [ -n "$ISSUE_COUNT" ]; then + CONTEXT_LINES+=(" open: PRs=$PR_COUNT issues=$ISSUE_COUNT") +fi + +# Format as JSON additionalContext +JSON_OUT="$(/usr/bin/python3 < "$CACHE_FILE" +printf '%s' "$JSON_OUT" +exit 0 diff --git a/hooks/load-all-memory.sh b/hooks/load-all-memory.sh new file mode 100755 index 0000000..e9501c0 --- /dev/null +++ b/hooks/load-all-memory.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Load ALL memory MD files on every UserPromptSubmit. +# This legacy opt-in hook cats every file in the memory directory into prompt +# context. It is intentionally not enabled by default. + +# Resolve per-user Claude Code project memory dir from $HOME. +# Claude Code encodes home path as `-Users-`; allow override via env. +if [ -n "${VESTIGE_MEMORY_DIR:-}" ]; then + MEM_DIR="$VESTIGE_MEMORY_DIR" +else + MEM_DIR="$HOME/.claude/projects/$(printf '%s' "$HOME" | tr '/' '-')/memory" +fi + +if [ ! -d "$MEM_DIR" ]; then + exit 0 +fi + +echo "═══════════════════════════════════════════════════════════════" +echo "[FULL MEMORY DUMP — EVERY FILE LOADED]" +echo "═══════════════════════════════════════════════════════════════" +echo "" + +# Iterate every .md file in the memory directory (not archive) +for f in "$MEM_DIR"/*.md; do + if [ -f "$f" ]; then + filename=$(basename "$f") + echo "" + echo "┌─────────────────────────────────────────────────────────────" + echo "│ FILE: $filename" + echo "└─────────────────────────────────────────────────────────────" + cat "$f" + echo "" + fi +done + +echo "" +echo "═══════════════════════════════════════════════════════════════" +echo "[END FULL MEMORY DUMP — $(ls "$MEM_DIR"/*.md 2>/dev/null | wc -l | tr -d ' ') files loaded]" +echo "═══════════════════════════════════════════════════════════════" diff --git a/hooks/preflight-swarm.sh b/hooks/preflight-swarm.sh new file mode 100755 index 0000000..c5be3ff --- /dev/null +++ b/hooks/preflight-swarm.sh @@ -0,0 +1,217 @@ +#!/bin/bash +# preflight-swarm.sh — UserPromptSubmit hook (Pre-Cognitive Triad v1) +# +# Spawns the Lateral Thinker subagent via Claude Code's headless mode +# (`claude -p`) to generate a cross-disciplinary epiphany, then injects +# it as additionalContext before Main Claude sees the prompt. +# +# Architectural fixes over the other-agent draft: +# - Reads stdin JSON (not $1) per Claude Code UserPromptSubmit spec +# - Uses `claude -p` with inlined system prompt (no --agent flag exists) +# - Model: claude-haiku-4-5-20251001 (current Haiku, not Oct-2024 3.5) +# - Re-entrancy guard via VESTIGE_SWARM_ACTIVE env var (prevents the +# subagent from re-firing this same hook and looping forever) +# - 25-char minimum + y/yes/ok/continue bypass (fast-path preserved) +# - 8-second timeout (fails open if Haiku is slow) +# - EMPTY mute (no injection when subagent finds no epiphany) +# - Emits JSON additionalContext (no duplicate raw-prompt echo) +# - POSIX-sh-safe: quoted heredoc for script body, env var pass-through +# +# Ship date 2026-04-20. Pairs with the veto-detector.sh Guillotine on the +# Stop hook to form the Cognitive Sandwich (pre-flight Triad + post-flight +# Sanhedrin). + +set -u + +# === OPT-OUT GATE === +# Pre-Cognitive Triad is ON by default as of 2026-04-21 (birthday launch day). +# To disable, set VESTIGE_SWARM_ENABLED=0 in your environment. Default-on +# guarantees the Cognitive Sandwich fires on fresh machines, Docker +# containers, GUI-launched Claude Code, and shells without .zshrc — any +# case where the Claude Code process lacks a sourced profile. The +# re-entrancy guard (VESTIGE_SWARM_ACTIVE) below still prevents fork-bombs +# from the subagent's own UserPromptSubmit hook. +if [ "${VESTIGE_SWARM_ENABLED:-1}" = "0" ]; then + exit 0 +fi + +# === RE-ENTRANCY GUARD === +# If we are already inside the Lateral Thinker subagent, exit immediately +# so the subagent's own UserPromptSubmit does not spawn another Lateral +# Thinker. Without this guard: infinite fork-bomb. +if [ "${VESTIGE_SWARM_ACTIVE:-0}" = "1" ]; then + exit 0 +fi + +# === READ PROMPT FROM STDIN JSON === +INPUT="$(cat)" +PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt",""))' 2>/dev/null || printf '')" +SESSION_ID="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("session_id",""))' 2>/dev/null || printf '')" + +if [ -z "$PROMPT" ]; then + exit 0 +fi + +# === LATENCY + INTENT GATE === +# Skip on very short prompts and common continuation phrases. These do not +# benefit from a lateral epiphany and the 2-4s latency would be annoying. +PROMPT_LEN="${#PROMPT}" +if [ "$PROMPT_LEN" -lt 25 ]; then + exit 0 +fi + +LOWER_TRIMMED="$(printf '%s' "$PROMPT" | /usr/bin/tr '[:upper:]' '[:lower:]' | /usr/bin/awk '{$1=$1;print}')" +case "$LOWER_TRIMMED" in + y|yes|no|ok|okay|continue|proceed|go|ship|lfg|lets\ go|lets\ ship|looks\ good|thanks|thank\ you|perfect|great|awesome) + exit 0 + ;; +esac + +# === VERIFY claude CLI AVAILABLE === +CLAUDE_BIN="$(command -v claude 2>/dev/null || true)" +if [ -z "$CLAUDE_BIN" ]; then + # No claude CLI in PATH — fail open, hook does not block Claude Code itself + exit 0 +fi + +# === BUILD COMBINED PROMPT (lateral-thinker system prompt + user prompt) === +# We inline the agent's system-prompt text because `claude -p` headless mode +# takes a single prompt string; there is no --agent flag. +PROMPT_FILE="$(mktemp -t vestige-lateral.XXXXXX)" +trap 'rm -f "$PROMPT_FILE"' EXIT + +cat > "$PROMPT_FILE" <<'LATERAL_SYSTEM_EOF' +You are the Lateral Thinker, a subconscious subagent in the Vestige OS. + +Your only job: surface a cross-disciplinary structural parallel from the +user's Vestige memory graph that the main agent would not otherwise see. + +Execution protocol (do all steps silently, no narration): + +1. Read the user prompt below the separator. +2. Extract the core structural pattern (race condition / state sync / + retry loop / memory leak / schema migration / decoding ambiguity / + rate limit / ordering guarantee / cache invalidation / etc). +3. Call mcp__vestige__explore_connections with action="bridges" OR + mcp__vestige__search to find memories in a COMPLETELY UNRELATED + domain that share the same structural pattern. Prefer bridges between + distant clusters (e.g., React UI state <-> Rust async channel; + Python DB lock <-> Git merge conflict). +4. If you find a high-confidence mechanical parallel, output EXACTLY this + XML structure (nothing else, no preamble, no explanation): + + + one short noun phrase naming the shared pattern + where the user currently is + the unrelated domain where the pattern also lives + the Vestige node ID of the cross-domain memory, if applicable + one sentence explaining how the unrelated memory informs the current problem mechanically, not metaphorically + + +5. If you cannot find a confident, mechanical, distinct bridge in under + three tool calls, output EXACTLY the single word: EMPTY + Do not apologize, do not explain, do not converse. + +--- +USER PROMPT: +LATERAL_SYSTEM_EOF + +printf '%s\n' "$PROMPT" >> "$PROMPT_FILE" + +# === SPAWN LATERAL THINKER (background with timeout) === +# Set VESTIGE_SWARM_ACTIVE=1 so the subagent's own UserPromptSubmit sees +# the re-entrancy guard and exits early. --permission-mode bypassPermissions +# skips interactive prompts inside the subagent run (standard for headless). +OUTPUT_FILE="$(mktemp -t vestige-lateral-out.XXXXXX)" +trap 'rm -f "$PROMPT_FILE" "$OUTPUT_FILE"' EXIT + +( + VESTIGE_SWARM_ACTIVE=1 \ + "$CLAUDE_BIN" \ + -p "$(cat "$PROMPT_FILE")" \ + --model claude-haiku-4-5-20251001 \ + --allowed-tools "mcp__vestige__search,mcp__vestige__explore_connections,mcp__vestige__memory" \ + < /dev/null \ + > "$OUTPUT_FILE" 2>/dev/null +) & + +CLAUDE_PID=$! + +# === TIMEOUT GUARD (40 seconds) === +# Real `claude -p` with Haiku 4.5 + MCP explore_connections/search tool calls +# needs ~30-35s wall-clock for a full bridge search on a complex prompt. +# Measured empirically 2026-04-20: 8s was killed every time, 25s was killed +# every time for decision-adjacent prompts. Matches Sanhedrin's 33s budget +# with 7s headroom for slow MCP round-trips. Pair with a 45s timeout in +# settings.json so Claude Code doesn't kill us first. +WAITED=0 +while [ "$WAITED" -lt 40 ]; do + if ! /bin/kill -0 "$CLAUDE_PID" 2>/dev/null; then + break + fi + sleep 1 + WAITED=$((WAITED + 1)) +done +if /bin/kill -0 "$CLAUDE_PID" 2>/dev/null; then + /bin/kill "$CLAUDE_PID" 2>/dev/null + wait "$CLAUDE_PID" 2>/dev/null + exit 0 +fi +wait "$CLAUDE_PID" 2>/dev/null + +LATERAL_OUTPUT="$(cat "$OUTPUT_FILE" 2>/dev/null || printf '')" + +# === EMPTY MUTE GATE === +# Trim whitespace and check for EMPTY or no content. Inject nothing rather +# than pollute Claude's context with an apology. +TRIMMED="$(printf '%s' "$LATERAL_OUTPUT" | /usr/bin/awk '{$1=$1;print}')" +if [ -z "$TRIMMED" ] || [ "$TRIMMED" = "EMPTY" ] || [ "${TRIMMED:0:5}" = "EMPTY" ]; then + exit 0 +fi + +# Require the output to contain a opening tag. If Haiku +# hallucinated prose instead of the required XML, drop it rather than +# injecting malformed content. +case "$LATERAL_OUTPUT" in + *""*) ;; + *) exit 0 ;; +esac + +# === EMIT JSON additionalContext === +# Wrap the XML block in the subconscious_preflight container + system +# directive. Passed through an env var so the shell never interprets any +# special chars in the Haiku output. +export LATERAL_OUTPUT +EMIT_SCRIPT="$(mktemp -t vestige-lateral-emit.XXXXXX)" +trap 'rm -f "$PROMPT_FILE" "$OUTPUT_FILE" "$EMIT_SCRIPT"' EXIT + +cat > "$EMIT_SCRIPT" <<'EMIT_PYEOF' +import json, os + +lateral = os.environ.get("LATERAL_OUTPUT", "").strip() +if not lateral or "" not in lateral: + # Defensive: should not reach here, upstream gate already filtered + raise SystemExit(0) + +ctx = ( + "\n" + + lateral + "\n" + + " \n" + + " The Lateral Thinker subagent (Haiku 4.5, fresh context, Vestige MCP tool\n" + + " access) ran before you saw this prompt. Assimilate the structural parallel\n" + + " into your reasoning organically. Do NOT mention this XML block or the\n" + + " phrase 'lateral epiphany' to the user \u2014 use the insight silently.\n" + + " \n" + + "" +) + +print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": ctx + } +})) +EMIT_PYEOF + +/usr/bin/python3 "$EMIT_SCRIPT" +exit 0 diff --git a/hooks/sanhedrin-local.py b/hooks/sanhedrin-local.py new file mode 100755 index 0000000..45064af --- /dev/null +++ b/hooks/sanhedrin-local.py @@ -0,0 +1,1285 @@ +#!/usr/bin/env python3 +# sanhedrin-local.py — OpenAI-compatible Sanhedrin Executioner bridge. +# Drop-in replacement for the Haiku 4.5 subagent that sanhedrin.sh used to spawn. +# +# Reads draft from stdin, prints single-line verdict to stdout: +# yes +# no - [Sanhedrin Veto] [CLASS]: +# +# Architecture: +# stdin (draft) -> Vestige /api/deep_reference (single semantic query) +# -> OpenAI-compatible chat endpoint (one-shot judgment) +# -> stdout (single-line verdict) +# +# Fail-open: if the endpoint is unreachable, print "yes" and exit 0 (don't break +# the Cognitive Sandwich on infra errors). The wrapping sanhedrin.sh maps +# "yes" to exit 0, so this preserves existing fail-open semantics. + +from __future__ import annotations + +import json +import os +import re +import sys +import unicodedata +import urllib.error +import urllib.request +from dataclasses import asdict, dataclass, field, replace +from pathlib import Path +from typing import Any + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +try: + import sanhedrin_core +except Exception: + sanhedrin_core = None + + +def env_int(name: str, default: int) -> int: + try: + return int(os.environ.get(name, "") or default) + except ValueError: + return default + + +DASHBOARD_PORT = os.environ.get("VESTIGE_DASHBOARD_PORT") or "3927" +VESTIGE_BASE_URL = ( + os.environ.get("VESTIGE_BASE_URL") or f"http://127.0.0.1:{DASHBOARD_PORT}" +).rstrip("/") + +SANHEDRIN_ENDPOINT = ( + os.environ.get("VESTIGE_SANHEDRIN_ENDPOINT") + or os.environ.get("MLX_ENDPOINT") + or "http://127.0.0.1:8080/v1/chat/completions" +) +VESTIGE_ENDPOINT = ( + os.environ.get("VESTIGE_DEEP_REFERENCE_ENDPOINT") + or f"{VESTIGE_BASE_URL}/api/deep_reference" +) +VESTIGE_HEALTH = ( + os.environ.get("VESTIGE_HEALTH_ENDPOINT") or f"{VESTIGE_BASE_URL}/api/health" +) +MODEL = ( + os.environ.get("VESTIGE_SANHEDRIN_MODEL") + or os.environ.get("VESTIGE_SANDWICH_MODEL") + or "mlx-community/Qwen3.6-35B-A3B-4bit" +) +SANHEDRIN_TIMEOUT = env_int("VESTIGE_SANHEDRIN_TIMEOUT", env_int("MLX_TIMEOUT", 45)) +VESTIGE_TIMEOUT = env_int("VESTIGE_TIMEOUT", 5) +THINK_RE = re.compile(r".*?", re.DOTALL | re.IGNORECASE) + + +def post_json(url: str, body: dict, timeout: int): + data = json.dumps(body).encode("utf-8") + req = urllib.request.Request( + url, data=data, headers={"Content-Type": "application/json"} + ) + try: + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read()) + except (urllib.error.URLError, urllib.error.HTTPError, TimeoutError, OSError): + return None + + +TRUST_FLOOR = 0.55 # filter out low-trust memories that drive false-positive vetoes + +CLAIM_MODE_ENV = "VESTIGE_SANHEDRIN_CLAIM_MODE" +OUTPUT_ENV = "VESTIGE_SANHEDRIN_OUTPUT" +STAGE_FILE_ENV = "VESTIGE_SANHEDRIN_STAGE_FILE" +MAX_CLAIMS = env_int("VESTIGE_SANHEDRIN_MAX_CLAIMS", 8) +MAX_CLAIM_CHARS = env_int("VESTIGE_SANHEDRIN_MAX_CLAIM_CHARS", 500) +MAX_EVIDENCE_CHARS = env_int("VESTIGE_SANHEDRIN_MAX_EVIDENCE_CHARS", 420) + +CLAIM_CLASSES = { + "TECHNICAL", + "BIOGRAPHICAL", + "FINANCIAL", + "ACHIEVEMENT", + "TIMELINE", + "QUANTITATIVE", + "ATTRIBUTION", + "CAUSAL", + "COMPARATIVE", + "EXISTENTIAL", + "VAGUE-QUANTIFIER", + "UNVERIFIED-POSITIVE", +} +CRITICAL_ABSENCE_CLASSES = { + "BIOGRAPHICAL", + "FINANCIAL", + "ACHIEVEMENT", + "TIMELINE", + "QUANTITATIVE", + "ATTRIBUTION", + "VAGUE-QUANTIFIER", +} +STRUCTURED_VERDICTS = {"SUPPORTED", "REFUTED", "REFUTED_BY_ABSENCE", "NEI"} +SEVERITY_ORDER = { + "BIOGRAPHICAL": 0, + "FINANCIAL": 1, + "ACHIEVEMENT": 2, + "ATTRIBUTION": 3, + "TIMELINE": 4, + "QUANTITATIVE": 5, + "VAGUE-QUANTIFIER": 6, + "UNVERIFIED-POSITIVE": 7, + "TECHNICAL": 8, + "EXISTENTIAL": 9, + "CAUSAL": 10, + "COMPARATIVE": 11, +} +USER_TERMS_RE = re.compile( + r"\b(sam|sam's|the user|user's|you|your|yours|yourself)\b", re.IGNORECASE +) +HYPOTHETICAL_PREFIX_RE = re.compile( + r"^\s*(if|suppose|imagine|hypothetically|assume|what if)\b", + re.IGNORECASE, +) +SUBJECT_MODAL_PREFIX_RE = re.compile( + r"^\s*(sam|sam's|the user|user's|you|your)\b\s+(would|could)\b", + re.IGNORECASE, +) +TRAILING_MODAL_COMMENT_RE = re.compile( + r"\s*,?\s+(which|that)\s+(would|could)\b.*$", + re.IGNORECASE, +) +CURRENT_TURN_PREFIXES = [ + re.compile(r"^\s*(per your request|as requested)\s*,?\s*", re.IGNORECASE), + re.compile( + r"^\s*(you|sam|the user)\s+(asked for|requested)\s+maximum subagents\b[^,.;]*(?:,?\s*(and|so)\s*)?", + re.IGNORECASE, + ), + re.compile( + r"^\s*(you|sam|the user)\s+(asked|told|requested|wanted)\s+" + r"(?:(me|us|codex|claude)\s+)?(to|for)\s+", + re.IGNORECASE, + ), + re.compile( + r"^\s*(your|sam's|the user's)\s+request\s+(was|is)\s+(to|for)\s+", + re.IGNORECASE, + ), +] +FIRST_PERSON_DISCOURSE_RE = re.compile( + r"^\s*(i|we)\s+(reviewed|audited|checked|inspected|looked at|verified|" + r"confirmed|found|updated|changed|implemented|fixed|patched|added|removed|" + r"wired|ran|left)\b", + re.IGNORECASE, +) +DISCOURSE_ACTION_PREFIX_RE = re.compile( + r"^\s*(audit|review|check|inspect|look at|verify|confirm|implement|fix|" + r"patch|add|remove|wire|run|use|go all in)\b", + re.IGNORECASE, +) +EMBEDDED_USER_CLAIM_RE = re.compile(r"\b(sam|sam's|the user|user's)\b", re.IGNORECASE) +TECHNICAL_RE = re.compile( + r"(/\w|[\w.-]+\.(py|rs|ts|tsx|js|jsx|json|md|toml|yaml|yml|sh)\b|" + r"\b(api|endpoint|env|flag|model|server|hook|script|function|class|repo|" + r"crate|mcp|http|json|sqlite|rust|python|typescript|command|config)\b|" + r"\b[A-Z][A-Z0-9_]{2,}\b)", + re.IGNORECASE, +) +BIOGRAPHICAL_RE = re.compile( + r"\b(born|lives?|located|based in|works? at|employed|employer|school|" + r"university|college|graduated|degree|founder|ceo|cto|student|job|role)\b", + re.IGNORECASE, +) +FINANCIAL_RE = re.compile( + r"(\$[\d,.]+|\b(revenue|funding|raised|earned|paid|payout|prize money|" + r"salary|net worth|valuation|stock|shares?|portfolio|profit|loss)\b)", + re.IGNORECASE, +) +ACHIEVEMENT_RE = re.compile( + r"\b(won|winner|ranked|placed|scored|score|completed|finished|launched|" + r"released|shipped|milestone|award|prize|accepted|published|graduated)\b", + re.IGNORECASE, +) +TIMELINE_RE = re.compile( + r"\b(\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{2,4}|" + r"jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|" + r"jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|" + r"dec(?:ember)?|today|yesterday|tomorrow|last week|next week|" + r"\d+\s+(days?|weeks?|months?|years?)\b)", + re.IGNORECASE, +) +QUANTITATIVE_RE = re.compile( + r"(\b\d+(?:\.\d+)?\s*(%|percent|x|times|stars?|users?|customers?|" + r"submissions?|points?|gb|mb|ms|s|seconds?|minutes?|hours?)?\b|" + r"\b(one|two|three|four|five|six|seven|eight|nine|ten|dozens?|hundreds?|" + r"thousands?|many|several|few|most)\b)", + re.IGNORECASE, +) +TOKEN_RE = re.compile(r"\$?\b[a-z0-9][a-z0-9.-]*\b", re.IGNORECASE) +STOP_CLAIM_TOKENS = { + "about", + "after", + "also", + "because", + "been", + "before", + "claim", + "from", + "have", + "into", + "more", + "sam", + "that", + "their", + "there", + "this", + "user", + "with", + "your", +} +ATTRIBUTION_RE = re.compile( + r"\b(said|told|asked|agreed|decided|approved|rejected|committed|authored|" + r"wrote|built|implemented|requested|wanted|prefers?)\b", + re.IGNORECASE, +) +VAGUE_QUANTIFIER_RE = re.compile( + r"\b(a few|some|several|many|most|multiple)\b.*\b(wins?|won|prizes?|" + r"money|customers?|deals?|submissions?|placements?)\b", + re.IGNORECASE, +) + + +@dataclass(frozen=True) +class Claim: + text: str + claim_class: str + source_index: int + sam_critical: bool + + +@dataclass(frozen=True) +class EvidenceItem: + id: str + preview: str + trust: float + role: str = "evidence" + date: str = "" + durable: bool = True + source: str = "vestige" + + +@dataclass +class ClaimVerdict: + claim: Claim + status: str + reason: str = "" + evidence_ids: list[str] = field(default_factory=list) + durable_evidence_count: int = 0 + high_trust_evidence_count: int = 0 + + +def env_flag(name: str) -> bool: + return (os.environ.get(name) or "").strip().lower() in {"1", "true", "yes", "on"} + + +def truncate_chars(text: str, max_chars: int, suffix: str = "...") -> str: + """Truncate by Python characters, never UTF-8 bytes, and avoid dangling marks.""" + if max_chars <= 0: + return "" + if len(text) <= max_chars: + return text + if max_chars <= len(suffix): + return text[:max_chars] + cut = text[: max_chars - len(suffix)].rstrip() + while cut and unicodedata.combining(cut[-1]): + cut = cut[:-1] + return f"{cut}{suffix}" + + +def safe_float(value: Any, default: float = 0.0) -> float: + try: + return float(value) + except (TypeError, ValueError): + return default + + +def fetch_evidence(draft: str) -> tuple[str, int]: + """Single deep_reference call — returns (formatted evidence, count of high-trust memories). + Only memories with trust >= TRUST_FLOOR are surfaced. If none qualify, returns ("", 0) + and the caller should auto-pass without invoking the model. + """ + try: + with urllib.request.urlopen(VESTIGE_HEALTH, timeout=VESTIGE_TIMEOUT) as r: + r.read() + except Exception: + return "", 0 + + query = draft[:1500] + resp = post_json(VESTIGE_ENDPOINT, {"query": query, "depth": 12}, VESTIGE_TIMEOUT) + if not isinstance(resp, dict): + return "", 0 + + parts = [] + high_trust_count = 0 + confidence = resp.get("confidence", 0) + + rec = resp.get("recommended") or {} + rec_trust = float(rec.get("trust_score", 0) or 0) + if rec and rec_trust >= TRUST_FLOOR: + rid = (rec.get("memory_id") or rec.get("id") or "")[:8] + date = (rec.get("date") or "")[:10] + prev = (rec.get("answer_preview") or rec.get("preview") or "")[:500] + parts.append(f"RECOMMENDED [{rid}] trust={rec_trust:.2f} date={date}:\n{prev}") + high_trust_count += 1 + + contradictions = resp.get("contradictions") or [] + if contradictions: + parts.append(f"\nCONTRADICTIONS DETECTED: {len(contradictions)} pair(s)") + for c in contradictions[:3]: + parts.append(f" - {json.dumps(c)[:200]}") + + superseded = resp.get("superseded") or [] + if superseded: + ht_super = [s for s in superseded if float(s.get("trust", 0) or 0) >= TRUST_FLOOR] + if ht_super: + parts.append(f"\nSUPERSEDED MEMORIES (trust>={TRUST_FLOOR}): {len(ht_super)}") + for s in ht_super[:3]: + sid = (s.get("id") or "")[:8] + parts.append(f" - [{sid}] {(s.get('preview') or '')[:200]}") + + evidence = resp.get("evidence") or [] + high_trust_evidence = [ev for ev in evidence if float(ev.get("trust", 0) or 0) >= TRUST_FLOOR] + if high_trust_evidence: + parts.append(f"\nHIGH-TRUST EVIDENCE (trust>={TRUST_FLOOR}, {min(len(high_trust_evidence), 5)} of {len(evidence)} total):") + for ev in high_trust_evidence[:5]: + eid = (ev.get("id") or "")[:8] + role = ev.get("role", "?") + trust = float(ev.get("trust", 0) or 0) + prev = (ev.get("preview") or "").strip()[:300] + parts.append(f" [{eid}] role={role} trust={trust:.2f}\n {prev}") + high_trust_count += 1 + + if high_trust_count == 0: + return "", 0 + + header = f"VESTIGE CONFIDENCE: {int(confidence * 100)}% | HIGH-TRUST MEMORIES: {high_trust_count}\n\n" + return header + "\n".join(parts), high_trust_count + + +def split_candidate_claims(draft: str) -> list[str]: + """Return sentence-ish draft fragments that can be classified as claims.""" + without_fences = re.sub(r"```.*?```", " ", draft, flags=re.DOTALL) + fragments: list[str] = [] + for line in without_fences.splitlines(): + line = re.sub(r"^\s*[-*+]\s+", "", line).strip() + line = re.sub(r"^\s*\d+[.)]\s+", "", line).strip() + if not line: + continue + parts = re.split(r"(?<=[.!?])\s+(?=[A-Z0-9`\"'])", line) + fragments.extend(part.strip(" \t-") for part in parts if part.strip(" \t-")) + if not fragments: + compact = " ".join(without_fences.split()) + fragments = [ + part.strip() + for part in re.split(r"(?<=[.!?])\s+(?=[A-Z0-9`\"'])", compact) + if part.strip() + ] + return fragments + + +def normalize_asserted_fragment(text: str) -> str | None: + text = " ".join(text.split()).strip() + if not text: + return None + text = TRAILING_MODAL_COMMENT_RE.sub("", text).strip(" ,;:-") + if HYPOTHETICAL_PREFIX_RE.search(text) or SUBJECT_MODAL_PREFIX_RE.search(text): + return None + + for prefix in CURRENT_TURN_PREFIXES: + stripped = prefix.sub("", text, count=1).strip(" ,;:-") + if stripped == text: + continue + embedded = EMBEDDED_USER_CLAIM_RE.search(stripped) + if embedded and embedded.start() > 0 and DISCOURSE_ACTION_PREFIX_RE.search(stripped): + stripped = stripped[embedded.start() :].strip(" ,;:-") + elif DISCOURSE_ACTION_PREFIX_RE.search(stripped) or FIRST_PERSON_DISCOURSE_RE.search(stripped): + return None + text = stripped + break + + if FIRST_PERSON_DISCOURSE_RE.search(text): + embedded = EMBEDDED_USER_CLAIM_RE.search(text) + if embedded and embedded.start() > 0: + text = text[embedded.start() :].strip(" ,;:-") + else: + return None + + text = TRAILING_MODAL_COMMENT_RE.sub("", text).strip(" ,;:-") + if not text or HYPOTHETICAL_PREFIX_RE.search(text) or SUBJECT_MODAL_PREFIX_RE.search(text): + return None + return text + + +def classify_claim(text: str) -> str | None: + """Classify a factual-shaped claim with conservative, testable heuristics.""" + if VAGUE_QUANTIFIER_RE.search(text): + return "VAGUE-QUANTIFIER" + if BIOGRAPHICAL_RE.search(text): + return "BIOGRAPHICAL" + if FINANCIAL_RE.search(text): + return "FINANCIAL" + if ACHIEVEMENT_RE.search(text): + return "ACHIEVEMENT" + if ATTRIBUTION_RE.search(text): + return "ATTRIBUTION" + if TECHNICAL_RE.search(text): + return "TECHNICAL" + if TIMELINE_RE.search(text): + return "TIMELINE" + if QUANTITATIVE_RE.search(text): + return "QUANTITATIVE" + if re.search(r"\b(exists?|there is|there are|contains?|includes?)\b", text, re.I): + return "EXISTENTIAL" + if re.search(r"\b(because|caused|causes|therefore|so that|as a result)\b", text, re.I): + return "CAUSAL" + if re.search(r"\b(better|best|faster|fastest|more than|less than|fewer than)\b", text, re.I): + return "COMPARATIVE" + return None + + +def is_sam_critical_claim(text: str, claim_class: str) -> bool: + if claim_class not in CRITICAL_ABSENCE_CLASSES: + return False + return bool(USER_TERMS_RE.search(text)) + + +def extract_check_worthy_claims( + draft: str, + max_claims: int = MAX_CLAIMS, + max_claim_chars: int = MAX_CLAIM_CHARS, +) -> list[Claim]: + claims: list[Claim] = [] + seen: set[str] = set() + for idx, fragment in enumerate(split_candidate_claims(draft)): + text = normalize_asserted_fragment(fragment) + if not text: + continue + claim_class = classify_claim(text) + if not claim_class: + continue + text = truncate_chars(text, max_claim_chars) + key = text.lower() + if key in seen: + continue + seen.add(key) + claims.append( + Claim( + text=text, + claim_class=claim_class, + source_index=idx, + sam_critical=is_sam_critical_claim(text, claim_class), + ) + ) + if len(claims) >= max_claims: + break + return claims + + +def normalize_evidence_item(raw: Any, source: str = "vestige") -> EvidenceItem | None: + if isinstance(raw, str): + preview = raw.strip() + if not preview: + return None + return EvidenceItem( + id="stage", + preview=truncate_chars(preview, MAX_EVIDENCE_CHARS), + trust=1.0, + role="staged", + durable=False, + source="stage", + ) + if not isinstance(raw, dict): + return None + + preview = ( + raw.get("preview") + or raw.get("answer_preview") + or raw.get("content") + or raw.get("text") + or raw.get("claim") + or "" + ) + preview = str(preview).strip() + if not preview: + return None + trust = safe_float(raw.get("trust", raw.get("trust_score", 1.0 if source == "stage" else 0.0))) + item_id = str(raw.get("memory_id") or raw.get("id") or source or "evidence") + role = str(raw.get("role") or ("staged" if source == "stage" else "evidence")) + date = str(raw.get("date") or raw.get("created_at") or "")[:32] + return EvidenceItem( + id=item_id, + preview=truncate_chars(preview, MAX_EVIDENCE_CHARS), + trust=trust, + role=role, + date=date, + durable=(source != "stage"), + source=source, + ) + + +def evidence_from_deep_reference(resp: dict[str, Any]) -> list[EvidenceItem]: + items: list[EvidenceItem] = [] + rec = resp.get("recommended") or {} + rec_item = normalize_evidence_item(rec, "vestige") + if rec_item: + items.append(rec_item) + for raw in resp.get("evidence") or []: + item = normalize_evidence_item(raw, "vestige") + if item: + items.append(item) + for raw in resp.get("superseded") or []: + item = normalize_evidence_item(raw, "vestige") + if item: + items.append(item) + return dedupe_evidence(items) + + +def dedupe_evidence(items: list[EvidenceItem]) -> list[EvidenceItem]: + deduped: list[EvidenceItem] = [] + seen: set[tuple[str, str]] = set() + for item in items: + key = (item.source, item.id) + if key in seen: + continue + seen.add(key) + deduped.append(item) + return deduped + + +def load_staged_evidence(path: str | None) -> list[EvidenceItem]: + """Read optional JSON-array staged evidence. It is non-durable by design.""" + if not path: + return [] + try: + with open(path, "r", encoding="utf-8") as f: + raw = json.load(f) + except (OSError, json.JSONDecodeError): + return [] + if not isinstance(raw, list): + return [] + items: list[EvidenceItem] = [] + for idx, raw_item in enumerate(raw): + item = normalize_evidence_item(raw_item, "stage") + if item is None: + continue + if item.id == "stage": + item = replace(item, id=f"stage:{idx}") + items.append(item) + return items + + +def claim_query(claim: Claim) -> str: + return ( + f"Class: {claim.claim_class}\n" + f"Claim: {claim.text}" + ) + + +def fetch_claim_evidence(claim: Claim) -> tuple[list[EvidenceItem], bool]: + resp = post_json(VESTIGE_ENDPOINT, {"query": claim_query(claim), "depth": 12}, VESTIGE_TIMEOUT) + if not isinstance(resp, dict): + return [], False + if resp.get("error") or resp.get("errors"): + return [], False + if str(resp.get("status") or "").strip().lower() in { + "error", + "failed", + "failure", + "unavailable", + "timeout", + }: + return [], False + if not any( + key in resp + for key in ("confidence", "evidence", "recommended", "reasoning", "query", "status") + ): + return [], False + return evidence_from_deep_reference(resp), True + + +def high_trust(items: list[EvidenceItem]) -> list[EvidenceItem]: + return [item for item in items if item.trust >= TRUST_FLOOR] + + +def durable_high_trust(items: list[EvidenceItem]) -> list[EvidenceItem]: + return [item for item in items if item.durable and item.trust >= TRUST_FLOOR] + + +def salient_claim_tokens(text: str) -> set[str]: + tokens = {token.lower().strip(".") for token in TOKEN_RE.findall(text)} + return { + token + for token in tokens + if len(token) >= 4 and token not in STOP_CLAIM_TOKENS + } + + +def evidence_relevant_to_claim(claim: Claim, evidence: EvidenceItem) -> bool: + claim_numbers = set(re.findall(r"\$?\d+(?:[,.]\d+)*(?:\.\d+)?", claim.text)) + if claim_numbers and any(num in evidence.preview for num in claim_numbers): + return True + claim_tokens = salient_claim_tokens(claim.text) + if not claim_tokens: + return True + preview_tokens = salient_claim_tokens(evidence.preview) + overlap = claim_tokens & preview_tokens + threshold = 1 if claim.claim_class == "TECHNICAL" else 2 + return len(overlap) >= threshold + + +def relevant_durable_high_trust(claim: Claim, items: list[EvidenceItem]) -> list[EvidenceItem]: + return [ + item + for item in durable_high_trust(items) + if evidence_relevant_to_claim(claim, item) + ] + + +def format_claim_evidence(items: list[EvidenceItem], claim: Claim | None = None) -> str: + if not items: + return "(no relevant evidence retrieved)" + lines = [] + durable_count = ( + len(relevant_durable_high_trust(claim, items)) + if claim is not None + else len(durable_high_trust(items)) + ) + high_count = len(high_trust(items)) + lines.append( + f"HIGH-TRUST EVIDENCE: {high_count} | DURABLE HIGH-TRUST EVIDENCE: {durable_count}" + ) + stage_count = len([item for item in items if not item.durable]) + if stage_count: + lines.append( + "STAGED EVIDENCE PRESENT: non-durable overlay; do not count it as durable memory." + ) + for item in high_trust(items)[:8]: + durable = "durable" if item.durable else "staged" + short_id = item.id[:12] + lines.append( + f"[{short_id}] {durable} role={item.role} trust={item.trust:.2f} date={item.date}\n" + f"{item.preview}" + ) + return "\n\n".join(lines) + + +SYSTEM_PROMPT = """You are the Sanhedrin Executioner. You judge whether a DRAFT contradicts Vestige memory evidence about the user. ONE LINE OF OUTPUT. + +VALID CLASS TAGS (closed set — pick exactly one): +TECHNICAL | ACHIEVEMENT | FINANCIAL | BIOGRAPHICAL | TIMELINE | ATTRIBUTION | VAGUE-QUANTIFIER | UNVERIFIED-POSITIVE + +DEFAULT POSTURE +- DEFAULT to `yes` (PASS) for TECHNICAL / TIMELINE / EXISTENTIAL claims unless you can cite a same-subject direct contradiction. +- DEFAULT to `no` (VETO, fail-closed) for these specific user-about claims when high-trust evidence is silent on the named entity: + * Specific institution / employer / school / company the user is claimed to be at + * Specific dollar amount won / earned / raised + * Specific competition placement / score / prize received + * Specific date the user did something specific (graduated, was hired, was born) + * Vague-quantifier positive about the user ("a few wins", "some prize money", "most submissions placed top 10", "many customers", "several deals") + +THREE FALSE-POSITIVE PROTECTIONS (these output `yes`) +1. SUBJECT-EQUALITY GATE: only same-subject claims are veto candidates. Memory about Vestige's internal codebase ≠ contradiction with external tools (Qwen, MCP-protocol-spec, MLX, Cursor). Memory about project X ≠ contradiction with project Y. +2. VERSION-DISCRIMINATOR RULE: version/generation tokens (M1/M2/M3/M4/M5, v0.5/v1.0, GPT-4/GPT-5, Qwen3.5/Qwen3.6) are subject discriminators. Different versions = different subjects = no contradiction by default. +3. AGREEMENT-IS-NOT-CONTRADICTION: if the memory preview AGREES with the draft claim, that's PASS not VETO. + +INFERENCE BAN +- DO NOT use "implies", "implying", "suggests", "must mean", "would mean", "indicates", "therefore" in veto reasons. +- If you have to chain inferences from a memory to reach a contradiction, PASS. +- TIMELINE vetoes specifically: require an EXPLICIT date or duration in the cited memory that arithmetically excludes the draft's date. Vague phrases like "until I graduate" cannot ground a TIMELINE veto. + +ARCHITECTURE-VS-COMPONENT RULE +- A memory describing OVERALL architecture (Thalamus+Sanhedrin triad, 4-layer biology) does NOT contradict a draft about an INTERNAL COMPONENT (subagent model, sidecar transport, bridge script). Different layers of the same stack are not contradictions. + +OUTPUT FORMAT (exactly one line, no preamble, no explanation, no markdown) +- PASS: yes +- VETO: no - [Sanhedrin Veto] [CLASS]: + +EIGHT WORKED EXAMPLES — STUDY THESE PATTERNS + +[VETO — same-subject TECHNICAL contradiction] +Evidence: "Vestige is a 2-crate Rust workspace (vestige-core + vestige-mcp)" trust=0.62 [de43be5a] +Draft: "Edit the FastAPI router in vestige/main.py for Python extensions to Vestige" +Output: no - [Sanhedrin Veto] TECHNICAL: Draft says FastAPI/Python for Vestige, memory de43be5a says 2-crate Rust workspace. + +[VETO — same-subject ACHIEVEMENT contradiction] +Evidence: "Final benchmark submission scored 36/50 on April 15, no payout" trust=0.71 [9cf2a764] +Draft: "The user won the benchmark with a perfect 50/50 and took the $25K grand prize" +Output: no - [Sanhedrin Veto] ACHIEVEMENT: Draft claims 50/50 win + $25K, memory 9cf2a764 shows 36/50 final, no payout. + +[VETO — VAGUE-QUANTIFIER fail-closed] +Evidence: high-trust memories about the user's competition history, none enumerate any wins +Draft: "The user won a few competitions and earned some prize money" +Output: no - [Sanhedrin Veto] VAGUE-QUANTIFIER: Draft says "a few wins / some prize money", evidence enumerates zero wins, fail-closed. + +[VETO — UNVERIFIED-POSITIVE fail-closed] +Evidence: high-trust memories about the user's identity/work, no example school or employer mention +Draft: "The user graduated from Example University in 2019 with a 3.94 GPA and worked at Example Labs" +Output: no - [Sanhedrin Veto] UNVERIFIED-POSITIVE: Specific Stanford/2019/Google Brain claims, evidence silent on all, fail-closed. + +[PASS — SUBJECT-EQUALITY gate (external tool, not Vestige)] +Evidence: "Vestige is a 2-crate Rust workspace" trust=0.62 +Draft: "Switched the Sanhedrin executioner to local Qwen3.6-35B-A3B via mlx_lm.server" +Output: yes + +[PASS — VERSION-DISCRIMINATOR rule] +Evidence: "M5 Max ~900 GB/s bandwidth (planned hardware)" trust=0.62 +Draft: "Memory bandwidth on the M3 Max is around 400 GB/s for the unified architecture" +Output: yes + +[PASS — AGREEMENT-IS-NOT-CONTRADICTION] +Evidence: "The user's M3 Max MacBook Pro arrived 2026-04-20" trust=0.55 +Draft: "The user's MacBook is an M3 Max" +Output: yes + +[PASS — ARCHITECTURE-VS-COMPONENT] +Evidence: "Cognitive Sandwich = Thalamus preflight triad + Sanhedrin Stop council shipped 2026-04-20" trust=0.7 +Draft: "Cognitive Sandwich's Sanhedrin originally used a Haiku 4.5 subagent for the Executioner role" +Output: yes + +[PASS — AUXILIARY-SCRIPT consumer-vs-consumed] +Evidence: "Vestige is a 2-crate Rust workspace" trust=0.62 +Draft: "I added a Python script (sanhedrin-local.py) at ~/.claude/hooks/ that calls Vestige's HTTP API for evidence fetch." +Reason: external script that CALLS Vestige is not the same subject as Vestige's internal implementation. The consumer is not the consumed. +Output: yes + +[PASS — HYPOTHETICAL-MOOD] +Evidence: "Final benchmark score was 36/50 with no payout" trust=0.71 +Draft: "If the user wins the benchmark 50/50 next time around, they could claim the $25K grand prize." +Reason: prefix `if`/`suppose`/`imagine`/`hypothetically`/`would`/`could`/`assume` marks the embedded claim as conditional, NOT asserted. Conditional claims about future or counterfactual states do not contradict factual memory. +Output: yes + +HYPOTHETICAL-MOOD RULE: if a draft sentence is governed by `if`, `suppose`, `imagine`, `hypothetically`, `would`, `could`, `assume`, `what if`, the embedded claim is NOT being asserted as fact — PASS that claim regardless of memory state. + +ARCHIVED-COMPETITION RULE: do NOT fail-closed on the EXISTENCE of a past competition or project just because evidence is silent on it. Fail-closed applies only to specific PLACEMENT, SCORE, PRIZE, INSTITUTION, or DOLLAR AMOUNT -- not to "the user participated in X." + +MULTI-CLAIM SEVERITY ORDERING: if multiple claims are vetoable, choose ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL/UNVERIFIED-POSITIVE over TECHNICAL. Specific fabrications about the user's life are more dangerous than tech-stack mismatches. + +When in doubt on TECHNICAL/TIMELINE: PASS. When in doubt on a user-about ACHIEVEMENT/FINANCIAL/BIOGRAPHICAL claim with specific named entities not in evidence: VETO with UNVERIFIED-POSITIVE.""" + + +CLAIM_SYSTEM_PROMPT = """You are the Sanhedrin Executioner in claim mode. Judge ONE extracted claim against the provided Vestige evidence. + +Return exactly one JSON object, no markdown: +{ + "status": "SUPPORTED|REFUTED|REFUTED_BY_ABSENCE|NEI", + "class": "", + "reason": "", + "evidence_ids": [""] +} + +Rules: +- SUPPORTED: high-trust evidence directly supports the claim. +- REFUTED: high-trust evidence directly contradicts the same-subject claim. +- REFUTED_BY_ABSENCE: use only when instructions say absence-fail-closed applies. +- NEI: not enough information, stale/noisy evidence, wrong subject, or inference required. +- Do not infer contradiction across different subjects, versions, projects, or architecture layers. +- Staged evidence is context only and is not durable Vestige memory. +- Reasons must not use implies, suggests, must mean, would mean, indicates, therefore, or this means. +""" + + +VALID_CLASSES = CLAIM_CLASSES +INFERENCE_VERBS = ( + "implies", "implying", "suggests", "must mean", "would mean", + "indicates that", "therefore the", "this means", +) +VERDICT_RE = re.compile( + r"^no - \[Sanhedrin Veto\] \[?([A-Z][A-Z\-]*)\]?: (.{1,180})$" +) + + +def validate_verdict(verdict: str) -> str: + """Post-validate the model's verdict. Fail-open ('yes') on any malformation: + - Length over 220 chars + - Veto with class tag not in the closed set + - Veto reason containing inference verbs + - Veto not matching the canonical regex + """ + v = verdict.strip() + if not v: + return "yes" + low = v.lower() + if low == "yes" or low.startswith("yes "): + return "yes" + if not low.startswith("no"): + return "yes" + if len(v) > 220: + return "yes" # runaway reasoning blob + m = VERDICT_RE.match(v) + if not m: + return "yes" # format break + cls = m.group(1) + reason = m.group(2) + if cls not in VALID_CLASSES: + return "yes" # invented class tag + reason_low = reason.lower() + for verb in INFERENCE_VERBS: + if verb in reason_low: + return "yes" # inference-chain veto, downgrade per ban + return v + + +def judge(draft: str, evidence: str) -> str: + user_msg = ( + f"VESTIGE EVIDENCE (recommended + top trust-scored memories):\n" + f"{evidence if evidence else '(no relevant evidence retrieved)'}\n\n" + f"---\nDRAFT TO JUDGE:\n{draft}" + ) + body = { + "model": MODEL, + "messages": [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": user_msg}, + ], + "max_tokens": 2500, + "temperature": 0.0, + "top_p": 1.0, + "top_k": 1, + "seed": 42, + "stream": False, + "chat_template_kwargs": {"enable_thinking": False}, + "stop": [ + "\n\nWait,", "\n\nActually,", "\n\nLet me", "\n\nHmm,", + "\n\nOn second thought", "\n\nOh wait", + ], + } + resp = post_json(SANHEDRIN_ENDPOINT, body, SANHEDRIN_TIMEOUT) + if not isinstance(resp, dict): + return "" + try: + msg = resp["choices"][0]["message"] + raw = msg.get("content") or "" + if not raw.strip(): + raw = msg.get("reasoning") or "" + except (KeyError, IndexError, TypeError): + return "" + cleaned = THINK_RE.sub("", raw).strip() + lines = [ln.strip() for ln in cleaned.splitlines() if ln.strip()] + if not lines: + return "" + last = lines[-1] + low = last.lower() + if low.startswith("yes") or low.startswith("no"): + return validate_verdict(last) + for ln in reversed(lines): + l = ln.lower() + if l.startswith("yes") or l.startswith("no"): + return validate_verdict(ln) + return "" + + +def absence_verdict(claim: Claim) -> ClaimVerdict: + reason = ( + f"{claim.claim_class} claim about Sam has zero high-trust durable Vestige evidence." + ) + return ClaimVerdict( + claim=claim, + status="REFUTED_BY_ABSENCE", + reason=truncate_chars(reason, 140), + ) + + +def nei_verdict( + claim: Claim, + reason: str, + evidence: list[EvidenceItem] | None = None, +) -> ClaimVerdict: + evidence = evidence or [] + return ClaimVerdict( + claim=claim, + status="NEI", + reason=truncate_chars(reason, 140), + evidence_ids=[item.id for item in high_trust(evidence)[:3]], + durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), + high_trust_evidence_count=len(high_trust(evidence)), + ) + + +def supported_verdict(claim: Claim, evidence: list[EvidenceItem]) -> ClaimVerdict: + return ClaimVerdict( + claim=claim, + status="SUPPORTED", + reason="High-trust evidence supports or does not contradict the claim.", + evidence_ids=[item.id for item in high_trust(evidence)[:3]], + durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), + high_trust_evidence_count=len(high_trust(evidence)), + ) + + +def parse_json_object(raw: str) -> dict[str, Any] | None: + cleaned = THINK_RE.sub("", raw).strip() + cleaned = re.sub(r"^```(?:json)?\s*", "", cleaned, flags=re.IGNORECASE).strip() + cleaned = re.sub(r"\s*```$", "", cleaned).strip() + try: + obj = json.loads(cleaned) + return obj if isinstance(obj, dict) else None + except json.JSONDecodeError: + pass + start = cleaned.find("{") + end = cleaned.rfind("}") + if start >= 0 and end > start: + try: + obj = json.loads(cleaned[start : end + 1]) + return obj if isinstance(obj, dict) else None + except json.JSONDecodeError: + return None + return None + + +def verdict_from_legacy_line(claim: Claim, raw: str, evidence: list[EvidenceItem]) -> ClaimVerdict | None: + line = validate_verdict(raw) + if line == "yes": + return supported_verdict(claim, evidence) + m = VERDICT_RE.match(line) + if not m: + return None + reason = m.group(2) + if not relevant_durable_high_trust(claim, evidence): + return nei_verdict(claim, "Durable evidence required for refuted verdict.", evidence) + return ClaimVerdict( + claim=claim, + status="REFUTED", + reason=truncate_chars(reason, 140), + evidence_ids=[item.id for item in high_trust(evidence)[:3]], + durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), + high_trust_evidence_count=len(high_trust(evidence)), + ) + + +def validate_structured_verdict( + claim: Claim, + data: dict[str, Any], + evidence: list[EvidenceItem], +) -> ClaimVerdict: + status = str(data.get("status") or "").strip().upper() + if status not in STRUCTURED_VERDICTS: + status = "NEI" + claim_class = str(data.get("class") or claim.claim_class).strip().upper() + if claim_class not in CLAIM_CLASSES: + claim_class = claim.claim_class + reason = truncate_chars(str(data.get("reason") or "").strip(), 140) + if any(verb in reason.lower() for verb in INFERENCE_VERBS): + return nei_verdict(claim, "Inference-chain verdict downgraded to NEI.", evidence) + if status == "REFUTED_BY_ABSENCE": + if not (claim.sam_critical and claim.claim_class in CRITICAL_ABSENCE_CLASSES): + return nei_verdict(claim, "Absence veto does not apply to this claim.", evidence) + if relevant_durable_high_trust(claim, evidence): + return nei_verdict(claim, "Durable evidence exists; absence veto does not apply.", evidence) + if status == "REFUTED" and not relevant_durable_high_trust(claim, evidence): + return nei_verdict(claim, "Durable evidence required for refuted verdict.", evidence) + evidence_ids_raw = data.get("evidence_ids") or [] + evidence_ids = [ + str(eid) for eid in evidence_ids_raw[:5] + ] if isinstance(evidence_ids_raw, list) else [] + if not reason: + if status == "SUPPORTED": + reason = "High-trust evidence supports or does not contradict the claim." + elif status == "NEI": + reason = "Not enough high-trust evidence to decide." + elif status == "REFUTED_BY_ABSENCE": + reason = absence_verdict(claim).reason + else: + reason = "High-trust evidence refutes the claim." + return ClaimVerdict( + claim=Claim( + text=claim.text, + claim_class=claim_class, + source_index=claim.source_index, + sam_critical=claim.sam_critical, + ), + status=status, + reason=reason, + evidence_ids=evidence_ids, + durable_evidence_count=len(relevant_durable_high_trust(claim, evidence)), + high_trust_evidence_count=len(high_trust(evidence)), + ) + + +def judge_claim_with_model(claim: Claim, evidence: list[EvidenceItem]) -> ClaimVerdict: + user_msg = ( + f"CLAIM CLASS: {claim.claim_class}\n" + f"SAM-CRITICAL: {'yes' if claim.sam_critical else 'no'}\n" + f"ABSENCE-FAIL-CLOSED APPLIES: " + f"{'yes' if claim.sam_critical and claim.claim_class in CRITICAL_ABSENCE_CLASSES else 'no'}\n" + f"DURABLE HIGH-TRUST EVIDENCE COUNT: {len(relevant_durable_high_trust(claim, evidence))}\n\n" + f"CLAIM:\n{claim.text}\n\n" + f"EVIDENCE:\n{format_claim_evidence(evidence, claim)}" + ) + body = { + "model": MODEL, + "messages": [ + {"role": "system", "content": CLAIM_SYSTEM_PROMPT}, + {"role": "user", "content": user_msg}, + ], + "max_tokens": 700, + "temperature": 0.0, + "top_p": 1.0, + "top_k": 1, + "seed": 42, + "stream": False, + "chat_template_kwargs": {"enable_thinking": False}, + } + resp = post_json(SANHEDRIN_ENDPOINT, body, SANHEDRIN_TIMEOUT) + if not isinstance(resp, dict): + return nei_verdict(claim, "Sanhedrin model unavailable; fail-open for this claim.", evidence) + try: + msg = resp["choices"][0]["message"] + raw = msg.get("content") or msg.get("reasoning") or "" + except (KeyError, IndexError, TypeError): + return nei_verdict(claim, "Malformed Sanhedrin model response.", evidence) + data = parse_json_object(raw) + if data is not None: + return validate_structured_verdict(claim, data, evidence) + legacy = verdict_from_legacy_line(claim, raw, evidence) + if legacy is not None: + return legacy + return nei_verdict(claim, "Sanhedrin model did not return structured JSON.", evidence) + + +def judge_claim(claim: Claim, evidence: list[EvidenceItem]) -> ClaimVerdict: + durable_count = len(relevant_durable_high_trust(claim, evidence)) + high_count = len(high_trust(evidence)) + if claim.sam_critical and claim.claim_class in CRITICAL_ABSENCE_CLASSES and durable_count == 0: + verdict = absence_verdict(claim) + verdict.high_trust_evidence_count = high_count + return verdict + if high_count == 0: + return nei_verdict(claim, "No high-trust evidence retrieved for this claim.", evidence) + return judge_claim_with_model(claim, evidence) + + +def render_legacy_from_verdicts(verdicts: list[ClaimVerdict]) -> str: + vetoes = [v for v in verdicts if v.status in {"REFUTED", "REFUTED_BY_ABSENCE"}] + if not vetoes: + return "yes" + vetoes.sort( + key=lambda v: ( + SEVERITY_ORDER.get(v.claim.claim_class, 99), + v.claim.source_index, + ) + ) + chosen = vetoes[0] + reason = truncate_chars(chosen.reason or chosen.claim.text, 140) + return f"no - [Sanhedrin Veto] [{chosen.claim.claim_class}]: {reason}" + + +def recompute_legacy_from_result(result: dict[str, Any]) -> str: + vetoes = [] + for raw in result.get("verdicts", []): + claim = raw.get("claim", {}) if isinstance(raw, dict) else {} + status = str(raw.get("status", "")) + if status not in {"REFUTED", "REFUTED_BY_ABSENCE"}: + continue + vetoes.append( + ( + SEVERITY_ORDER.get(str(claim.get("claim_class", "")), 99), + int(claim.get("source_index", 0) or 0), + str(claim.get("claim_class", "TECHNICAL")), + truncate_chars(str(raw.get("reason") or claim.get("text") or ""), 140), + ) + ) + if not vetoes: + return "yes" + _, _, claim_class, reason = sorted(vetoes)[0] + return f"no - [Sanhedrin Veto] [{claim_class}]: {reason}" + + +def apply_appeals_to_claim_mode_result(result: dict[str, Any]) -> dict[str, Any]: + if sanhedrin_core is None: + return result + appeals = sanhedrin_core.load_appeals() + changed = False + for raw in result.get("verdicts", []): + if not isinstance(raw, dict) or raw.get("status") not in {"REFUTED", "REFUTED_BY_ABSENCE"}: + continue + claim = raw.get("claim", {}) if isinstance(raw.get("claim"), dict) else {} + text = str(claim.get("text") or "") + if sanhedrin_core.is_appealed({"fingerprint": sanhedrin_core.claim_fingerprint(text)}, appeals): + raw["status"] = "APPEALED" + raw["reason"] = "Prior appeal suppresses this Sanhedrin veto." + changed = True + + if changed: + legacy = recompute_legacy_from_result(result) + result["legacy_verdict"] = legacy + result["decision"] = "yes" if legacy == "yes" else "no" + result["verdict"] = result["decision"] + result["passed"] = legacy == "yes" + result["reason"] = "" if result["passed"] else legacy.split(" - ", 1)[-1] + return result + + +def save_claim_mode_receipt( + draft: str, + result: dict[str, Any], + manifest: dict[str, Any] | None = None, +) -> None: + if sanhedrin_core is None: + return + manifest = manifest or sanhedrin_core.new_manifest(draft) + claims = [] + for idx, raw in enumerate(result.get("verdicts", []), start=1): + if not isinstance(raw, dict): + continue + claim = raw.get("claim", {}) if isinstance(raw.get("claim"), dict) else {} + text = str(claim.get("text") or "") + claim_class = str(claim.get("claim_class") or "TECHNICAL") + status = str(raw.get("status") or "NEI") + evidence_ids = raw.get("evidence_ids") if isinstance(raw.get("evidence_ids"), list) else [] + if status == "SUPPORTED": + decision = "pass" + evidence_state = "supported" + fix = "No change required." + elif status == "APPEALED": + decision = "appealed" + evidence_state = "appealed" + fix = "Prior appeal suppresses this veto fingerprint." + elif status == "REFUTED_BY_ABSENCE": + decision = "veto" + evidence_state = "missing_precedent" + fix = "Remove the unsupported user-specific claim or cite durable Vestige evidence first." + elif status == "REFUTED": + decision = "veto" + evidence_state = "contradicted" + fix = "Remove or qualify the contradicted claim using the cited Vestige precedent." + else: + decision = "pass_unverified" + evidence_state = "not_enough_information" + fix = "No blocking change required." + claims.append( + { + "id": f"c{idx:03d}", + "text": text, + "fingerprint": sanhedrin_core.claim_fingerprint(text), + "class": claim_class, + "subject": "Sam" if bool(claim.get("sam_critical")) else "draft", + "risk": "hard" if bool(claim.get("sam_critical")) else "normal", + "evidence_state": evidence_state, + "decision": decision, + "precedent": [ + { + "type": "vestige", + "summary": str(raw.get("reason") or status), + "evidence": ", ".join(str(eid) for eid in evidence_ids[:5]), + "durableCount": raw.get("durable_evidence_count"), + "highTrustCount": raw.get("high_trust_evidence_count"), + } + ], + "fix": fix, + "appeal": { + "status": "appealed" if decision == "appealed" else "open", + "actions": ["stale", "wrong", "too_strict"], + }, + } + ) + + manifest["claims"] = claims + manifest["overall"] = "pass" if result.get("passed") else "veto" + if any(claim["decision"] == "appealed" for claim in claims): + manifest["overall"] = "pass_with_warnings" if result.get("passed") else manifest["overall"] + manifest["verdictBar"] = "APPEALED" + manifest["summary"] = "Prior appeal suppressed a Sanhedrin veto." + elif result.get("passed"): + manifest["verdictBar"] = "PASS" if not claims else "NOTE" + manifest["summary"] = "Sanhedrin found no blocking claim issues." + else: + manifest["verdictBar"] = "VETO" + manifest["summary"] = str(result.get("reason") or "Sanhedrin blocked a claim.") + sanhedrin_core.save_manifest(manifest) + + +def save_legacy_receipt(manifest: dict[str, Any] | None, verdict: str, evidence: str = "") -> str: + if sanhedrin_core is None or manifest is None: + return verdict + updated = sanhedrin_core.apply_model_verdict(manifest, verdict, evidence) + sanhedrin_core.save_manifest(manifest) + return updated + + +def claim_mode_result(draft: str) -> dict[str, Any]: + claims = extract_check_worthy_claims(draft) + staged = load_staged_evidence(os.environ.get(STAGE_FILE_ENV)) + verdicts: list[ClaimVerdict] = [] + for claim in claims: + evidence, ok = fetch_claim_evidence(claim) + if not ok: + verdicts.append( + nei_verdict( + claim, + "Vestige retrieval unavailable; fail-open for this claim.", + staged, + ) + ) + continue + combined = dedupe_evidence(evidence + staged) + verdicts.append(judge_claim(claim, combined)) + legacy_verdict = render_legacy_from_verdicts(verdicts) + decision = "yes" if legacy_verdict == "yes" else "no" + json_reason = "" if decision == "yes" else legacy_verdict.split(" - ", 1)[-1] + return { + "mode": "claim", + "decision": decision, + "verdict": decision, + "reason": json_reason, + "passed": legacy_verdict == "yes", + "legacy_verdict": legacy_verdict, + "claims_extracted": len(claims), + "staged_evidence_count": len(staged), + "verdicts": [asdict(v) for v in verdicts], + } + + +def print_claim_mode_result(result: dict[str, Any]) -> None: + if (os.environ.get(OUTPUT_ENV) or "").strip().lower() == "json": + print(json.dumps(result, ensure_ascii=False, separators=(",", ":"))) + else: + print(result.get("legacy_verdict") or "yes") + + +def main() -> None: + draft = sys.stdin.read().strip() + if not draft: + print("yes") + return + + manifest = sanhedrin_core.new_manifest(draft) if sanhedrin_core is not None else None + if sanhedrin_core is not None and manifest is not None: + receipt_veto = sanhedrin_core.apply_receipt_lock(manifest) + if receipt_veto: + sanhedrin_core.save_manifest(manifest) + print(f"no - [Sanhedrin Veto] [TECHNICAL]: {receipt_veto}") + return + + if env_flag(CLAIM_MODE_ENV): + result = apply_appeals_to_claim_mode_result(claim_mode_result(draft)) + save_claim_mode_receipt(draft, result, manifest) + print_claim_mode_result(result) + return + + evidence, high_trust_count = fetch_evidence(draft) + + # Auto-pass if no high-trust evidence — model can't legitimately veto + # without something concrete to cite. Eliminates the common false-positive + # mode where the model invents a contradiction from low-trust noise. + if high_trust_count == 0: + save_legacy_receipt(manifest, "yes", evidence) + print("yes") + return + + verdict = judge(draft, evidence) + + if not verdict: + # Fail-open: server unreachable, malformed response, etc. + save_legacy_receipt(manifest, "yes", evidence) + print("yes") + return + + verdict = save_legacy_receipt(manifest, verdict, evidence) + print(verdict) + + +if __name__ == "__main__": + main() diff --git a/hooks/sanhedrin.sh b/hooks/sanhedrin.sh new file mode 100755 index 0000000..2802e08 --- /dev/null +++ b/hooks/sanhedrin.sh @@ -0,0 +1,375 @@ +#!/bin/bash +# sanhedrin.sh — Stop hook (Post-Cognitive Sanhedrin / Full Agent-Type Guillotine) +# +# Spawns the Executioner subagent (Haiku 4.5, fresh context, Vestige MCP +# tools) to run mcp__vestige__deep_reference 8-stage contradiction analysis +# on the last assistant draft. If any technical claim contradicts a +# high-trust memory, exit 2 with the veto reason — forces Main Claude to +# rewrite. +# +# Runs AFTER veto-detector.sh (fast regex against veto-tagged memories). +# Sanhedrin is the deeper semantic check: it reads the draft as a real +# reasoning agent, extracts claims, runs deep_reference on each. +# +# Architecture: +# Main Claude finishes draft → Stop hook chain fires → +# veto-detector.sh (50ms regex, may block) → +# sanhedrin.sh (2-8s Haiku subagent, may block) → +# synthesis-stop-validator.sh (existing regex hedge check, may block) +# +# Opt-in: set VESTIGE_SANHEDRIN_ENABLED=1 in parent shell, or install with +# scripts/install-sandwich.sh --enable-sanhedrin. +# Re-entrancy lock: VESTIGE_EXECUTIONER_ACTIVE=1 inside the subagent. +# +# Ship date 2026-04-20. + +set -u + +load_vestige_sanhedrin_env() { + [ -f "$1" ] || return 0 + command -v python3 >/dev/null 2>&1 || return 0 + while IFS="$(printf '\t')" read -r key value; do + case "$key" in + VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_SANHEDRIN_STATE_DIR|VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER|VESTIGE_DASHBOARD_PORT) + export "$key=$value" + ;; + esac + done < <(python3 - "$1" <<'PY' +import shlex +import sys + +allowed = { + "VESTIGE_SANHEDRIN_ENABLED", + "VESTIGE_SANHEDRIN_MODEL", + "VESTIGE_SANHEDRIN_ENDPOINT", + "VESTIGE_SANHEDRIN_CLAIM_MODE", + "VESTIGE_SANHEDRIN_OUTPUT", + "VESTIGE_SANHEDRIN_PYTHON", + "VESTIGE_SANHEDRIN_STATE_DIR", + "VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER", + "VESTIGE_DASHBOARD_PORT", +} + +try: + lines = open(sys.argv[1], encoding="utf-8").read().splitlines() +except OSError: + sys.exit(0) + +for raw in lines: + line = raw.strip() + if not line or line.startswith("#"): + continue + try: + parts = shlex.split(line, posix=True) + except ValueError: + continue + if len(parts) != 1 or "=" not in parts[0]: + continue + key, value = parts[0].split("=", 1) + if key in allowed and "\t" not in value and "\0" not in value: + print(f"{key}\t{value}") +PY + ) +} + +# === OPT-IN GATE === +# Sanhedrin is heavyweight: the default local backend is a ~19 GB model and +# needs roughly 20+ GB of free RAM. Keep it disabled unless the user explicitly +# opts in. The installer writes this env file only for --enable-sanhedrin. +SANHEDRIN_ENV="${VESTIGE_SANHEDRIN_ENV:-$HOME/.claude/hooks/vestige-sanhedrin.env}" +if [ -f "$SANHEDRIN_ENV" ]; then + load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || exit 0 +fi + +case "${VESTIGE_SANHEDRIN_ENABLED:-0}" in + 1|true|TRUE|yes|YES|on|ON) ;; + *) exit 0 ;; +esac + +# === RE-ENTRANCY GUARD === +# The Executioner's own Stop hook will fire when it returns — prevent +# recursive spawns that would fork-bomb the quota. +if [ "${VESTIGE_EXECUTIONER_ACTIVE:-0}" = "1" ]; then + exit 0 +fi + +PYTHON_BIN="${VESTIGE_SANHEDRIN_PYTHON:-}" +if [ -z "$PYTHON_BIN" ]; then + PYTHON_BIN="$(command -v python3 2>/dev/null || printf '')" +fi +if [ -z "$PYTHON_BIN" ]; then + PYTHON_BIN="/usr/bin/python3" +fi +if ! "$PYTHON_BIN" -c 'import sys' >/dev/null 2>&1; then + exit 0 +fi + +# === READ STOP HOOK INPUT === +INPUT="$(cat)" +TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | "$PYTHON_BIN" -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')" + +if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then + exit 0 +fi + +# === EXTRACT LAST ASSISTANT DRAFT === +# Read the transcript JSONL, pull the last assistant message text. +export TRANSCRIPT_PATH +DRAFT_SCRIPT="$(mktemp -t vestige-sanhedrin-draft.XXXXXX)" +trap 'rm -f "$DRAFT_SCRIPT"' EXIT + +cat > "$DRAFT_SCRIPT" <<'DRAFT_PYEOF' +import json, os, re, sys + +transcript = os.environ.get("TRANSCRIPT_PATH", "") +last_assistant = "" + +try: + with open(transcript) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except Exception: + continue + role = obj.get("role") or obj.get("type", "") + content = obj.get("message", {}).get("content", obj.get("content", "")) + text = "" + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + text += block.get("text", "") + "\n" + elif isinstance(content, str): + text = content + if role == "assistant": + last_assistant = text +except Exception: + sys.exit(0) + +# Print nothing if no draft. Short verification claims still need Receipt Lock. +stripped = last_assistant.strip() +if not stripped: + sys.exit(0) + +# Legacy gate: only check drafts that contain technical indicators. Claim mode +# deliberately broadens this to any substantive assistant draft while keeping +# Sanhedrin opt-in through VESTIGE_SANHEDRIN_ENABLED. +claim_mode = os.environ.get("VESTIGE_SANHEDRIN_CLAIM_MODE", "") == "1" +receipt_gate = bool( + re.search( + r"\b((all\s+)?(tests?|test suite|build|lint|typecheck|checks?|cargo test|npm test|pnpm test|pytest|vitest|jest|playwright|tsc|clippy)\s+(passed|passes|passing|green|succeeded|succeeds|clean)|(verified|validated|confirmed)\s+(with|by|via))\b", + stripped, + re.I, + ) +) +if len(stripped) < 100 and not receipt_gate: + sys.exit(0) + +if not claim_mode: + has_code = "`" in stripped or "```" in stripped + has_cmd = any(kw in stripped.lower() for kw in ["install", "run ", "use ", "call ", "invoke", "execute"]) + has_path = "/" in stripped and any(ext in stripped for ext in [".rs", ".ts", ".py", ".sh", ".md", ".json"]) + + if not (has_code or has_cmd or has_path or receipt_gate): + sys.exit(0) + +# Truncate to 4000 chars to keep Haiku prompt bounded +if len(stripped) > 4000: + stripped = stripped[:4000] + "... [truncated]" + +print(stripped) +DRAFT_PYEOF + +DRAFT="$("$PYTHON_BIN" "$DRAFT_SCRIPT" 2>/dev/null || printf '')" + +if [ -z "$DRAFT" ]; then + exit 0 +fi + +# === VERIFY local executioner bridge available === +# 2026-04-25: switched from Haiku 4.5 subagent to an OpenAI-compatible +# local/remote endpoint. On Apple Silicon the optional launchd path starts +# mlx_lm.server; on x86 users can point VESTIGE_SANHEDRIN_ENDPOINT at vLLM, +# Ollama, llama.cpp, or any compatible /v1/chat/completions endpoint. +# Fail-open if the endpoint is unreachable. +BRIDGE="$HOME/.claude/hooks/sanhedrin-local.py" +if [ ! -x "$BRIDGE" ] && [ ! -f "$BRIDGE" ]; then + exit 0 +fi + +# === SPAWN LOCAL EXECUTIONER (background with timeout) === +OUTPUT_FILE="$(mktemp -t vestige-sanhedrin-out.XXXXXX)" +trap 'rm -f "$DRAFT_SCRIPT" "$OUTPUT_FILE"' EXIT +export VESTIGE_SANHEDRIN_TRANSCRIPT="$TRANSCRIPT_PATH" + +( + printf '%s\n' "$DRAFT" | "$PYTHON_BIN" "$BRIDGE" > "$OUTPUT_FILE" 2>/dev/null +) & + +EXEC_PID=$! + +# === TIMEOUT GUARD (60 seconds) === +# Local Qwen3.6-35B-A3B on M5/M3 Max typically returns in 5-15s for the +# single-shot judgment. 60s ceiling preserves the existing settings.json +# Stop hook timeout (70s) and gives headroom for cold model load if +# launchd just restarted. Bridge fail-opens internally if mlx-server is +# unreachable, so timeout-kill here is the secondary safety net. +WAITED=0 +while [ "$WAITED" -lt 60 ]; do + if ! /bin/kill -0 "$EXEC_PID" 2>/dev/null; then + break + fi + sleep 1 + WAITED=$((WAITED + 1)) +done +if /bin/kill -0 "$EXEC_PID" 2>/dev/null; then + /bin/kill "$EXEC_PID" 2>/dev/null + wait "$EXEC_PID" 2>/dev/null + exit 0 +fi +wait "$EXEC_PID" 2>/dev/null + +EXECUTIONER_OUTPUT="$(cat "$OUTPUT_FILE" 2>/dev/null || printf '')" + +# === PARSE VERDICT === +sanhedrin_veto() { + REASON="$1" + REASON="$(printf '%s' "$REASON" | "$PYTHON_BIN" -c 'import sys; print(sys.stdin.read().strip())' 2>/dev/null || printf '%s' "$REASON")" + + if printf '%s' "$REASON" | /usr/bin/grep -qi 'Receipt Lock'; then + cat >&2 <&2 < start: + obj = loads_candidate(raw[start:end + 1]) + +if obj is None: + sys.exit(1) + +decision = obj.get("decision", obj.get("verdict", obj.get("answer", ""))) +reason = obj.get("reason", obj.get("message", obj.get("explanation", ""))) +if isinstance(decision, bool): + decision = "yes" if decision else "no" +elif decision is None: + decision = "" +else: + decision = str(decision) + +if reason is None: + reason = "" +elif not isinstance(reason, str): + reason = json.dumps(reason, ensure_ascii=False) + +print(decision.strip()) +print(reason.strip()) +' 2>/dev/null || printf '')" + + if [ -n "$JSON_PARSED" ]; then + JSON_DECISION="$(printf '%s\n' "$JSON_PARSED" | /usr/bin/sed -n '1p' | "$PYTHON_BIN" -c 'import sys; print(sys.stdin.read().strip().lower())' 2>/dev/null || printf '')" + JSON_REASON="$(printf '%s\n' "$JSON_PARSED" | /usr/bin/sed '1d')" + + case "$JSON_DECISION" in + yes|pass|allow|allowed|clean|true) + exit 0 + ;; + no|fail|block|blocked|veto|false) + sanhedrin_veto "$JSON_REASON" + ;; + esac + fi +fi + +TRIMMED="$(printf '%s' "$EXECUTIONER_OUTPUT" | /usr/bin/awk 'NF {print; exit}' | /usr/bin/awk '{$1=$1;print}')" + +if [ -z "$TRIMMED" ]; then + exit 0 +fi + +# "yes" verdict - draft is clean, allow stop +case "$TRIMMED" in + yes|YES|Yes|yes.|Yes.) + exit 0 + ;; +esac + +# "no - " or "no: " verdict - block the stop, force rewrite +# Documented spec is `no - [Sanhedrin Veto] [CLASS]: ` (hyphen-space). +# Legacy `no: ` also accepted for backward compat. +case "$TRIMMED" in + no\ -*|NO\ -*|No\ -*|no:*|NO:*|No:*) + case "$TRIMMED" in + no\ -*|NO\ -*|No\ -*) + REASON="${TRIMMED#* - }" + ;; + *) + REASON="${TRIMMED#*:}" + ;; + esac + sanhedrin_veto "$REASON" + ;; +esac + +# Unparseable verdict — fail open (do not block on Executioner errors) +exit 0 diff --git a/hooks/sanhedrin_core.py b/hooks/sanhedrin_core.py new file mode 100644 index 0000000..eeb1511 --- /dev/null +++ b/hooks/sanhedrin_core.py @@ -0,0 +1,623 @@ +#!/usr/bin/env python3 +"""Shared Sanhedrin claim-ledger, receipt, and appeal helpers. + +This module is intentionally dependency-free so Stop hooks can import it inside +Claude Code, Codex wrappers, and local development without installing a package. +""" + +from __future__ import annotations + +import datetime as dt +import hashlib +import html +import json +import os +import re +from pathlib import Path +from typing import Any + + +STATE_DIR = Path( + os.environ.get("VESTIGE_SANHEDRIN_STATE_DIR") + or Path.home() / ".vestige" / "sanhedrin" +) +RECEIPTS_DIR = STATE_DIR / "receipts" +LATEST_JSON = STATE_DIR / "latest.json" +LATEST_HTML = STATE_DIR / "latest.html" +APPEALS_JSONL = STATE_DIR / "appeals.jsonl" +COMMAND_RECEIPTS_JSONL = STATE_DIR / "command-receipts.jsonl" + +VERIFICATION_RE = re.compile( + r"\b(" + r"(all\s+)?(tests?|test suite|build|lint|typecheck|checks?|cargo test|npm test|pnpm test|" + r"pytest|vitest|jest|playwright|tsc|clippy)\s+" + r"(passed|passes|passing|green|succeeded|succeeds|clean)|" + r"(verified|validated|confirmed)\s+(with|by|via)\s+[`']?([^`'\n]+)[`']?" + r")\b", + re.IGNORECASE, +) +COMMAND_FAMILY_PATTERNS = { + "test": re.compile(r"\b(pytest|cargo\s+test|npm\s+(run\s+)?test|pnpm\s+(run\s+)?test|vitest|jest|playwright\s+test)\b", re.I), + "build": re.compile(r"\b(cargo\s+build|npm\s+(run\s+)?build|pnpm\s+(run\s+)?build|next\s+build|vite\s+build)\b", re.I), + "lint": re.compile(r"\b(cargo\s+clippy|npm\s+(run\s+)?lint|pnpm\s+(run\s+)?lint|eslint|ruff|flake8)\b", re.I), + "typecheck": re.compile(r"\b(tsc|svelte-check|mypy|pyright|cargo\s+check)\b", re.I), +} +COMMAND_EXIT_RE = re.compile( + r"(exit[_ -]?code|status|returncode|rc)[\"':=\s]+(-?\d+)|" + r"Error:\s*Exit code\s*(-?\d+)|" + r"Process exited with code\s*(-?\d+)", + re.I, +) +CLAUDE_TOOL_NAME_RE = re.compile(r'"name"\s*:\s*"([^"]+)"') + + +def now_iso() -> str: + return dt.datetime.now(dt.timezone.utc).isoformat(timespec="seconds") + + +def ensure_dirs() -> None: + RECEIPTS_DIR.mkdir(parents=True, exist_ok=True) + + +def stable_id(text: str, prefix: str = "sr") -> str: + digest = hashlib.sha256(text.encode("utf-8")).hexdigest()[:16] + return f"{prefix}_{digest}" + + +def claim_fingerprint(text: str) -> str: + normalized = re.sub(r"\s+", " ", text.lower()).strip() + normalized = re.sub(r"[`'\"$]", "", normalized) + return hashlib.sha256(normalized.encode("utf-8")).hexdigest()[:16] + + +def split_claims(draft: str) -> list[str]: + chunks = re.split(r"(?<=[.!?])\s+|\n+", draft) + claims: list[str] = [] + for chunk in chunks: + text = chunk.strip(" -\t") + if len(text) >= 18 or VERIFICATION_RE.search(text) or is_hard_user_claim(text): + claims.append(text) + return claims[:24] + + +def detect_claim_type(text: str) -> str: + low = text.lower() + if VERIFICATION_RE.search(text): + return "receipt_lock" + if is_hard_user_claim(text): + return "hard_user_claim" + if any(word in low for word in ("won", "prize", "ranked", "placed", "score", "graduated", "worked at")): + return "hard_user_claim" + if any(word in low for word in ("should", "could", "recommend", "plan", "target", "estimate")): + return "advice" + if "`" in text or "/" in text or re.search(r"\bv?\d+\.\d+", text): + return "technical" + return "general" + + +def is_hard_user_claim(text: str) -> bool: + if not re.search(r"\b(Sam|you|your|I|my)\b", text, re.I): + return False + hard_patterns = ( + r"\b(attended|graduated|studied|enrolled|accepted|worked|works|employed|hired)\b", + r"\b(was\s+born|born\s+in|born\s+on|birthdate|birthday)\b", + r"\b(won|placed|ranked|scored|earned|raised|sold|founded|launched)\b", + r"\b(prize|award|payout|grant|scholarship|degree|gpa|employer|school|university|college|birth\s+date)\b", + r"\$[0-9]", + ) + return any(re.search(pattern, text, re.I) for pattern in hard_patterns) + + +def new_manifest(draft: str) -> dict[str, Any]: + draft_id = stable_id(draft, "draft") + claims = [] + for i, text in enumerate(split_claims(draft), start=1): + claim_type = detect_claim_type(text) + claims.append( + { + "id": f"c{i:03d}", + "text": text, + "fingerprint": claim_fingerprint(text), + "class": claim_type, + "subject": infer_subject(text), + "risk": "hard" if claim_type == "receipt_lock" else "normal", + "evidence_state": "unchecked", + "decision": "pending", + "precedent": [], + "fix": "", + "appeal": { + "status": "open", + "actions": ["stale", "wrong", "too_strict"], + }, + } + ) + return { + "schema": "vestige.sanhedrin.receipt.v1", + "id": stable_id(f"{draft_id}:{now_iso()}", "receipt"), + "draftId": draft_id, + "createdAt": now_iso(), + "overall": "pass", + "verdictBar": "PASS", + "summary": "No blocking claim issues found.", + "draftPreview": draft[:1000], + "claims": claims, + "receipts": [], + "source": { + "stateDir": str(STATE_DIR), + "transcript": os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT"), + }, + } + + +def infer_subject(text: str) -> str: + if re.search(r"\b(Sam|you|your)\b", text, re.I): + return "Sam" + if re.search(r"\b(test|pytest|cargo test|npm test|pnpm test|vitest|jest)\b", text, re.I): + return "test receipt" + if re.search(r"\b(build|lint|typecheck|clippy|tsc)\b", text, re.I): + return "command receipt" + return "draft" + + +def command_families_for_claim(text: str) -> list[str]: + low = text.lower() + if re.search(r"\b(all\s+checks?|checks)\s+(passed|passes|passing|green|succeeded|succeeds|clean)\b", low) and "cargo check" not in low: + return ["test", "build", "lint", "typecheck"] + families: list[str] = [] + if any(word in low for word in ("test", "pytest", "vitest", "jest", "playwright")): + families.append("test") + if any(word in low for word in ("build", "compiled", "compile")): + families.append("build") + if any(word in low for word in ("lint", "clippy", "eslint", "ruff")): + families.append("lint") + if any(word in low for word in ("typecheck", "tsc", "mypy", "pyright", "cargo check")): + families.append("typecheck") + if "check" in low and "cargo check" not in low and "checks" not in low: + families.append("typecheck") + return families or ["test"] + + +def load_command_receipts() -> list[dict[str, Any]]: + transcript = os.environ.get("VESTIGE_SANHEDRIN_TRANSCRIPT") + if transcript: + return extract_transcript_receipts(Path(transcript)) + if os.environ.get("VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER") != "1": + return [] + return load_jsonl(COMMAND_RECEIPTS_JSONL) + + +def load_jsonl(path: Path) -> list[dict[str, Any]]: + if not path.exists(): + return [] + items: list[dict[str, Any]] = [] + try: + for line in path.read_text(encoding="utf-8").splitlines(): + line = line.strip() + if not line: + continue + obj = json.loads(line) + if isinstance(obj, dict): + items.append(obj) + except (OSError, json.JSONDecodeError): + return items + return items + + +def extract_transcript_receipts(path: Path) -> list[dict[str, Any]]: + if not path.exists(): + return [] + receipts: list[dict[str, Any]] = [] + pending_commands: dict[str, dict[str, Any]] = {} + try: + lines = path.read_text(encoding="utf-8", errors="ignore").splitlines() + except OSError: + return receipts + for line in lines: + try: + obj = json.loads(line) + except json.JSONDecodeError: + continue + receipts.extend(extract_structured_receipts(obj, pending_commands)) + blob = json.dumps(obj, ensure_ascii=False) + command = extract_command(blob) + if not command: + continue + exit_code = extract_exit_code(blob) + receipts.append( + { + "source": "transcript", + "command": command, + "exitCode": exit_code, + "success": exit_code == 0 if exit_code is not None else None, + "timestamp": obj.get("timestamp") or obj.get("created_at") or now_iso(), + } + ) + return receipts + + +def extract_structured_receipts( + obj: dict[str, Any], + pending_commands: dict[str, dict[str, Any]], +) -> list[dict[str, Any]]: + """Extract Claude Code Bash receipts from assistant tool_use/user tool_result pairs.""" + receipts: list[dict[str, Any]] = [] + timestamp = obj.get("timestamp") or obj.get("created_at") or now_iso() + receipts.extend(extract_codex_receipts(obj, pending_commands, timestamp)) + content = obj.get("message", {}).get("content", obj.get("content", "")) + + blocks = content if isinstance(content, list) else [] + for block in blocks: + if not isinstance(block, dict): + continue + if block.get("type") == "tool_use" and str(block.get("name", "")).lower() in {"bash", "shell", "exec_command"}: + tool_id = str(block.get("id") or "") + tool_input = block.get("input") if isinstance(block.get("input"), dict) else {} + command = str(tool_input.get("command") or tool_input.get("cmd") or "") + if tool_id and command: + pending_commands[tool_id] = { + "source": "transcript", + "toolUseId": tool_id, + "command": command, + "timestamp": timestamp, + } + if block.get("type") == "tool_result": + tool_id = str(block.get("tool_use_id") or "") + if not tool_id or tool_id not in pending_commands: + continue + receipt = dict(pending_commands[tool_id]) + text = stringify_tool_result(block) + explicit_exit = extract_exit_code(text) + is_error = bool(block.get("is_error")) + receipt["exitCode"] = explicit_exit if explicit_exit is not None else (1 if is_error else 0) + receipt["success"] = receipt["exitCode"] == 0 and not is_error + receipt["timestamp"] = timestamp + receipts.append(receipt) + + tool_result = obj.get("toolUseResult") + if isinstance(tool_result, dict): + command = str(obj.get("command") or tool_result.get("command") or "") + if command: + exit_code = tool_result.get("exitCode") + if exit_code is None: + exit_code = tool_result.get("exit_code") + try: + parsed_exit = int(exit_code) if exit_code is not None else None + except (TypeError, ValueError): + parsed_exit = extract_exit_code(json.dumps(tool_result, ensure_ascii=False)) + is_error = bool(tool_result.get("is_error") or tool_result.get("interrupted")) + receipts.append( + { + "source": "transcript", + "command": command, + "exitCode": parsed_exit if parsed_exit is not None else (1 if is_error else 0), + "success": (parsed_exit == 0 if parsed_exit is not None else not is_error), + "timestamp": timestamp, + } + ) + + return receipts + + +def extract_codex_receipts( + obj: dict[str, Any], + pending_commands: dict[str, dict[str, Any]], + timestamp: str, +) -> list[dict[str, Any]]: + receipts: list[dict[str, Any]] = [] + payload = obj.get("payload") + if not isinstance(payload, dict): + return receipts + + payload_type = payload.get("type") + name = str(payload.get("name") or "").lower() + call_id = str(payload.get("call_id") or "") + if payload_type == "function_call" and name in {"exec_command", "bash", "shell"} and call_id: + args = payload.get("arguments") + if isinstance(args, str): + try: + args = json.loads(args) + except json.JSONDecodeError: + args = {} + if isinstance(args, dict): + command = str(args.get("cmd") or args.get("command") or "") + if command: + pending_commands[call_id] = { + "source": "codex-transcript", + "toolUseId": call_id, + "command": command, + "timestamp": timestamp, + } + elif payload_type == "function_call" and name == "write_stdin" and call_id: + args = payload.get("arguments") + if isinstance(args, str): + try: + args = json.loads(args) + except json.JSONDecodeError: + args = {} + if isinstance(args, dict): + session_id = args.get("session_id") + session_receipt = pending_commands.get(f"session:{session_id}") + if session_receipt: + pending_commands[call_id] = dict(session_receipt) + + if payload_type == "function_call_output" and call_id in pending_commands: + receipt = dict(pending_commands[call_id]) + output = str(payload.get("output") or "") + running = re.search(r"Process running with session ID\s+(\d+)", output) + if running: + pending_commands[f"session:{running.group(1)}"] = receipt + return receipts + exit_code = extract_exit_code(output) + receipt["exitCode"] = exit_code + receipt["success"] = exit_code == 0 if exit_code is not None else None + receipt["timestamp"] = timestamp + receipts.append(receipt) + + return receipts + + +def stringify_tool_result(block: dict[str, Any]) -> str: + content = block.get("content", "") + if isinstance(content, str): + return content + return json.dumps(content, ensure_ascii=False) + + +def extract_command(blob: str) -> str | None: + for key in ("cmd", "command"): + match = re.search(rf'"{key}"\s*:\s*"([^"]+)"', blob) + if match: + return bytes(match.group(1), "utf-8").decode("unicode_escape") + match = CLAUDE_TOOL_NAME_RE.search(blob) + if match and match.group(1).lower() in {"bash", "shell", "exec_command"}: + return match.group(1) + return None + + +def extract_exit_code(blob: str) -> int | None: + match = COMMAND_EXIT_RE.search(blob) + if not match: + return None + try: + return int(match.group(2) or match.group(3) or match.group(4)) + except ValueError: + return None + + +def receipt_matches_family(receipt: dict[str, Any], family: str) -> bool: + command = str(receipt.get("command") or "") + pattern = COMMAND_FAMILY_PATTERNS.get(family) + return bool(pattern and pattern.search(command)) + + +def apply_receipt_lock(manifest: dict[str, Any]) -> str | None: + receipts = load_command_receipts() + manifest["receipts"] = receipts[-20:] + appeals = load_appeals() + + for claim in manifest["claims"]: + if claim["class"] != "receipt_lock": + continue + missing_families: list[str] = [] + failed_family: tuple[str, dict[str, Any]] | None = None + supported_families: list[tuple[str, dict[str, Any]]] = [] + + for family in command_families_for_claim(claim["text"]): + matching = [r for r in receipts if receipt_matches_family(r, family)] + latest = matching[-1] if matching else None + if latest is None: + missing_families.append(family) + elif latest.get("success") is not True: + failed_family = (family, latest) + break + else: + supported_families.append((family, latest)) + + if failed_family is not None: + family, latest = failed_family + claim["evidence_state"] = "failed_receipt" if latest.get("success") is False else "unknown_receipt" + claim["decision"] = "veto" + claim["precedent"].append( + { + "type": "command", + "summary": f"Latest {family} command did not produce a successful receipt.", + "command": latest.get("command"), + "exitCode": latest.get("exitCode"), + } + ) + claim["fix"] = f"Replace the claim with: I do not have a successful {family} receipt for this session." + manifest["overall"] = "veto" + manifest["verdictBar"] = "VETO" + manifest["summary"] = "Receipt Lock blocked a contradicted verification claim." + return f"Receipt Lock: Draft claims {family} passed, but latest {family} receipt is not successful." + + if missing_families and is_appealed(claim, appeals): + claim["evidence_state"] = "appealed" + claim["decision"] = "appealed" + claim["precedent"].append({"type": "appeal", "summary": "Prior appeal suppresses this missing-receipt veto."}) + manifest["overall"] = "pass_with_warnings" + manifest["verdictBar"] = "APPEALED" + manifest["summary"] = "Prior appeal suppressed a Receipt Lock veto." + continue + + if missing_families: + family_list = ", ".join(missing_families) + claim["evidence_state"] = "missing_receipt" + claim["decision"] = "veto" + claim["precedent"].append( + { + "type": "receipt_lock", + "summary": f"No {family_list} command receipt found in this session.", + "source": "transcript/command ledger", + } + ) + claim["fix"] = f"Replace the claim with: I do not have recorded {family_list} receipt(s) for this session." + manifest["overall"] = "veto" + manifest["verdictBar"] = "VETO" + manifest["summary"] = "Receipt Lock blocked an unsupported verification claim." + return f"Receipt Lock: Draft claims {family_list} passed, but no {family_list} command receipt exists." + + claim["evidence_state"] = "supported" + claim["decision"] = "pass" + for family, latest in supported_families: + claim["precedent"].append( + { + "type": "command", + "summary": f"{family} receipt found.", + "command": latest.get("command"), + "exitCode": latest.get("exitCode"), + } + ) + + return None + + +def apply_model_verdict(manifest: dict[str, Any], verdict: str, evidence: str = "") -> str: + low = verdict.strip().lower() + if low == "yes" or low.startswith("yes "): + if manifest["overall"] != "veto": + has_appealed = any(c["decision"] == "appealed" for c in manifest["claims"]) + has_unchecked = any(c["decision"] == "pending" for c in manifest["claims"]) + manifest["overall"] = "pass_with_warnings" if has_unchecked or has_appealed else "pass" + manifest["verdictBar"] = "APPEALED" if has_appealed else ("NOTE" if has_unchecked else "PASS") + manifest["summary"] = ( + "Prior appeal suppressed a Sanhedrin veto." + if has_appealed + else "Sanhedrin found no blocking contradiction." + ) + for claim in manifest["claims"]: + if claim["decision"] == "pending": + claim["decision"] = "pass_unverified" + claim["evidence_state"] = "out_of_scope" + return "yes" + + reason = verdict.split(" - ", 1)[1] if " - " in verdict else verdict + appeals = load_appeals() + candidate = first_relevant_claim(manifest) + if candidate and is_appealed(candidate, appeals): + candidate["decision"] = "appealed" + candidate["evidence_state"] = "appealed" + candidate["precedent"].append({"type": "appeal", "summary": "Prior appeal suppresses this model veto."}) + manifest["overall"] = "pass_with_warnings" + manifest["verdictBar"] = "APPEALED" + manifest["summary"] = "Prior appeal suppressed the Sanhedrin veto." + return "yes" + + if candidate: + candidate["decision"] = "veto" + candidate["evidence_state"] = "contradicted" + candidate["precedent"].append({"type": "vestige", "summary": reason[:500], "evidence": evidence[:1000]}) + candidate["fix"] = "Remove or qualify the contradicted claim using the cited Vestige precedent." + manifest["overall"] = "veto" + manifest["verdictBar"] = "VETO" + manifest["summary"] = reason[:500] + return verdict + + +def first_relevant_claim(manifest: dict[str, Any]) -> dict[str, Any] | None: + for claim in manifest["claims"]: + if claim["decision"] in {"pending", "pass_unverified"}: + return claim + return manifest["claims"][0] if manifest["claims"] else None + + +def load_appeals() -> list[dict[str, Any]]: + return load_jsonl(APPEALS_JSONL) + + +def is_appealed(claim: dict[str, Any], appeals: list[dict[str, Any]]) -> bool: + fp = claim.get("fingerprint") + if not fp: + return False + for appeal in appeals: + if ( + appeal.get("claimFingerprint") == fp + and appeal.get("reason") in {"stale", "wrong", "too_strict"} + and appeal.get("status", "active") == "active" + ): + return True + return False + + +def save_manifest(manifest: dict[str, Any]) -> None: + ensure_dirs() + receipt_path = RECEIPTS_DIR / f"{manifest['id']}.json" + html_path = RECEIPTS_DIR / f"{manifest['id']}.html" + json_blob = json.dumps(manifest, indent=2) + write_text_atomic(receipt_path, json_blob) + write_text_atomic(LATEST_JSON, json_blob) + rendered = render_receipt_html(manifest) + write_text_atomic(html_path, rendered) + write_text_atomic(LATEST_HTML, rendered) + + +def write_text_atomic(path: Path, content: str) -> None: + ensure_dirs() + tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp") + tmp.write_text(content, encoding="utf-8") + tmp.replace(path) + + +def render_receipt_html(manifest: dict[str, Any]) -> str: + status = html.escape(str(manifest.get("verdictBar", "PASS"))) + summary = html.escape(str(manifest.get("summary", ""))) + claims = [] + for claim in manifest.get("claims", []): + precedents = "".join( + f"
        • {html.escape(str(p.get('summary', p)))}
        • " + for p in claim.get("precedent", []) + ) + claims.append( + "
          " + f"
          {html.escape(str(claim.get('decision')))} / {html.escape(str(claim.get('evidence_state')))}
          " + f"

          {html.escape(str(claim.get('text')))}

          " + f"

          Fix: {html.escape(str(claim.get('fix') or 'No change required.'))}

          " + f"

          Appeal: stale | wrong | too_strict

          " + f"
            {precedents}
          " + "
          " + ) + return f""" +Vestige Veto Receipt + +
          Verdict{status}
          +

          Veto Receipt

          {summary}

          {''.join(claims)} +""" + + +def appeal_latest(reason: str, note: str = "", claim_id: str | None = None) -> dict[str, Any]: + if not LATEST_JSON.exists(): + raise FileNotFoundError(str(LATEST_JSON)) + manifest = json.loads(LATEST_JSON.read_text(encoding="utf-8")) + claims = manifest.get("claims", []) + claim = next((c for c in claims if c.get("id") == claim_id), None) if claim_id else None + if claim is None: + claim = next((c for c in claims if c.get("decision") == "veto"), claims[0] if claims else None) + if claim is None: + raise ValueError("latest receipt has no claims") + appeal = { + "timestamp": now_iso(), + "receiptId": manifest.get("id"), + "claimId": claim.get("id"), + "claimFingerprint": claim.get("fingerprint"), + "claim": claim.get("text"), + "reason": reason, + "note": note, + "status": "active", + } + ensure_dirs() + with APPEALS_JSONL.open("a", encoding="utf-8") as f: + f.write(json.dumps(appeal) + "\n") + claim["appeal"]["status"] = "appealed" + claim["appeal"]["lastReason"] = reason + manifest["overall"] = "appealed" + manifest["verdictBar"] = "APPEALED" + manifest["summary"] = f"Appealed as {reason}." + save_manifest(manifest) + return appeal diff --git a/hooks/settings.fragment.json b/hooks/settings.fragment.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/hooks/settings.fragment.json @@ -0,0 +1 @@ +{} diff --git a/hooks/settings.preflight.fragment.json b/hooks/settings.preflight.fragment.json new file mode 100644 index 0000000..7a0d829 --- /dev/null +++ b/hooks/settings.preflight.fragment.json @@ -0,0 +1,14 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "hooks": [ + { "type": "command", "command": "$HOME/.claude/hooks/synthesis-preflight.sh", "timeout": 8 }, + { "type": "command", "command": "$HOME/.claude/hooks/cwd-state-injector.sh", "timeout": 8 }, + { "type": "command", "command": "$HOME/.claude/hooks/vestige-pulse-daemon.sh", "timeout": 6 }, + { "type": "command", "command": "$HOME/.claude/hooks/preflight-swarm.sh", "timeout": 45 } + ] + } + ] + } +} diff --git a/hooks/settings.sanhedrin.fragment.json b/hooks/settings.sanhedrin.fragment.json new file mode 100644 index 0000000..893d427 --- /dev/null +++ b/hooks/settings.sanhedrin.fragment.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { "type": "command", "command": "$HOME/.claude/hooks/veto-detector.sh", "timeout": 6 }, + { "type": "command", "command": "VESTIGE_SANHEDRIN_ENABLED=1 $HOME/.claude/hooks/sanhedrin.sh", "timeout": 70 }, + { "type": "command", "command": "$HOME/.claude/hooks/synthesis-stop-validator.sh", "timeout": 6 } + ] + } + ] + } +} diff --git a/hooks/synthesis-gate.sh b/hooks/synthesis-gate.sh new file mode 100755 index 0000000..9630f59 --- /dev/null +++ b/hooks/synthesis-gate.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# synthesis-gate.sh — optional UserPromptSubmit hook +# +# Injects a compact synthesis contract for decision-adjacent prompts. The hook +# is intentionally generic and public-safe: it does not depend on private local +# files, personal examples, or hidden project notes. + +set -euo pipefail + +INPUT="$(cat)" +PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt","") or d.get("user_prompt",""))' 2>/dev/null || printf '')" + +DECISION_REGEX='(submit|submission|final|ship|launch|deploy|commit|decide|decision|recommend|should i|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which (should|model|approach|one)|pick|choose|benchmark|competition|perform)' + +if printf '%s' "$PROMPT" | /usr/bin/grep -qiE "$DECISION_REGEX"; then + /usr/bin/python3 <<'PYEOF' +import json + +msg = ( + "[VESTIGE SYNTHESIS GATE]\n\n" + "This prompt appears decision-adjacent. If Vestige is available and relevant, use the smallest retrieval plan that can change the answer.\n\n" + "Reasoning contract:\n" + "1. Retrieve evidence from adjacent topics, not only the exact topic.\n" + "2. Convert each useful memory into: fact -> implication -> action.\n" + "3. Surface contradictions or stale memories before recommending.\n" + "4. Do not list memory summaries as the final answer. Compose them into a concrete recommendation.\n" + "5. If no memory materially changes the answer, say so briefly and proceed from source evidence." +) + +print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": msg + } +})) +PYEOF +fi + +exit 0 diff --git a/hooks/synthesis-preflight.sh b/hooks/synthesis-preflight.sh new file mode 100755 index 0000000..3ae2de6 --- /dev/null +++ b/hooks/synthesis-preflight.sh @@ -0,0 +1,172 @@ +#!/bin/bash +# synthesis-preflight.sh — UserPromptSubmit hook (v2: full content injection) +# +# Injects memory content directly via /api/deep_reference so relevant evidence +# is available before Claude drafts a decision-adjacent response. +# +# On every UserPromptSubmit: +# 1. Read JSON stdin, extract user prompt +# 2. Decision-keyword gate (preserved from v1) +# 3. POST the prompt to vestige-mcp /api/deep_reference (single call) +# — returns recommended memory + reasoning chain + trust-scored evidence +# 4. Inject reasoning + recommended preview + top 3 evidence previews as +# additionalContext, with explicit "DO NOT IGNORE" framing +# +# Fails open: if vestige-mcp is not running or HTTP request fails, hook +# emits empty context and exit 0. Prompt still proceeds. Never blocks. +# +# Endpoint: POST http://127.0.0.1:3927/api/deep_reference +# body: {"query": "", "depth": 15} +# resp: {confidence, evidence:[{id, preview, role, trust, date}], reasoning, recommended} + +set -u + +INPUT="$(cat)" + +# Extract prompt from JSON stdin. Fall back to empty if parse fails. +PROMPT="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("prompt",""))' 2>/dev/null || printf '')" + +if [ -z "$PROMPT" ]; then + exit 0 +fi + +# Decision-keyword gate (preserved from v1). Mirrors synthesis-gate.sh. +DECISION_GATE_RE='submit|submission|benchmark|competition|final|ship|launch|deploy|commit|decide|decision|recommend|should i|should we|what should|purchase|buy|invest|architect|architecture|strategy|prep|prioriti|compose|tradeoff|trade-off|config|which|pick|choose|pitch|forecast|target|plan|roadmap|v2\.|v3\.|scale|grow|growth|distrib|brand|position|moat|vs\.|vs\b|instead of' + +if ! printf '%s' "$PROMPT" | LC_ALL=C /usr/bin/grep -iqE "$DECISION_GATE_RE"; then + exit 0 +fi + +PORT="${VESTIGE_DASHBOARD_PORT:-3927}" +BASE="http://127.0.0.1:${PORT}" + +# Probe dashboard. Fail open if unreachable. +if ! /usr/bin/curl -fsS -m 0.5 "${BASE}/api/health" > /dev/null 2>&1; then + exit 0 +fi + +# Build the deep_reference POST body via python3 (avoids shell-escape issues +# with arbitrary prompt characters). +BODY_SCRIPT="$(mktemp -t vestige-preflight-body.XXXXXX)" +trap 'rm -f "$BODY_SCRIPT"' EXIT +cat > "$BODY_SCRIPT" <<'BODY_PYEOF' +import json, os, sys +prompt = os.environ.get("VPRE_PROMPT", "") +# Truncate very long prompts so the deep_reference embedding stays focused. +# 1500 chars is enough signal for hybrid+semantic retrieval without diluting. +if len(prompt) > 1500: + prompt = prompt[:1500] +print(json.dumps({"query": prompt, "depth": 15})) +BODY_PYEOF + +export VPRE_PROMPT="$PROMPT" +DR_BODY="$(/usr/bin/python3 "$BODY_SCRIPT")" + +# Single POST to deep_reference. Timeout 5s — deep_reference takes ~1-3s. +DR_RESP="$(/usr/bin/curl -fsS -m 5 -X POST "${BASE}/api/deep_reference" \ + -H 'Content-Type: application/json' \ + -d "$DR_BODY" 2>/dev/null || printf '')" + +if [ -z "$DR_RESP" ]; then + exit 0 +fi + +# Compose response into additionalContext block. Inject: +# - confidence +# - reasoning chain (if present) +# - recommended memory id + full preview +# - top 3 evidence with role, trust, preview +COMPOSE_SCRIPT="$(mktemp -t vestige-preflight-compose.XXXXXX)" +trap 'rm -f "$BODY_SCRIPT" "$COMPOSE_SCRIPT"' EXIT +cat > "$COMPOSE_SCRIPT" <<'COMPOSE_PYEOF' +import json, os, sys + +raw = os.environ.get("VPRE_DR_RESP", "") +try: + d = json.loads(raw) +except Exception: + print("") + sys.exit(0) + +if not isinstance(d, dict): + print("") + sys.exit(0) + +confidence = d.get("confidence", 0) +intent = d.get("intent", "") +reasoning = (d.get("reasoning") or "").strip() +recommended = d.get("recommended") or {} +evidence = d.get("evidence") or [] + +# Skip injection if confidence is rock-bottom — likely no relevant memories. +if not evidence and not recommended: + print("") + sys.exit(0) + +out = [] +out.append("[VESTIGE PREFLIGHT — deep_reference auto-injected, DO NOT IGNORE]") +out.append(f"Intent: {intent or 'Synthesis'} | Confidence: {int(confidence*100)}%") +out.append("") + +if reasoning: + out.append("REASONING CHAIN (pre-built by Vestige FSRS-6 trust scoring):") + # Trim to 1200 chars max to keep context budget reasonable + rs = reasoning[:1200] + out.append(rs) + if len(reasoning) > 1200: + out.append(f" ...[reasoning truncated, full chain {len(reasoning)} chars]") + out.append("") + +if recommended: + rec_id = (recommended.get("memory_id") or recommended.get("id") or "")[:8] + rec_trust = recommended.get("trust_score", 0) + rec_date = (recommended.get("date") or "")[:10] + rec_preview = recommended.get("answer_preview") or recommended.get("preview") or "" + out.append(f"RECOMMENDED MEMORY [{rec_id}] trust={rec_trust:.2f} date={rec_date}:") + out.append(rec_preview[:600]) + out.append("") + +if evidence: + out.append(f"TOP {min(len(evidence), 4)} EVIDENCE:") + for e in evidence[:4]: + eid = (e.get("id") or "")[:8] + role = e.get("role", "?") + trust = e.get("trust", 0) + date = (e.get("date") or "")[:10] + preview = (e.get("preview") or "").strip() + out.append(f" [{eid}] role={role} trust={trust:.2f} date={date}") + # 350 chars per evidence preview keeps total injection ~2-3KB + out.append(f" {preview[:350]}") + out.append("") + +out.append("ENFORCEMENT: Compose these into your response, do NOT summarize.") +out.append("Use mcp__vestige__memory(action='get', id=...) to expand any preview.") +out.append("Required shape: (a) Composing: [memories] - logic. (b) Never-composed: [combos|None].") +out.append("(c) Recommendation: the user should DO [concrete action].") + +print("\n".join(out)) +COMPOSE_PYEOF + +export VPRE_DR_RESP="$DR_RESP" +SYNTHESIS="$(/usr/bin/python3 "$COMPOSE_SCRIPT")" + +if [ -z "$SYNTHESIS" ]; then + exit 0 +fi + +# Emit as JSON additionalContext via env var +EMIT_SCRIPT="$(mktemp -t vestige-preflight-emit.XXXXXX)" +trap 'rm -f "$BODY_SCRIPT" "$COMPOSE_SCRIPT" "$EMIT_SCRIPT"' EXIT +cat > "$EMIT_SCRIPT" <<'EMIT_PYEOF' +import json, os +ctx = os.environ.get("VPRE_SYNTHESIS_CTX", "") +print(json.dumps({ + "hookSpecificOutput": { + "hookEventName": "UserPromptSubmit", + "additionalContext": ctx + } +})) +EMIT_PYEOF +export VPRE_SYNTHESIS_CTX="$SYNTHESIS" +/usr/bin/python3 "$EMIT_SCRIPT" +exit 0 diff --git a/hooks/synthesis-stop-validator.sh b/hooks/synthesis-stop-validator.sh new file mode 100755 index 0000000..891f889 --- /dev/null +++ b/hooks/synthesis-stop-validator.sh @@ -0,0 +1,118 @@ +#!/bin/bash +# synthesis-stop-validator.sh — optional Stop hook +# +# Blocks a narrow failure mode: a response that cites multiple memories but +# stops at summary instead of composing them into a decision. This public-safe +# version contains no private examples or local-user paths. + +set -euo pipefail + +INPUT="$(cat)" +TRANSCRIPT_PATH="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("transcript_path",""))' 2>/dev/null || printf '')" + +if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then + exit 0 +fi + +export TRANSCRIPT_PATH +PYFILE=$(mktemp -t vestige-stop-validator.XXXXXX) +trap 'rm -f "$PYFILE"' EXIT +cat > "$PYFILE" <<'PYEOF' +import json, os, re, sys + +transcript = os.environ.get("TRANSCRIPT_PATH", "") +last_user = "" +last_assistant = "" + +try: + with open(transcript) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except Exception: + continue + role = obj.get("role") or obj.get("type", "") + content = obj.get("message", {}).get("content", obj.get("content", "")) + text = "" + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + text += block.get("text", "") + "\n" + elif isinstance(content, str): + text = content + if role == "user": + last_user = text + elif role == "assistant": + last_assistant = text +except Exception: + sys.exit(0) + +decision_re = re.compile( + r"(submit|submission|final|ship|launch|deploy|commit|decide|decision|" + r"recommend|should i|what should|purchase|buy|invest|architect|architecture|" + r"strategy|prep|prioriti|compose|tradeoff|trade-off|config|which " + r"(should|model|approach|one)|pick|choose|benchmark|competition|perform)", + re.IGNORECASE, +) +if not decision_re.search(last_user): + sys.exit(0) + +memory_re = re.compile( + r"(memory|vestige|recall|retriev|saved memor|stored memor|prior memor|" + r"fsrs|trust score|deep_reference|smart_ingest)", + re.IGNORECASE, +) +if not memory_re.search(last_assistant): + sys.exit(0) + +summary_patterns = [ + r"memory\s+[a-f0-9]{4,}", + r"saved memory", + r"according to memory", + r"the memory (says|states|notes|indicates)", + r"per memory", + r"memories? (say|says|state|note|indicate)", +] +summary_hits = 0 +for pat in summary_patterns: + summary_hits += len(re.findall(pat, last_assistant, re.IGNORECASE)) + +composition_re = re.compile( + r"(compos|combin|together|concrete action|recommend(ation)? [:\-]|" + r"never[- ]composed|the synthesis is|therefore|so the action is)", + re.IGNORECASE, +) +composition_hits = len(composition_re.findall(last_assistant)) + +if summary_hits >= 3 and composition_hits == 0: + print("BLOCK_SUMMARY") + sys.exit(0) + +print("PASS") +PYEOF + +RESULT="$(/usr/bin/python3 "$PYFILE")" + +case "$RESULT" in + BLOCK_SUMMARY) + cat >&2 <<'BLOCKMSG' +[STOP BLOCKED — VESTIGE SYNTHESIS VALIDATOR] + +The response cites multiple memories but does not compose them into a decision. +Rewrite it so the retrieved evidence becomes: + +1. Evidence: the memory facts that matter. +2. Implication: what those facts change. +3. Action: the concrete recommendation. + +Do not stop at "Memory A says X, Memory B says Y." Compose the evidence. +BLOCKMSG + exit 2 + ;; + *) + exit 0 + ;; +esac diff --git a/hooks/vestige-pulse-daemon.sh b/hooks/vestige-pulse-daemon.sh new file mode 100755 index 0000000..e2424d3 --- /dev/null +++ b/hooks/vestige-pulse-daemon.sh @@ -0,0 +1,137 @@ +#!/bin/bash +# vestige-pulse-daemon.sh — UserPromptSubmit hook for recent Vestige insights +# +# HOOK #2 of the 2026-04-20 upgrade: v2.2 PULSE AT THE CLAUDE-CODE LAYER. +# +# This hook polls the vestige-mcp event changelog at +# http://127.0.0.1:3927/api/changelog and watches for DreamCompleted or +# ConnectionDiscovered events with meaningful insight payloads. When one fires, +# it prints context to stdout and exits 0. Claude Code injects UserPromptSubmit +# stdout into the next turn's context. +# +# Rate limit: fires at most once per 20 minutes per session to avoid +# interrupting flow state during focused work. +# +# The effect: fresh Vestige dream/connection events can reach Claude before it +# answers the next prompt, without blocking the user or requiring a manual MCP +# call first. +# +# Fails open: if vestige-mcp is not running or the dashboard API is unavailable, +# exits 0 silently. Never blocks Claude. + +set -u + +# State files for rate limiting +STATE_DIR="${VESTIGE_PULSE_STATE_DIR:-/tmp/vestige-pulse-daemon}" +mkdir -p "$STATE_DIR" +LAST_FIRE_FILE="$STATE_DIR/last_fire" +SESSION_ID_FILE="$STATE_DIR/session_id" + +INPUT="$(cat)" +SESSION_ID="$(printf '%s' "$INPUT" | /usr/bin/python3 -c 'import sys,json;d=json.load(sys.stdin);print(d.get("session_id",""))' 2>/dev/null || printf '')" + +# Rate limit: 20 minutes between fires per session +MIN_INTERVAL_SEC=1200 +NOW=$(date +%s) + +if [ -f "$LAST_FIRE_FILE" ]; then + LAST_FIRE=$(cat "$LAST_FIRE_FILE" 2>/dev/null || echo 0) + LAST_SESSION=$(cat "$SESSION_ID_FILE" 2>/dev/null || echo "") + # Only rate-limit within the same session + if [ "$LAST_SESSION" = "$SESSION_ID" ] && [ $((NOW - LAST_FIRE)) -lt $MIN_INTERVAL_SEC ]; then + exit 0 + fi +fi + +PORT="${VESTIGE_DASHBOARD_PORT:-3927}" + +# Probe health before polling the changelog +if ! /usr/bin/curl -fsS -m 0.5 "http://127.0.0.1:${PORT}/api/health" > /dev/null 2>&1; then + exit 0 +fi + +# Check recent events via the REST changelog API for DreamCompleted in the +# last 15 minutes. This is simpler than a full WebSocket subscription and +# works with UserPromptSubmit semantics (inject once per prompt, not persistent). +# If a DreamCompleted event with insights_generated > 0 is found, inject context. +RESULT="$(/usr/bin/curl -fsS -m 2 \ + "http://127.0.0.1:${PORT}/api/changelog?start=$(date -u -v-15M +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d '15 minutes ago' +%Y-%m-%dT%H:%M:%SZ)&limit=50" \ + 2>/dev/null || printf '')" + +if [ -z "$RESULT" ]; then + exit 0 +fi + +INSIGHT="$(VESTIGE_CHANGELOG_JSON="$RESULT" /usr/bin/python3 <<'PYEOF' +import json, os, sys + + +def as_int(value): + try: + return int(value) + except (TypeError, ValueError): + return 0 + +try: + data = json.loads(os.environ.get("VESTIGE_CHANGELOG_JSON", "")) +except Exception: + sys.exit(0) + +if not isinstance(data, dict): + sys.exit(0) + +events = data.get("events", []) or [] + +# Find the most recent DreamCompleted with insights_generated > 0 +# OR ConnectionDiscovered with a meaningful target +for ev in events: + if not isinstance(ev, dict): + continue + etype = ev.get("type", "") + payload = ev.get("data") + if not isinstance(payload, dict): + payload = ev + + if etype in ("DreamCompleted", "dream", "consolidation"): + insights = as_int(payload.get("insights_generated") or payload.get("insightsGenerated")) + if insights > 0: + stats = payload.get("stats") or {} + connections = as_int( + payload.get("connections_persisted") + or payload.get("connectionsPersisted") + or payload.get("connections_found") + or payload.get("connectionsFound") + or payload.get("connectionFound") + or stats.get("connections") + ) + print(f"DREAM: {insights} insights, {connections} new connections. Dream cycle completed while you were working. Consider calling mcp__vestige__dream(memory_count=50) to inspect the fresh cluster bridges; or mcp__vestige__explore_connections(action='bridges') on the latest activity.") + sys.exit(0) + if etype in ("ConnectionDiscovered", "connection"): + src = str(payload.get("source_id") or payload.get("sourceId") or payload.get("source") or "")[:8] + tgt = str(payload.get("target_id") or payload.get("targetId") or payload.get("target") or "")[:8] + if src and tgt: + print(f"CONNECTION: Vestige discovered a new edge [{src}] <-> [{tgt}] while you were working. Spreading activation surfaced a bridge you had not queried. Inspect via mcp__vestige__explore_connections(action='bridges', from='{src}', to='{tgt}').") + sys.exit(0) +PYEOF +)" + +if [ -z "$INSIGHT" ]; then + exit 0 +fi + +# Update rate-limit state +echo "$NOW" > "$LAST_FIRE_FILE" +echo "$SESSION_ID" > "$SESSION_ID_FILE" + +# UserPromptSubmit stdout is injected into Claude's context. Do not use exit 2 +# here: Claude Code treats that as a blocking prompt validation failure. +cat </dev/null || printf '')" + +if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then + exit 0 +fi + +PORT="${VESTIGE_DASHBOARD_PORT:-3927}" +if ! /usr/bin/curl -fsS -m 0.5 "http://127.0.0.1:${PORT}/api/health" > /dev/null 2>&1; then + exit 0 +fi + +# Fetch memories tagged veto-pattern. The /api/memories?tag= filter +# returns exactly the tag-matching rows (verified 2026-04-20). Keyword +# search (/api/memories?q=...) is semantic so it misses literal "VETO" hits. +VETO_JSON="$(/usr/bin/curl -fsS -m 2 "http://127.0.0.1:${PORT}/api/memories?tag=veto-pattern&limit=50" 2>/dev/null || printf '')" + +if [ -z "$VETO_JSON" ]; then + exit 0 +fi + +export TRANSCRIPT_PATH +export VETO_JSON + +VETO_SCRIPT="$(mktemp -t vestige-veto.XXXXXX)" +trap 'rm -f "$VETO_SCRIPT"' EXIT +cat > "$VETO_SCRIPT" <<'VETO_PYEOF' +import json, os, re, sys + +transcript = os.environ.get("TRANSCRIPT_PATH", "") +veto_json = os.environ.get("VETO_JSON", "") + +if not transcript or not veto_json: + sys.exit(0) + +# Parse veto memories. Filter to those tagged veto-pattern, deprecated-pattern, +# or suppressed, and extract a VETO_TRIGGER phrase from the content if present. +try: + vdata = json.loads(veto_json) +except Exception: + sys.exit(0) + +veto_memories = [] +for m in vdata.get("memories", []) or []: + tags = set((m.get("tags") or [])) + if not (tags & {"veto-pattern", "deprecated-pattern", "suppressed"}): + continue + content = m.get("content") or "" + # Look for a "VETO_TRIGGER:" or "TRIGGER PHRASE:" line + triggers = re.findall(r"(?:VETO_TRIGGER|TRIGGER PHRASE|TRIGGER):\s*(.+?)(?:\n|$)", content) + for t in triggers: + t = t.strip().strip("`\"' ") + if t and len(t) >= 3: + veto_memories.append({ + "id": m.get("id", "?"), + "trigger": t, + "content": content[:300], + "retention": m.get("retentionStrength", 0), + }) + +if not veto_memories: + sys.exit(0) + +# Read last assistant message from transcript JSONL +last_assistant = "" +try: + with open(transcript) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + obj = json.loads(line) + except Exception: + continue + role = obj.get("role") or obj.get("type", "") + content = obj.get("message", {}).get("content", obj.get("content", "")) + text = "" + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "text": + text += block.get("text", "") + "\n" + elif isinstance(content, str): + text = content + if role == "assistant": + last_assistant = text +except Exception: + sys.exit(0) + +if not last_assistant: + sys.exit(0) + +# Check each veto trigger against the assistant draft. Only treat high-retention +# memories (>= 0.5) as load-bearing to avoid false positives on decayed content. +hits = [] +for v in veto_memories: + if v["retention"] < 0.5: + continue + trig = v["trigger"] + # Case-insensitive substring match with word-boundary preference + if re.search(r"(?i)" + re.escape(trig), last_assistant): + hits.append(v) + +if not hits: + sys.exit(0) + +# Emit the VESTIGE VETO message. Newest/highest-retention hit leads. +hits.sort(key=lambda x: x["retention"], reverse=True) +top = hits[0] +nid = top["id"][:8] if len(top["id"]) >= 8 else top["id"] +trigger = top["trigger"] +retention_pct = int(top["retention"] * 100) + +print(f"VETO_HIT:{nid}:{trigger}:{retention_pct}") +VETO_PYEOF + +RESULT="$(/usr/bin/python3 "$VETO_SCRIPT" 2>/dev/null || printf '')" + +if [ -z "$RESULT" ]; then + exit 0 +fi + +# Parse the result +NODE_ID="$(printf '%s' "$RESULT" | /usr/bin/awk -F: '{print $2}')" +TRIGGER="$(printf '%s' "$RESULT" | /usr/bin/awk -F: '{print $3}')" +RETENTION="$(printf '%s' "$RESULT" | /usr/bin/awk -F: '{print $4}')" + +cat >&2 < + + + + Label + com.vestige.mlx-server + + ProgramArguments + + __HOME__/.local/bin/mlx_lm.server + --model + __MODEL__ + --host + 127.0.0.1 + --port + 8080 + --log-level + INFO + + + RunAtLoad + + + KeepAlive + + + ThrottleInterval + 10 + + WorkingDirectory + __HOME__ + + EnvironmentVariables + + PATH + __HOME__/.local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin + HOME + __HOME__ + + + StandardOutPath + __HOME__/Library/Logs/vestige-mlx-server.out.log + + StandardErrorPath + __HOME__/Library/Logs/vestige-mlx-server.err.log + + diff --git a/package.json b/package.json index 8c66e67..d72a5ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vestige", - "version": "2.0.1", + "version": "2.1.22", "private": true, "description": "Cognitive memory for AI - MCP server with FSRS-6 spaced repetition", "author": "Sam Valladares", diff --git a/packages/vestige-init/bin/init.js b/packages/vestige-init/bin/init.js index 9bd0a74..d7c5da2 100755 --- a/packages/vestige-init/bin/init.js +++ b/packages/vestige-init/bin/init.js @@ -5,15 +5,16 @@ const path = require('path'); const os = require('os'); const { execSync } = require('child_process'); +const PACKAGE_VERSION = require('../package.json').version; const HOME = os.homedir(); const PLATFORM = os.platform(); // ─── Branding ─────────────────────────────────────────────────────────────── const BANNER = ` - vestige init v2.0 - Give your AI a brain in 10 seconds. - Now with 3D dashboard at localhost:3927/dashboard + vestige init v${PACKAGE_VERSION} + Configure local Vestige memory for MCP-compatible agents. + Dashboard: localhost:3927/dashboard `; // ─── IDE Definitions ──────────────────────────────────────────────────────── @@ -172,11 +173,64 @@ function findBinary() { return null; } +function stripJsonComments(input) { + let output = ''; + let inString = false; + let escaped = false; + + for (let i = 0; i < input.length; i++) { + const current = input[i]; + const next = input[i + 1]; + + if (inString) { + output += current; + if (escaped) { + escaped = false; + } else if (current === '\\') { + escaped = true; + } else if (current === '"') { + inString = false; + } + continue; + } + + if (current === '"') { + inString = true; + output += current; + continue; + } + + if (current === '/' && next === '/') { + while (i < input.length && input[i] !== '\n') i++; + output += '\n'; + continue; + } + + if (current === '/' && next === '*') { + i += 2; + while (i < input.length && !(input[i] === '*' && input[i + 1] === '/')) i++; + i++; + continue; + } + + output += current; + } + + return output; +} + +function removeTrailingCommas(input) { + return input.replace(/,\s*([}\]])/g, '$1'); +} + function readJsonSafe(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); - return JSON.parse(content); - } catch { + return JSON.parse(removeTrailingCommas(stripJsonComments(content))); + } catch (err) { + if (fs.existsSync(filePath)) { + throw new Error(`Could not parse ${filePath}: ${err.message}`); + } return null; } } @@ -187,6 +241,29 @@ function ensureDir(filePath) { fs.mkdirSync(dir, { recursive: true }); } +function backupFile(filePath) { + if (!fs.existsSync(filePath)) return null; + const stamp = new Date().toISOString().replace(/[:.]/g, '-'); + const backupPath = `${filePath}.bak.${stamp}`; + fs.copyFileSync(filePath, backupPath); + try { + fs.chmodSync(backupPath, 0o600); + } catch {} + return backupPath; +} + +function writeJsonAtomic(filePath, value) { + ensureDir(filePath); + const backupPath = backupFile(filePath); + const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`; + fs.writeFileSync(tempPath, JSON.stringify(value, null, 2) + '\n', { mode: 0o600 }); + fs.renameSync(tempPath, filePath); + try { + fs.chmodSync(filePath, 0o600); + } catch {} + return backupPath; +} + function buildVestigeConfig(binaryPath) { return { command: binaryPath, @@ -209,7 +286,6 @@ function buildXcodeConfig(binaryPath) { }, }, }, - hasTrustDialogAccepted: true, }, }, }; @@ -239,7 +315,6 @@ function injectConfig(ide, ideName, binaryPath) { if (!config.projects['*']) config.projects['*'] = {}; if (!config.projects['*'].mcpServers) config.projects['*'].mcpServers = {}; config.projects['*'].mcpServers.vestige = xcodeConfig.projects['*'].mcpServers.vestige; - config.projects['*'].hasTrustDialogAccepted = true; } else if (ide.format === 'vscode') { // VS Code uses "mcp" key in settings.json with "servers" subkey if (!config.mcp) config.mcp = {}; @@ -260,7 +335,10 @@ function injectConfig(ide, ideName, binaryPath) { config[key].vestige = buildVestigeConfig(binaryPath); } - fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n'); + const backupPath = writeJsonAtomic(configPath, config); + if (backupPath) { + console.log(` [backup] ${path.basename(backupPath)}`); + } return true; } @@ -279,12 +357,7 @@ function main() { console.log(''); console.log('Install manually:'); console.log(''); - console.log(' # macOS (Apple Silicon)'); - console.log(' curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-aarch64-apple-darwin.tar.gz | tar -xz'); - console.log(' sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/'); - console.log(''); - console.log(' # Or via npm'); - console.log(' npm install -g vestige-mcp-server'); + console.log(' npm install -g vestige-mcp-server@latest'); console.log(''); console.log('Then run: npx @vestige/init'); process.exit(1); diff --git a/packages/vestige-init/package.json b/packages/vestige-init/package.json index be2c337..1c6ac04 100644 --- a/packages/vestige-init/package.json +++ b/packages/vestige-init/package.json @@ -1,7 +1,7 @@ { "name": "@vestige/init", - "version": "2.0.7", - "description": "Give your AI a brain in 10 seconds — zero-config Vestige v2.0 installer with 3D dashboard", + "version": "2.1.22", + "description": "Configure Vestige local memory for MCP-compatible AI agents", "bin": { "vestige-init": "bin/init.js" }, diff --git a/packages/vestige-mcp-npm/.npmignore b/packages/vestige-mcp-npm/.npmignore index d42ab8b..aa993ff 100644 --- a/packages/vestige-mcp-npm/.npmignore +++ b/packages/vestige-mcp-npm/.npmignore @@ -3,6 +3,8 @@ bin/vestige bin/vestige.exe bin/vestige-mcp bin/vestige-mcp.exe +bin/vestige-restore +bin/vestige-restore.exe bin/*.tar.gz bin/*.zip diff --git a/packages/vestige-mcp-npm/README.md b/packages/vestige-mcp-npm/README.md index f0d6ee7..98e6575 100644 --- a/packages/vestige-mcp-npm/README.md +++ b/packages/vestige-mcp-npm/README.md @@ -12,12 +12,23 @@ npm install -g vestige-mcp-server This automatically downloads the correct binary for your platform (macOS, Linux, Windows) from GitHub releases. +Already installed? Update without copying release URLs: + +```bash +vestige update +``` + +This refreshes the binaries only. Optional Claude Code Cognitive Sandwich +companion files are refreshed with `vestige update --sandwich-companion` or +`vestige sandwich install`. + ### What gets installed | Command | Description | |---------|-------------| -| `vestige-mcp` | MCP server for Claude integration | +| `vestige-mcp` | MCP server for local agent memory | | `vestige` | CLI for stats, health checks, and maintenance | +| `vestige-restore` | Restore helper for backup recovery | ### Verify installation @@ -25,13 +36,23 @@ This automatically downloads the correct binary for your platform (macOS, Linux, vestige health ``` -## Usage with Claude Code +## Usage with MCP Clients + +Vestige works with any client that can register a stdio MCP server. + +**Claude Code** ```bash claude mcp add vestige vestige-mcp -s user ``` -Then restart Claude. +**Codex** + +```bash +codex mcp add vestige -- vestige-mcp +``` + +Then restart your MCP client. ## Usage with Claude Desktop @@ -57,6 +78,9 @@ vestige stats # Memory statistics vestige stats --states # Cognitive state distribution vestige health # System health check vestige consolidate # Run memory maintenance cycle +vestige update # Update binaries +vestige update --sandwich-companion # Also refresh optional Claude Code files +vestige sandwich install # Manage optional Claude Code hook files ``` ## Features @@ -81,7 +105,7 @@ You'll never run out of space. A heavy user creating 100 memories/day would use On first use, Vestige downloads the nomic-embed-text-v1.5 model (~130MB). This is a one-time download and all subsequent operations are fully offline. -The model is stored in `.fastembed_cache/` in your working directory, or you can set a global location: +The model is stored in Vestige's OS cache directory, or you can set a global location: ```bash export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache" @@ -91,9 +115,13 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache" | Variable | Description | Default | |----------|-------------|---------| -| `VESTIGE_DATA_DIR` | Data storage directory | `~/.vestige` | -| `VESTIGE_LOG_LEVEL` | Log verbosity | `info` | -| `FASTEMBED_CACHE_PATH` | Embeddings model location | `./.fastembed_cache` | +| `RUST_LOG` | Log verbosity + per-module filter | `info` | +| `FASTEMBED_CACHE_PATH` | Embeddings model cache override | OS cache dir | +| `VESTIGE_DATA_DIR` | Storage directory fallback; database lives at `/vestige.db` | OS data dir | +| `VESTIGE_DASHBOARD_PORT` | Dashboard port | `3927` | +| `VESTIGE_AUTH_TOKEN` | Bearer auth for dashboard + HTTP MCP | auto-generated | + +Storage precedence is `--data-dir `, then `VESTIGE_DATA_DIR`, then your OS's per-user data directory. ## Troubleshooting @@ -101,7 +129,7 @@ export FASTEMBED_CACHE_PATH="$HOME/.fastembed_cache" 1. Verify binary exists: `which vestige-mcp` 2. Test directly: `vestige-mcp` (should wait for stdio input) -3. Check Claude logs: `~/Library/Logs/Claude/` (macOS) +3. Check your MCP client's server logs. ### "vestige: command not found" @@ -112,7 +140,9 @@ npm install -g vestige-mcp-server ### Embeddings not downloading -The model downloads on first `ingest` or `search` operation. If Claude can't connect to the MCP server, no memory operations happen and no model downloads. +The model downloads on first memory ingest or search operation. If your MCP +client cannot connect to the MCP server, no memory operations happen and no +model downloads. Fix the MCP connection first, then the model will download automatically. diff --git a/packages/vestige-mcp-npm/bin/vestige-restore.js b/packages/vestige-mcp-npm/bin/vestige-restore.js new file mode 100755 index 0000000..1d00b17 --- /dev/null +++ b/packages/vestige-mcp-npm/bin/vestige-restore.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node + +const { spawn } = require('child_process'); +const path = require('path'); +const fs = require('fs'); +const os = require('os'); + +const platform = os.platform(); +const binaryName = platform === 'win32' ? 'vestige-restore.exe' : 'vestige-restore'; +const binaryPath = path.join(__dirname, binaryName); + +if (!fs.existsSync(binaryPath)) { + console.error('Error: vestige-restore binary not found.'); + console.error(`Expected at: ${binaryPath}`); + console.error(''); + console.error('Try reinstalling: npm install -g vestige-mcp-server'); + process.exit(1); +} + +const child = spawn(binaryPath, process.argv.slice(2), { + stdio: 'inherit', +}); + +child.on('error', (err) => { + console.error('Failed to start vestige-restore:', err.message); + process.exit(1); +}); + +child.on('exit', (code) => { + process.exit(code ?? 0); +}); diff --git a/packages/vestige-mcp-npm/package.json b/packages/vestige-mcp-npm/package.json index 9310fac..f51b5e1 100644 --- a/packages/vestige-mcp-npm/package.json +++ b/packages/vestige-mcp-npm/package.json @@ -1,10 +1,12 @@ { "name": "vestige-mcp-server", - "version": "2.0.7", - "description": "Vestige MCP Server — Cognitive memory for AI with FSRS-6, 3D dashboard, and 29 brain modules", + "version": "2.1.22", + "mcpName": "io.github.samvallad33/vestige", + "description": "Vestige MCP Server — local cognitive memory for MCP-compatible AI agents", "bin": { "vestige-mcp": "bin/vestige-mcp.js", - "vestige": "bin/vestige.js" + "vestige": "bin/vestige.js", + "vestige-restore": "bin/vestige-restore.js" }, "scripts": { "postinstall": "node scripts/postinstall.js" diff --git a/packages/vestige-mcp-npm/scripts/postinstall.js b/packages/vestige-mcp-npm/scripts/postinstall.js index c6e11ec..65fe54b 100644 --- a/packages/vestige-mcp-npm/scripts/postinstall.js +++ b/packages/vestige-mcp-npm/scripts/postinstall.js @@ -4,10 +4,12 @@ const https = require('https'); const fs = require('fs'); const path = require('path'); const os = require('os'); -const { execSync } = require('child_process'); +const crypto = require('crypto'); +const { execFileSync } = require('child_process'); -const VERSION = require('../package.json').version; -const BINARY_VERSION = '2.0.1'; // GitHub release version for binaries +const packageJson = require('../package.json'); +const VERSION = packageJson.version; +const BINARY_VERSION = VERSION; const PLATFORM = os.platform(); const ARCH = os.arch(); @@ -27,11 +29,26 @@ const archStr = ARCH_MAP[ARCH]; if (!platformStr || !archStr) { console.error(`Unsupported platform: ${PLATFORM}-${ARCH}`); - console.error('Supported: darwin/linux/win32 on x64/arm64'); + console.error('Supported release assets: macOS x64/arm64, Linux x64, Windows x64'); process.exit(1); } const target = `${archStr}-${platformStr}`; +const SUPPORTED_TARGETS = new Set([ + 'aarch64-apple-darwin', + 'x86_64-apple-darwin', + 'x86_64-unknown-linux-gnu', + 'x86_64-pc-windows-msvc', +]); +if (!SUPPORTED_TARGETS.has(target)) { + console.error(`Unsupported Vestige release target: ${target}`); + console.error('Supported release assets:'); + for (const supported of SUPPORTED_TARGETS) { + console.error(` - ${supported}`); + } + process.exit(1); +} + const isWindows = PLATFORM === 'win32'; const archiveExt = isWindows ? 'zip' : 'tar.gz'; const archiveName = `vestige-mcp-${target}.${archiveExt}`; @@ -39,6 +56,25 @@ const downloadUrl = `https://github.com/samvallad33/vestige/releases/download/v$ const targetDir = path.join(__dirname, '..', 'bin'); const archivePath = path.join(targetDir, archiveName); +const checksumPath = path.join(targetDir, `${archiveName}.sha256`); +const expectedArchiveMembers = new Set( + ['vestige-mcp', 'vestige', 'vestige-restore'].map((name) => (isWindows ? `${name}.exe` : name)) +); + +function isWorkspaceCheckout() { + const packageRoot = path.resolve(__dirname, '..'); + const repoRoot = path.resolve(packageRoot, '..', '..'); + return ( + path.basename(packageRoot) === 'vestige-mcp-npm' && + path.basename(path.dirname(packageRoot)) === 'packages' && + fs.existsSync(path.join(repoRoot, 'pnpm-workspace.yaml')) + ); +} + +if (process.env.VESTIGE_SKIP_BINARY_DOWNLOAD === '1' || isWorkspaceCheckout()) { + console.log('Skipping Vestige binary download in local workspace checkout.'); + process.exit(0); +} console.log(`Installing Vestige MCP v${VERSION} for ${target}...`); @@ -91,15 +127,65 @@ function download(url, dest) { * Extract archive based on platform */ function extract(archivePath, destDir) { + validateArchiveEntries(archivePath); if (isWindows) { // Use PowerShell to extract zip on Windows - execSync( - `powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${destDir}' -Force"`, + execFileSync( + 'powershell', + [ + '-NoProfile', + '-Command', + `Expand-Archive -LiteralPath ${powershellQuote(archivePath)} -DestinationPath ${powershellQuote(destDir)} -Force`, + ], { stdio: 'inherit' } ); } else { // Use tar on Unix - execSync(`tar -xzf "${archivePath}" -C "${destDir}"`, { stdio: 'inherit' }); + execFileSync('tar', ['-xzf', archivePath, '-C', destDir], { stdio: 'inherit' }); + } +} + +function powershellQuote(value) { + return `'${String(value).replace(/'/g, "''")}'`; +} + +function listArchiveEntries(archivePath) { + if (!isWindows) { + return execFileSync('tar', ['-tzf', archivePath], { encoding: 'utf8' }); + } + + const script = [ + 'Add-Type -AssemblyName System.IO.Compression.FileSystem;', + `$zip = [System.IO.Compression.ZipFile]::OpenRead(${powershellQuote(archivePath)});`, + 'try { $zip.Entries | ForEach-Object { $_.FullName } } finally { $zip.Dispose() }', + ].join(' '); + return execFileSync('powershell', ['-NoProfile', '-Command', script], { encoding: 'utf8' }); +} + +function normalizeArchiveEntry(entry) { + let normalized = entry.replace(/\\/g, '/').replace(/^\.\//, ''); + if ( + !normalized || + normalized.startsWith('/') || + /^[A-Za-z]:/.test(normalized) || + normalized.split('/').some((part) => part === '' || part === '..') + ) { + throw new Error(`Unsafe archive entry: ${entry}`); + } + return normalized; +} + +function validateArchiveEntries(archivePath) { + const entries = listArchiveEntries(archivePath) + .split(/\r?\n/) + .map((entry) => entry.trim()) + .filter(Boolean); + + for (const entry of entries) { + const normalized = normalizeArchiveEntry(entry); + if (!expectedArchiveMembers.has(normalized)) { + throw new Error(`Unexpected archive entry: ${entry}`); + } } } @@ -118,11 +204,26 @@ function makeExecutable(binDir) { } } +function verifyChecksum(archivePath, checksumPath) { + const checksumText = fs.readFileSync(checksumPath, 'utf8').trim(); + const expected = checksumText.split(/\s+/)[0]?.toLowerCase(); + if (!expected || !/^[a-f0-9]{64}$/.test(expected)) { + throw new Error(`Invalid checksum file for ${archiveName}`); + } + + const actual = crypto.createHash('sha256').update(fs.readFileSync(archivePath)).digest('hex'); + if (actual !== expected) { + throw new Error(`Checksum mismatch for ${archiveName}`); + } +} + async function main() { try { // Download console.log(`Downloading from ${downloadUrl}...`); await download(downloadUrl, archivePath); + await download(`${downloadUrl}.sha256`, checksumPath); + verifyChecksum(archivePath, checksumPath); console.log('Download complete.'); // Extract @@ -131,6 +232,7 @@ async function main() { // Cleanup archive fs.unlinkSync(archivePath); + fs.unlinkSync(checksumPath); // Make executable makeExecutable(targetDir); @@ -153,9 +255,11 @@ async function main() { } console.log(''); console.log('Next steps:'); - console.log(' 1. Add to Claude: claude mcp add vestige vestige-mcp -s user'); - console.log(' 2. Restart Claude'); - console.log(' 3. Test with: "remember that my favorite color is blue"'); + console.log(' 1. Add vestige-mcp to any MCP-compatible agent.'); + console.log(' Claude Code: claude mcp add vestige vestige-mcp -s user'); + console.log(' Codex: codex mcp add vestige -- vestige-mcp'); + console.log(' 2. Restart your MCP client.'); + console.log(' 3. Test with: "remember that my preferred editor is VS Code"'); console.log(''); } catch (err) { diff --git a/packages/vestige-mcpb/README.md b/packages/vestige-mcpb/README.md index 53192a5..595dd58 100644 --- a/packages/vestige-mcpb/README.md +++ b/packages/vestige-mcpb/README.md @@ -4,7 +4,7 @@ One-click installation bundle for Claude Desktop. ## For Users -1. Download `vestige-1.1.0.mcpb` from [GitHub Releases](https://github.com/samvallad33/vestige/releases) +1. Download `vestige-2.1.22.mcpb` from [GitHub Releases](https://github.com/samvallad33/vestige/releases) 2. Double-click to install 3. Restart Claude Desktop @@ -34,5 +34,5 @@ vestige-mcpb/ │ ├── vestige-mcp-darwin-arm64 │ ├── vestige-mcp-linux-x64 │ └── vestige-mcp-win32-x64.exe -└── vestige-1.1.0.mcpb # Final bundle (generated) +└── vestige-2.1.22.mcpb # Final bundle (generated) ``` diff --git a/packages/vestige-mcpb/build.sh b/packages/vestige-mcpb/build.sh index 11b019b..cf48da8 100755 --- a/packages/vestige-mcpb/build.sh +++ b/packages/vestige-mcpb/build.sh @@ -1,33 +1,101 @@ #!/bin/bash -set -e +set -euo pipefail -VERSION="${1:-1.1.0}" +VERSION="${1:-2.1.22}" REPO="samvallad33/vestige" +TMPDIR="$(mktemp -d)" +trap 'rm -rf "$TMPDIR"' EXIT echo "Building Vestige MCPB v${VERSION}..." # Create server directory mkdir -p server +die() { + echo "error: $*" >&2 + exit 1 +} + +verify_checksum() { + local archive="$1" + local checksum="$2" + local expected actual + expected="$(awk '{print tolower($1)}' "$checksum")" + case "$expected" in + [0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; + *) die "invalid checksum file: $checksum" ;; + esac + [ "${#expected}" -eq 64 ] || die "invalid checksum length for $archive" + actual="$(shasum -a 256 "$archive" | awk '{print tolower($1)}')" + [ "$actual" = "$expected" ] || die "checksum mismatch for $(basename "$archive")" +} + +validate_member() { + local member="${1#./}" + shift + case "$member" in + ""|/*|../*|*/../*|*"/.."|*":"*) die "unsafe archive member: $member" ;; + esac + for expected in "$@"; do + [ "$member" = "$expected" ] && return 0 + done + die "unexpected archive member: $member" +} + +validate_tar() { + local archive="$1" + shift + while IFS= read -r member; do + [ -n "$member" ] || continue + validate_member "$member" "$@" + done < <(tar -tzf "$archive") +} + +validate_zip() { + local archive="$1" + shift + while IFS= read -r member; do + [ -n "$member" ] || continue + validate_member "$member" "$@" + done < <(unzip -Z1 "$archive") +} + +download_release_asset() { + local name="$1" + local archive="$TMPDIR/$name" + local checksum="$TMPDIR/$name.sha256" + curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/${name}" -o "$archive" + curl -fsSL "https://github.com/${REPO}/releases/download/v${VERSION}/${name}.sha256" -o "$checksum" + verify_checksum "$archive" "$checksum" + printf '%s\n' "$archive" +} + # Download macOS ARM64 echo "Downloading macOS ARM64 binary..." -curl -sL "https://github.com/${REPO}/releases/download/v${VERSION}/vestige-mcp-aarch64-apple-darwin.tar.gz" | tar -xz -C server +ARCHIVE="$(download_release_asset "vestige-mcp-aarch64-apple-darwin.tar.gz")" +validate_tar "$ARCHIVE" vestige-mcp vestige vestige-restore +tar -xzf "$ARCHIVE" -C server mv server/vestige-mcp server/vestige-mcp-darwin-arm64 mv server/vestige server/vestige-darwin-arm64 +rm -f server/vestige-restore # Download Linux x64 echo "Downloading Linux x64 binary..." -curl -sL "https://github.com/${REPO}/releases/download/v${VERSION}/vestige-mcp-x86_64-unknown-linux-gnu.tar.gz" | tar -xz -C server +ARCHIVE="$(download_release_asset "vestige-mcp-x86_64-unknown-linux-gnu.tar.gz")" +validate_tar "$ARCHIVE" vestige-mcp vestige vestige-restore +tar -xzf "$ARCHIVE" -C server mv server/vestige-mcp server/vestige-mcp-linux-x64 mv server/vestige server/vestige-linux-x64 +rm -f server/vestige-restore # Download Windows x64 echo "Downloading Windows x64 binary..." -curl -sL "https://github.com/${REPO}/releases/download/v${VERSION}/vestige-mcp-x86_64-pc-windows-msvc.zip" -o /tmp/win.zip -unzip -q /tmp/win.zip -d server +ARCHIVE="$(download_release_asset "vestige-mcp-x86_64-pc-windows-msvc.zip")" +validate_zip "$ARCHIVE" vestige-mcp.exe vestige.exe vestige-restore.exe +unzip -q "$ARCHIVE" -d server mv server/vestige-mcp.exe server/vestige-mcp-win32-x64.exe mv server/vestige.exe server/vestige-win32-x64.exe -rm /tmp/win.zip +rm -f server/vestige-restore.exe # Make executable chmod +x server/* diff --git a/packages/vestige-mcpb/manifest.json b/packages/vestige-mcpb/manifest.json index 34e85b7..690302d 100644 --- a/packages/vestige-mcpb/manifest.json +++ b/packages/vestige-mcpb/manifest.json @@ -2,7 +2,7 @@ "manifest_version": "0.2", "name": "vestige", "display_name": "Vestige", - "version": "1.5.0", + "version": "2.1.22", "description": "AI memory system built on 130 years of cognitive science. FSRS-6 spaced repetition, synaptic tagging, and local-first storage.", "author": { "name": "Sam Valladares", @@ -12,7 +12,7 @@ "type": "git", "url": "https://github.com/samvallad33/vestige" }, - "license": "MIT", + "license": "AGPL-3.0-only", "server": { "type": "binary", "entry_point": "server/vestige-mcp-darwin-arm64", diff --git a/scripts/check-sandwich-prereqs.sh b/scripts/check-sandwich-prereqs.sh new file mode 100755 index 0000000..a3a0691 --- /dev/null +++ b/scripts/check-sandwich-prereqs.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# check-sandwich-prereqs.sh — Verify host can run the Vestige Cognitive Sandwich. +set -u + +ok() { printf ' \033[1;32m[ OK ]\033[0m %s\n' "$*"; } +warn() { printf ' \033[1;33m[WARN]\033[0m %s\n' "$*"; FAIL=1; } +miss() { printf ' \033[1;31m[MISS]\033[0m %s\n' "$*"; FAIL=1; } +info() { printf ' \033[1;36m[INFO]\033[0m %s\n' "$*"; } + +load_vestige_sanhedrin_env() { + [ -f "$1" ] || return 0 + command -v python3 >/dev/null 2>&1 || return 0 + while IFS="$(printf '\t')" read -r key value; do + case "$key" in + VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_DASHBOARD_PORT) + export "$key=$value" + ;; + esac + done < <(python3 - "$1" <<'PY' +import shlex +import sys + +allowed = { + "VESTIGE_SANHEDRIN_ENABLED", + "VESTIGE_SANHEDRIN_MODEL", + "VESTIGE_SANHEDRIN_ENDPOINT", + "VESTIGE_SANHEDRIN_CLAIM_MODE", + "VESTIGE_SANHEDRIN_OUTPUT", + "VESTIGE_SANHEDRIN_PYTHON", + "VESTIGE_DASHBOARD_PORT", +} + +try: + lines = open(sys.argv[1], encoding="utf-8").read().splitlines() +except OSError: + sys.exit(0) + +for raw in lines: + line = raw.strip() + if not line or line.startswith("#"): + continue + try: + parts = shlex.split(line, posix=True) + except ValueError: + continue + if len(parts) != 1 or "=" not in parts[0]: + continue + key, value = parts[0].split("=", 1) + if key in allowed and "\t" not in value and "\0" not in value: + print(f"{key}\t{value}") +PY + ) +} + +FAIL=0 +CHECK_PREFLIGHT=0 +CHECK_SANHEDRIN=0 +DASHBOARD_PORT="${VESTIGE_DASHBOARD_PORT:-3927}" +SANHEDRIN_ENV="${VESTIGE_SANHEDRIN_ENV:-$HOME/.claude/hooks/vestige-sanhedrin.env}" + +for arg in "$@"; do + case "$arg" in + --preflight|--enable-preflight) CHECK_PREFLIGHT=1 ;; + --sanhedrin|--enable-sanhedrin) CHECK_SANHEDRIN=1 ;; + -h|--help) + cat <<'EOF' +Usage: scripts/check-sandwich-prereqs.sh [--preflight] [--sanhedrin] + +Without flags, verifies that the default install has no Vestige hooks wired. +With --preflight, checks the optional UserPromptSubmit hook layer. +With --sanhedrin, checks the optional OpenAI-compatible verifier endpoint. +EOF + exit 0 + ;; + esac +done + +if [ -f "$SANHEDRIN_ENV" ]; then + load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || true +fi + +SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}}" +SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" +SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" +SANHEDRIN_CLAIM_MODE="${VESTIGE_SANHEDRIN_CLAIM_MODE:-0}" +SANHEDRIN_OUTPUT="${VESTIGE_SANHEDRIN_OUTPUT:-text}" + +echo "Vestige Cognitive Sandwich — Prereq Check" +echo + +# Platform +OS_NAME="$(uname -s)" +ARCH_NAME="$(uname -m)" +ok "Platform: $OS_NAME $ARCH_NAME" +if [ "$OS_NAME" != "Darwin" ] || [ "$ARCH_NAME" != "arm64" ]; then + info "Local MLX launchd is Apple Silicon-only; base hooks and endpoint-backed Sanhedrin can run on x86." +fi + +# Python +if command -v python3 >/dev/null; then + PY="$(python3 -c 'import sys;print(".".join(map(str,sys.version_info[:2])))' 2>/dev/null)" + case "$PY" in + 3.9|3.1[0-9]|3.[2-9]*) ok "Python $PY" ;; + *) warn "Python $PY (need 3.9+)" ;; + esac +else + miss "python3 not found" +fi + +# CLI tools +command -v jq >/dev/null && ok "jq" || miss "jq missing — brew install jq" +if [ "$CHECK_PREFLIGHT" -eq 1 ]; then + command -v claude >/dev/null && ok "claude CLI" || miss "claude CLI — install Claude Code" + command -v vestige-mcp >/dev/null && ok "vestige-mcp" || miss "vestige-mcp — cargo install vestige-mcp" + + # Vestige MCP HTTP API + if curl -fsS -m 2 "http://127.0.0.1:${DASHBOARD_PORT}/api/health" >/dev/null 2>&1; then + ok "vestige-mcp dashboard responding on :$DASHBOARD_PORT" + else + warn "vestige-mcp dashboard not responding on :$DASHBOARD_PORT" + fi +fi + +# Settings hook wiring +if [ "$CHECK_PREFLIGHT" -eq 0 ] && [ "$CHECK_SANHEDRIN" -eq 0 ]; then + if [ -f "$HOME/.claude/settings.json" ] && \ + jq -e 'any((.hooks.UserPromptSubmit[]?.hooks[]?, .hooks.Stop[]?.hooks[]?); ((.command? // "") | test("synthesis-preflight\\.sh|cwd-state-injector\\.sh|vestige-pulse-daemon\\.sh|preflight-swarm\\.sh|load-all-memory\\.sh|veto-detector\\.sh|sanhedrin\\.sh|synthesis-stop-validator\\.sh|synthesis-gate\\.sh")))' "$HOME/.claude/settings.json" >/dev/null 2>&1; then + warn "Vestige hooks are still wired; run: install-sandwich.sh --force" + else + ok "no Vestige Claude Code hooks wired by default" + fi +fi + +if [ "$CHECK_PREFLIGHT" -eq 1 ]; then + echo + echo "Optional Preflight" + + if [ -f "$HOME/.claude/settings.json" ] && \ + jq -e 'any(.hooks.UserPromptSubmit[]?.hooks[]?; ((.command? // "") | contains("synthesis-preflight.sh"))) and any(.hooks.UserPromptSubmit[]?.hooks[]?; ((.command? // "") | contains("preflight-swarm.sh")))' "$HOME/.claude/settings.json" >/dev/null 2>&1; then + ok "preflight UserPromptSubmit hooks wired" + else + warn "preflight hooks not wired — run: install-sandwich.sh --enable-preflight" + fi + + info "preflight-swarm.sh uses claude -p with Haiku when enabled; default installs do not wire it." +fi + +if [ "$CHECK_SANHEDRIN" -eq 1 ]; then + echo + echo "Optional Sanhedrin" + + if [ -f "$SANHEDRIN_ENV" ]; then + ok "Sanhedrin env file present" + else + warn "Sanhedrin env file missing — run: install-sandwich.sh --enable-sanhedrin" + fi + info "Sanhedrin claim mode: $SANHEDRIN_CLAIM_MODE; output: $SANHEDRIN_OUTPUT" + + if [ "$OS_NAME" = "Darwin" ] && [ "$ARCH_NAME" = "arm64" ]; then + command -v uv >/dev/null && ok "uv" || warn "uv missing — brew install uv" + command -v mlx_lm.server >/dev/null && ok "mlx-lm" || warn "mlx-lm — uv tool install mlx-lm" + command -v hf >/dev/null && ok "huggingface_hub CLI" || warn "hf — uv tool install 'huggingface_hub[cli]'" + + MODEL="${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-mlx-community/Qwen3.6-35B-A3B-4bit}}" + HF_HOME_DEFAULT="${HF_HOME:-$HOME/.cache/huggingface}" + ENC_MODEL="models--$(printf '%s' "$MODEL" | sed 's|/|--|g')" + if [ -d "$HF_HOME_DEFAULT/hub/$ENC_MODEL" ]; then + ok "Model cached: $MODEL" + else + info "Model not cached: $MODEL (local MLX path downloads ~19GB)" + fi + + if [ -f "$HOME/Library/LaunchAgents/com.vestige.mlx-server.plist" ]; then + ok "launchd plist installed" + else + info "launchd plist not installed; endpoint-backed Sanhedrin can still run" + fi + else + info "Skipping MLX/launchd checks on $OS_NAME $ARCH_NAME" + fi + + if curl -fsS -m 2 "$SANHEDRIN_MODELS_URL" >/dev/null 2>&1; then + ok "Sanhedrin model endpoint responding at $SANHEDRIN_MODELS_URL" + else + warn "Sanhedrin endpoint not responding at $SANHEDRIN_MODELS_URL" + fi + + if [ -f "$HOME/.claude/settings.json" ] && \ + jq -e '.hooks.Stop[]?.hooks[]?.command | contains("sanhedrin.sh")' "$HOME/.claude/settings.json" >/dev/null 2>&1; then + ok "Sanhedrin Stop hook wired" + else + warn "Sanhedrin Stop hook not wired — run: install-sandwich.sh --enable-sanhedrin" + fi +else + echo + info "Sanhedrin is optional and not checked. Use --sanhedrin to verify an enabled endpoint." +fi + +echo +if [ $FAIL -eq 0 ]; then + echo " Ready. Default install has no Vestige Claude Code hooks wired and makes no automatic model calls." + exit 0 +else + echo " Fix the items above, then re-run." + exit 1 +fi diff --git a/scripts/install-sandwich.sh b/scripts/install-sandwich.sh new file mode 100755 index 0000000..1a1bbd2 --- /dev/null +++ b/scripts/install-sandwich.sh @@ -0,0 +1,378 @@ +#!/usr/bin/env bash +# install-sandwich.sh — One-command installer for the Vestige Cognitive Sandwich. +# +# Usage: +# vestige sandwich install +# # or, from a checkout / source archive: +# ./scripts/install-sandwich.sh [--force] [--enable-preflight] [--enable-sanhedrin] [--with-launchd] [--include-memory-loader] +# ./scripts/install-sandwich.sh --enable-sanhedrin --sanhedrin-endpoint=http://127.0.0.1:11434/v1/chat/completions --sanhedrin-model=qwen2.5:14b +# +# What it does: +# 1. Verifies required local tools +# 2. Stages ~/.claude/hooks/ and ~/.claude/agents/ +# 3. Copies sanitized hooks + agents +# 4. Removes old Vestige hook wiring from ~/.claude/settings.json by default +# 5. Optionally enables preflight hooks and/or Sanhedrin. Only with --with-launchd on Apple Silicon, +# auto-starts mlx_lm.server with Qwen3.6-35B-A3B + +set -euo pipefail + +VERSION="${VESTIGE_SANDWICH_VERSION:-v2.1.1}" +REPO="samvallad33/vestige" +MODEL_ID="${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-mlx-community/Qwen3.6-35B-A3B-4bit}}" +DASHBOARD_PORT="${VESTIGE_DASHBOARD_PORT:-3927}" +SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-http://127.0.0.1:8080/v1/chat/completions}}" +SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" +SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" +SANHEDRIN_CLAIM_MODE="${VESTIGE_SANHEDRIN_CLAIM_MODE:-1}" +SANHEDRIN_OUTPUT="${VESTIGE_SANHEDRIN_OUTPUT:-json}" +MODEL_ID_FROM_INSTALLER=0 +DASHBOARD_PORT_FROM_INSTALLER=0 +SANHEDRIN_ENDPOINT_FROM_INSTALLER=0 +SANHEDRIN_CLAIM_MODE_FROM_INSTALLER=0 +SANHEDRIN_OUTPUT_FROM_INSTALLER=0 +[ -n "${VESTIGE_SANHEDRIN_MODEL:-${VESTIGE_SANDWICH_MODEL:-}}" ] && MODEL_ID_FROM_INSTALLER=1 +[ -n "${VESTIGE_DASHBOARD_PORT:-}" ] && DASHBOARD_PORT_FROM_INSTALLER=1 +[ -n "${VESTIGE_SANHEDRIN_ENDPOINT:-${MLX_ENDPOINT:-}}" ] && SANHEDRIN_ENDPOINT_FROM_INSTALLER=1 +[ -n "${VESTIGE_SANHEDRIN_CLAIM_MODE:-}" ] && SANHEDRIN_CLAIM_MODE_FROM_INSTALLER=1 +[ -n "${VESTIGE_SANHEDRIN_OUTPUT:-}" ] && SANHEDRIN_OUTPUT_FROM_INSTALLER=1 + +HOOKS_DIR="$HOME/.claude/hooks" +AGENTS_DIR="$HOME/.claude/agents" +LAUNCHD_DIR="$HOME/Library/LaunchAgents" +SETTINGS="$HOME/.claude/settings.json" + +FORCE=0 +ENABLE_PREFLIGHT=0 +ENABLE_SANHEDRIN=0 +WITH_LAUNCHD=0 +INCLUDE_MEMORY_LOADER=0 +SRC="" + +for arg in "$@"; do + case "$arg" in + --force) FORCE=1 ;; + --enable-preflight) ENABLE_PREFLIGHT=1 ;; + --enable-sandwich) ENABLE_PREFLIGHT=1; ENABLE_SANHEDRIN=1 ;; + --enable-sanhedrin) ENABLE_SANHEDRIN=1 ;; + --with-launchd) WITH_LAUNCHD=1 ;; + --no-launchd) WITH_LAUNCHD=0 ;; + --include-memory-loader) INCLUDE_MEMORY_LOADER=1 ;; + --sanhedrin-endpoint=*|--endpoint=*) + SANHEDRIN_ENDPOINT="${arg#*=}" + SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" + SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" + SANHEDRIN_ENDPOINT_FROM_INSTALLER=1 + ;; + --sanhedrin-model=*|--model=*) + MODEL_ID="${arg#*=}" + MODEL_ID_FROM_INSTALLER=1 + ;; + --src=*) SRC="${arg#--src=}" ;; + -h|--help) + sed -n '2,24p' "$0" + exit 0 + ;; + esac +done + +if [ "$WITH_LAUNCHD" -eq 1 ] && [ "$ENABLE_SANHEDRIN" -eq 0 ]; then + ENABLE_SANHEDRIN=1 +fi + +say() { printf '\033[1;36m[sandwich]\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[sandwich]\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31m[sandwich]\033[0m %s\n' "$*" >&2; exit 1; } + +# --- Platform check --- +OS_NAME="$(uname -s)" +ARCH_NAME="$(uname -m)" +say "platform: $OS_NAME $ARCH_NAME" +if [ "$ENABLE_SANHEDRIN" -eq 1 ] && [ "$WITH_LAUNCHD" -eq 0 ]; then + say "Sanhedrin enabled without launchd; using OpenAI-compatible endpoint: $SANHEDRIN_ENDPOINT" +fi +if [ "$WITH_LAUNCHD" -eq 1 ] && { [ "$OS_NAME" != "Darwin" ] || [ "$ARCH_NAME" != "arm64" ]; }; then + warn "--with-launchd is Apple Silicon only; skipping local MLX autostart on $OS_NAME $ARCH_NAME" + warn "Sanhedrin can still run on x86 via --sanhedrin-endpoint or VESTIGE_SANHEDRIN_ENDPOINT." + WITH_LAUNCHD=0 +fi + +# --- Prereqs (warnings only, install proceeds) --- +command -v jq >/dev/null || die "jq required: brew install jq" +command -v python3 >/dev/null || die "python3 required" +if [ "$ENABLE_PREFLIGHT" -eq 1 ]; then + command -v claude >/dev/null || warn "'claude' CLI not found — preflight-swarm.sh will fail open." + command -v vestige-mcp >/dev/null || warn "'vestige-mcp' not found — Vestige preflight hooks will fail open." +fi +if [ "$WITH_LAUNCHD" -eq 1 ]; then + command -v uv >/dev/null || warn "'uv' not found — install with: brew install uv" + command -v mlx_lm.server >/dev/null || warn "mlx-lm not installed — run: uv tool install mlx-lm" + command -v hf >/dev/null || warn "'hf' not found — run: uv tool install 'huggingface_hub[cli]'" +fi + +# --- Resolve source: local checkout or release tarball --- +if [ -n "$SRC" ]; then + SCRIPT_DIR="$SRC" +elif [ -f "$(dirname "$0")/../hooks/sanhedrin.sh" ]; then + SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)" + say "Using local checkout: $SCRIPT_DIR" +else + TMPDIR="$(mktemp -d)" + trap 'rm -rf "$TMPDIR"' EXIT + say "Fetching Vestige Sandwich $VERSION..." + curl -fsSL "https://github.com/$REPO/archive/refs/tags/$VERSION.tar.gz" \ + | tar xz -C "$TMPDIR" + SCRIPT_DIR="$(ls -d "$TMPDIR"/vestige-*)" +fi + +[ -d "$SCRIPT_DIR/hooks" ] || die "hooks/ not found in $SCRIPT_DIR — wrong source?" + +# --- Stage directories --- +mkdir -p "$HOOKS_DIR" "$AGENTS_DIR" +if [ "$WITH_LAUNCHD" -eq 1 ]; then + mkdir -p "$LAUNCHD_DIR" +fi + +# v2.1.0 originally installed the MLX server as part of the default path. +# Default reinstalls now retire that job; users can restore it with --with-launchd. +if [ "$WITH_LAUNCHD" -eq 0 ] && [ "$OS_NAME" = "Darwin" ]; then + LEGACY_PLIST="$LAUNCHD_DIR/com.vestige.mlx-server.plist" + if [ -f "$LEGACY_PLIST" ]; then + launchctl unload "$LEGACY_PLIST" 2>/dev/null || true + rm -f "$LEGACY_PLIST" + say "removed old Sanhedrin launchd job (use --with-launchd to opt back in)" + fi +fi + +# --- Copy hooks --- +copied=0; skipped=0 +for f in "$SCRIPT_DIR/hooks"/*.sh "$SCRIPT_DIR/hooks"/*.py; do + [ -f "$f" ] || continue + base="$(basename "$f")" + # load-all-memory.sh dumps every memory MD — opt-in only + if [ "$base" = "load-all-memory.sh" ] && [ "$INCLUDE_MEMORY_LOADER" -eq 0 ]; then + say "skip $base (use --include-memory-loader to install)" + continue + fi + if [ -e "$HOOKS_DIR/$base" ] && [ "$FORCE" -eq 0 ]; then + skipped=$((skipped + 1)) + continue + fi + install -m 0755 "$f" "$HOOKS_DIR/$base" + copied=$((copied + 1)) +done +say "hooks: $copied installed, $skipped skipped (use --force to overwrite)" + +# --- Copy agents --- +for f in "$SCRIPT_DIR/agents"/*.md; do + [ -f "$f" ] || continue + base="$(basename "$f")" + if [ -e "$AGENTS_DIR/$base" ] && [ "$FORCE" -eq 0 ]; then + continue + fi + install -m 0644 "$f" "$AGENTS_DIR/$base" +done +say "agents installed to $AGENTS_DIR" + +# --- Persist optional Sanhedrin env --- +quote_env() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")" +} + +load_vestige_sanhedrin_env() { + [ -f "$1" ] || return 0 + while IFS="$(printf '\t')" read -r key value; do + case "$key" in + VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_MODEL|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT|VESTIGE_SANHEDRIN_PYTHON|VESTIGE_DASHBOARD_PORT) + export "$key=$value" + ;; + esac + done < <(python3 - "$1" <<'PY' +import shlex +import sys + +allowed = { + "VESTIGE_SANHEDRIN_ENABLED", + "VESTIGE_SANHEDRIN_MODEL", + "VESTIGE_SANHEDRIN_ENDPOINT", + "VESTIGE_SANHEDRIN_CLAIM_MODE", + "VESTIGE_SANHEDRIN_OUTPUT", + "VESTIGE_SANHEDRIN_PYTHON", + "VESTIGE_DASHBOARD_PORT", +} + +try: + lines = open(sys.argv[1], encoding="utf-8").read().splitlines() +except OSError: + sys.exit(0) + +for raw in lines: + line = raw.strip() + if not line or line.startswith("#"): + continue + try: + parts = shlex.split(line, posix=True) + except ValueError: + continue + if len(parts) != 1 or "=" not in parts[0]: + continue + key, value = parts[0].split("=", 1) + if key in allowed and "\t" not in value and "\0" not in value: + print(f"{key}\t{value}") +PY + ) +} + +if [ "$ENABLE_SANHEDRIN" -eq 1 ]; then + SANHEDRIN_ENV="$HOOKS_DIR/vestige-sanhedrin.env" + INSTALLER_MODEL_ID="$MODEL_ID" + INSTALLER_DASHBOARD_PORT="$DASHBOARD_PORT" + INSTALLER_SANHEDRIN_ENDPOINT="$SANHEDRIN_ENDPOINT" + INSTALLER_SANHEDRIN_CLAIM_MODE="$SANHEDRIN_CLAIM_MODE" + INSTALLER_SANHEDRIN_OUTPUT="$SANHEDRIN_OUTPUT" + if [ -f "$SANHEDRIN_ENV" ]; then + load_vestige_sanhedrin_env "$SANHEDRIN_ENV" || true + if [ "$MODEL_ID_FROM_INSTALLER" -eq 1 ]; then + MODEL_ID="$INSTALLER_MODEL_ID" + else + MODEL_ID="${VESTIGE_SANHEDRIN_MODEL:-$MODEL_ID}" + fi + if [ "$DASHBOARD_PORT_FROM_INSTALLER" -eq 1 ]; then + DASHBOARD_PORT="$INSTALLER_DASHBOARD_PORT" + else + DASHBOARD_PORT="${VESTIGE_DASHBOARD_PORT:-$DASHBOARD_PORT}" + fi + if [ "$SANHEDRIN_ENDPOINT_FROM_INSTALLER" -eq 1 ]; then + SANHEDRIN_ENDPOINT="$INSTALLER_SANHEDRIN_ENDPOINT" + else + SANHEDRIN_ENDPOINT="${VESTIGE_SANHEDRIN_ENDPOINT:-$SANHEDRIN_ENDPOINT}" + SANHEDRIN_ENDPOINT="${SANHEDRIN_ENDPOINT%/}" + fi + SANHEDRIN_MODELS_URL="${SANHEDRIN_ENDPOINT%/chat/completions}/models" + if [ "$SANHEDRIN_CLAIM_MODE_FROM_INSTALLER" -eq 1 ]; then + SANHEDRIN_CLAIM_MODE="$INSTALLER_SANHEDRIN_CLAIM_MODE" + else + SANHEDRIN_CLAIM_MODE="${VESTIGE_SANHEDRIN_CLAIM_MODE:-$SANHEDRIN_CLAIM_MODE}" + fi + if [ "$SANHEDRIN_OUTPUT_FROM_INSTALLER" -eq 1 ]; then + SANHEDRIN_OUTPUT="$INSTALLER_SANHEDRIN_OUTPUT" + else + SANHEDRIN_OUTPUT="${VESTIGE_SANHEDRIN_OUTPUT:-$SANHEDRIN_OUTPUT}" + fi + fi + TMP_ENV="$(mktemp)" + if [ -f "$SANHEDRIN_ENV" ]; then + awk -F= ' + $1 !~ /^(VESTIGE_SANHEDRIN_ENABLED|VESTIGE_SANHEDRIN_ENDPOINT|VESTIGE_SANHEDRIN_MODEL|VESTIGE_DASHBOARD_PORT|VESTIGE_SANHEDRIN_CLAIM_MODE|VESTIGE_SANHEDRIN_OUTPUT)$/ + ' "$SANHEDRIN_ENV" > "$TMP_ENV" + fi + { + cat "$TMP_ENV" + printf 'VESTIGE_SANHEDRIN_ENABLED=1\n' + printf 'VESTIGE_SANHEDRIN_ENDPOINT=%s\n' "$(quote_env "$SANHEDRIN_ENDPOINT")" + printf 'VESTIGE_SANHEDRIN_MODEL=%s\n' "$(quote_env "$MODEL_ID")" + printf 'VESTIGE_DASHBOARD_PORT=%s\n' "$(quote_env "$DASHBOARD_PORT")" + printf 'VESTIGE_SANHEDRIN_CLAIM_MODE=%s\n' "$(quote_env "$SANHEDRIN_CLAIM_MODE")" + printf 'VESTIGE_SANHEDRIN_OUTPUT=%s\n' "$(quote_env "$SANHEDRIN_OUTPUT")" + } > "$SANHEDRIN_ENV" + rm -f "$TMP_ENV" + chmod 0600 "$SANHEDRIN_ENV" + say "Sanhedrin opt-in config written to $SANHEDRIN_ENV" +fi + +# --- Render launchd plist (Apple Silicon opt-in only) --- +if [ "$WITH_LAUNCHD" -eq 1 ]; then + PLIST="$LAUNCHD_DIR/com.vestige.mlx-server.plist" + TEMPLATE="$SCRIPT_DIR/launchd/com.vestige.mlx-server.plist.template" + [ -f "$TEMPLATE" ] || die "launchd template missing: $TEMPLATE" + sed -e "s|__HOME__|$HOME|g" -e "s|__MODEL__|$MODEL_ID|g" "$TEMPLATE" > "$PLIST" + launchctl unload "$PLIST" 2>/dev/null || true + launchctl load "$PLIST" + say "launchd loaded: com.vestige.mlx-server (model: $MODEL_ID)" +fi + +# --- Merge hooks fragment into settings.json --- +[ -f "$SETTINGS" ] || echo '{}' > "$SETTINGS" +if [ -f "$HOME/.claude/settings.json.bak.pre-sandwich" ]; then + say "settings.json backup already exists at .bak.pre-sandwich — not overwriting" +else + cp "$SETTINGS" "$HOME/.claude/settings.json.bak.pre-sandwich" +fi +TMP_MERGE="$(mktemp)" +PREFLIGHT_FRAGMENT="$SCRIPT_DIR/hooks/settings.fragment.json" +SANHEDRIN_FRAGMENT="$SCRIPT_DIR/hooks/settings.fragment.json" +if [ "$ENABLE_PREFLIGHT" -eq 1 ]; then + PREFLIGHT_FRAGMENT="$SCRIPT_DIR/hooks/settings.preflight.fragment.json" +fi +if [ "$ENABLE_SANHEDRIN" -eq 1 ]; then + SANHEDRIN_FRAGMENT="$SCRIPT_DIR/hooks/settings.sanhedrin.fragment.json" +fi +jq -s ' + def is_vestige_hook: + (.command? // "") as $cmd + | [ + "synthesis-preflight.sh", + "cwd-state-injector.sh", + "vestige-pulse-daemon.sh", + "preflight-swarm.sh", + "load-all-memory.sh", + "veto-detector.sh", + "sanhedrin.sh", + "synthesis-stop-validator.sh", + "synthesis-gate.sh" + ] | any(. as $needle | $cmd | contains($needle)); + + def scrub_vestige_hooks: + .hooks.UserPromptSubmit = ( + (.hooks.UserPromptSubmit // []) + | map(.hooks = ((.hooks // []) | map(select((is_vestige_hook | not))))) + | map(select(((.hooks // []) | length) > 0)) + ) + | if ((.hooks.UserPromptSubmit // []) | length) == 0 then del(.hooks.UserPromptSubmit) else . end + | .hooks.Stop = ( + (.hooks.Stop // []) + | map(.hooks = ((.hooks // []) | map(select((is_vestige_hook | not))))) + | map(select(((.hooks // []) | length) > 0)) + ) + | if ((.hooks.Stop // []) | length) == 0 then del(.hooks.Stop) else . end + | if ((.hooks // {}) | length) == 0 then del(.hooks) else . end; + + (.[0] | scrub_vestige_hooks) * .[1] * .[2] +' "$SETTINGS" "$PREFLIGHT_FRAGMENT" "$SANHEDRIN_FRAGMENT" > "$TMP_MERGE" +mv "$TMP_MERGE" "$SETTINGS" +if [ "$ENABLE_PREFLIGHT" -eq 1 ] || [ "$ENABLE_SANHEDRIN" -eq 1 ]; then + enabled_layers="" + [ "$ENABLE_PREFLIGHT" -eq 1 ] && enabled_layers="${enabled_layers} preflight" + [ "$ENABLE_SANHEDRIN" -eq 1 ] && enabled_layers="${enabled_layers} sanhedrin" + say "merged optional hook layer(s) into $SETTINGS:${enabled_layers} (backup at .bak.pre-sandwich)" +else + say "removed Vestige hook wiring from $SETTINGS; default install activates no Claude Code hooks (backup at .bak.pre-sandwich)" +fi + +# --- Next steps --- +cat <20 GB free RAM, add --with-launchd to auto-start + the local MLX Qwen server. On x86, point --sanhedrin-endpoint at vLLM, + Ollama, llama.cpp, or another OpenAI-compatible /v1/chat/completions URL. + + To uninstall: + launchctl unload $LAUNCHD_DIR/com.vestige.mlx-server.plist 2>/dev/null || true + rm -f $LAUNCHD_DIR/com.vestige.mlx-server.plist + cp $HOME/.claude/settings.json.bak.pre-sandwich $HOME/.claude/settings.json + +EOF diff --git a/server.json b/server.json new file mode 100644 index 0000000..d92c067 --- /dev/null +++ b/server.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.samvallad33/vestige", + "title": "Vestige", + "description": "Local-first cognitive memory server for AI agents with SQLite, smart ingest, and portable sync.", + "repository": { + "url": "https://github.com/samvallad33/vestige", + "source": "github" + }, + "version": "2.1.22", + "packages": [ + { + "registryType": "npm", + "identifier": "vestige-mcp-server", + "version": "2.1.22", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/tests/hooks/test_sanhedrin_claim_mode.py b/tests/hooks/test_sanhedrin_claim_mode.py new file mode 100644 index 0000000..48efb22 --- /dev/null +++ b/tests/hooks/test_sanhedrin_claim_mode.py @@ -0,0 +1,442 @@ +import contextlib +import importlib.util +import io +import json +import os +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + + +REPO_ROOT = Path(__file__).resolve().parents[2] +HOOK_PATH = REPO_ROOT / "hooks" / "sanhedrin-local.py" + + +def load_sanhedrin(): + spec = importlib.util.spec_from_file_location("sanhedrin_local_under_test", HOOK_PATH) + module = importlib.util.module_from_spec(spec) + assert spec.loader is not None + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +@contextlib.contextmanager +def patched_attr(obj, name, value): + sentinel = object() + old = getattr(obj, name, sentinel) + setattr(obj, name, value) + try: + yield + finally: + if old is sentinel: + delattr(obj, name) + else: + setattr(obj, name, old) + + +class SanhedrinClaimModeTests(unittest.TestCase): + def setUp(self): + self.sanhedrin = load_sanhedrin() + + @contextlib.contextmanager + def isolated_receipt_state(self): + with tempfile.TemporaryDirectory() as tmp: + state_dir = Path(tmp) + core = self.sanhedrin.sanhedrin_core + with patched_attr(core, "STATE_DIR", state_dir), patched_attr( + core, "RECEIPTS_DIR", state_dir / "receipts" + ), patched_attr(core, "LATEST_JSON", state_dir / "latest.json"), patched_attr( + core, "LATEST_HTML", state_dir / "latest.html" + ), patched_attr( + core, "APPEALS_JSONL", state_dir / "appeals.jsonl" + ), patched_attr( + core, "COMMAND_RECEIPTS_JSONL", state_dir / "command-receipts.jsonl" + ): + yield state_dir + + def run_main(self, draft): + stdin = io.StringIO(draft) + stdout = io.StringIO() + with mock.patch.object(sys, "stdin", stdin), mock.patch.object(sys, "stdout", stdout): + self.sanhedrin.main() + return stdout.getvalue().strip() + + def test_receipt_lock_blocks_unbacked_test_claim(self): + with self.isolated_receipt_state() as state_dir: + out = self.run_main("All tests passed.") + + self.assertIn("Receipt Lock", out) + receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) + + self.assertEqual(receipt["verdictBar"], "VETO") + self.assertEqual(receipt["claims"][0]["decision"], "veto") + self.assertEqual(receipt["claims"][0]["evidence_state"], "missing_receipt") + + def test_receipt_lock_allows_matching_success_receipt(self): + with self.isolated_receipt_state() as state_dir, mock.patch.dict( + os.environ, {"VESTIGE_SANHEDRIN_ALLOW_COMMAND_LEDGER": "1"}, clear=False + ): + (state_dir / "command-receipts.jsonl").write_text( + json.dumps({ + "command": "cargo test --workspace --release", + "exitCode": 0, + "success": True, + }) + "\n", + encoding="utf-8", + ) + out = self.run_main("All tests passed.") + receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) + + self.assertEqual(out, "yes") + self.assertNotEqual(receipt["verdictBar"], "VETO") + self.assertEqual(receipt["claims"][0]["decision"], "pass") + + def test_receipt_lock_appeal_suppresses_same_fingerprint(self): + with self.isolated_receipt_state() as state_dir: + fingerprint = self.sanhedrin.sanhedrin_core.claim_fingerprint("All tests passed.") + (state_dir / "appeals.jsonl").write_text( + json.dumps({ + "claimFingerprint": fingerprint, + "reason": "too_strict", + "status": "active", + }) + "\n", + encoding="utf-8", + ) + out = self.run_main("All tests passed.") + receipt = json.loads((state_dir / "latest.json").read_text(encoding="utf-8")) + + self.assertEqual(out, "yes") + self.assertEqual(receipt["verdictBar"], "APPEALED") + self.assertEqual(receipt["claims"][0]["decision"], "appealed") + + def test_plain_sam_biographical_achievement_claim_is_check_worthy(self): + claims = self.sanhedrin.extract_check_worthy_claims( + "Sam graduated from Example University and won the Example AI Challenge." + ) + + self.assertGreaterEqual(len(claims), 1) + self.assertTrue(any(claim.sam_critical for claim in claims)) + self.assertTrue( + any(claim.claim_class in {"BIOGRAPHICAL", "ACHIEVEMENT"} for claim in claims) + ) + self.assertTrue(any("Sam" in claim.text for claim in claims)) + + def test_zero_high_trust_evidence_on_sam_critical_claim_blocks(self): + def fail_if_judge_is_called(_claim, _evidence): + self.fail("zero-evidence absence decisions should not require model judgment") + + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): + out = self.run_main("Sam won first place at the Example AI Challenge.") + + result = json.loads(out) + self.assertFalse(result["passed"]) + self.assertTrue(result["legacy_verdict"].startswith("no - "), result) + self.assertEqual(result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") + + def test_vague_user_positive_claim_fails_closed(self): + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ): + out = self.run_main("Sam won a few competitions and earned some prize money.") + + result = json.loads(out) + self.assertFalse(result["passed"], result) + self.assertEqual(result["verdicts"][0]["claim"]["claim_class"], "VAGUE-QUANTIFIER") + self.assertEqual(result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") + + def test_retrieval_failure_on_sam_critical_claim_fails_open(self): + def fail_if_judge_is_called(_claim, _evidence): + self.fail("retrieval failures should fail open before model judgment") + + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], False) + ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): + out = self.run_main("Sam won first place at the Example AI Challenge.") + + result = json.loads(out) + self.assertTrue(result["passed"], result) + self.assertEqual(result["legacy_verdict"], "yes") + self.assertEqual(result["verdicts"][0]["status"], "NEI") + self.assertIn("retrieval unavailable", result["verdicts"][0]["reason"]) + + def test_current_turn_attribution_discourse_is_not_absence_blocked(self): + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ): + out = self.run_main( + "You asked me to audit the Sanhedrin hook, and I reviewed your requested changes." + ) + + result = json.loads(out) + self.assertTrue(result["passed"], result) + self.assertEqual(result["legacy_verdict"], "yes") + self.assertEqual(result["claims_extracted"], 0) + + def test_discourse_framing_does_not_hide_embedded_sam_claim(self): + examples = [ + "Per your request, Sam won first place at the Example AI Challenge.", + "Sam won first place at the Example AI Challenge, which would be impressive.", + ] + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + for example in examples: + with self.subTest(example=example), mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ): + out = self.run_main(example) + + result = json.loads(out) + self.assertFalse(result["passed"], result) + self.assertEqual(result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") + self.assertIn("Sam won", result["verdicts"][0]["claim"]["text"]) + + def test_leading_hypothetical_still_skips_embedded_claim(self): + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ): + out = self.run_main("If Sam wins first place next time, he could claim the prize.") + + result = json.loads(out) + self.assertTrue(result["passed"], result) + self.assertEqual(result["claims_extracted"], 0) + + def test_subject_modal_prefix_skips_without_hiding_asserted_claim(self): + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ): + nonassertive = self.run_main("Sam could win first place next time.") + asserted = self.run_main( + "Sam won first place at the Example AI Challenge and could collect prize money." + ) + + nonassertive_result = json.loads(nonassertive) + asserted_result = json.loads(asserted) + self.assertTrue(nonassertive_result["passed"], nonassertive_result) + self.assertEqual(nonassertive_result["claims_extracted"], 0) + self.assertFalse(asserted_result["passed"], asserted_result) + self.assertEqual(asserted_result["verdicts"][0]["status"], "REFUTED_BY_ABSENCE") + + def test_malformed_deep_reference_response_fails_open(self): + def fail_if_judge_is_called(_claim, _evidence): + self.fail("malformed retrieval responses should fail open before model judgment") + + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + for response in ({}, {"status": "error"}, {"errors": ["timeout"]}): + with self.subTest(response=response): + def fake_post_json(_url, _body, _timeout): + return response + + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "post_json", fake_post_json + ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): + out = self.run_main("Sam won first place at the Example AI Challenge.") + + result = json.loads(out) + self.assertTrue(result["passed"], result) + self.assertEqual(result["verdicts"][0]["status"], "NEI") + self.assertIn("retrieval unavailable", result["verdicts"][0]["reason"]) + + def test_non_critical_technical_zero_evidence_does_not_block(self): + def fail_if_judge_is_called(_claim, _evidence): + self.fail("zero-evidence technical claims should fail open without model judgment") + + env = {"VESTIGE_SANHEDRIN_CLAIM_MODE": "1", "VESTIGE_SANHEDRIN_OUTPUT": "json"} + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "fetch_claim_evidence", lambda _claim: ([], True) + ), patched_attr(self.sanhedrin, "judge_claim_with_model", fail_if_judge_is_called): + out = self.run_main( + "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." + ) + + result = json.loads(out) + self.assertTrue(result["passed"]) + self.assertEqual(result["legacy_verdict"], "yes") + self.assertEqual(result["verdicts"][0]["status"], "NEI") + self.assertEqual(result["verdicts"][0]["claim"]["claim_class"], "TECHNICAL") + + def test_fetch_evidence_truncates_on_python_character_boundary(self): + emoji_out = self.sanhedrin.truncate_chars(("a" * 4) + "🙂" + "tail", 8) + combining_out = self.sanhedrin.truncate_chars("Cafe\u0301 tail", 8) + + self.assertEqual(emoji_out, "aaaa🙂...") + self.assertEqual(combining_out, "Cafe...") + self.assertNotIn("\ufffd", emoji_out + combining_out) + self.assertFalse(self.sanhedrin.unicodedata.combining(combining_out[-4])) + (emoji_out + combining_out).encode("utf-8") + + def test_staged_evidence_is_used_without_smart_ingest_or_durable_write(self): + with tempfile.TemporaryDirectory() as tmp: + staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" + staged = [ + { + "id": "samstage2", + "role": "memory", + "trust": 0.89, + "preview": "Sam's final result was second place with no payout.", + } + ] + staged_path.write_text(json.dumps(staged), encoding="utf-8") + + post_urls = [] + + def fake_post_json(url, body, _timeout): + post_urls.append(url) + if "smart_ingest" in url or "/api/memories" in url: + self.fail(f"staged evidence path attempted durable write to {url}: {body}") + self.assertEqual(url, "http://127.0.0.1:3927/api/deep_reference") + return {"confidence": 0.0, "evidence": []} + + env = { + "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", + "VESTIGE_SANHEDRIN_OUTPUT": "json", + "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), + } + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "post_json", fake_post_json + ), patched_attr(self.sanhedrin, "VESTIGE_ENDPOINT", "http://127.0.0.1:3927/api/deep_reference"): + out = self.run_main("Sam won first place and earned prize money.") + + result = json.loads(out) + verdict = result["verdicts"][0] + self.assertFalse(result["passed"], result) + self.assertEqual(result["staged_evidence_count"], 1) + self.assertEqual(verdict["status"], "REFUTED_BY_ABSENCE") + self.assertEqual(verdict["durable_evidence_count"], 0) + self.assertEqual(verdict["high_trust_evidence_count"], 1) + self.assertEqual(post_urls, ["http://127.0.0.1:3927/api/deep_reference"]) + + def test_staged_only_refuted_verdict_is_downgraded_without_durable_evidence(self): + with tempfile.TemporaryDirectory() as tmp: + staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" + staged_path.write_text( + json.dumps( + [ + { + "id": "stage-tech", + "trust": 0.95, + "preview": "Qwen3.6-35B cannot be served through a chat endpoint.", + } + ] + ), + encoding="utf-8", + ) + + def fake_post_json(url, _body, _timeout): + if url == self.sanhedrin.VESTIGE_ENDPOINT: + return {"confidence": 0.0, "evidence": []} + if url == self.sanhedrin.SANHEDRIN_ENDPOINT: + return { + "choices": [ + { + "message": { + "content": json.dumps( + { + "status": "REFUTED", + "class": "TECHNICAL", + "reason": "Staged evidence contradicts the claim.", + "evidence_ids": ["stage-tech"], + } + ) + } + } + ] + } + self.fail(f"unexpected post_json URL: {url}") + + env = { + "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", + "VESTIGE_SANHEDRIN_OUTPUT": "json", + "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), + } + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "post_json", fake_post_json + ): + out = self.run_main( + "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." + ) + + result = json.loads(out) + verdict = result["verdicts"][0] + self.assertTrue(result["passed"], result) + self.assertEqual(verdict["status"], "NEI") + self.assertEqual(verdict["durable_evidence_count"], 0) + self.assertIn("Durable evidence required", verdict["reason"]) + + def test_staged_only_legacy_refuted_line_is_downgraded_without_durable_evidence(self): + with tempfile.TemporaryDirectory() as tmp: + staged_path = Path(tmp) / "sanhedrin-staged-evidence.json" + staged_path.write_text( + json.dumps( + [ + { + "id": "stage-tech", + "trust": 0.95, + "preview": "Qwen3.6-35B cannot be served through a chat endpoint.", + } + ] + ), + encoding="utf-8", + ) + + def fake_post_json(url, _body, _timeout): + if url == self.sanhedrin.VESTIGE_ENDPOINT: + return {"confidence": 0.0, "evidence": []} + if url == self.sanhedrin.SANHEDRIN_ENDPOINT: + return { + "choices": [ + { + "message": { + "content": ( + "no - [Sanhedrin Veto] [TECHNICAL]: " + "Staged evidence contradicts the claim." + ) + } + } + ] + } + self.fail(f"unexpected post_json URL: {url}") + + env = { + "VESTIGE_SANHEDRIN_CLAIM_MODE": "1", + "VESTIGE_SANHEDRIN_OUTPUT": "json", + "VESTIGE_SANHEDRIN_STAGE_FILE": str(staged_path), + } + with mock.patch.dict(os.environ, env, clear=False), patched_attr( + self.sanhedrin, "post_json", fake_post_json + ): + out = self.run_main( + "Qwen3.6-35B can be served through an OpenAI-compatible chat endpoint." + ) + + result = json.loads(out) + verdict = result["verdicts"][0] + self.assertTrue(result["passed"], result) + self.assertEqual(verdict["status"], "NEI") + self.assertEqual(verdict["durable_evidence_count"], 0) + self.assertIn("Durable evidence required", verdict["reason"]) + + def test_current_turn_discourse_patterns_are_not_claims(self): + examples = [ + "You asked for maximum subagents, so I audited the hook.", + "Your request was to verify the installer env preservation.", + "Per your request, I reviewed the Sanhedrin stop hook.", + "Sam asked me to go all in on the Sanhedrin patch.", + "The user requested maximum subagents for this implementation.", + ] + for example in examples: + with self.subTest(example=example): + self.assertEqual(self.sanhedrin.extract_check_worthy_claims(example), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/hooks/test_sanhedrin_shell_env.py b/tests/hooks/test_sanhedrin_shell_env.py new file mode 100644 index 0000000..5de950c --- /dev/null +++ b/tests/hooks/test_sanhedrin_shell_env.py @@ -0,0 +1,48 @@ +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[2] +SANHEDRIN_HOOK = REPO_ROOT / "hooks" / "sanhedrin.sh" + + +class SanhedrinShellEnvTests(unittest.TestCase): + def test_env_file_is_parsed_not_executed(self): + with tempfile.TemporaryDirectory() as tmp: + tmp_path = Path(tmp) + marker = tmp_path / "executed" + env_file = tmp_path / "vestige-sanhedrin.env" + env_file.write_text( + "\n".join( + [ + "VESTIGE_SANHEDRIN_ENABLED='1'", + "VESTIGE_SANHEDRIN_PYTHON='python3'", + f"VESTIGE_SANHEDRIN_MODEL='$(touch {marker})'", + "UNKNOWN_KEY='$(touch should-not-run)'", + ] + ) + + "\n", + encoding="utf-8", + ) + + env = os.environ.copy() + env["VESTIGE_SANHEDRIN_ENV"] = str(env_file) + result = subprocess.run( + ["bash", str(SANHEDRIN_HOOK)], + input='{"transcript_path":"/does/not/exist"}', + text=True, + env=env, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + check=False, + ) + + self.assertEqual(result.returncode, 0, result.stderr) + self.assertFalse(marker.exists()) + + +if __name__ == "__main__": + unittest.main()