diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 5b45d8a..cc1c176 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -52,6 +52,7 @@ body: attributes: label: IDE / Client options: + - Codex - Claude Code - Claude Desktop - Cursor 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 3dc7c73..6315588 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -27,14 +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: "" + 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-14 + os: macos-latest archive: tar.gz - cargo_flags: "--no-default-features" + cargo_flags: "--no-default-features --features ort-dynamic,vector-search" - target: aarch64-apple-darwin os: macos-latest archive: tar.gz @@ -55,8 +62,13 @@ jobs: - 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' diff --git a/.gitignore b/.gitignore index a5523d3..4236e68 100644 --- a/.gitignore +++ b/.gitignore @@ -136,3 +136,4 @@ apps/dashboard/node_modules/ # External repos (forks, submodules) # ============================================================================= fastembed-rs/ +.mcp.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f913bb5..c500cea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,261 @@ 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). +## [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. + +### Added + +- **`POST /api/memories/{id}/suppress`** — Dashboard users can now trigger top-down inhibitory control (Anderson 2025 SIF + Davis Rac1 cascade) without dropping to raw MCP. Optional JSON body `{"reason": "..."}` logged for audit. Each call compounds; response includes `suppressionCount`, `priorCount`, `retrievalPenalty`, `reversibleUntil`, `estimatedCascadeNeighbors`, and `labileWindowHours`. Emits the existing `MemorySuppressed` WebSocket event so the 3D graph plays the violet implosion + compounding pulse shipped in v2.0.6. +- **`POST /api/memories/{id}/unsuppress`** — Reverses a suppression inside the 24h labile window. Returns `stillSuppressed: bool` so the UI can tell a full reversal from a compounded-down state. Emits `MemoryUnsuppressed` for the rainbow-burst reversal animation. +- **Suppress button on the Memories page** — Third action alongside Promote / Demote / Delete, hover-tooltip explaining the neuroscience ("Top-down inhibition (Anderson 2025). Compounds. Reversible for 24h."). +- **Uptime in the sidebar footer** — The Heartbeat WebSocket event has carried `uptime_secs` since v2.0.5 but was never rendered. Now displays as `up 3d 4h` / `up 18m` / `up 47s` (compact two-most-significant-units format) next to memory count + retention. + +### Fixed + +- **`execute_export` no longer panics on unknown format.** The write-out match arm at `maintenance.rs` was `_ => unreachable!()` — defensive `Err(...)` now returns a clean "unsupported export format" message instead of unwinding through the MCP dispatcher. +- **Dashboard graph page distinguishes empty-database from API failure** (landed in the first half of this branch). Before v2.0.7 any error from `/api/graph` rendered as "No memories yet," which masked real failures. Now the regex + node-count gate splits the two; real errors surface as "Failed to load graph: [sanitized message]" with filesystem paths stripped for info-disclosure hardening. +- **`predict` MCP tool surfaces a `predict_degraded` flag** instead of silently returning empty vecs on lock poisoning. `tracing::warn!` logs the per-channel error for observability. +- **`memory_changelog` honors `start` / `end` ISO-8601 bounds.** Previously advertised in the schema since v1.7 but runtime-ignored. Malformed timestamps now return a helpful error instead of silently dropping the filter. Response includes a `filter` field echoing the applied window. +- **`intention` check honors `include_snoozed`.** Previously silent no-op; snoozed intentions were invisible to check regardless of the arg. Dedup via HashSet guards against storage overlap. +- **`intention` check response exposes `status` and `snoozedUntil`** so callers can distinguish active-triggered from snoozed-overdue intentions. +- **Server tool-count comment at `server.rs:212`** updated (23 → 24) to match the runtime assertion. + +### Removed + +- **Migration V11: drops dead `knowledge_edges` + `compressed_memories` tables.** Both were added speculatively in V4 and marked deprecated in the same migration that created them. Zero INSERT or SELECT anywhere in `crates/`. Frees schema space for future migrations. +- **`execute_health_check` (71 LOC) + `execute_stats` (179 LOC) in `maintenance.rs`.** Both `#[allow(dead_code)]` since v1.7 with in-file comments routing users to `execute_system_status` instead. Zero callers workspace-wide. Net -273 LOC in the touched file. +- **`x86_64-apple-darwin` job from `.github/workflows/release.yml`.** The Intel Mac build failed the v2.0.5 AND v2.0.6 release workflows because `ort-sys 2.0.0-rc.11` (pinned by `fastembed 5.13.2`) does not ship Intel Mac prebuilts. `ci.yml` had already dropped the target; `release.yml` is now in sync. README documents the build-from-source path. Future releases should publish clean on all three supported platforms (macOS ARM64, Linux x86_64, Windows MSVC). + +### Docs + +- Reconciled tool / module / test counts across `README.md`, `CONTRIBUTING.md`, `docs/integrations/windsurf.md`, `docs/integrations/xcode.md`. Ground truth: **24 MCP tools · 29 cognitive modules · 1,292 Rust tests + 171 dashboard tests.** +- Historical CHANGELOG entries and `docs/launch/*.md` launch materials left unchanged because they are time-stamped artifacts of their respective releases. + +### Tests + +- **+7 assertions** covering the v2.0.7 behavioral changes: V11 migration drops dead tables + is idempotent on replay, `predict_degraded` false on happy path, `include_snoozed` both paths + `status` field exposure, malformed `start` returns helpful error + `filter` field echo. +- Full suite: **1,292 Rust passing / 0 failed** across `cargo test --workspace --release`. **171 dashboard tests passing.** Zero clippy warnings on `vestige-core` or `vestige-mcp` under `-D warnings`. + +### Audit + +Pre-merge audited by 4 parallel reviewers (security, code quality, end-to-end flow trace, external verification). Zero CRITICAL or HIGH findings. Two MEDIUM fixes landed in the branch: graph error-message path sanitization (strip `/path/to/*.{sqlite,rs,db,toml,lock}`, cap 200 chars) and `intention` response `status` field exposure. + +--- + +## [2.0.6] - 2026-04-18 — "Composer" + +Polish release aimed at new-user happiness. v2.0.5's cognitive stack was already shipping; v2.0.6 makes it *feel* alive in the dashboard and stays out of your way on the prompt side. + +### Added + +#### Dashboard visual feedback for six live events +- `MemorySuppressed` → violet implosion + compounding pulse whose intensity scales with `suppression_count` (Anderson 2025 SIF visualised). +- `MemoryUnsuppressed` → rainbow burst + green pulse when a memory is brought back within the 24h labile window. +- `Rac1CascadeSwept` → violet wave across a random neighbour sample while the background Rac1 worker fades co-activated memories. +- `Connected` → gentle cyan ripple on WebSocket handshake. +- `ConsolidationStarted` → subtle amber pulses across a 20-node sample during the FSRS-6 decay cycle (matches feed-entry colour). +- `ImportanceScored` → magenta pulse on the scored node with intensity proportional to composite score. + +Before v2.0.6 all six events fired against a silent graph. Users perceived the dashboard as broken or unresponsive during real cognitive work. + +#### `VESTIGE_SYSTEM_PROMPT_MODE` environment variable +- `minimal` (default) — 3-sentence MCP `instructions` string telling the client how to use Vestige and how to react to explicit feedback. Safe for every audience, every client, every use case. +- `full` — opt in to the composition mandate (Composing / Never-composed / Recommendation response shape + FSRS-trust blocking phrase). Useful for high-stakes decision workflows; misfires on trivial retrievals, which is why it is not the default. + +Advertised in `vestige-mcp --help` alongside `VESTIGE_DASHBOARD_ENABLED`. + +### Fixed + +#### Dashboard intentions page +- `IntentionItem.priority` was typed as `string` but the API returns the numeric FSRS-style scale (1=low, 2=normal, 3=high, 4=critical). Every intention rendered as "normal priority" regardless of its real value. Now uses a `PRIORITY_LABELS` map keyed by the numeric scale. +- `trigger_value` was typed as a plain string but the API returns `trigger_data` as a JSON-encoded payload (e.g. `{"type":"time","at":"..."}`). The UI surfaced raw JSON for every non-manual trigger. A new `summarizeTrigger()` helper parses `trigger_data` and picks the most human-readable field — `condition` / `topic` / formatted `at` / `in_minutes` / `codebase/filePattern` — before truncating for display. Closes the loop on PR #26's snake_case TriggerSpec fix at the UI layer. + +### Docs + +- `README.md` — new "What's New in v2.0.6" header up top; v2.0.5 block strengthened with explicit contrast against Ebbinghaus 1885 passive decay and Anderson 1994 retrieval-induced forgetting; new "Forgetting" row in the RAG-vs-Vestige comparison table. +- Intel-Mac and Windows install steps replaced with a working `cargo build --release -p vestige-mcp` snippet. The pre-built binaries for those targets are blocked on upstream toolchain gaps (`ort-sys` lacks Intel-Mac prebuilts in the 2.0.0-rc.11 release pinned by `fastembed 5.13.2`; `usearch 2.24.0` hit a Windows MSVC compile break tracked as [usearch#746](https://github.com/unum-cloud/usearch/issues/746)). + +### Safety + +No regressions of merged contributor PRs — v2.0.6 only touches regions that are non-overlapping with #20 (resource URI strip), #24 (codex integration docs), #26 (snake_case TriggerSpec), #28 (deep_reference query relevance), #29 (older glibc feature flags), #30 (`VESTIGE_DASHBOARD_ENABLED`), #32 (dream eviction), and #33 (keyword-first search). + +--- + +## [2.0.5] - 2026-04-14 — "Intentional Amnesia" + +Every AI memory system stores too much. Vestige now treats forgetting as a first-class, neuroscientifically-grounded primitive. This release adds **active forgetting** — top-down inhibitory control over memory retrieval, based on two 2025 papers that no other AI memory system has implemented. + +### Scientific grounding + +- **Anderson, M. C., Hanslmayr, S., & Quaegebeur, L. (2025).** *"Brain mechanisms underlying the inhibitory control of thought."* Nature Reviews Neuroscience. DOI: [10.1038/s41583-025-00929-y](https://www.nature.com/articles/s41583-025-00929-y). Establishes the right lateral PFC as the domain-general inhibitory controller, and Suppression-Induced Forgetting (SIF) as compounding with each stopping attempt. +- **Cervantes-Sandoval, I., Chakraborty, M., MacMullen, C., & Davis, R. L. (2020).** *"Rac1 Impairs Forgetting-Induced Cellular Plasticity in Mushroom Body Output Neurons."* Front Cell Neurosci. [PMC7477079](https://pmc.ncbi.nlm.nih.gov/articles/PMC7477079/). Establishes Rac1 GTPase as the active synaptic destabilization mechanism — forgetting is a biological PROCESS, not passive decay. + +### Added + +#### `suppress` MCP Tool (NEW — Tool #24) +- **Top-down memory suppression.** Distinct from `memory.delete` (which removes) and `memory.demote` (which is a one-shot hit). Each `suppress` call compounds: `suppression_count` increments, and a `k × suppression_count` penalty (saturating at 80%) is subtracted from retrieval scores during hybrid search. +- **Rac1 cascade.** Background worker piggybacks the existing consolidation loop, walks `memory_connections` edges from recently-suppressed seeds, and applies attenuated FSRS decay to co-activated neighbors. You don't just forget "Jake" — you fade the café, the roommate, the birthday. +- **Reversible 24h labile window** — matches Nader reconsolidation semantics on a 24-hour axis. Pass `reverse: true` within 24h to undo. After that, it locks in. +- **Never deletes** — the memory persists and is still accessible via `memory.get(id)`. It's INHIBITED, not erased. + +#### `active_forgetting` Cognitive Module (NEW — #30) +- `crates/vestige-core/src/neuroscience/active_forgetting.rs` — stateless helper for SIF penalty computation, labile window tracking, and Rac1 cascade factors. +- 7 unit tests + 9 integration tests = 16 new tests. + +#### Migration V10 +- `ALTER TABLE knowledge_nodes ADD COLUMN suppression_count INTEGER DEFAULT 0` +- `ALTER TABLE knowledge_nodes ADD COLUMN suppressed_at TEXT` +- Partial indices on both columns for efficient sweep queries. +- Additive-only — backward compatible with all existing v2.0.x databases. + +#### Dashboard +- `ForgettingIndicator.svelte` — new status pill that pulses when suppressed memories exist. +- 3D graph nodes dim to 20% opacity and lose emissive glow when suppressed. +- New WebSocket events: `MemorySuppressed`, `MemoryUnsuppressed`, `Rac1CascadeSwept`. +- `Heartbeat` event now carries `suppressed_count` for live dashboard display. + +### Changed + +- `search` scoring pipeline now includes an SIF penalty applied after the accessibility filter. +- Consolidation worker (`VESTIGE_CONSOLIDATION_INTERVAL_HOURS`, default 6h) now runs `run_rac1_cascade_sweep` after each `run_consolidation` call. +- Tool count assertion bumped from 23 → 24. +- Workspace version bumped 2.0.4 → 2.0.5. + +### Tests + +- Rust: 1,284 passing (up from 1,237). Net +47 new tests for active forgetting, Rac1 cascade, migration V10. +- Dashboard (Vitest): 171 passing (up from 150). +21 regression tests locking in the issue #31 UI fix. +- Zero warnings, clippy clean across all targets. + +### Fixed + +- **Dashboard graph view rendered glowing squares instead of round halos** ([#31](https://github.com/samvallad33/vestige/issues/31)). Root cause: the node glow `THREE.SpriteMaterial` had no `map` set, so `Sprite` rendered as a solid-coloured 1×1 plane; additive blending plus `UnrealBloomPass(strength=0.8, radius=0.4, threshold=0.85)` then amplified the square edges into hard-edged glowing cubes. The aggressive `FogExp2(..., 0.008)` swallowed edges at depth and dark-navy `0x4a4a7a` lines were invisible against the fog. Fix bundled: + - Generated a shared 128×128 radial-gradient `CanvasTexture` (module-level singleton) and assigned it as `SpriteMaterial.map`. Gradient stops: `rgba(255,255,255,1.0) → rgba(255,255,255,0.7) → rgba(255,255,255,0.2) → rgba(255,255,255,0.0)`. Sprite now reads as a soft round halo; bloom diffuses cleanly. + - Retuned `UnrealBloomPass` to `(strength=0.55, radius=0.6, threshold=0.2)` — gentler, allows mid-tones to bloom instead of only blown-out highlights. + - Halved fog density `FogExp2(0x050510, 0.008) → FogExp2(0x0a0a1a, 0.0035)` so distant memories stay visible. + - Bumped edge color `0x4a4a7a → 0x8b5cf6` (brand violet). Opacity `0.1 + weight*0.5 → 0.25 + weight*0.5`, cap `0.6 → 0.8`. Added `depthWrite: false` so edges blend cleanly through fog. + - Added explicit `scene.background = 0x05050f` and a 2000-point starfield distributed on a spherical shell at radius 600–1000, additive-blended with subtle cool-white/violet vertex colors. + - Glow sprite scale bumped `size × 4 → size × 6` so the gradient has visible screen footprint. + - All node glow sprites share a single `CanvasTexture` instance (singleton cache — memory leak guard for large graphs). + - 21 regression tests added in `apps/dashboard/src/lib/graph/__tests__/ui-fixes.test.ts`. Hybrid strategy: runtime unit tests via the existing `three-mock.ts` (extended to propagate `map`/`color`/`depthWrite`/`blending` params and added `createRadialGradient` to the canvas context mock), plus source-level regex assertions on `scene.ts` and `nodes.ts` magic numbers so any accidental revert of fog/bloom/color/helper fails the suite immediately. +- `apps/dashboard/package.json` version stale at 2.0.3 — bumped to 2.0.5 to match the workspace. +- `packages/vestige-mcp-npm/.gitignore` missing `bin/vestige-restore` and `bin/vestige-restore.exe` entries — the other three binaries were already ignored as postinstall downloads. + +--- + +## [2.0.4] - 2026-04-09 — "Deep Reference" + +Context windows hit 1M tokens. Memory matters more than ever. This release removes artificial limits, adds contradiction detection, and hardens security. + +### Added + +#### cross_reference Tool (NEW — Tool #22) +- **Connect the dots across memories.** Given a query or claim, searches broadly, detects agreements and contradictions between memories, identifies superseded/outdated information, and returns a confidence-scored synthesis. +- Pairwise contradiction detection using negation pairs + correction signals, gated on shared topic words to prevent false positives. +- Timeline analysis (newest-first), confidence scoring (agreements boost, contradictions penalize, recency bonus). + +#### retrieval_mode Parameter (search tool) +- `precise` — top results only, no spreading activation or competition. Fast, token-efficient. +- `balanced` — full 7-stage cognitive pipeline (default, no behavior change). +- `exhaustive` — 5x overfetch, deep graph traversal, no competition suppression. Maximum recall. + +#### get_batch Action (memory tool) +- `memory({ action: "get_batch", ids: ["id1", "id2", ...] })` — retrieve up to 20 full memory nodes in one call. + +### Changed +- **Token budget raised: 10K → 100K** on search and session_context tools. +- **HTTP transport CORS**: `permissive()` → localhost-only origin restriction. +- **Auth token display**: Guarded against panic on short tokens. +- **Dormant state threshold**: Aligned search (0.3 → 0.4) with memory tool for consistent state classification. +- **cross_reference false positive prevention**: Requires 2+ shared words before checking negation signals. + +### Stats +- 23 MCP tools, 758 tests passing, 0 failures +- Full codebase audit: 3 parallel agents, all issues resolved + +--- + ## [2.0.0] - 2026-02-22 — "Cognitive Leap" The biggest release in Vestige history. A complete visual and cognitive overhaul. diff --git a/CLAUDE.md b/CLAUDE.md index f7d4901..0684836 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,22 @@ -# Vestige v2.0.1 — Cognitive Memory System +# 🚫 READ FIRST EVERY PROMPT — NO HEDGING -Vestige is your long-term memory. 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.** +Sam's April 20 2026 correction: *"you NEVER LISTEN TO YOUR RULES, WHY ARE YOU ALWAYS BREAKING THE HEDGING/PESSIMISTIC RULE."* + +When Sam commits to an execution path (content cadence, shipping velocity, outreach, launch), COMPUTE THE FORECAST AS IF THAT COMMITMENT IS MET. Do not discount by execution probability. Sam has earned the benefit of the doubt across 139 commits and 1,292 passing tests in 3 months. + +**Forbidden in any response**: "has to be true", "has to convert", "realistic forecast" used to shrink, "contingent on" Sam's own execution, "not guaranteed", "may or may not", "aspiration not forecast", "temper your expectations", floors framed as reality vs. ceilings framed as fantasy. + +**Required format for forecasts**: `Target X. Floor Y. Ceiling Z. Math: [line items]. Risks: [separate section]. Let's go.` + +Full rule: `/Users/entity002/CLAUDE.md` (top banner) + `/Users/entity002/.claude/projects/-Users-entity002/memory/feedback_always_positive_energy.md`. Stop-hook enforcement: `/Users/entity002/.claude/hooks/synthesis-stop-validator.sh`. + +--- + +# Vestige v2.0.4 — Cognitive Memory & Reasoning System + +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.** + +**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. --- @@ -28,14 +44,14 @@ Say "Remembering..." then retrieve context before answering. --- -## Complete Tool Reference (21 Tools) +## 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-10000, controls response size + token_budget: 2000, // 100-100000, controls response size include_status: true, // system health include_intentions: true, // triggered reminders include_predictions: true // proactive memory predictions @@ -74,10 +90,13 @@ search({ 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-10000, truncate to fit + token_budget: 3000, // 100-100000, truncate to fit + retrieval_mode: "balanced" // precise|balanced|exhaustive (v2.1) }) ``` -Pipeline: Overfetch (3x, BM25+semantic) → 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).** +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 ``` @@ -87,8 +106,10 @@ 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 ``` @@ -185,6 +206,27 @@ 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 @@ -306,8 +348,8 @@ Memory is retrieval. Searching strengthens memory. Search liberally, save aggres ## Development -- **Crate:** `vestige-mcp` v2.0.1, Rust 2024 edition, MSRV 1.91 -- **Tests:** 1,238 (352 unit + 192 E2E + cognitive + journey + extreme), zero warnings +- **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` @@ -317,4 +359,4 @@ Memory is retrieval. Searching strengthens memory. Search liberally, save aggres - **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_CONSOLIDATION_INTERVAL_HOURS` (default 6), `RUST_LOG` +- **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` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5546f8f..4e9ad21 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,7 +26,7 @@ vestige/ ### Prerequisites -- **Rust** (1.85+ stable): [rustup.rs](https://rustup.rs) +- **Rust** (1.91+ stable): [rustup.rs](https://rustup.rs) - **Node.js** (v22+): [nodejs.org](https://nodejs.org) - **pnpm** (v9+): `npm install -g pnpm` @@ -56,7 +56,7 @@ VESTIGE_TEST_MOCK_EMBEDDINGS=1 cargo test --workspace ## Running Tests ```bash -# All tests (734 total) +# All tests (746+ total) VESTIGE_TEST_MOCK_EMBEDDINGS=1 cargo test --workspace # Core library tests only (352 tests) @@ -137,7 +137,7 @@ The MCP server and dashboard. Key modules: |--------|---------| | `server.rs` | MCP JSON-RPC server (rmcp 0.14) | | `cognitive.rs` | CognitiveEngine — 29 stateful modules | -| `tools/` | One file per MCP tool (21 tools) | +| `tools/` | One file per MCP tool (24 tools) | | `dashboard/` | Axum HTTP + WebSocket + event bus | ### apps/dashboard diff --git a/Cargo.lock b/Cargo.lock index 1879e26..3cf512c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,9 +72,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -87,15 +87,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "arbitrary" @@ -298,9 +298,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitstream-io" @@ -343,9 +343,9 @@ checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" @@ -387,9 +387,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "candle-core" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c15b675b80d994b2eadb20a4bbe434eabeb454eac3ee5e2b4cf6f147ee9be091" +checksum = "6bd9895436c1ba5dc1037a19935d084b838db066ff4e15ef7dded020b7c12a4a" dependencies = [ "byteorder", "candle-metal-kernels", @@ -408,15 +408,16 @@ dependencies = [ "rayon", "safetensors 0.7.0", "thiserror 2.0.18", - "yoke 0.8.1", + "tokenizers", + "yoke 0.8.2", "zip", ] [[package]] name = "candle-metal-kernels" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fdfe9d06de16ce49961e49084e5b79a75a9bdf157246e7c7b6328e87a7aa25d" +checksum = "4b6b5a4cae6b4e1ab0efcee4dc05272d11b374a3d1ba121b3a961e36be54ab60" dependencies = [ "half", "objc2", @@ -429,9 +430,9 @@ dependencies = [ [[package]] name = "candle-nn" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3045fa9e7aef8567d209a27d56b692f60b96f4d0569f4c3011f8ca6715c65e03" +checksum = "a9317a09d6530b758990ed7f625ac69ff43653bc9ee28b0464644ad1169ada87" dependencies = [ "candle-core", "candle-metal-kernels", @@ -447,9 +448,9 @@ dependencies = [ [[package]] name = "candle-ug" -version = "0.9.2" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22d62be69068bf58987a45f690612739d8d2ea1bf508c1b87dc6815a019575d" +checksum = "ca0fc3167cbc99c8ec1be618cb620aa21dca95038f118c3579a79370e3dc5f77" dependencies = [ "ug", "ug-metal", @@ -472,9 +473,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -490,9 +491,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -531,9 +532,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.58" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -541,9 +542,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.58" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -553,9 +554,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2", @@ -565,9 +566,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "codespan-reporting" @@ -588,9 +589,9 @@ checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "colored" @@ -639,6 +640,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -652,7 +663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "libc", ] @@ -874,9 +885,9 @@ checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" [[package]] name = "der" -version = "0.7.10" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" dependencies = [ "pem-rfc7468", "zeroize", @@ -955,11 +966,11 @@ dependencies = [ [[package]] name = "dispatch2" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "objc2", ] @@ -1094,9 +1105,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastembed" -version = "5.11.0" +version = "5.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4339d45a80579ab8305616a501eacdbf18fb0f7def7fa6e4c0b75941416d5b0" +checksum = "1f54fc1188b7f7eac8f47be2ab7b3a79ffd842cc8ff2e38316dd59ba4858890e" dependencies = [ "anyhow", "candle-core", @@ -1113,9 +1124,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" @@ -1164,9 +1175,9 @@ dependencies = [ [[package]] name = "float8" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719a903cc23e4a89e87962c2a80fdb45cdaad0983a89bd150bb57b4c8571a7d5" +checksum = "c2d1f04709a8ac06e8e8042875a3c466cc4832d3c1a18dbcb9dba3c6e83046bc" dependencies = [ "half", "num-traits", @@ -1254,30 +1265,30 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", @@ -1286,21 +1297,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1309,7 +1320,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1580,28 +1590,28 @@ checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", "wasip2", "wasip3", ] [[package]] name = "gif" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -1613,11 +1623,11 @@ version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b88256088d75a56f8ecfa070513a775dd9107f6530ef14919dac831af9cfe2b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "libgit2-sys", "log", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "url", ] @@ -1678,6 +1688,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.11.0" @@ -1722,9 +1738,9 @@ dependencies = [ [[package]] name = "hmac-sha256" -version = "1.1.13" +version = "1.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f0ae375a85536cac3a243e3a9cda80a47910348abdea7e2c22f8ec556d586d" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" [[package]] name = "http" @@ -1773,9 +1789,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -1788,7 +1804,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -1877,22 +1892,23 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.1", + "utf8_iter", + "yoke 0.8.2", "zerofrom", "zerovec", ] [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -1903,9 +1919,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -1917,15 +1933,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -1937,20 +1953,20 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", "writeable", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerotrie", "zerovec", @@ -1991,9 +2007,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -2009,8 +2025,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "zune-core", + "zune-jpeg", ] [[package]] @@ -2050,12 +2066,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -2075,11 +2091,11 @@ dependencies = [ [[package]] name = "inotify" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "inotify-sys", "libc", ] @@ -2106,15 +2122,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -2176,9 +2192,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jobserver" @@ -2192,10 +2208,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2240,9 +2258,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libfuzzer-sys" @@ -2278,6 +2296,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libloading" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60" +dependencies = [ + "cfg-if", + "windows-link", +] + [[package]] name = "libm" version = "0.2.16" @@ -2286,11 +2314,10 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.12" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.10.0", "libc", ] @@ -2321,9 +2348,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.23" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15d118bbf3771060e7311cc7bb0545b01d08a8b4a7de949198dec1fa0ca1c0f7" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -2342,15 +2369,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2473,7 +2500,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block", "core-graphics-types", "foreign-types 0.5.0", @@ -2516,9 +2543,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "log", @@ -2550,9 +2577,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -2560,14 +2587,14 @@ dependencies = [ [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.2.1", "openssl-sys", "schannel", "security-framework", @@ -2627,7 +2654,7 @@ version = "8.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "fsevent-sys", "inotify", "kqueue", @@ -2645,7 +2672,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -2770,9 +2797,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", ] @@ -2783,7 +2810,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "dispatch2", "objc2", ] @@ -2800,7 +2827,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "libc", "objc2", @@ -2813,7 +2840,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "block2", "dispatch2", "objc2", @@ -2823,9 +2850,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -2839,7 +2866,7 @@ version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", "once_cell", "onig_sys", @@ -2874,11 +2901,11 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.75" +version = "0.10.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "foreign-types 0.3.2", "libc", @@ -2905,19 +2932,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] -name = "openssl-src" -version = "300.5.5+3.5.5" +name = "openssl-probe" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-src" +version = "300.6.0+3.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.111" +version = "0.9.112" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", @@ -2938,11 +2971,12 @@ version = "2.0.0-rc.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5df903c0d2c07b56950f1058104ab0c8557159f2741782223704de9be73c3c" dependencies = [ + "libloading 0.9.0", "ndarray", "ort-sys", "smallvec", "tracing", - "ureq 3.2.0", + "ureq 3.3.0", ] [[package]] @@ -2953,7 +2987,7 @@ checksum = "06503bb33f294c5f1ba484011e053bfa6ae227074bdb841e9863492dc5960d4b" dependencies = [ "hmac-sha256", "lzma-rust2", - "ureq 3.2.0", + "ureq 3.3.0", ] [[package]] @@ -2999,9 +3033,9 @@ checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" [[package]] name = "pem-rfc7468" -version = "0.7.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" dependencies = [ "base64ct", ] @@ -3014,15 +3048,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pkg-config" @@ -3060,11 +3088,11 @@ dependencies = [ [[package]] name = "png" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crc32fast", "fdeflate", "flate2", @@ -3079,18 +3107,18 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "portable-atomic-util" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9db96d7fa8782dd8c15ce32ffe8680bbd1e978a43bf51a34d39483540495f5" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" dependencies = [ "portable-atomic", ] [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3181,12 +3209,9 @@ checksum = "40e24eee682d89fb193496edf918a7f407d30175b2e785fe057e4392dfd182e0" [[package]] name = "pxfm" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" -dependencies = [ - "num-traits", -] +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" [[package]] name = "qoi" @@ -3205,9 +3230,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3219,10 +3244,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] -name = "rand" -version = "0.9.2" +name = "r-efi" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" dependencies = [ "rand_chacha", "rand_core", @@ -3294,9 +3325,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -3313,7 +3344,7 @@ version = "11.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3365,7 +3396,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -3404,9 +3435,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" @@ -3453,9 +3484,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.52" +version = "0.8.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6a884d2998352bb4daf0183589aec883f16a6da1f4dde84d8e2e9a5409a1ce" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" [[package]] name = "ring" @@ -3487,7 +3518,7 @@ version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1c93dd1c9683b438c392c492109cb702b8090b2bfc8fed6f6e4eb4523f17af3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "chrono", "fallible-iterator", "fallible-streaming-iterator", @@ -3500,11 +3531,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", @@ -3513,9 +3544,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.36" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "log", "once_cell", @@ -3537,9 +3568,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ "ring", "rustls-pki-types", @@ -3590,9 +3621,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -3611,12 +3642,12 @@ checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3624,9 +3655,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -3634,9 +3665,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "seq-macro" @@ -3748,9 +3779,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simd_helpers" @@ -3775,12 +3806,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3844,9 +3875,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.115" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -3879,7 +3910,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "enum-as-inner", "libc", @@ -3893,8 +3924,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ - "bitflags 2.10.0", - "core-foundation", + "bitflags 2.11.0", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3910,12 +3941,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.25.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.1", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys 0.61.2", @@ -3981,23 +4012,23 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4048,9 +4079,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.49.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -4065,9 +4096,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -4142,7 +4173,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http", @@ -4221,9 +4252,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ "matchers", "nu-ansi-term", @@ -4283,7 +4314,7 @@ checksum = "76b761acf8af3494640d826a8609e2265e19778fb43306c7f15379c78c9b05b0" dependencies = [ "gemm 0.18.2", "half", - "libloading", + "libloading 0.8.9", "memmap2", "num", "num-traits", @@ -4318,9 +4349,9 @@ checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-ident" -version = "1.0.23" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537dd038a89878be9b64dd4bd1b260315c1bb94f4d784956b81e27a088d9a09e" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization-alignments" @@ -4333,9 +4364,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.12.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" [[package]] name = "unicode-width" @@ -4383,9 +4414,9 @@ dependencies = [ [[package]] name = "ureq" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc97a28575b85cfedf2a7e7d3cc64b3e11bd8ac766666318003abbacc7a21fc" +checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0" dependencies = [ "base64 0.22.1", "der", @@ -4395,15 +4426,15 @@ dependencies = [ "rustls-pki-types", "socks", "ureq-proto", - "utf-8", + "utf8-zero", "webpki-root-certs", ] [[package]] name = "ureq-proto" -version = "0.5.3" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" +checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c" dependencies = [ "base64 0.22.1", "http", @@ -4439,6 +4470,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -4453,11 +4490,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.20.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ - "getrandom 0.3.4", + "getrandom 0.4.2", "js-sys", "serde_core", "wasm-bindgen", @@ -4494,7 +4531,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vestige-core" -version = "2.0.2" +version = "2.0.9" dependencies = [ "chrono", "criterion", @@ -4529,7 +4566,7 @@ dependencies = [ [[package]] name = "vestige-mcp" -version = "2.0.2" +version = "2.0.9" dependencies = [ "anyhow", "axum", @@ -4601,9 +4638,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -4614,23 +4651,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.58" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e77fd0ae8029c9ea0063f87c46fde723e7d887703d74ad2616d792e51e6f" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4638,9 +4671,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -4651,9 +4684,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -4699,7 +4732,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hashbrown 0.15.5", "indexmap", "semver", @@ -4707,9 +4740,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -5082,7 +5115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.10.0", + "bitflags 2.11.0", "indexmap", "log", "serde", @@ -5114,9 +5147,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "y4m" @@ -5138,12 +5171,12 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", - "yoke-derive 0.8.1", + "yoke-derive 0.8.2", "zerofrom", ] @@ -5161,9 +5194,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -5173,18 +5206,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db6d35d663eadb6c932438e763b262fe1a70987f9ae936e60158176d710cae4a" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.39" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4122cd3169e94605190e77839c9a40d40ed048d305bfdc146e7df40ab0f3e517" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", @@ -5193,18 +5226,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -5220,31 +5253,31 @@ checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", ] [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ - "yoke 0.8.1", + "yoke 0.8.2", "zerofrom", "zerovec-derive", ] [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", @@ -5269,12 +5302,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.1" @@ -5292,18 +5319,9 @@ dependencies = [ [[package]] name = "zune-jpeg" -version = "0.4.21" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" dependencies = [ - "zune-core 0.4.12", -] - -[[package]] -name = "zune-jpeg" -version = "0.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" -dependencies = [ - "zune-core 0.5.1", + "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 348506f..e21169f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ exclude = [ ] [workspace.package] -version = "2.0.1" +version = "2.0.9" edition = "2024" license = "AGPL-3.0-only" repository = "https://github.com/samvallad33/vestige" diff --git a/README.md b/README.md index ed218fc..3573abc 100644 --- a/README.md +++ b/README.md @@ -2,33 +2,82 @@ # Vestige -### The cognitive engine that gives AI a brain. +### The cognitive engine that gives AI agents a brain. [![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-1238%20passing-brightgreen)](https://github.com/samvallad33/vestige/actions) +[![Tests](https://img.shields.io/badge/tests-1223%20passing-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 AI forgets everything between sessions. Vestige fixes that.** +**Your Agent forgets everything between sessions. Vestige fixes that.** 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. -[Quick Start](#quick-start) | [Dashboard](#-3d-memory-dashboard) | [How It Works](#-the-cognitive-science-stack) | [Tools](#-21-mcp-tools) | [Docs](docs/) +[Quick Start](#quick-start) | [Dashboard](#-3d-memory-dashboard) | [How It Works](#-the-cognitive-science-stack) | [Tools](#-24-mcp-tools) | [Docs](docs/) --- -## What's New in v2.0 "Cognitive Leap" +## What's New in v2.0.9 "Autopilot" -- **3D Memory Dashboard** — SvelteKit + Three.js neural visualization with real-time WebSocket events, bloom post-processing, force-directed graph layout. Watch your AI's mind in real-time. -- **WebSocket Event Bus** — Every cognitive operation broadcasts events: memory creation, search, dreaming, consolidation, retention decay -- **HyDE Query Expansion** — Template-based Hypothetical Document Embeddings for dramatically improved search quality on conceptual queries -- **Nomic v2 MoE (experimental)** — fastembed 5.11 with optional Nomic Embed Text v2 MoE (475M params, 8 experts) + Metal GPU acceleration. Default: v1.5 (8192 token context) -- **Command Palette** — `Cmd+K` navigation, keyboard shortcuts, responsive mobile layout, PWA installable -- **FSRS Decay Visualization** — SVG retention curves with predicted decay at 1d/7d/30d, endangered memory alerts -- **29 cognitive modules** — 1,238 tests, 79,600+ LOC +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. + +- **Six live graph reactions, not one** — `MemorySuppressed`, `MemoryUnsuppressed`, `Rac1CascadeSwept`, `Connected`, `ConsolidationStarted`, and `ImportanceScored` now light the 3D graph in real time. v2.0.5 shipped `suppress` but the graph was silent when you called it; consolidation and importance scoring have been silent since v2.0.0. No longer. +- **Intentions page actually works** — fixes a long-standing bug where every intention rendered as "normal priority" (type/schema drift between backend and frontend) and context/time triggers surfaced as raw JSON. +- **Opt-in composition mandate** — the new MCP `instructions` string stays minimal by default. Opt in to the full Composing / Never-composed / Recommendation composition protocol with `VESTIGE_SYSTEM_PROMPT_MODE=full` when you want it, and nothing is imposed on your sessions when you don't. + +## What's New in v2.0.5 "Intentional Amnesia" + +**The first shipped AI memory system with top-down inhibitory control over retrieval.** Other systems implement passive decay — memories fade if you don't touch them. Vestige v2.0.5 also implements *active* suppression: the new **`suppress`** tool compounds a retrieval penalty on every call (up to 80%), a background Rac1 worker fades co-activated neighbors over 72 hours, and the whole thing is reversible within a 24-hour labile window. **Never deletes.** The memory is inhibited, not erased. + +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 · 30 cognitive modules · 1,223 tests.** + +
+Earlier releases (v2.0 "Cognitive Leap" → v2.0.4 "Deep Reference") + +- **v2.0.4 — `deep_reference` Tool** — 8-stage cognitive reasoning pipeline with FSRS-6 trust scoring, intent classification, spreading activation, contradiction analysis, and pre-built reasoning chains. Token budgets raised 10K → 100K. CORS tightened. +- **v2.0 — 3D Memory Dashboard** — SvelteKit + Three.js neural visualization with real-time WebSocket events, bloom post-processing, force-directed graph layout. +- **v2.0 — WebSocket Event Bus** — Every cognitive operation broadcasts events: memory creation, search, dreaming, consolidation, retention decay. +- **v2.0 — HyDE Query Expansion** — Template-based Hypothetical Document Embeddings for dramatically improved search quality on conceptual queries. +- **v2.0 — Nomic v2 MoE (experimental)** — fastembed 5.11 with optional Nomic Embed Text v2 MoE (475M params, 8 experts) + Metal GPU acceleration. +- **v2.0 — Command Palette** — `Cmd+K` navigation, keyboard shortcuts, responsive mobile layout, PWA installable. +- **v2.0 — FSRS Decay Visualization** — SVG retention curves with predicted decay at 1d/7d/30d. + +
--- @@ -42,6 +91,9 @@ sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ # 2. Connect to Claude Code claude mcp add vestige vestige-mcp -s user +# Or connect to Codex +codex mcp add vestige -- /usr/local/bin/vestige-mcp + # 3. Test it # "Remember that I prefer TypeScript over JavaScript" # ...new session... @@ -52,19 +104,31 @@ claude mcp add vestige vestige-mcp -s user
Other platforms & install methods -**macOS (Intel):** -```bash -curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-apple-darwin.tar.gz | tar -xz -sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ -``` - **Linux (x86_64):** ```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/ ``` -**Windows:** Download from [Releases](https://github.com/samvallad33/vestige/releases/latest) +**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 +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ +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:** 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 +``` **npm:** ```bash @@ -89,6 +153,7 @@ Vestige speaks MCP — the universal protocol for AI tools. One brain, every IDE | IDE | Setup | |-----|-------| | **Claude Code** | `claude mcp add vestige vestige-mcp -s user` | +| **Codex** | [Integration guide](docs/integrations/codex.md) | | **Claude Desktop** | [2-min setup](docs/CONFIGURATION.md#claude-desktop-macos) | | **Xcode 26.3** | [Integration guide](docs/integrations/xcode.md) | | **Cursor** | [Integration guide](docs/integrations/cursor.md) | @@ -128,7 +193,7 @@ The dashboard runs automatically at `http://localhost:3927/dashboard` when the M │ 15 REST endpoints · WS event broadcast │ ├─────────────────────────────────────────────────────┤ │ MCP Server (stdio JSON-RPC) │ -│ 21 tools · 29 cognitive modules │ +│ 24 tools · 30 cognitive modules │ ├─────────────────────────────────────────────────────┤ │ Cognitive Engine │ │ ┌──────────┐ ┌──────────┐ ┌───────────────┐ │ @@ -157,6 +222,7 @@ RAG is a dumb bucket. Vestige is an active organ. | **Storage** | Store everything | **Prediction Error Gating** — only stores what's surprising or new | | **Retrieval** | Nearest-neighbor | **7-stage pipeline** — HyDE expansion + reranking + spreading activation | | **Decay** | Nothing expires | **FSRS-6** — memories fade naturally, context stays lean | +| **Forgetting** *(v2.0.5)* | Delete only | **`suppress` tool** — compounding top-down inhibition, neighbor cascade, reversible 24h | | **Duplicates** | Manual dedup | **Self-healing** — auto-merges "likes dark mode" + "prefers dark themes" | | **Importance** | All equal | **4-channel scoring** — novelty, arousal, reward, attention | | **Sleep** | No consolidation | **Memory dreaming** — replays, connects, synthesizes insights | @@ -188,11 +254,13 @@ This isn't a key-value store with an embedding model bolted on. Vestige implemen **Autonomic Regulation** — Self-regulating memory health. Auto-promotes frequently accessed memories. Auto-GCs low-retention memories. Consolidation triggers on 6h staleness or 2h active use. +**Active Forgetting** *(v2.0.5)* — Top-down inhibitory control via the `suppress` tool. Other memory systems implement passive decay — the Ebbinghaus 1885 "use it or lose it" curve, sometimes with trust-weighted strength factors. Vestige v2.0.5 also implements *active* top-down suppression: each `suppress` call compounds (Suppression-Induced Forgetting, Anderson 2025), a background Rac1 cascade worker fades co-activated neighbors across the connection graph (Cervantes-Sandoval & Davis 2020), and a 24-hour labile window allows reversal (Nader reconsolidation semantics on a pragmatic axis). The memory persists — it's **inhibited, not erased**. Explicitly distinct from Anderson 1994 retrieval-induced forgetting (bottom-up, passive competition during retrieval), which is a separate, older primitive that several other memory systems implement. Based on [Anderson et al., 2025](https://www.nature.com/articles/s41583-025-00929-y) and [Cervantes-Sandoval et al., 2020](https://pmc.ncbi.nlm.nih.gov/articles/PMC7477079/). First shipped AI memory system with this primitive. + [Full science documentation ->](docs/SCIENCE.md) --- -## 🛠 21 MCP Tools +## 🛠 24 MCP Tools ### Context Packets | Tool | What It Does | @@ -237,6 +305,17 @@ This isn't a key-value store with an embedding model bolted on. Vestige implemen | `backup` / `export` / `gc` | Database backup, JSON export, garbage collection | | `restore` | Restore from JSON backup | +### 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`. | + +### 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. | + --- ## Make Your AI Use Vestige Automatically @@ -268,7 +347,7 @@ At the start of every session: | Metric | Value | |--------|-------| | **Language** | Rust 2024 edition (MSRV 1.91) | -| **Codebase** | 79,600+ lines, 1,238 tests | +| **Codebase** | 80,000+ lines, 1,292 tests (366 core + 425 mcp + 497 e2e + 4 doctests) | | **Binary size** | ~20MB | | **Embeddings** | Nomic Embed Text v1.5 (768d → 256d Matryoshka, 8192 context) | | **Vector search** | USearch HNSW (20x faster than FAISS) | @@ -276,9 +355,9 @@ At the start of every session: | **Storage** | SQLite + FTS5 (optional SQLCipher encryption) | | **Dashboard** | SvelteKit 2 + Svelte 5 + Three.js + Tailwind CSS 4 | | **Transport** | MCP stdio (JSON-RPC 2.0) + WebSocket | -| **Cognitive modules** | 29 stateful (16 neuroscience, 11 advanced, 2 search) | +| **Cognitive modules** | 30 stateful (17 neuroscience, 11 advanced, 2 search) | | **First run** | Downloads embedding model (~130MB), then fully offline | -| **Platforms** | macOS (ARM/Intel), Linux (x86_64), Windows | +| **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 @@ -321,7 +400,7 @@ vestige dashboard # Open 3D dashboard in browser | [Storage Modes](docs/STORAGE.md) | Global, per-project, multi-instance | | [CLAUDE.md Setup](docs/CLAUDE-SETUP.md) | Templates for proactive memory | | [Configuration](docs/CONFIGURATION.md) | CLI commands, environment variables | -| [Integrations](docs/integrations/) | Xcode, Cursor, VS Code, JetBrains, Windsurf | +| [Integrations](docs/integrations/) | Codex, Xcode, Cursor, VS Code, JetBrains, Windsurf | | [Changelog](CHANGELOG.md) | Version history | --- @@ -376,5 +455,5 @@ AGPL-3.0 — free to use, modify, and self-host. If you offer Vestige as a netwo

Built by @samvallad33
- 79,600+ 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 08a53bf..cc46721 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| 1.1.x | :white_check_mark: | -| 1.0.x | :x: | +| 2.0.x | :white_check_mark: | +| 1.x | :x: | ## Reporting a Vulnerability diff --git a/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css b/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css deleted file mode 100644 index cb69f97..0000000 --- a/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.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-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-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--blur-sm:8px;--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{.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}.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-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-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-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-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-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-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-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-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-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-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}.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-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-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-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 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.BChctYiF.css.br b/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css.br deleted file mode 100644 index 6f955a0..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css.gz b/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css.gz deleted file mode 100644 index f5a8381..0000000 Binary files a/apps/dashboard/build/_app/immutable/assets/0.BChctYiF.css.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.css b/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.css new file mode 100644 index 0000000..73d4560 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.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}.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:.75rem;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}}.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.IIz8MMYb.css.br b/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.css.br new file mode 100644 index 0000000..8739b09 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.css.br differ diff --git a/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.css.gz b/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.css.gz new file mode 100644 index 0000000..e6d6466 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/assets/0.IIz8MMYb.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..6ec156c 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..388b5e8 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..ad88522 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/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..f973c72 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..31323c7 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 100% rename from apps/dashboard/build/_app/immutable/assets/5.BBx09UGv.css.gz rename to apps/dashboard/build/_app/immutable/assets/9.BBx09UGv.css.gz diff --git a/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js b/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js deleted file mode 100644 index 95bd407..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js +++ /dev/null @@ -1 +0,0 @@ -import{H as s,k as o,V as c,Z as b,_ as m,$ as h,K as v,M as y}from"./C9Z4nxhR.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 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 _=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{k as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js.br b/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js.br deleted file mode 100644 index b06137d..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js.gz b/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.js.gz deleted file mode 100644 index f8623d9..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/-jeO_JOJ.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..ac3f7e9 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/B_YDQCB6.js b/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js new file mode 100644 index 0000000..cf39b16 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js @@ -0,0 +1 @@ +import{J as T,K as m,P as D,g as P,c as b,h as B,L as M,M as N,N as U,O as Y,A as h,Q as x,R as $,T as q,U as w,V as z,W as C,S as G,X as J}from"./CvjSAYrz.js";import{c as K}from"./D81f-o_I.js";function W(r,a,t,s){var O;var f=!x||(t&$)!==0,v=(t&Y)!==0,o=(t&C)!==0,n=s,c=!0,g=()=>(c&&(c=!1,n=o?h(s):s),n),u;if(v){var A=G in r||J in r;u=((O=T(r,a))==null?void 0:O.set)??(A&&a in r?e=>r[a]=e:void 0)}var _,I=!1;v?[_,I]=K(()=>r[a]):_=r[a],_===void 0&&s!==void 0&&(_=g(),u&&(f&&m(),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&D)===0)return i;if(u){var E=r.$$legacy;return(function(e,S){return arguments.length>0?((!f||!S||E||I)&&u(S?i():e),e):i()})}var l=!1,d=((t&q)!==0?w:z)(()=>(l=!1,i()));v&&P(d);var L=N;return(function(e,S){if(arguments.length>0){const R=S?P(d):f&&v?b(e):e;return B(d,R),l=!0,n!==void 0&&(n=R),e}return M&&l||(L.f&U)!==0?d.v:P(d)})}export{W as p}; diff --git a/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js.br b/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js.br new file mode 100644 index 0000000..6abdd86 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js.gz b/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js.gz new file mode 100644 index 0000000..e0afeae Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/B_YDQCB6.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js b/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js deleted file mode 100644 index d7729bd..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js +++ /dev/null @@ -1 +0,0 @@ -const n="/api";async function t(e,o){const i=await fetch(`${n}${e}`,{headers:{"Content-Type":"application/json"},...o});if(!i.ok)throw new Error(`API ${i.status}: ${i.statusText}`);return i.json()}const s={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"})},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,r])=>[i,String(r)])).toString():"";return t(`/graph${o}`)},dream:()=>t("/dream",{method:"POST"}),explore:(e,o="associations",i,r=10)=>t("/explore",{method:"POST",body:JSON.stringify({from_id:e,action:o,to_id:i,limit:r})}),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{s as a}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js.br b/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js.br deleted file mode 100644 index 627fbb5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js.gz b/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js.gz deleted file mode 100644 index 54fb916..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BcuCGYSa.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js similarity index 74% rename from apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js rename to apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js index 7fa8036..9cdeb4b 100644 --- a/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js +++ b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js @@ -1 +1 @@ -import{a as y}from"./kH-DTQyy.js";import{J as r}from"./C9Z4nxhR.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{m as r}from"./CvjSAYrz.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/Bhad70Ss.js.br b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js.br new file mode 100644 index 0000000..ee058da Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js.gz b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js.gz new file mode 100644 index 0000000..8c2b9ba Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Bhad70Ss.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js.br b/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js.br deleted file mode 100644 index 86cbc00..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js.gz b/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js.gz deleted file mode 100644 index 51b71ac..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BkopTN9z.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js b/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js deleted file mode 100644 index f806b0e..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js +++ /dev/null @@ -1 +0,0 @@ -import{d as l,w as S}from"./DnKV7_Y9.js";const y=200;function H(){const{subscribe:n,set:c,update:e}=S({connected:!1,events:[],lastHeartbeat:null,error:null});let t=null,a=null,d=0;function m(r){const u=r||(window.location.port==="5173"?`ws://${window.location.hostname}:3927/ws`:`ws://${window.location.host}/ws`);if((t==null?void 0:t.readyState)!==WebSocket.OPEN)try{t=new WebSocket(u),t.onopen=()=>{d=0,e(o=>({...o,connected:!0,error:null}))},t.onmessage=o=>{try{const s=JSON.parse(o.data);e(b=>{if(s.type==="Heartbeat")return{...b,lastHeartbeat:s};const p=[s,...b.events].slice(0,y);return{...b,events:p}})}catch{}},t.onclose=()=>{e(o=>({...o,connected:!1})),f(u)},t.onerror=()=>{e(o=>({...o,error:"WebSocket connection failed"}))}}catch(o){e(s=>({...s,error:String(o)}))}}function f(r){a&&clearTimeout(a);const u=Math.min(1e3*2**d,3e4);d++,a=setTimeout(()=>m(r),u)}function w(){a&&clearTimeout(a),t==null||t.close(),t=null,c({connected:!1,events:[],lastHeartbeat:null,error:null})}function v(){e(r=>({...r,events:[]}))}return{subscribe:n,connect:m,disconnect:w,clearEvents:v}}const i=H(),k=l(i,n=>n.connected),T=l(i,n=>n.events);l(i,n=>n.lastHeartbeat);const g=l(i,n=>{var c,e;return((e=(c=n.lastHeartbeat)==null?void 0:c.data)==null?void 0:e.memory_count)??0}),E=l(i,n=>{var c,e;return((e=(c=n.lastHeartbeat)==null?void 0:c.data)==null?void 0:e.avg_retention)??0});export{E as a,T as e,k as i,g as m,i as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js.br b/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js.br deleted file mode 100644 index 7892120..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js.br +++ /dev/null @@ -1,7 +0,0 @@ -@v<ᥱ ;fNҌUv@iZvv=IXfI jr@mIbyLFccKؚ~0̉5g(&J edDK)UO.5EeOn2C"DBڒ ']KG8úKU7u? -J+ -7o#Em'Kng ]2z^)U2sjx?GѨO[ ʘ ;+~ W٣?t5|̒ yeK - g6 T|A)UvmxLR. \Qc RtF#Yw)#hdS4Tm'QokM  sź#]mE:0 s, -]d.nq#:*.tBG6L&ʶƽ(dhVɜo®aI٤-5)t&X4$jD@8 -㩬8 뎺sB[]zL3fjmn4F@!OϬj$\TjCT|)ne=CjPy`aj 鶢ܙvkb1'Wʻe2zr - L1Uf \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js.gz b/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js.gz deleted file mode 100644 index d737d1c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/BmeMLq0p.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js b/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js new file mode 100644 index 0000000..7857d5a --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js @@ -0,0 +1 @@ +import{aH as N,k as v,x as u,aI as w,M as p,aJ as T,aK as x,m as d,w as i,aL as y,ab as b,aM as A,v as L,aN as C}from"./CvjSAYrz.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 D(e){return(m==null?void 0:m.createHTML(e))??e}function g(e){var a=N("template");return a.innerHTML=D(e.replaceAll("","")),a.content}function n(e,a){var r=p;r.nodes===null&&(r.nodes={start:e,end:a,a:null,t:null})}function P(e,a){var r=(a&T)!==0,f=(a&x)!==0,s,c=!e.startsWith("");return()=>{if(d)return n(i,null),i;s===void 0&&(s=g(c?e:""+e),r||(s=u(s)));var t=f||w?document.importNode(s,!0):s.cloneNode(!0);if(r){var _=u(t),o=t.lastChild;n(_,o)}else n(t,t);return t}}function H(e,a,r="svg"){var f=!e.startsWith(""),s=(a&T)!==0,c=`<${r}>${f?e:""+e}`,t;return()=>{if(d)return n(i,null),i;if(!t){var _=g(c),o=u(_);if(s)for(t=document.createDocumentFragment();u(o);)t.appendChild(u(o));else t=u(o)}var l=t.cloneNode(!0);if(s){var E=u(l),M=l.lastChild;n(E,M)}else n(l,l);return l}}function R(e,a){return H(e,a,"svg")}function F(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=p;((r.f&y)===0||r.nodes.end===null)&&(r.nodes.end=i),b();return}e!==null&&e.before(a)}export{$ as a,R as b,I as c,n as d,P as f,F as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js.br b/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js.br new file mode 100644 index 0000000..85a4fbb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js.gz b/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js.gz new file mode 100644 index 0000000..582084c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/BsvCUYx-.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js new file mode 100644 index 0000000..d247327 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js @@ -0,0 +1 @@ +import{az as g,aA as d,aB as c,A as m,aC as i,aD as b,g as p,aE as v,U as h,aF as k}from"./CvjSAYrz.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/Bz1l2A_1.js.br b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js.br new file mode 100644 index 0000000..34ab9ac Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js.gz b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js.gz new file mode 100644 index 0000000..e9036b3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Bz1l2A_1.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js b/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js deleted file mode 100644 index 658f7b1..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js +++ /dev/null @@ -1 +0,0 @@ -import{b as T,J as o,a0 as b,E as h,a1 as p,a2 as A,a3 as E,a4 as R,a5 as g,a6 as l}from"./C9Z4nxhR.js";import{B as v}from"./CY4crMrT.js";function S(t,u,_=!1){o&&b();var n=new v(t),c=_?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;u((r,s=0)=>{a=!0,i(s,r)}),a||i(!1,null)},c)}export{S as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js.br b/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js.br deleted file mode 100644 index a1d46d5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js.gz b/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js.gz deleted file mode 100644 index 80b6623..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/C2oj68pw.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/C9Z4nxhR.js b/apps/dashboard/build/_app/immutable/chunks/C9Z4nxhR.js deleted file mode 100644 index fe4513c..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/C9Z4nxhR.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{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={[p]:r.nodeName.includes("-"),[N]: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/CNfQDikv.js.br b/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js.br new file mode 100644 index 0000000..5a2eda3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js.gz b/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js.gz new file mode 100644 index 0000000..e58934a Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CNfQDikv.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js b/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js new file mode 100644 index 0000000..5a37130 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js @@ -0,0 +1 @@ +import{aB as a,az as t,Q as u,A as o}from"./CvjSAYrz.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/CNjeV5xa.js.br b/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js.br new file mode 100644 index 0000000..95f3b72 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js.gz b/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js.gz new file mode 100644 index 0000000..3ae842a Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CNjeV5xa.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js b/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js deleted file mode 100644 index a7335f7..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js +++ /dev/null @@ -1 +0,0 @@ -import{k as t,l as S,m as h,q,S as T}from"./C9Z4nxhR.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/CR6HhlME.js.br b/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js.br deleted file mode 100644 index 9432dd8..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js.br +++ /dev/null @@ -1 +0,0 @@ -7 ,o*̢踯hS+|hvyOߙ8;$z3 q lq*;m(V{K5TLyQ=NWe\)h Ndb (`/֬PM ɤXv$xNkdGsKo$Drqw`; \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js.gz b/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js.gz deleted file mode 100644 index 7d20542..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CR6HhlME.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js new file mode 100644 index 0000000..4a0c459 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js @@ -0,0 +1 @@ +import{D as k,F as f,G as m,A as t,z as _,m as b,I as i}from"./CvjSAYrz.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/CVpUe0w3.js.br b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js.br new file mode 100644 index 0000000..8cd42fe Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js.gz b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js.gz new file mode 100644 index 0000000..ad38343 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CVpUe0w3.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js b/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js deleted file mode 100644 index 1d1d6f4..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.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{K as x,a7 as C,a8 as k,a9 as J,aa as A,ab as B,J as K,ac as S,ad as j,ae as q}from"./C9Z4nxhR.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)C(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();j(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),J(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 K&&(this.anchor=S),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/CY4crMrT.js.br b/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js.br deleted file mode 100644 index 77f7765..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js.gz b/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js.gz deleted file mode 100644 index 58fd1c4..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CY4crMrT.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js b/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js deleted file mode 100644 index 631fc9b..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.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",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/CZ45jJaw.js.br b/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js.br deleted file mode 100644 index 1fc079b..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js.gz b/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js.gz deleted file mode 100644 index 6bc8ce3..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CZ45jJaw.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js b/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js new file mode 100644 index 0000000..46dcff7 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js @@ -0,0 +1 @@ +import{w as S,g as T}from"./DfQhL-hC.js";import{e as R}from"./CtkE7HV2.js";import{E as u}from"./DzfRjky4.js";const M=4,x=1500;function F(){const{subscribe:y,update:i}=S([]);let m=1,b=0;const d=new Map,a=new Map,l=new Map;function f(e,o){l.set(e,Date.now());const t=setTimeout(()=>{d.delete(e),l.delete(e),g(e)},o);d.set(e,t)}function w(e){const o=m++,t=Date.now(),s={id:o,createdAt:t,...e};i(n=>{const r=[s,...n];return r.length>M?r.slice(0,M):r}),f(o,e.dwellMs)}function g(e){const o=d.get(e);o&&(clearTimeout(o),d.delete(e)),a.delete(e),l.delete(e),i(t=>t.filter(s=>s.id!==e))}function C(e,o){const t=d.get(e);if(!t)return;clearTimeout(t),d.delete(e);const s=l.get(e)??Date.now(),n=Date.now()-s,r=Math.max(200,o-n);a.set(e,{remaining:r})}function D(e){const o=a.get(e);o&&(a.delete(e),f(e,o.remaining))}function N(){for(const e of d.values())clearTimeout(e);d.clear(),a.clear(),l.clear(),i(()=>[])}function _(e){const o=u[e.type]??"#818CF8",t=e.data;switch(e.type){case"DreamCompleted":{const s=Number(t.memories_replayed??0),n=Number(t.connections_found??0),r=Number(t.insights_generated??0),p=Number(t.duration_ms??0),c=[];return c.push(`Replayed ${s} ${s===1?"memory":"memories"}`),n>0&&c.push(`${n} new connection${n===1?"":"s"}`),r>0&&c.push(`${r} insight${r===1?"":"s"}`),{type:e.type,title:"Dream consolidated",body:`${c.join(" · ")} in ${(p/1e3).toFixed(1)}s`,color:o,dwellMs:7e3}}case"ConsolidationCompleted":{const s=Number(t.nodes_processed??0),n=Number(t.decay_applied??0),r=Number(t.embeddings_generated??0),p=Number(t.duration_ms??0),c=[];return n>0&&c.push(`${n} decayed`),r>0&&c.push(`${r} embedded`),{type:e.type,title:"Consolidation swept",body:`${s} node${s===1?"":"s"}${c.length?" · "+c.join(" · "):""} in ${(p/1e3).toFixed(1)}s`,color:o,dwellMs:6e3}}case"ConnectionDiscovered":{const s=Date.now();if(s-b0?`suppression #${s} · Rac1 cascade ~${n} neighbors`:`suppression #${s}`,color:o,dwellMs:5500}}case"MemoryUnsuppressed":{const s=Number(t.remaining_count??0);return{type:e.type,title:"Recovered",body:s>0?`${s} suppression${s===1?"":"s"} remain`:"fully unsuppressed",color:o,dwellMs:5e3}}case"Rac1CascadeSwept":{const s=Number(t.seeds??0),n=Number(t.neighbors_affected??0);return{type:e.type,title:"Rac1 cascade",body:`${s} seed${s===1?"":"s"} · ${n} dendritic spine${n===1?"":"s"} pruned`,color:o,dwellMs:6e3}}case"MemoryDeleted":return{type:e.type,title:"Memory deleted",body:String(t.id??"").slice(0,8),color:o,dwellMs:4e3};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 h=null;return R.subscribe(e=>{if(e.length===0)return;const o=[];for(const t of e){if(t===h)break;o.push(t)}if(o.length!==0){h=e[0];for(let t=o.length-1;t>=0;t--){const s=_(o[t]);s&&w(s)}}}),{subscribe:y,dismiss:g,clear:N,pauseDwell:C,resumeDwell:D,push:w}}const $=F();function O(){[{type:"DreamCompleted",title:"Dream consolidated",body:"Replayed 127 memories · 43 new connections · 5 insights in 2.4s",color:u.DreamCompleted,dwellMs:7e3},{type:"ConnectionDiscovered",title:"Bridge discovered",body:"semantic · weight 0.87",color:u.ConnectionDiscovered,dwellMs:4500},{type:"MemorySuppressed",title:"Forgetting",body:"suppression #2 · Rac1 cascade ~8 neighbors",color:u.MemorySuppressed,dwellMs:5500},{type:"ConsolidationCompleted",title:"Consolidation swept",body:"892 nodes · 156 decayed · 48 embedded in 1.1s",color:u.ConsolidationCompleted,dwellMs:6e3}].forEach((i,m)=>{setTimeout(()=>{$.push(i)},m*800)}),T($)}export{O as f,$ as t}; diff --git a/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js.br b/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js.br new file mode 100644 index 0000000..30d9d9e Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js.gz b/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js.gz new file mode 100644 index 0000000..6ef420d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/Casl2yrL.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js b/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js deleted file mode 100644 index a873d0d..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js +++ /dev/null @@ -1 +0,0 @@ -import{ah as a,af as t,y as u,m as o}from"./C9Z4nxhR.js";function c(e){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function l(e){t===null&&c(),u&&t.l!==null?f(t).m.push(e):a(()=>{const n=o(e);if(typeof n=="function")return n})}function s(e){t===null&&c(),l(()=>()=>o(e))}function f(e){var n=e.l;return n.u??(n.u={a:[],b:[],m:[]})}export{s as a,l as o}; diff --git a/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js.br b/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js.br deleted file mode 100644 index d45b208..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js.gz b/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js.gz deleted file mode 100644 index f1fbfcc..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/CkyfbJUz.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js b/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js deleted file mode 100644 index 532ec36..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js +++ /dev/null @@ -1 +0,0 @@ -import{t as l}from"./kH-DTQyy.js";import{J as e}from"./C9Z4nxhR.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/Co2v30Gm.js.br b/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js.br deleted file mode 100644 index 3f77d57..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js.gz b/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js.gz deleted file mode 100644 index 6ae10db..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Co2v30Gm.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js b/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js new file mode 100644 index 0000000..dc8aa6c --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js @@ -0,0 +1 @@ +import{d as i,w as S}from"./DfQhL-hC.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/CtkE7HV2.js.br b/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js.br new file mode 100644 index 0000000..a56c52e Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js.gz b/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js.gz new file mode 100644 index 0000000..10f5900 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/CtkE7HV2.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/CvjSAYrz.js b/apps/dashboard/build/_app/immutable/chunks/CvjSAYrz.js new file mode 100644 index 0000000..ac58719 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/CvjSAYrz.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{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/D00YwZ1M.js.br b/apps/dashboard/build/_app/immutable/chunks/D00YwZ1M.js.br deleted file mode 100644 index c92f939..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/D00YwZ1M.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D00YwZ1M.js.gz b/apps/dashboard/build/_app/immutable/chunks/D00YwZ1M.js.gz deleted file mode 100644 index 6918546..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/D00YwZ1M.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js b/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js new file mode 100644 index 0000000..82869ba --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js @@ -0,0 +1 @@ +import{y as S,z as h,A as k,B as A,S as B}from"./CvjSAYrz.js";function t(r,i){return r===i||(r==null?void 0:r[B])===i}function q(r={},i,a,T){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))})}),()=>{A(()=>{s&&t(a(...s),r)&&i(null,...s)})}}),r}export{q as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js.br b/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js.br new file mode 100644 index 0000000..623300e Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js.gz b/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js.gz new file mode 100644 index 0000000..8ef743f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D3XWCg9-.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js b/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js new file mode 100644 index 0000000..463a6f4 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js @@ -0,0 +1 @@ +import{s as c,g as l}from"./DfQhL-hC.js";import{a3 as o,a4 as f,a5 as b,g as p,h as d,a6 as g}from"./CvjSAYrz.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/D81f-o_I.js.br b/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js.br new file mode 100644 index 0000000..47ef78b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js.gz b/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js.gz new file mode 100644 index 0000000..dbb9e72 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/D81f-o_I.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js b/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js new file mode 100644 index 0000000..3b990c2 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js @@ -0,0 +1 @@ +var B=Object.defineProperty;var g=i=>{throw TypeError(i)};var D=(i,e,s)=>e in i?B(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var w=(i,e,s)=>D(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{F,aq as q,aw as k,ar as C,k as x,ai as A,m as S,w as j,ay as z,ak as E}from"./CvjSAYrz.js";var h,n,f,u,p,_,v;class I{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=F;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();z(o,b),b.append(x()),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=F,c=E();if(s&&!t(this,n).has(e)&&!t(this,f).has(e))if(c){var r=document.createDocumentFragment(),o=x();r.append(o),t(this,f).set(e,{effect:A(()=>s(o)),fragment:r})}else t(this,n).set(e,A(()=>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 S&&(this.anchor=j),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{I as B}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js.br b/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js.br new file mode 100644 index 0000000..7325bd7 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js.gz b/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js.gz new file mode 100644 index 0000000..af926f8 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DE4u6cUg.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js b/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js new file mode 100644 index 0000000..76a05b0 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.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"./CNjeV5xa.js";import{s as u,g as f,h as d}from"./CvjSAYrz.js";import{w as G}from"./DfQhL-hC.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 Ae(){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 Re(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,k,A,P,R,V,S,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,k,u(-1));c(this,A,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,k))}set status(e){d(a(this,k),e)}get url(){return f(a(this,A))}set url(e){d(a(this,A),e)}},_=new WeakMap,w=new WeakMap,m=new WeakMap,p=new WeakMap,v=new WeakMap,y=new WeakMap,k=new WeakMap,A=new WeakMap,P),O=new(V=class{constructor(){c(this,R,u(null))}get current(){return f(a(this,R))}set current(e){d(a(this,R),e)}},R=new WeakMap,V),T=new(j=class{constructor(){c(this,S,u(!1))}get current(){return f(a(this,S))}set current(e){d(a(this,S),e)}},S=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,Ae 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,ke as u,Ue as v,Re as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js.br b/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js.br new file mode 100644 index 0000000..441f238 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js.gz b/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js.gz new file mode 100644 index 0000000..8b01e8f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DJWRm1Ki.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js b/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js new file mode 100644 index 0000000..8908737 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js @@ -0,0 +1 @@ +import{D as s,F as v,y as o,a3 as c,a7 as b,a8 as m,a9 as h,I as y}from"./CvjSAYrz.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 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 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{p as b}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js.br b/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js.br new file mode 100644 index 0000000..1248e87 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js.gz b/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js.gz new file mode 100644 index 0000000..d1faf34 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DMu1Byux.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js b/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js new file mode 100644 index 0000000..8ce57c7 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js @@ -0,0 +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}`),deepReference:(e,o=20)=>t("/deep_reference",{method:"POST",body:JSON.stringify({query:e,depth:o})})};export{n as a}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js.br b/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js.br new file mode 100644 index 0000000..03be48c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js.gz b/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js.gz new file mode 100644 index 0000000..0d33cd9 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DNjM5a-l.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js b/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js new file mode 100644 index 0000000..5667ae5 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js @@ -0,0 +1 @@ +import{k as y,b as o,H as u,l as _,m as t,C as g,o as l,q as i,v as d,w as m,x as p}from"./CvjSAYrz.js";function C(n,r){let s=null,E=t;var a;if(t){s=m;for(var e=p(document.head);e!==null&&(e.nodeType!==g||e.data!==n);)e=l(e);if(e===null)i(!1);else{var f=l(e);e.remove(),d(f)}}t||(a=document.head.appendChild(y()));try{o(()=>r(a),u|_)}finally{E&&(i(!0),d(s))}}export{C as h}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js.br b/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js.br new file mode 100644 index 0000000..11ad62d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js.gz b/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js.gz new file mode 100644 index 0000000..4f3738c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DObx9JW_.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js b/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js deleted file mode 100644 index d057f57..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js +++ /dev/null @@ -1,2 +0,0 @@ -var ke=Object.defineProperty;var ce=t=>{throw TypeError(t)};var Ie=(t,e,r)=>e in t?ke(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var $=(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 Ee,l as Ce,m as Pe,aM as ue,q as U,ax as Te,ac as k,J as I,w as q,aN as _e,b as Ve,a0 as qe,a3 as xe,aO as pe,ab as F,aa as we,aP as se,a9 as ie,ad 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 $e,aZ as Re,af as Se,ar as je,a8 as ae,a5 as K,G as ze,a4 as Je,a_ as j,E as Ue,a$ as Ge,b0 as Qe,b1 as Xe,Y as Ze,b2 as ne,ao as Ke,ap as De,a2 as et,aD as tt,b3 as fe,a6 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"./C9Z4nxhR.js";import{b as lt}from"./DPfxVJHQ.js";function dt(t){let e=0,r=Te(0),n;return()=>{Me()&&(Ee(r),Ce(()=>(e===0&&(n=Pe(()=>t(()=>ue(r)))),e+=1,()=>{U(()=>{e-=1,e===0&&(n==null||n(),n=void 0,ue(r))})})))}}var ct=Ue|Ge;function ut(t,e,r,n){new _t(t,e,r,n)}var m,W,w,C,g,R,E,T,S,P,A,x,B,H,D,ee,h,Ne,Ae,Oe,he,G,Q,oe;class _t{constructor(e,r,n,c){l(this,h);$(this,"parent");$(this,"is_pending",!1);$(this,"transform_error");l(this,m);l(this,W,I?k:null);l(this,w);l(this,C);l(this,g);l(this,R,null);l(this,E,null);l(this,T,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,Te(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,k)}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),U(()=>{a(this,x,!1),s(this,D)&&je(s(this,D),s(this,P))}))}get_effect_pending(){return s(this,ee).call(this),Ee(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,E)&&(ae(s(this,E)),a(this,E,null)),s(this,T)&&(ae(s(this,T)),a(this,T,null)),I&&(K(s(this,W)),ze(),K(Je()));var c=!1,i=!1;const f=()=>{if(c){Xe();return}c=!0,i&&Qe(),s(this,T)!==null&&ie(s(this,T),()=>{a(this,T,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){j(d,s(this,g)&&s(this,g).parent)}n&&a(this,T,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 j(d,s(this,g).parent),null}}))};U(()=>{var o;try{o=this.transform_error(e)}catch(d){j(d,s(this,g)&&s(this,g).parent);return}o!==null&&typeof o=="object"&&typeof o.then=="function"?o.then(u,d=>j(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,E=new WeakMap,T=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,T,F(()=>{r(s(this,m),()=>e,()=>()=>{})}))},Oe=function(){const e=s(this,w).pending;e&&(this.is_pending=!0,a(this,E,F(()=>e(s(this,m)))),U(()=>{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,E),()=>{a(this,E,null)}),p(this,h,G).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,E,F(()=>r(s(this,m))))}else p(this,h,G).call(this)}catch(r){this.error(r)}},G=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 $e(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,G).call(this),s(this,E)&&ie(s(this,E),()=>{a(this,E,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"),Ye=new Set,le=new Set;function Et(t,e,r){(e[M]??(e[M]={}))[t]=r}function Tt(t){for(var e=0;e{throw Y});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=k;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 J=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=k,k===null||k.nodeType!==De||k.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 _=J.get(b),y=_.get(v);--y==0?(b.removeEventListener(v,me),_.delete(v),_.size===0&&J.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{Et as a,Tt as d,Rt as h,vt as m,wt as s,St as u}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js.br b/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js.br deleted file mode 100644 index b9527e5..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js.gz b/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js.gz deleted file mode 100644 index 972b700..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DP9qWekZ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js b/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js deleted file mode 100644 index a27f404..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js +++ /dev/null @@ -1 +0,0 @@ -import{aE as h,aa as d,ao as l,aF as p,w as _,aG as E,aH as g,J as u,ac as s,aI as y,a0 as M,aJ as N,a5 as x,aK as A}from"./C9Z4nxhR.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 v=l(o),T=o.lastChild;a(v,T)}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/DPfxVJHQ.js.br b/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js.br deleted file mode 100644 index 647c6e3..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js.gz b/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js.gz deleted file mode 100644 index 3ee52cd..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DPfxVJHQ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js b/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js new file mode 100644 index 0000000..aa52f13 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js @@ -0,0 +1 @@ +import{t as N}from"./BKuqSeVd.js";import{m as o}from"./CvjSAYrz.js";function p(i,b,f,A,u,r){var l=i.__className;if(o||l!==f||l===void 0){var t=N(f,A,r);(!o||t!==i.getAttribute("class"))&&(t==null?i.removeAttribute("class"):b?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/DPl3NjBv.js.br b/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js.br new file mode 100644 index 0000000..9061a9f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js.gz b/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js.gz new file mode 100644 index 0000000..c1b4428 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DPl3NjBv.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js b/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js new file mode 100644 index 0000000..2ddb1e4 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js @@ -0,0 +1 @@ +import{k as z,b as fe,aa as re,m as R,v as q,x as ie,ab as le,g as Z,ac as ue,ad as se,ae as $,q as L,w as O,C as oe,af as ve,ag as y,F as te,ah as T,ai as V,aj as de,ak as ce,V as pe,a7 as _e,al as U,am as he,an as ge,a5 as Ee,ao as j,ap as me,aq as ne,ar as ae,as as B,B as Te,at as Ce,au as we,av as Ae,aw as Ie,o as Ne}from"./CvjSAYrz.js";function Re(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;Y(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;Ae(n),n.append(v),e.items.clear()}Y(i,!f)}else s={pending:new Set(i),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(s)}function Y(e,i=!0){for(var l=0;l{var a=l();return _e(a)?a:a==null?[]:U(a)}),o,d=!0;function w(){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(R){var x=ue(u)===se;x!==(a===0)&&(u=$(),q(u),L(!1),S=!0)}for(var _=new Set,A=te,b=ce(),p=0;ps(u)):(n=V(()=>s(ee??(ee=z()))),n.f|=T)),a>_.size&&de(),R&&a>0&&q($()),!d)if(b){for(const[D,F]of c)_.has(D)||A.skip_effect(F.e);A.oncommit(w),A.ondiscard(()=>{})}else w();S&&L(!0),Z(E)}),r={effect:N,items:c,outrogroups:null,fallback:n};d=!1,R&&(u=O)}function H(e){for(;e!==null&&(e.f&Ce)===0;)e=e.next;return e}function xe(e,i,l,t,g){var h,D,F,X,G,J,K,P,Q;var s=(t&we)!==0,u=i.length,c=e.items,f=H(e.effect.first),v,n=null,E,o=[],d=[],w,N,r,a;if(s)for(a=0;a0){var k=(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 be(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:V(()=>(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 C(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,Re as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js.br b/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js.br new file mode 100644 index 0000000..53ec356 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js.gz b/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js.gz new file mode 100644 index 0000000..d64bcbd Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DTnG8poT.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js b/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js deleted file mode 100644 index 103d488..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js +++ /dev/null @@ -1 +0,0 @@ -import{s as c,g as l}from"./DnKV7_Y9.js";import{V as o,W as a,X as b,g as p,h as d,Y as g}from"./C9Z4nxhR.js";let s=!1,i=Symbol();function y(e,n,r){const u=r[n]??(r[n]={store:null,source:b(void 0),unsubscribe:a});if(u.store!==e&&!(i in r))if(u.unsubscribe(),u.store=e??null,e==null)u.source.v=void 0,u.unsubscribe=a;else{var t=!0;u.unsubscribe=c(e,f=>{t?u.source.v=f:d(u.source,f)}),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/DWr9YED7.js.br b/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js.br deleted file mode 100644 index 268d8ef..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js.br +++ /dev/null @@ -1,2 +0,0 @@ -z`Vmwr}%&n9Es3+st׋։ E!M7Uۡe3Րt+xN㯗qw]$H~٬<̨zq(+no09G#ZvSSO WbIcjbIgoBpt lx]!L8P}4!Z.?ZsI@] \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js.gz b/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js.gz deleted file mode 100644 index 7555032..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DWr9YED7.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js b/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js new file mode 100644 index 0000000..3f77100 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js @@ -0,0 +1 @@ +import{a4 as a,A as m,aG as q,aC as A}from"./CvjSAYrz.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 x(e,t){return{subscribe:z(e,t).subscribe}}function z(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 x(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{B as d,C as g,_ as s,z as w}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js.br b/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js.br new file mode 100644 index 0000000..ca2c57a Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js.gz b/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js.gz new file mode 100644 index 0000000..b45b0bd Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DfQhL-hC.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js b/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js deleted file mode 100644 index b9ec889..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js +++ /dev/null @@ -1 +0,0 @@ -import{W as a,m as w,am as q,ai as x}from"./C9Z4nxhR.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}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js.br b/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js.br deleted file mode 100644 index 818be5e..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js.gz b/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js.gz deleted file mode 100644 index 6c1fdbe..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DnKV7_Y9.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js b/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js deleted file mode 100644 index 3c92625..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.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"./C9Z4nxhR.js";import{c as G}from"./DWr9YED7.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/Do8TgQ-j.js.br b/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js.br deleted file mode 100644 index 40fa331..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js.br +++ /dev/null @@ -1,2 +0,0 @@ - ¶rYk$IteSޱPq?*J[,Tyo?p -/4cuRD"I2g3IyqvqFD!(rAg\RO 0#G !eT Ctftw{۬PbTV#m : vxZZ${D4,8C1P.N ]~Z*KH)IZlj}ر:܅rI,Wz9jt`,5=ZNno7jRu`=ǁ1fZ680%)NK #Uo ~ZIZ@ǎYFcR2 _epvLexkX3dF(2)͗lN qr㖂89iPJ$=|jPK+y%\a^9ihfu|ssZFp \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js.gz b/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js.gz deleted file mode 100644 index e29dd4d..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/Do8TgQ-j.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js b/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js deleted file mode 100644 index f86de56..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js +++ /dev/null @@ -1 +0,0 @@ -var Xt=t=>{throw TypeError(t)};var Pe=(t,e,n)=>e.has(t)||Xt("Cannot "+n);var w=(t,e,n)=>(Pe(t,e,"read from private field"),n?n.call(t):e.get(t)),A=(t,e,n)=>e.has(t)?Xt("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(t):e.set(t,n);import{s as T,g as I,h as P,I as pt,b8 as Oe}from"./C9Z4nxhR.js";import{w as jt}from"./DnKV7_Y9.js";import{o as Qt}from"./CkyfbJUz.js";class Nt{constructor(e,n){this.status=e,typeof n=="string"?this.body={message:n}:n?this.body=n:this.body={message:`Error: ${e}`}}toString(){return JSON.stringify(this.body)}}class qt{constructor(e,n){this.status=e,this.location=n}}class Dt extends Error{constructor(e,n,r){super(r),this.status=e,this.text=n}}new URL("sveltekit-internal://");function $e(t,e){return t==="/"||e==="ignore"?t:e==="never"?t.endsWith("/")?t.slice(0,-1):t:e==="always"&&!t.endsWith("/")?t+"/":t}function Ce(t){return t.split("%25").map(decodeURI).join("%25")}function je(t){for(const e in t)t[e]=decodeURIComponent(t[e]);return t}function Lt({href:t}){return t.split("#")[0]}function Ne(...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 a=r.length;for(;a;)e=e*33^r[--a]}else throw new TypeError("value must be a string or TypedArray");return(e>>>0).toString(36)}new TextEncoder;new TextDecoder;function qe(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"&&W.delete(Vt(t)),De(t,e));const W=new Map;function Ve(t,e){const n=Vt(t,e),r=document.querySelector(n);if(r!=null&&r.textContent){r.remove();let{body:a,...s}=JSON.parse(r.textContent);const o=r.getAttribute("data-ttl");return o&&W.set(n,{body:a,init:s,ttl:1e3*Number(o)}),r.getAttribute("data-b64")!==null&&(a=qe(a)),Promise.resolve(new Response(a,s))}return window.fetch(t,e)}function ze(t,e,n){if(W.size>0){const r=Vt(t,n),a=W.get(r);if(a){if(performance.now(){const a=/^\[\.\.\.(\w+)(?:=(\w+))?\]$/.exec(r);if(a)return e.push({name:a[1],matcher:a[2],optional:!1,rest:!0,chained:!0}),"(?:/([^]*))?";const s=/^\[\[(\w+)(?:=(\w+))?\]\]$/.exec(r);if(s)return e.push({name:s[1],matcher:s[2],optional:!0,rest:!1,chained:!0}),"(?:/([^/]+))?";if(!r)return;const o=r.split(/\[(.+?)\](?!\])/);return"/"+o.map((l,c)=>{if(c%2){if(l.startsWith("x+"))return Ut(String.fromCharCode(parseInt(l.slice(2),16)));if(l.startsWith("u+"))return Ut(String.fromCharCode(...l.slice(2).split("-").map(_=>parseInt(_,16))));const d=Be.exec(l),[,u,v,f,h]=d;return e.push({name:f,matcher:h,optional:!!u,rest:!!v,chained:v?c===1&&o[0]==="":!1}),v?"([^]*?)":u?"([^/]*)?":"([^/]+?)"}return Ut(l)}).join("")}).join("")}/?$`),params:e}}function Me(t){return t!==""&&!/^\([^)]+\)$/.test(t)}function Fe(t){return t.slice(1).split("/").filter(Me)}function Ge(t,e,n){const r={},a=t.slice(1),s=a.filter(i=>i!==void 0);let o=0;for(let i=0;id).join("/"),o=0),c===void 0)if(l.rest)c="";else continue;if(!l.matcher||n[l.matcher](c)){r[l.name]=c;const d=e[i+1],u=a[i+1];d&&!d.rest&&d.optional&&u&&l.chained&&(o=0),!d&&!u&&Object.keys(r).length===s.length&&(o=0);continue}if(l.optional&&l.chained){o++;continue}return}if(!o)return r}function Ut(t){return t.normalize().replace(/[[\]]/g,"\\$&").replace(/%/g,"%25").replace(/\//g,"%2[Ff]").replace(/\?/g,"%3[Ff]").replace(/#/g,"%23").replace(/[.*+?^${}()|\\]/g,"\\$&")}function We({nodes:t,server_loads:e,dictionary:n,matchers:r}){const a=new Set(e);return Object.entries(n).map(([i,[l,c,d]])=>{const{pattern:u,params:v}=Ke(i),f={id:i,exec:h=>{const _=u.exec(h);if(_)return Ge(_,v,r)},errors:[1,...d||[]].map(h=>t[h]),layouts:[0,...c||[]].map(o),leaf:s(l)};return f.errors.length=f.layouts.length=Math.max(f.errors.length,f.layouts.length),f});function s(i){const l=i<0;return l&&(i=~i),[l,t[i]]}function o(i){return i===void 0?i:[a.has(i),t[i]]}}function ue(t,e=JSON.parse){try{return e(sessionStorage[t])}catch{}}function Zt(t,e,n=JSON.stringify){const r=n(e);try{sessionStorage[t]=r}catch{}}var se;const U=((se=globalThis.__sveltekit_1bqbquz)==null?void 0:se.base)??"/dashboard";var ie;const Ye=((ie=globalThis.__sveltekit_1bqbquz)==null?void 0:ie.assets)??U??"",He="1772569216424",de="sveltekit:snapshot",he="sveltekit:scroll",pe="sveltekit:states",Je="sveltekit:pageurl",M="sveltekit:history",H="sveltekit:navigation",q={tap:1,hover:2,viewport:3,eager:4,off:-1,false:-1},Et=location.origin;function zt(t){if(t instanceof URL)return t;let e=document.baseURI;if(!e){const n=document.getElementsByTagName("base");e=n.length?n[0].href:document.URL}return new URL(t,e)}function V(){return{x:pageXOffset,y:pageYOffset}}function K(t,e){return t.getAttribute(`data-sveltekit-${e}`)}const te={...q,"":q.hover};function ge(t){let e=t.assignedSlot??t.parentNode;return(e==null?void 0:e.nodeType)===11&&(e=e.host),e}function me(t,e){for(;t&&t!==e;){if(t.nodeName.toUpperCase()==="A"&&t.hasAttribute("href"))return t;t=ge(t)}}function It(t,e,n){let r;try{if(r=new URL(t instanceof SVGAElement?t.href.baseVal:t.href,document.baseURI),n&&r.hash.match(/^#[^/]/)){const i=location.hash.split("#")[1]||"/";r.hash=`#${i}${r.hash}`}}catch{}const a=t instanceof SVGAElement?t.target.baseVal:t.target,s=!r||!!a||St(r,e,n)||(t.getAttribute("rel")||"").split(/\s+/).includes("external"),o=(r==null?void 0:r.origin)===Et&&t.hasAttribute("download");return{url:r,external:s,target:a,download:o}}function gt(t){let e=null,n=null,r=null,a=null,s=null,o=null,i=t;for(;i&&i!==document.documentElement;)r===null&&(r=K(i,"preload-code")),a===null&&(a=K(i,"preload-data")),e===null&&(e=K(i,"keepfocus")),n===null&&(n=K(i,"noscroll")),s===null&&(s=K(i,"reload")),o===null&&(o=K(i,"replacestate")),i=ge(i);function l(c){switch(c){case"":case"true":return!0;case"off":case"false":return!1;default:return}}return{preload_code:te[r??"off"],preload_data:te[a??"off"],keepfocus:l(e),noscroll:l(n),reload:l(s),replace_state:l(o)}}function ee(t){const e=jt(t);let n=!0;function r(){n=!0,e.update(o=>o)}function a(o){n=!1,e.set(o)}function s(o){let i;return e.subscribe(l=>{(i===void 0||n&&l!==i)&&o(i=l)})}return{notify:r,set:a,subscribe:s}}const _e={v:()=>{}};function Xe(){const{set:t,subscribe:e}=jt(!1);let n;async function r(){clearTimeout(n);try{const a=await fetch(`${Ye}/_app/version.json`,{headers:{pragma:"no-cache","cache-control":"no-cache"}});if(!a.ok)return!1;const o=(await a.json()).version!==He;return o&&(t(!0),_e.v(),clearTimeout(n)),o}catch{return!1}}return{subscribe:e,check:r}}function St(t,e,n){return t.origin!==Et||!t.pathname.startsWith(e)?!0:n?t.pathname!==location.pathname:!1}function Ln(t){}const we=new Set(["load","prerender","csr","ssr","trailingSlash","config"]);[...we];const Qe=new Set([...we]);[...Qe];function Ze(t){return t.filter(e=>e!=null)}function Bt(t){return t instanceof Nt||t instanceof Dt?t.status:500}function tn(t){return t instanceof Dt?t.text:"Internal Error"}let R,J,At;const en=Qt.toString().includes("$$")||/function \w+\(\) \{\}/.test(Qt.toString());var et,nt,at,rt,ot,st,it,lt,le,ct,ce,ft,fe;en?(R={data:{},form:null,error:null,params:{},route:{id:null},state:{},status:-1,url:new URL("https://example.com")},J={current:null},At={current:!1}):(R=new(le=class{constructor(){A(this,et,T({}));A(this,nt,T(null));A(this,at,T(null));A(this,rt,T({}));A(this,ot,T({id:null}));A(this,st,T({}));A(this,it,T(-1));A(this,lt,T(new URL("https://example.com")))}get data(){return I(w(this,et))}set data(e){P(w(this,et),e)}get form(){return I(w(this,nt))}set form(e){P(w(this,nt),e)}get error(){return I(w(this,at))}set error(e){P(w(this,at),e)}get params(){return I(w(this,rt))}set params(e){P(w(this,rt),e)}get route(){return I(w(this,ot))}set route(e){P(w(this,ot),e)}get state(){return I(w(this,st))}set state(e){P(w(this,st),e)}get status(){return I(w(this,it))}set status(e){P(w(this,it),e)}get url(){return I(w(this,lt))}set url(e){P(w(this,lt),e)}},et=new WeakMap,nt=new WeakMap,at=new WeakMap,rt=new WeakMap,ot=new WeakMap,st=new WeakMap,it=new WeakMap,lt=new WeakMap,le),J=new(ce=class{constructor(){A(this,ct,T(null))}get current(){return I(w(this,ct))}set current(e){P(w(this,ct),e)}},ct=new WeakMap,ce),At=new(fe=class{constructor(){A(this,ft,T(!1))}get current(){return I(w(this,ft))}set current(e){P(w(this,ft),e)}},ft=new WeakMap,fe),_e.v=()=>At.current=!0);function nn(t){Object.assign(R,t)}const an=new Set(["icon","shortcut icon","apple-touch-icon"]),j=ue(he)??{},X=ue(de)??{},C={url:ee({}),page:ee({}),navigating:jt(null),updated:Xe()};function Kt(t){j[t]=V()}function rn(t,e){let n=t+1;for(;j[n];)delete j[n],n+=1;for(n=e+1;X[n];)delete X[n],n+=1}function Q(t,e=!1){return e?location.replace(t.href):location.href=t.href,new Promise(()=>{})}async function ve(){if("serviceWorker"in navigator){const t=await navigator.serviceWorker.getRegistration(U||"/");t&&await t.update()}}function ne(){}let Mt,Pt,mt,O,Ot,E;const _t=[],wt=[];let y=null;function $t(){var t;(t=y==null?void 0:y.fork)==null||t.then(e=>e==null?void 0:e.discard()),y=null}const ht=new Map,ye=new Set,on=new Set,Y=new Set;let m={branch:[],error:null,url:null},be=!1,vt=!1,ae=!0,Z=!1,G=!1,ke=!1,Ft=!1,Ee,k,L,D;const yt=new Set,re=new Map;async function In(t,e,n){var s,o,i,l,c;(s=globalThis.__sveltekit_1bqbquz)!=null&&s.data&&globalThis.__sveltekit_1bqbquz.data,document.URL!==location.href&&(location.href=location.href),E=t,await((i=(o=t.hooks).init)==null?void 0:i.call(o)),Mt=We(t),O=document.documentElement,Ot=e,Pt=t.nodes[0],mt=t.nodes[1],Pt(),mt(),k=(l=history.state)==null?void 0:l[M],L=(c=history.state)==null?void 0:c[H],k||(k=L=Date.now(),history.replaceState({...history.state,[M]:k,[H]:L},""));const r=j[k];function a(){r&&(history.scrollRestoration="manual",scrollTo(r.x,r.y))}n?(a(),await vn(Ot,n)):(await F({type:"enter",url:zt(E.hash?kn(new URL(location.href)):location.href),replace_state:!0}),a()),wn()}function sn(){_t.length=0,Ft=!1}function Se(t){wt.some(e=>e==null?void 0:e.snapshot)&&(X[t]=wt.map(e=>{var n;return(n=e==null?void 0:e.snapshot)==null?void 0:n.capture()}))}function Re(t){var e;(e=X[t])==null||e.forEach((n,r)=>{var a,s;(s=(a=wt[r])==null?void 0:a.snapshot)==null||s.restore(n)})}function oe(){Kt(k),Zt(he,j),Se(L),Zt(de,X)}async function xe(t,e,n,r){let a;e.invalidateAll&&$t(),await F({type:"goto",url:zt(t),keepfocus:e.keepFocus,noscroll:e.noScroll,replace_state:e.replaceState,state:e.state,redirect_count:n,nav_token:r,accept:()=>{e.invalidateAll&&(Ft=!0,a=[...re.keys()]),e.invalidate&&e.invalidate.forEach(_n)}}),e.invalidateAll&&pt().then(pt).then(()=>{re.forEach(({resource:s},o)=>{var i;a!=null&&a.includes(o)&&((i=s.refresh)==null||i.call(s))})})}async function ln(t){if(t.id!==(y==null?void 0:y.id)){$t();const e={};yt.add(e),y={id:t.id,token:e,promise:Ue({...t,preload:e}).then(n=>(yt.delete(e),n.type==="loaded"&&n.state.error&&$t(),n)),fork:null}}return y.promise}async function Tt(t){var n;const e=(n=await Rt(t,!1))==null?void 0:n.route;e&&await Promise.all([...e.layouts,e.leaf].filter(Boolean).map(r=>r[1]()))}async function Le(t,e,n){var a;m=t.state;const r=document.querySelector("style[data-sveltekit]");if(r&&r.remove(),Object.assign(R,t.props.page),Ee=new E.root({target:e,props:{...t.props,stores:C,components:wt},hydrate:n,sync:!1}),await Promise.resolve(),Re(L),n){const s={from:null,to:{params:m.params,route:{id:((a=m.route)==null?void 0:a.id)??null},url:new URL(location.href),scroll:j[k]??V()},willUnload:!1,type:"enter",complete:Promise.resolve()};Y.forEach(o=>o(s))}vt=!0}function bt({url:t,params:e,branch:n,status:r,error:a,route:s,form:o}){let i="never";if(U&&(t.pathname===U||t.pathname===U+"/"))i="always";else for(const f of n)(f==null?void 0:f.slash)!==void 0&&(i=f.slash);t.pathname=$e(t.pathname,i),t.search=t.search;const l={type:"loaded",state:{url:t,params:e,branch:n,error:a,route:s},props:{constructors:Ze(n).map(f=>f.node.component),page:Jt(R)}};o!==void 0&&(l.props.form=o);let c={},d=!R,u=0;for(let f=0;fi(new URL(o))))return!0;return!1}function Wt(t,e){return(t==null?void 0:t.type)==="data"?t:(t==null?void 0:t.type)==="skip"?e??null:null}function un(t,e){if(!t)return new Set(e.searchParams.keys());const n=new Set([...t.searchParams.keys(),...e.searchParams.keys()]);for(const r of n){const a=t.searchParams.getAll(r),s=e.searchParams.getAll(r);a.every(o=>s.includes(o))&&s.every(o=>a.includes(o))&&n.delete(r)}return n}function dn({error:t,url:e,route:n,params:r}){return{type:"loaded",state:{error:t,url:e,route:n,params:r,branch:[]},props:{page:Jt(R),constructors:[]}}}async function Ue({id:t,invalidating:e,url:n,params:r,route:a,preload:s}){if((y==null?void 0:y.id)===t)return yt.delete(y.token),y.promise;const{errors:o,layouts:i,leaf:l}=a,c=[...i,l];o.forEach(g=>g==null?void 0:g().catch(()=>{})),c.forEach(g=>g==null?void 0:g[1]().catch(()=>{}));const d=m.url?t!==kt(m.url):!1,u=m.route?a.id!==m.route.id:!1,v=un(m.url,n);let f=!1;const h=c.map(async(g,p)=>{var $;if(!g)return;const b=m.branch[p];return g[1]===(b==null?void 0:b.loader)&&!fn(f,u,d,v,($=b.universal)==null?void 0:$.uses,r)?b:(f=!0,Gt({loader:g[1],url:n,params:r,route:a,parent:async()=>{var ut;const N={};for(let z=0;z{});const _=[];for(let g=0;gPromise.resolve({}),server_data_node:Wt(s)}),i={node:await mt(),loader:mt,universal:null,server:null,data:null};return bt({url:n,params:a,branch:[o,i],status:t,error:e,route:null})}catch(o){if(o instanceof qt)return xe(new URL(o.location,location.href),{},0);throw o}}async function pn(t){const e=t.href;if(ht.has(e))return ht.get(e);let n;try{const r=(async()=>{let a=await E.hooks.reroute({url:new URL(t),fetch:async(s,o)=>cn(s,o,t).promise})??t;if(typeof a=="string"){const s=new URL(t);E.hash?s.hash=a:s.pathname=a,a=s}return a})();ht.set(e,r),n=await r}catch{ht.delete(e);return}return n}async function Rt(t,e){if(t&&!St(t,U,E.hash)){const n=await pn(t);if(!n)return;const r=gn(n);for(const a of Mt){const s=a.exec(r);if(s)return{id:kt(t),invalidating:e,route:a,params:je(s),url:t}}}}function gn(t){return Ce(E.hash?t.hash.replace(/^#/,"").replace(/[?#].+/,""):t.pathname.slice(U.length))||"/"}function kt(t){return(E.hash?t.hash.replace(/^#/,""):t.pathname)+t.search}function Ae({url:t,type:e,intent:n,delta:r,event:a,scroll:s}){let o=!1;const i=Ht(m,n,t,e,s??null);r!==void 0&&(i.navigation.delta=r),a!==void 0&&(i.navigation.event=a);const l={...i.navigation,cancel:()=>{o=!0,i.reject(new Error("navigation cancelled"))}};return Z||ye.forEach(c=>c(l)),o?null:i}async function F({type:t,url:e,popped:n,keepfocus:r,noscroll:a,replace_state:s,state:o={},redirect_count:i=0,nav_token:l={},accept:c=ne,block:d=ne,event:u}){var z;const v=D;D=l;const f=await Rt(e,!1),h=t==="enter"?Ht(m,f,e,t):Ae({url:e,type:t,delta:n==null?void 0:n.delta,intent:f,scroll:n==null?void 0:n.scroll,event:u});if(!h){d(),D===l&&(D=v);return}const _=k,g=L;c(),Z=!0,vt&&h.navigation.type!=="enter"&&C.navigating.set(J.current=h.navigation);let p=f&&await Ue(f);if(!p){if(St(e,U,E.hash))return await Q(e,s);p=await Te(e,{id:null},await tt(new Dt(404,"Not Found",`Not found: ${e.pathname}`),{url:e,params:{},route:{id:null}}),404,s)}if(e=(f==null?void 0:f.url)||e,D!==l)return h.reject(new Error("navigation aborted")),!1;if(p.type==="redirect"){if(i<20){await F({type:t,url:new URL(p.location,e),popped:n,keepfocus:r,noscroll:a,replace_state:s,state:o,redirect_count:i+1,nav_token:l}),h.fulfil(void 0);return}p=await Yt({status:500,error:await tt(new Error("Redirect loop"),{url:e,params:{},route:{id:null}}),url:e,route:{id:null}})}else p.props.page.status>=400&&await C.updated.check()&&(await ve(),await Q(e,s));if(sn(),Kt(_),Se(g),p.props.page.url.pathname!==e.pathname&&(e.pathname=p.props.page.url.pathname),o=n?n.state:o,!n){const S=s?0:1,dt={[M]:k+=S,[H]:L+=S,[pe]:o};(s?history.replaceState:history.pushState).call(history,dt,"",e),s||rn(k,L)}const b=f&&(y==null?void 0:y.id)===f.id?y.fork:null;y=null,p.props.page.state=o;let x;if(vt){const S=(await Promise.all(Array.from(on,B=>B(h.navigation)))).filter(B=>typeof B=="function");if(S.length>0){let B=function(){S.forEach(xt=>{Y.delete(xt)})};S.push(B),S.forEach(xt=>{Y.add(xt)})}m=p.state,p.props.page&&(p.props.page.url=e);const dt=b&&await b;dt?x=dt.commit():(Ee.$set(p.props),nn(p.props.page),x=(z=Oe)==null?void 0:z()),ke=!0}else await Le(p,Ot,!1);const{activeElement:$}=document;await x,await pt(),await pt();let N=null;if(ae){const S=n?n.scroll:a?V():null;S?scrollTo(S.x,S.y):(N=e.hash&&document.getElementById(Ie(e)))?N.scrollIntoView():scrollTo(0,0)}const ut=document.activeElement!==$&&document.activeElement!==document.body;!r&&!ut&&bn(e,!N),ae=!0,p.props.page&&Object.assign(R,p.props.page),Z=!1,t==="popstate"&&Re(L),h.fulfil(void 0),h.navigation.to&&(h.navigation.to.scroll=V()),Y.forEach(S=>S(h.navigation)),C.navigating.set(J.current=null)}async function Te(t,e,n,r,a){return t.origin===Et&&t.pathname===location.pathname&&!be?await Yt({status:r,error:n,url:t,route:e}):await Q(t,a)}function mn(){let t,e={element:void 0,href:void 0},n;O.addEventListener("mousemove",i=>{const l=i.target;clearTimeout(t),t=setTimeout(()=>{s(l,q.hover)},20)});function r(i){i.defaultPrevented||s(i.composedPath()[0],q.tap)}O.addEventListener("mousedown",r),O.addEventListener("touchstart",r,{passive:!0});const a=new IntersectionObserver(i=>{for(const l of i)l.isIntersecting&&(Tt(new URL(l.target.href)),a.unobserve(l.target))},{threshold:0});async function s(i,l){const c=me(i,O),d=c===e.element&&(c==null?void 0:c.href)===e.href&&l>=n;if(!c||d)return;const{url:u,external:v,download:f}=It(c,U,E.hash);if(v||f)return;const h=gt(c),_=u&&kt(m.url)===kt(u);if(!(h.reload||_))if(l<=h.preload_data){e={element:c,href:c.href},n=q.tap;const g=await Rt(u,!1);if(!g)return;ln(g)}else l<=h.preload_code&&(e={element:c,href:c.href},n=l,Tt(u))}function o(){a.disconnect();for(const i of O.querySelectorAll("a")){const{url:l,external:c,download:d}=It(i,U,E.hash);if(c||d)continue;const u=gt(i);u.reload||(u.preload_code===q.viewport&&a.observe(i),u.preload_code===q.eager&&Tt(l))}}Y.add(o),o()}function tt(t,e){if(t instanceof Nt)return t.body;const n=Bt(t),r=tn(t);return E.hooks.handleError({error:t,event:e,status:n,message:r})??{message:r}}function Pn(t,e={}){return t=new URL(zt(t)),t.origin!==Et?Promise.reject(new Error("goto: invalid URL")):xe(t,e,0)}function _n(t){if(typeof t=="function")_t.push(t);else{const{href:e}=new URL(t,location.href);_t.push(n=>n.href===e)}}function wn(){var e;history.scrollRestoration="manual",addEventListener("beforeunload",n=>{let r=!1;if(oe(),!Z){const a=Ht(m,void 0,null,"leave"),s={...a.navigation,cancel:()=>{r=!0,a.reject(new Error("navigation cancelled"))}};ye.forEach(o=>o(s))}r?(n.preventDefault(),n.returnValue=""):history.scrollRestoration="auto"}),addEventListener("visibilitychange",()=>{document.visibilityState==="hidden"&&oe()}),(e=navigator.connection)!=null&&e.saveData||mn(),O.addEventListener("click",async n=>{if(n.button||n.which!==1||n.metaKey||n.ctrlKey||n.shiftKey||n.altKey||n.defaultPrevented)return;const r=me(n.composedPath()[0],O);if(!r)return;const{url:a,external:s,target:o,download:i}=It(r,U,E.hash);if(!a)return;if(o==="_parent"||o==="_top"){if(window.parent!==window)return}else if(o&&o!=="_self")return;const l=gt(r);if(!(r instanceof SVGAElement)&&a.protocol!==location.protocol&&!(a.protocol==="https:"||a.protocol==="http:")||i)return;const[d,u]=(E.hash?a.hash.replace(/^#/,""):a.href).split("#"),v=d===Lt(location);if(s||l.reload&&(!v||!u)){Ae({url:a,type:"link",event:n})?Z=!0:n.preventDefault();return}if(u!==void 0&&v){const[,f]=m.url.href.split("#");if(f===u){if(n.preventDefault(),u===""||u==="top"&&r.ownerDocument.getElementById("top")===null)scrollTo({top:0});else{const h=r.ownerDocument.getElementById(decodeURIComponent(u));h&&(h.scrollIntoView(),h.focus())}return}if(G=!0,Kt(k),t(a),!l.replace_state)return;G=!1}n.preventDefault(),await new Promise(f=>{requestAnimationFrame(()=>{setTimeout(f,0)}),setTimeout(f,100)}),await F({type:"link",url:a,keepfocus:l.keepfocus,noscroll:l.noscroll,replace_state:l.replace_state??a.href===location.href,event:n})}),O.addEventListener("submit",n=>{if(n.defaultPrevented)return;const r=HTMLFormElement.prototype.cloneNode.call(n.target),a=n.submitter;if(((a==null?void 0:a.formTarget)||r.target)==="_blank"||((a==null?void 0:a.formMethod)||r.method)!=="get")return;const i=new URL((a==null?void 0:a.hasAttribute("formaction"))&&(a==null?void 0:a.formAction)||r.action);if(St(i,U,!1))return;const l=n.target,c=gt(l);if(c.reload)return;n.preventDefault(),n.stopPropagation();const d=new FormData(l,a);i.search=new URLSearchParams(d).toString(),F({type:"form",url:i,keepfocus:c.keepfocus,noscroll:c.noscroll,replace_state:c.replace_state??i.href===location.href,event:n})}),addEventListener("popstate",async n=>{var r;if(!Ct){if((r=n.state)!=null&&r[M]){const a=n.state[M];if(D={},a===k)return;const s=j[a],o=n.state[pe]??{},i=new URL(n.state[Je]??location.href),l=n.state[H],c=m.url?Lt(location)===Lt(m.url):!1;if(l===L&&(ke||c)){o!==R.state&&(R.state=o),t(i),j[k]=V(),s&&scrollTo(s.x,s.y),k=a;return}const u=a-k;await F({type:"popstate",url:i,popped:{state:o,scroll:s,delta:u},accept:()=>{k=a,L=l},block:()=>{history.go(-u)},nav_token:D,event:n})}else if(!G){const a=new URL(location.href);t(a),E.hash&&location.reload()}}}),addEventListener("hashchange",()=>{G&&(G=!1,history.replaceState({...history.state,[M]:++k,[H]:L},"",location.href))});for(const n of document.querySelectorAll("link"))an.has(n.rel)&&(n.href=n.href);addEventListener("pageshow",n=>{n.persisted&&C.navigating.set(J.current=null)});function t(n){m.url=R.url=n,C.page.set(Jt(R)),C.page.notify()}}async function vn(t,{status:e=200,error:n,node_ids:r,params:a,route:s,server_route:o,data:i,form:l}){be=!0;const c=new URL(location.href);let d;({params:a={},route:s={id:null}}=await Rt(c,!1)||{}),d=Mt.find(({id:f})=>f===s.id);let u,v=!0;try{const f=r.map(async(_,g)=>{const p=i[g];return p!=null&&p.uses&&(p.uses=yn(p.uses)),Gt({loader:E.nodes[_],url:c,params:a,route:s,parent:async()=>{const b={};for(let x=0;x{const i=history.state;Ct=!0,location.replace(new URL(`#${r}`,location.href)),history.replaceState(i,"",t),e&&scrollTo(s,o),Ct=!1})}else{const s=document.body,o=s.getAttribute("tabindex");s.tabIndex=-1,s.focus({preventScroll:!0,focusVisible:!1}),o!==null?s.setAttribute("tabindex",o):s.removeAttribute("tabindex")}const a=getSelection();if(a&&a.type!=="None"){const s=[];for(let o=0;o{if(a.rangeCount===s.length){for(let o=0;o{s=u,o=v});return i.catch(()=>{}),{navigation:{from:{params:t.params,route:{id:((c=t.route)==null?void 0:c.id)??null},url:t.url,scroll:V()},to:n&&{params:(e==null?void 0:e.params)??null,route:{id:((d=e==null?void 0:e.route)==null?void 0:d.id)??null},url:n,scroll:a},willUnload:!e,type:r,complete:i},fulfil:s,reject:o}}function Jt(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 kn(t){const e=new URL(t);return e.hash=decodeURIComponent(t.hash),e}function Ie(t){let e;if(E.hash){const[,,n]=t.hash.split("#",3);e=n??""}else e=t.hash.slice(1);return decodeURIComponent(e)}export{In as a,U as b,Pn as g,Ln as l,R as p,C as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js.br b/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js.br deleted file mode 100644 index 2eab214..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js.gz b/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js.gz deleted file mode 100644 index 8720c68..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/DunNqS1N.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js new file mode 100644 index 0000000..0f1e075 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.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",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/DzfRjky4.js.br b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js.br new file mode 100644 index 0000000..e2697c3 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js.gz b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js.gz new file mode 100644 index 0000000..fc5020b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/DzfRjky4.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js b/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js new file mode 100644 index 0000000..29ca25a --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js @@ -0,0 +1,2 @@ +var Me=Object.defineProperty;var ue=t=>{throw TypeError(t)};var Ye=(t,e,r)=>e in t?Me(t,e,{enumerable:!0,configurable:!0,writable:!0,value:r}):t[e]=r;var $=(t,e,r)=>Ye(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{aP as Ie,g as we,z as Le,A as xe,aQ as _e,B as q,ao as me,w as M,m as Y,M as C,aR as pe,b as Be,ab as Ce,ad as He,aS as ge,ai as F,k as Te,aT as se,ar as ie,ay as Pe,aU as ve,aV as Ve,aW as ye,aX as We,aY as qe,aZ as Z,a_ as G,a$ as be,b0 as ze,b1 as Re,az as Se,ag as $e,aw as ae,v as K,n as je,ae as Ue,b2 as j,E as Je,l as Qe,b3 as Xe,b4 as Ze,a6 as Ge,a3 as Ke,b5 as et,b6 as ne,x as tt,C as Ae,ax as rt,o as st,b7 as fe,q as U,b8 as it,av as at,b9 as nt,al as ft,p as ht,af as ot,ba as lt,a as ct}from"./CvjSAYrz.js";import{d as dt}from"./BsvCUYx-.js";function ut(t){let e=0,r=me(0),a;return()=>{Ie()&&(we(r),Le(()=>(e===0&&(a=xe(()=>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,T,L,g,R,w,m,S,x,N,H,P,V,A,ee,o,De,Ne,Oe,he,Q,X,oe;class gt{constructor(e,r,a,h){c(this,o);$(this,"parent");$(this,"is_pending",!1);$(this,"transform_error");c(this,E);c(this,z,Y?M:null);c(this,T);c(this,L);c(this,g);c(this,R,null);c(this,w,null);c(this,m,null);c(this,S,null);c(this,x,0);c(this,N,0);c(this,H,!1);c(this,P,new Set);c(this,V,new Set);c(this,A,null);c(this,ee,ut(()=>(n(this,A,me(s(this,x))),()=>{n(this,A,null)})));var i;n(this,E,e),n(this,T,r),n(this,L,f=>{var u=C;u.b=this,u.f|=pe,a(f)}),this.parent=C.b,this.transform_error=h??((i=this.parent)==null?void 0:i.transform_error)??(f=>f),n(this,g,Be(()=>{if(Y){const f=s(this,z);Ce();const u=f.data===He;if(f.data.startsWith(ge)){const d=JSON.parse(f.data.slice(ge.length));p(this,o,Ne).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)),Y&&n(this,E,M)}defer_effect(e){qe(e,s(this,P),s(this,V))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!s(this,T).pending}update_pending_count(e){p(this,o,oe).call(this,e),n(this,x,s(this,x)+e),!(!s(this,A)||s(this,H))&&(n(this,H,!0),q(()=>{n(this,H,!1),s(this,A)&&$e(s(this,A),s(this,x))}))}get_effect_pending(){return s(this,ee).call(this),we(s(this,A))}error(e){var r=s(this,T).onerror;let a=s(this,T).failed;if(!r&&!a)throw e;s(this,R)&&(ae(s(this,R)),n(this,R,null)),s(this,w)&&(ae(s(this,w)),n(this,w,null)),s(this,m)&&(ae(s(this,m)),n(this,m,null)),Y&&(K(s(this,z)),je(),K(Ue()));var h=!1,i=!1;const f=()=>{if(h){Ze();return}h=!0,i&&Xe(),s(this,m)!==null&&ie(s(this,m),()=>{n(this,m,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){j(d,s(this,g)&&s(this,g).parent)}a&&n(this,m,p(this,o,X).call(this,()=>{se.ensure();try{return F(()=>{var d=C;d.b=this,d.f|=pe,a(s(this,E),()=>l,()=>f)})}catch(d){return j(d,s(this,g).parent),null}}))};q(()=>{var l;try{l=this.transform_error(e)}catch(d){j(d,s(this,g)&&s(this,g).parent);return}l!==null&&typeof l=="object"&&typeof l.then=="function"?l.then(u,d=>j(d,s(this,g)&&s(this,g).parent)):u(l)})}}E=new WeakMap,z=new WeakMap,T=new WeakMap,L=new WeakMap,g=new WeakMap,R=new WeakMap,w=new WeakMap,m=new WeakMap,S=new WeakMap,x=new WeakMap,N=new WeakMap,H=new WeakMap,P=new WeakMap,V=new WeakMap,A=new WeakMap,ee=new WeakMap,o=new WeakSet,De=function(){try{n(this,R,F(()=>s(this,L).call(this,s(this,E))))}catch(e){this.error(e)}},Ne=function(e){const r=s(this,T).failed;r&&n(this,m,F(()=>{r(s(this,E),()=>e,()=>()=>{})}))},Oe=function(){const e=s(this,T).pending;e&&(this.is_pending=!0,n(this,w,F(()=>e(s(this,E)))),q(()=>{var r=n(this,S,document.createDocumentFragment()),a=Te();r.append(a),n(this,R,p(this,o,X).call(this,()=>(se.ensure(),F(()=>s(this,L).call(this,a))))),s(this,N)===0&&(s(this,E).before(r),n(this,S,null),ie(s(this,w),()=>{n(this,w,null)}),p(this,o,Q).call(this))}))},he=function(){try{if(this.is_pending=this.has_pending_snippet(),n(this,N,0),n(this,x,0),n(this,R,F(()=>{s(this,L).call(this,s(this,E))})),s(this,N)>0){var e=n(this,S,document.createDocumentFragment());Pe(s(this,R),e);const r=s(this,T).pending;n(this,w,F(()=>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,P))ve(e,Ve),ye(e);for(const e of s(this,V))ve(e,We),ye(e);s(this,P).clear(),s(this,V).clear()},X=function(e){var r=C,a=Re,h=Se;Z(s(this,g)),G(s(this,g)),be(s(this,g).ctx);try{return e()}catch(i){return ze(i),null}finally{Z(r),G(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,N,s(this,N)+e),s(this,N)===0&&(p(this,o,Q).call(this),s(this,w)&&ie(s(this,w),()=>{n(this,w,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"),ke=new Set,le=new Set;function bt(t,e,r,a={}){function h(i){if(a.capture||ce.call(e,i),!i.cancelBubble)return et(()=>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)&&Ke(()=>{e.removeEventListener(t,f,i)})}function St(t,e,r){(e[I]??(e[I]={}))[t]=r}function At(t){for(var e=0;e{throw k});throw D}}finally{t[I]=e,delete t.currentTarget,G(B),Z(W)}}}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 Fe(t,e)}function Nt(t,e){ne(),e.intro=e.intro??!1;const r=e.target,a=Y,h=M;try{for(var i=tt(r);i&&(i.nodeType!==Ae||i.data!==rt);)i=st(i);if(!i)throw fe;U(!0),K(i);const f=Fe(t,{...e,anchor:i});return U(!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),U(!1),Et(t,e)}finally{U(a),K(h)}}const J=new Map;function Fe(t,{target:e,anchor:r,props:a={},events:h,context:i,intro:f=!0,transformError:u}){ne();var l=void 0,d=nt(()=>{var B=r??e.appendChild(Te());pt(B,{pending:()=>{}},v=>{ht({});var _=Se;if(i&&(_.c=i),h&&(a.$$events=h),Y&&dt(v,null),l=t(v,a)||{},Y&&(C.nodes.end=M,M===null||M.nodeType!==Ae||M.data!==ot))throw lt(),fe;ct()},u);var W=new Set,D=v=>{for(var _=0;_{var O;for(var v of W)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),B!==r&&((O=B.parentNode)==null||O.removeChild(B))}});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,At as d,Rt as e,Nt as h,Et as m,Dt as s,Ot as u}; diff --git a/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js.br b/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js.br new file mode 100644 index 0000000..92f1bf8 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js.gz b/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js.gz new file mode 100644 index 0000000..091898c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/FzvEaXMa.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js b/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js deleted file mode 100644 index 52b89bd..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js +++ /dev/null @@ -1 +0,0 @@ -import{H as m,I as _,m as b,l as i,J as y,K as v,M as h}from"./C9Z4nxhR.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/P9ZHwQBL.js.br b/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js.br deleted file mode 100644 index 4ab9609..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js.gz b/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js.gz deleted file mode 100644 index 90c838c..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/P9ZHwQBL.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js b/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js new file mode 100644 index 0000000..2591087 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js @@ -0,0 +1 @@ +import{G as J,bd as ee}from"./CvjSAYrz.js";import{w as ae}from"./DfQhL-hC.js";import{c as ne,H as N,N as B,r as mt,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"./DJWRm1Ki.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 ge(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 me({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 ge(_,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 W=new Map,Mt=new Set,Ee=new Set,F=new Set;let m={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_er3k9h)!=null&&o.data&&globalThis.__sveltekit_er3k9h.data,document.URL!==location.href&&(location.href=location.href),b=t,await((i=(s=t.hooks).init)==null?void 0:i.call(s)),kt=me(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:mt(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 Gt(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 Wt(t,a,e,r){let n;a.invalidateAll&&pt(),await D({type:"goto",url:mt(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;m=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(),Gt(R),e){const o={from:null,to:{params:m.params,route:{id:((n=m.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(g=>g==null?void 0:g().catch(()=>{})),l.forEach(g=>g==null?void 0:g[1]().catch(()=>{}));const h=m.url?t!==rt(m.url):!1,u=m.route?n.id!==m.route.id:!1,w=xe(m.url,e);let f=!1;const d=l.map(async(g,p)=>{var A;if(!g)return;const y=m.branch[p];return g[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:g[1],url:e,params:r,route:n,parent:async()=>{var z;const T={};for(let j=0;j{});const _=[];for(let g=0;gPromise.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 Wt(new URL(s.location,location.href),{},0);throw s}}async function Ae(t){const a=t.href;if(W.has(a))return W.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})();W.set(a,r),e=await r}catch{W.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(m,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(m,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,g=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(g),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,G={[N]:E+=k,[B]:R+=k,[Nt]:s};(o?history.replaceState:history.pushState).call(history,G,"",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)})}m=p.state,p.props.page&&(p.props.page.url=a);const G=y&&await y;G?S=G.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"&&Gt(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(m.url)===rt(u);if(!(d.reload||_))if(c<=d.preload_data){a={element:l,href:l.href},e=q.tap;const g=await ot(u,!1);if(!g)return;ke(g)}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(mt(t)),t.origin!==Dt?Promise.reject(new Error("goto: invalid URL")):Wt(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(m,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]=m.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(!gt){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=m.url?it(location)===it(m.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){m.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(_,g)=>{const p=i[g];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;gt=!0,location.replace(new URL(`#${r}`,location.href)),history.replaceState(i,"",t),a&&scrollTo(o,s),gt=!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/S0ILvWpb.js.br b/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js.br new file mode 100644 index 0000000..cfb62ad Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js.gz b/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js.gz new file mode 100644 index 0000000..747e57f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/S0ILvWpb.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js b/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js deleted file mode 100644 index e888155..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js +++ /dev/null @@ -1 +0,0 @@ -import{J as i,q as d,N as n,O as v,Q as u,R as h,T as g,U as A}from"./C9Z4nxhR.js";const N=Symbol("is custom element"),T=Symbol("is html"),l=h?"link":"LINK";function S(r){if(i){var s=!1,e=()=>{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,d(e),n()}}function t(r,s,e,a){var o=p(r);i&&(o[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===l)||o[s]!==(o[s]=e)&&(s==="loading"&&(r[v]=e),e==null?r.removeAttribute(s):typeof e!="string"&&L(r).includes(s)?r[s]=e:r.setAttribute(s,e))}function p(r){return r.__attributes??(r.__attributes={[N]:r.nodeName.includes("-"),[T]:r.namespaceURI===u})}var c=new Map;function L(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=g(o)}return e}export{S as r,t as s}; diff --git a/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js.br b/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js.br deleted file mode 100644 index d37a483..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js.br +++ /dev/null @@ -1,2 +0,0 @@ -@VU\6u639TiܥX*;U7͡bV+y@k>S(Ԝ w[V?O`K5|MMrZ>/_Wl"?"_+*mk:ȼ +C5sm?BC9òx&y I vt\gνu}!x*j斨L5M!eSO1*HJo$VߛJ H)i',W8\YEt7rDLp<>-, Ekx@쪹I -7Qh>i'T$iJӜ8,\Z?B-쐌;Mu@PhH}n#*COROdJbϹ,"Dɢ6/zdZ(B5@M#[ O/s噬S,^޲/^XV"JFHIf%m2[~9 \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js.gz b/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js.gz deleted file mode 100644 index 30a5772..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/ZesQ8l8p.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js b/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js deleted file mode 100644 index 03ef2ab..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js +++ /dev/null @@ -1 +0,0 @@ -import{af as g,ag as d,ah as c,m,ai as i,aj as b,g as p,ak as h,B as k,al as v}from"./C9Z4nxhR.js";function x(t=!1){const a=g,e=a.l.u;if(!e)return;let o=()=>h(a.s);if(t){let n=0,s={};const _=k(()=>{let l=!1;const r=a.s;for(const f in r)r[f]!==s[f]&&(s[f]=r[f],l=!0);return l&&n++,n});o=()=>p(_)}e.b.length&&d(()=>{u(a,o),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,o),i(e.a)})}function u(t,a){if(t.l.s)for(const e of t.l.s)p(e);a()}v();export{x as i}; diff --git a/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js.br b/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js.br deleted file mode 100644 index 09e4908..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js.br +++ /dev/null @@ -1,2 +0,0 @@ -@X:tw.I?cY$_49ǟ{};ьU42T=%\O]$U2$(&f;`퇇 zD- ^vpkwB.H8P^N \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js.gz b/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js.gz deleted file mode 100644 index 9e03ee6..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/_Va07L2l.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js b/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js new file mode 100644 index 0000000..1fa46c8 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js @@ -0,0 +1 @@ +import{b as T,m as o,ab as b,E as h,ac as p,ax as v,ad as A,ae as E,v as R,q as l}from"./CvjSAYrz.js";import{B as g}from"./DE4u6cUg.js";function N(t,c,u=!1){o&&b();var n=new g(t),_=u?h:0;function i(a,r){if(o){const e=p(t);var s;if(e===v?s=0:e===A?s=!1:s=parseInt(e.substring(1)),a!==s){var f=E();R(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/ciN1mm2W.js.br b/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js.br new file mode 100644 index 0000000..ea04e43 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js.gz b/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js.gz new file mode 100644 index 0000000..59ddd4d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/ciN1mm2W.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js b/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js new file mode 100644 index 0000000..04f8dd3 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js @@ -0,0 +1 @@ +import{b as p,E as t}from"./CvjSAYrz.js";import{B as c}from"./DE4u6cUg.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/ckF4CxmX.js.br b/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js.br new file mode 100644 index 0000000..76088d7 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js.br differ diff --git a/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js.gz b/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js.gz new file mode 100644 index 0000000..1046007 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/chunks/ckF4CxmX.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js b/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js deleted file mode 100644 index 4447ab5..0000000 --- a/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js +++ /dev/null @@ -1 +0,0 @@ -import{aa as F,b as fe,an as ne,J as D,a5 as q,ao as ie,a0 as le,g as Q,a1 as ue,a3 as se,a4 as W,a6 as L,ac as z,ap as oe,aq as te,ar as $,K as ve,as as C,ab as y,at as de,ae as ce,C as pe,Z as _e,au as X,av as he,aw as ge,X as Ee,ax as j,ay as me,a7 as re,a9 as ae,az as B,q as Ce,aA as Te,aB as Ae,aC as we,a8 as Se,aD as Ie}from"./C9Z4nxhR.js";function De(e,r){return r}function Ne(e,r,l){for(var t=[],g=r.length,s,u=r.length,c=0;c{if(s){if(s.pending.delete(E),s.done.add(E),s.pending.size===0){var o=e.outrogroups;V(X(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,a=v.parentNode;we(a),a.append(v),e.items.clear()}V(r,!i)}else s={pending:new Set(r),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(s)}function V(e,r=!0){for(var l=0;l{var f=l();return _e(f)?f:f==null?[]:X(f)}),o,d=!0;function A(){n.fallback=a,xe(n,o,u,r,t),a!==null&&(o.length===0?(a.f&C)===0?re(a):(a.f^=C,M(a,null,u)):ae(a,()=>{a=null}))}var I=fe(()=>{o=Q(E);var f=o.length;let N=!1;if(D){var x=ue(u)===se;x!==(f===0)&&(u=W(),q(u),L(!1),N=!0)}for(var _=new Set,w=ve,b=ce(),p=0;ps(u)):(a=y(()=>s(ee??(ee=F()))),a.f|=C)),f>_.size&&de(),D&&f>0&&q(W()),!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),Q(E)}),n={effect:I,items:c,outrogroups:null,fallback:a};d=!1,D&&(u=z)}function H(e){for(;e!==null&&(e.f&Te)===0;)e=e.next;return e}function xe(e,r,l,t,g){var h,k,O,Y,J,K,U,Z,G;var s=(t&Ae)!==0,u=r.length,c=e.items,i=H(e.effect.first),v,a=null,E,o=[],d=[],A,I,n,f;if(s)for(f=0;f0){var R=(t&ne)!==0&&u===0?l:null;if(s){for(f=0;f{var m,P;if(E!==void 0)for(n of E)(P=(m=n.nodes)==null?void 0:m.a)==null||P.apply()})}function be(e,r,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(r,i??l,v??g,c),()=>{e.delete(t)}))}}function M(e,r,l){if(e.nodes)for(var t=e.nodes.start,g=e.nodes.end,s=r&&(r.f&C)===0?r.nodes.start:l;t!==null;){var u=Ie(t);if(s.before(t),t===g)return;t=u}}function T(e,r,l){r===null?e.effect.first=l:r.next=l,l===null?e.effect.last=r:l.prev=r}function Me(e,r,l){var t=e==null?"":""+e;return t===""?null:t}function ke(e,r){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/kH-DTQyy.js.br b/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js.br deleted file mode 100644 index 09fc4f1..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js.gz b/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js.gz deleted file mode 100644 index 7de25f7..0000000 Binary files a/apps/dashboard/build/_app/immutable/chunks/kH-DTQyy.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js b/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js deleted file mode 100644 index 427a1ea..0000000 --- a/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js +++ /dev/null @@ -1,2 +0,0 @@ -const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.ChrQNylP.js","../chunks/Bzak7iHL.js","../chunks/CkyfbJUz.js","../chunks/C9Z4nxhR.js","../chunks/DP9qWekZ.js","../chunks/DPfxVJHQ.js","../chunks/C2oj68pw.js","../chunks/CY4crMrT.js","../chunks/kH-DTQyy.js","../chunks/D00YwZ1M.js","../chunks/ZesQ8l8p.js","../chunks/Co2v30Gm.js","../chunks/P9ZHwQBL.js","../chunks/CR6HhlME.js","../chunks/DWr9YED7.js","../chunks/DnKV7_Y9.js","../chunks/DunNqS1N.js","../chunks/BmeMLq0p.js","../assets/0.BChctYiF.css","../nodes/1.CUEJZ2Iu.js","../chunks/_Va07L2l.js","../nodes/2.Bv9w28KX.js","../nodes/3.D_o4dH3z.js","../nodes/4.CeoFmj14.js","../chunks/BkopTN9z.js","../chunks/BcuCGYSa.js","../nodes/5.CgbdGsQS.js","../chunks/CZ45jJaw.js","../chunks/Do8TgQ-j.js","../assets/5.BBx09UGv.css","../nodes/6.DXXEUSu1.js","../chunks/-jeO_JOJ.js","../nodes/7.BI22Pt_j.js","../nodes/8.Cq7jwWnG.js","../nodes/9.19crbYuZ.js","../nodes/10.CsJcFbdU.js","../nodes/11.C5VMEnLV.js"])))=>i.map(i=>d[i]); -var M=r=>{throw TypeError(r)};var Q=(r,t,e)=>t.has(r)||M("Cannot "+e);var m=(r,t,e)=>(Q(r,t,"read from private field"),e?e.call(r):t.get(r)),H=(r,t,e)=>t.has(r)?M("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(r):t.set(r,e),J=(r,t,e,n)=>(Q(r,t,"write to private field"),n?n.call(r,e):t.set(r,e),e);import{J as Z,a0 as ut,b as lt,E as mt,a1 as _t,a4 as dt,a5 as ft,a6 as $,a2 as ht,ac as vt,h as q,L as gt,g as v,b7 as Et,Y as yt,X as pt,p as Pt,ag as Rt,ah as bt,I as Ot,f as L,d as At,a as Tt,s as W,e as Lt,r as wt,t as It,u as V}from"../chunks/C9Z4nxhR.js";import{h as kt,m as Dt,u as xt,s as Vt}from"../chunks/DP9qWekZ.js";import"../chunks/Bzak7iHL.js";import{o as St}from"../chunks/CkyfbJUz.js";import{i as B}from"../chunks/C2oj68pw.js";import{a as y,c as k,f as et,t as jt}from"../chunks/DPfxVJHQ.js";import{B as Ct}from"../chunks/CY4crMrT.js";import{b as S}from"../chunks/CR6HhlME.js";import{p as Y}from"../chunks/Do8TgQ-j.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 Yt{constructor(t){super({component:r,...t})}}}var p,d;class Yt{constructor(t){H(this,p);H(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 q(e.get(a)??n(a,i),i),Reflect.set(s,a,i)}});J(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(),J(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 qt="modulepreload",Ft=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=Ft(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":qt,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 Gt(r,t){Pt(t,!0);let e=Y(t,"components",23,()=>[]),n=Y(t,"data_0",3,null),o=Y(t,"data_1",3,null),l=Y(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=W(!1),a=W(!1),i=W(null);St(()=>{const u=t.stores.page.subscribe(()=>{v(s)&&(q(a,!0),Ot().then(()=>{q(i,document.title||"untitled page",!0)}))});return q(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 A=k(),D=L(A);j(D,()=>v(O),(T,w)=>{S(w(T,{get data(){return n()},get form(){return t.form},get params(){return t.page.params},children:(f,Jt)=>{var z=k(),at=L(z);{var st=I=>{const F=V(()=>t.constructors[1]);var x=k(),N=L(x);j(N,()=>v(F),(U,G)=>{S(G(U,{get data(){return o()},get form(){return t.form},get params(){return t.page.params},children:(h,Wt)=>{var K=k(),ot=L(K);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,K)},$$slots:{default:!0}}),h=>e()[1]=h,()=>{var h;return(h=e())==null?void 0:h[1]})}),y(I,x)},nt=I=>{const F=V(()=>t.constructors[1]);var x=k(),N=L(x);j(N,()=>v(F),(U,G)=>{S(G(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,z)},$$slots:{default:!0}}),f=>e()[0]=f,()=>{var f;return(f=e())==null?void 0:f[0]})}),y(u,A)},X=u=>{const O=V(()=>t.constructors[0]);var A=k(),D=L(A);j(D,()=>v(O),(T,w)=>{S(w(T,{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,A)};B(g,u=>{t.constructors[1]?u(R):u(X,!1)})}var E=At(g,2);{var b=u=>{var O=Nt(),A=Lt(O);{var D=T=>{var w=jt();It(()=>Vt(w,v(i))),y(T,w)};B(A,T=>{v(a)&&T(D)})}wt(O),y(u,O)};B(E,u=>{v(s)&&u(b)})}y(r,c),Tt()}const se=Bt(Gt),ne=[()=>_(()=>import("../nodes/0.ChrQNylP.js"),__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18]),import.meta.url),()=>_(()=>import("../nodes/1.CUEJZ2Iu.js"),__vite__mapDeps([19,1,20,3,4,5,16,15,2]),import.meta.url),()=>_(()=>import("../nodes/2.Bv9w28KX.js"),__vite__mapDeps([21,1,3,5,9,7]),import.meta.url),()=>_(()=>import("../nodes/3.D_o4dH3z.js"),__vite__mapDeps([22,1,20,3,2,16,15]),import.meta.url),()=>_(()=>import("../nodes/4.CeoFmj14.js"),__vite__mapDeps([23,1,3,4,5,6,7,8,10,11,24,12,25]),import.meta.url),()=>_(()=>import("../nodes/5.CgbdGsQS.js"),__vite__mapDeps([26,1,20,3,4,5,6,7,8,24,14,15,17,27,10,11,28,29]),import.meta.url),()=>_(()=>import("../nodes/6.DXXEUSu1.js"),__vite__mapDeps([30,1,2,3,4,5,6,7,8,10,11,24,12,31,14,15,13,28,27,25,17]),import.meta.url),()=>_(()=>import("../nodes/7.BI22Pt_j.js"),__vite__mapDeps([32,1,2,3,4,5,6,7,8,11,25]),import.meta.url),()=>_(()=>import("../nodes/8.Cq7jwWnG.js"),__vite__mapDeps([33,1,2,3,4,5,6,7,8,10,11,24,12,31,25,27]),import.meta.url),()=>_(()=>import("../nodes/9.19crbYuZ.js"),__vite__mapDeps([34,1,2,3,4,5,6,7,8,11,24,14,15,25,17]),import.meta.url),()=>_(()=>import("../nodes/10.CsJcFbdU.js"),__vite__mapDeps([35,1,2,3,4,5,6,7,8,24,25]),import.meta.url),()=>_(()=>import("../nodes/11.C5VMEnLV.js"),__vite__mapDeps([36,1,2,3,4,5,6,7,8,24,31,25,27]),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:{}},Ht=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)=>Ht[r](t);export{le as decode,Ht 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.Cznegg3r.js.br b/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js.br deleted file mode 100644 index 5578e84..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js.gz b/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js.gz deleted file mode 100644 index ef27d06..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/app.Cznegg3r.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js b/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js new file mode 100644 index 0000000..2ef6e20 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js @@ -0,0 +1,2 @@ +const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["../nodes/0.CFcocLwc.js","../chunks/Bzak7iHL.js","../chunks/CNjeV5xa.js","../chunks/CvjSAYrz.js","../chunks/FzvEaXMa.js","../chunks/BsvCUYx-.js","../chunks/ciN1mm2W.js","../chunks/DE4u6cUg.js","../chunks/DTnG8poT.js","../chunks/ckF4CxmX.js","../chunks/CNfQDikv.js","../chunks/DPl3NjBv.js","../chunks/BKuqSeVd.js","../chunks/CVpUe0w3.js","../chunks/D3XWCg9-.js","../chunks/D81f-o_I.js","../chunks/DfQhL-hC.js","../chunks/S0ILvWpb.js","../chunks/DJWRm1Ki.js","../chunks/CtkE7HV2.js","../chunks/Bz1l2A_1.js","../chunks/Bhad70Ss.js","../chunks/Casl2yrL.js","../chunks/DzfRjky4.js","../chunks/DNjM5a-l.js","../assets/0.IIz8MMYb.css","../nodes/1.--qOmhsd.js","../nodes/2.CD5F7bS_.js","../nodes/3.D16O8s7t.js","../nodes/4.BSlP3-UA.js","../chunks/B_YDQCB6.js","../nodes/5.B300rRjT.js","../chunks/DMu1Byux.js","../assets/5.DQ_AfUnN.css","../nodes/6.DBS_R5Hl.js","../chunks/DObx9JW_.js","../assets/6.BSSBWVKL.css","../nodes/7.br0Vbs-w.js","../assets/7.CCrNEDd3.css","../nodes/8.CDAVQcae.js","../nodes/9.DVbfK-u1.js","../assets/9.BBx09UGv.css","../nodes/10.CPGa_1iF.js","../nodes/11.j3H5l-xO.js","../nodes/12.DZiW_IZ_.js","../nodes/13.DReyqY5Q.js","../assets/13.Bjd0S47S.css","../nodes/14.BpCacSGt.js","../nodes/15.DFbOY736.js","../assets/15.ChjqzJHo.css","../nodes/16.DMIuRZWa.js","../assets/16.BnHgRQtR.css","../nodes/17.PvQmHhRC.js","../nodes/18.Df4fIuu-.js","../nodes/19.CMsn8k5A.js"])))=>i.map(i=>d[i]); +var M=r=>{throw TypeError(r)};var Q=(r,t,e)=>t.has(r)||M("Cannot "+e);var l=(r,t,e)=>(Q(r,t,"read from private field"),e?e.call(r):t.get(r)),H=(r,t,e)=>t.has(r)?M("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(r):t.set(r,e),W=(r,t,e,n)=>(Q(r,t,"write to private field"),n?n.call(r,e):t.set(r,e),e);import{m as Z,ab as ut,b as ct,E as _t,ac as lt,ae as dt,v as ft,q as $,ax as vt,w as ht,h as F,X as pt,g as h,bb as gt,a6 as Et,a5 as Pt,p as yt,aA as Rt,aB as Ot,G as bt,f as L,d as At,a as Tt,s as X,e as Lt,r as Dt,u as x,t as It}from"../chunks/CvjSAYrz.js";import{h as Vt,m as wt,u as kt,s as xt}from"../chunks/FzvEaXMa.js";import"../chunks/Bzak7iHL.js";import{o as St}from"../chunks/CNjeV5xa.js";import{i as B}from"../chunks/ciN1mm2W.js";import{a as E,c as V,f as et,t as jt}from"../chunks/BsvCUYx-.js";import{B as Ct}from"../chunks/DE4u6cUg.js";import{b as S}from"../chunks/D3XWCg9-.js";import{p as q}from"../chunks/B_YDQCB6.js";function j(r,t,e){var n;Z&&(n=ht,ut());var i=new Ct(r);ct(()=>{var _=t()??null;if(Z){var s=lt(n),a=s===vt,m=_!==null;if(a!==m){var y=dt();ft(y),i.anchor=y,$(!1),i.ensure(_,_&&(u=>e(u,_))),$(!0);return}}i.ensure(_,_&&(u=>e(u,_)))},_t)}function Bt(r){return class extends qt{constructor(t){super({component:r,...t})}}}var P,d;class qt{constructor(t){H(this,P);H(this,d);var _;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 F(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})),(!((_=t==null?void 0:t.props)!=null&&_.$$host)||t.sync===!1)&>(),W(this,P,i.$$events);for(const s of Object.keys(l(this,d)))s==="$set"||s==="$destroy"||s==="$on"||Et(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 Ft="modulepreload",Gt=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(R=>({status:"fulfilled",value:R}),R=>({status:"rejected",reason:R}))))};const a=document.getElementsByTagName("link"),m=document.querySelector("meta[property=csp-nonce]"),y=(m==null?void 0:m.nonce)||(m==null?void 0:m.getAttribute("nonce"));i=s(e.map(u=>{if(u=Gt(u,n),u in tt)return;tt[u]=!0;const p=u.endsWith(".css"),R=p?'[rel="stylesheet"]':"";if(!!n)for(let O=a.length-1;O>=0;O--){const c=a[O];if(c.href===u&&(!p||c.rel==="stylesheet"))return}else if(document.querySelector(`link[href="${u}"]${R}`))return;const g=document.createElement("link");if(g.rel=p?"stylesheet":Ft,p||(g.as="script"),g.crossOrigin="",g.href=u,y&&g.setAttribute("nonce",y),document.head.appendChild(g),p)return new Promise((O,c)=>{g.addEventListener("load",O),g.addEventListener("error",()=>c(new Error(`Unable to preload CSS for ${u}`)))})}))}function _(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"&&_(a.reason);return t().catch(_)})},ae={};var Nt=et('
'),Ut=et(" ",1);function Yt(r,t){yt(t,!0);let e=q(t,"components",23,()=>[]),n=q(t,"data_0",3,null),i=q(t,"data_1",3,null),_=q(t,"data_2",3,null);Rt(()=>t.stores.page.set(t.page)),Ot(()=>{t.stores,t.page,t.constructors,e(),t.form,n(),i(),_(),t.stores.page.notify()});let s=X(!1),a=X(!1),m=X(null);St(()=>{const c=t.stores.page.subscribe(()=>{h(s)&&(F(a,!0),bt().then(()=>{F(m,document.title||"untitled page",!0)}))});return F(s,!0),c});const y=x(()=>t.constructors[2]);var u=Ut(),p=L(u);{var R=c=>{const b=x(()=>t.constructors[0]);var A=V(),w=L(A);j(w,()=>h(b),(T,D)=>{S(D(T,{get data(){return n()},get form(){return t.form},get params(){return t.page.params},children:(f,Wt)=>{var J=V(),at=L(J);{var st=I=>{const G=x(()=>t.constructors[1]);var k=V(),N=L(k);j(N,()=>h(G),(U,Y)=>{S(Y(U,{get data(){return i()},get form(){return t.form},get params(){return t.page.params},children:(v,Xt)=>{var K=V(),nt=L(K);j(nt,()=>h(y),(it,mt)=>{S(mt(it,{get data(){return _()},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]})}),E(v,K)},$$slots:{default:!0}}),v=>e()[1]=v,()=>{var v;return(v=e())==null?void 0:v[1]})}),E(I,k)},ot=I=>{const G=x(()=>t.constructors[1]);var k=V(),N=L(k);j(N,()=>h(G),(U,Y)=>{S(Y(U,{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]})}),E(I,k)};B(at,I=>{t.constructors[2]?I(st):I(ot,!1)})}E(f,J)},$$slots:{default:!0}}),f=>e()[0]=f,()=>{var f;return(f=e())==null?void 0:f[0]})}),E(c,A)},z=c=>{const b=x(()=>t.constructors[0]);var A=V(),w=L(A);j(w,()=>h(b),(T,D)=>{S(D(T,{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]})}),E(c,A)};B(p,c=>{t.constructors[1]?c(R):c(z,!1)})}var g=At(p,2);{var O=c=>{var b=Nt(),A=Lt(b);{var w=T=>{var D=jt();It(()=>xt(D,h(m))),E(T,D)};B(A,T=>{h(a)&&T(w)})}Dt(b),E(c,b)};B(g,c=>{h(s)&&c(O)})}E(r,u),Tt()}const se=Bt(Yt),oe=[()=>o(()=>import("../nodes/0.CFcocLwc.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.--qOmhsd.js"),__vite__mapDeps([26,1,20,3,4,5,18,2,16,17]),import.meta.url),()=>o(()=>import("../nodes/2.CD5F7bS_.js"),__vite__mapDeps([27,1,3,5,9,7]),import.meta.url),()=>o(()=>import("../nodes/3.D16O8s7t.js"),__vite__mapDeps([28,1,20,3,2,17,16,18]),import.meta.url),()=>o(()=>import("../nodes/4.BSlP3-UA.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.B300rRjT.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.DBS_R5Hl.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.br0Vbs-w.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.CDAVQcae.js"),__vite__mapDeps([39,1,3,4,5,6,7,8,10,11,12,21,13,24]),import.meta.url),()=>o(()=>import("../nodes/9.DVbfK-u1.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.CPGa_1iF.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.j3H5l-xO.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.DZiW_IZ_.js"),__vite__mapDeps([44,1,2,3,4,5,6,7,8,11,12,24]),import.meta.url),()=>o(()=>import("../nodes/13.DReyqY5Q.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.BpCacSGt.js"),__vite__mapDeps([47,1,2,3,4,5,6,7,8,10,11,12,21]),import.meta.url),()=>o(()=>import("../nodes/15.DFbOY736.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.DMIuRZWa.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.PvQmHhRC.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.Df4fIuu-.js"),__vite__mapDeps([53,1,2,3,4,5,6,7,8,21,12,24]),import.meta.url),()=>o(()=>import("../nodes/19.CMsn8k5A.js"),__vite__mapDeps([54,1,2,3,4,5,6,7,8,21,12,32,24,23]),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]]},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,ce=(r,t)=>Ht[r](t);export{ce 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.Y1ipb8HW.js.br b/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js.br new file mode 100644 index 0000000..cd9b35b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js.br differ diff --git a/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js.gz b/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js.gz new file mode 100644 index 0000000..7b5f9c1 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/app.Y1ipb8HW.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js b/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js deleted file mode 100644 index 8c063a8..0000000 --- a/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js +++ /dev/null @@ -1 +0,0 @@ -import{l as o,a as r}from"../chunks/DunNqS1N.js";export{o as load_css,r as start}; diff --git a/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js.br b/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js.br deleted file mode 100644 index 0f16ff4..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js.gz b/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js.gz deleted file mode 100644 index c824cea..0000000 Binary files a/apps/dashboard/build/_app/immutable/entry/start.B2d_kw0s.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js b/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js new file mode 100644 index 0000000..ecb6143 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js @@ -0,0 +1 @@ +import{a as r}from"../chunks/S0ILvWpb.js";import{w as t}from"../chunks/DJWRm1Ki.js";export{t as load_css,r as start}; diff --git a/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js.br b/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js.br new file mode 100644 index 0000000..689a89b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js.br differ diff --git a/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js.gz b/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js.gz new file mode 100644 index 0000000..9b2d113 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/entry/start.Bjj_Sn2c.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js b/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js new file mode 100644 index 0000000..69973d1 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js @@ -0,0 +1,86 @@ +import"../chunks/Bzak7iHL.js";import{o as tt}from"../chunks/CNjeV5xa.js";import{f as de,d as o,e as s,r as t,t as I,p as Oe,a as Pe,n as W,g as e,s as ce,c as ut,h as C,u as B}from"../chunks/CvjSAYrz.js";import{s as y,d as Ye,a as Q,e as Ee}from"../chunks/FzvEaXMa.js";import{i as V}from"../chunks/ciN1mm2W.js";import{e as _e,i as Fe}from"../chunks/DTnG8poT.js";import{c as mt,a as h,f as x}from"../chunks/BsvCUYx-.js";import{s as ft}from"../chunks/ckF4CxmX.js";import{s as te,r as ht}from"../chunks/CNfQDikv.js";import{s as R}from"../chunks/DPl3NjBv.js";import{b as gt}from"../chunks/CVpUe0w3.js";import{b as bt}from"../chunks/D3XWCg9-.js";import{a as j,s as ye}from"../chunks/D81f-o_I.js";import{s as xt,g as Qe}from"../chunks/S0ILvWpb.js";import{b as U}from"../chunks/DJWRm1Ki.js";import{s as at,m as st,e as kt,a as rt,w as Xe,u as _t,i as yt,f as wt}from"../chunks/CtkE7HV2.js";import{i as $t}from"../chunks/Bz1l2A_1.js";import{s as nt}from"../chunks/Bhad70Ss.js";import{t as ee}from"../chunks/Casl2yrL.js";import{a as Ze}from"../chunks/DNjM5a-l.js";import{d as Mt,w as it,g as ot}from"../chunks/DfQhL-hC.js";const Ct=()=>{const a=xt;return{page:{subscribe:a.page.subscribe},navigating:{subscribe:a.navigating.subscribe},updated:a.updated}},At={subscribe(a){return Ct().page.subscribe(a)}};var Dt=x('
');function Tt(a){const r=()=>j(at,"$suppressedCount",l),[l,u]=ye();var g=mt(),$=de(g);{var w=A=>{var _=Dt(),b=o(s(_),2),m=s(b);t(b),t(_),I(()=>y(m,`Actively forgetting ${r()??""} ${r()===1?"memory":"memories"}`)),h(A,_)};V($,A=>{r()>0&&A(w)})}h(a,g),u()}var Et=x(''),Ft=x('
');function St(a,r){Oe(r,!1);const l=()=>j(ee,"$toasts",u),[u,g]=ye(),$={DreamCompleted:"✦",ConsolidationCompleted:"◉",ConnectionDiscovered:"⟷",MemoryPromoted:"↑",MemoryDemoted:"↓",MemorySuppressed:"◬",MemoryUnsuppressed:"◉",Rac1CascadeSwept:"✺",MemoryDeleted:"✕"};function w(m){return $[m]??"◆"}function A(m){ee.dismiss(m.id)}function _(m,d){(m.key==="Enter"||m.key===" ")&&(m.preventDefault(),ee.dismiss(d.id))}$t();var b=Ft();_e(b,5,l,m=>m.id,(m,d)=>{var k=Et(),D=o(s(k),2),S=s(D),G=s(S),L=s(G,!0);t(G);var N=o(G,2),Z=s(N,!0);t(N),t(S);var H=o(S,2),q=s(H,!0);t(H),t(D),W(2),t(k),I(z=>{te(k,"aria-label",`${e(d).title??""}: ${e(d).body??""}. Click to dismiss.`),nt(k,`--toast-color: ${e(d).color??""}; --toast-dwell: ${e(d).dwellMs??""}ms;`),y(L,z),y(Z,e(d).title),y(q,e(d).body)},[()=>w(e(d).type)]),Q("click",k,()=>A(e(d))),Q("keydown",k,z=>_(z,e(d))),Ee("mouseenter",k,()=>ee.pauseDwell(e(d).id,e(d).dwellMs)),Ee("mouseleave",k,()=>ee.resumeDwell(e(d).id)),Ee("focus",k,()=>ee.pauseDwell(e(d).id,e(d).dwellMs)),Ee("blur",k,()=>ee.resumeDwell(e(d).id)),h(m,k)}),t(b),h(a,b),Pe(),g()}Ye(["click","keydown"]);function ve(a){const r=a.data;if(!r||typeof r!="object")return null;const l=r.timestamp??r.at??r.occurred_at;if(l==null)return null;if(typeof l=="number")return Number.isFinite(l)?l>1e12?l:l*1e3:null;if(typeof l!="string")return null;const u=Date.parse(l);return Number.isFinite(u)?u:null}const qe=10,lt=3e4,It=qe*lt;function Lt(a,r){const l=r-It,u=new Array(qe).fill(0);for(const $ of a){if($.type==="Heartbeat")continue;const w=ve($);if(w===null||wr)continue;const A=Math.min(qe-1,Math.floor((w-l)/lt));u[A]+=1}const g=Math.max(1,...u);return u.map($=>({count:$,ratio:$/g}))}function Nt(a,r){const l=r-864e5;for(const u of a){if(u.type!=="DreamCompleted")continue;return(ve(u)??r)>=l?u:null}return null}function Rt(a){if(!a||!a.data)return null;const r=a.data,l=typeof r.insights_generated=="number"?r.insights_generated:typeof r.insightsGenerated=="number"?r.insightsGenerated:null;return l!==null&&Number.isFinite(l)?l:null}function jt(a,r){let l=null,u=null;for(const A of a)if(!l&&A.type==="DreamStarted"&&(l=A),!u&&A.type==="DreamCompleted"&&(u=A),l&&u)break;if(!l)return!1;const g=ve(l)??r,$=r-300*1e3;return g<$?!1:u?(ve(u)??r)=u}return!1}var Bt=x(' at risk',1),Gt=x('0 at risk',1),Ht=x(' at risk',1),qt=x(' intentions',1),zt=x('— intentions'),Ot=x('· insights',1),Pt=x(' Last dream: ',1),Yt=x('No recent dream'),Wt=x('
'),Qt=x('
DREAMING...
',1),Xt=x(''),Zt=x('
memories · avg retention
');function Jt(a,r){Oe(r,!0);const l=()=>j(rt,"$avgRetention",$),u=()=>j(kt,"$eventFeed",$),g=()=>j(st,"$memoryCount",$),[$,w]=ye(),A=B(()=>Math.round((l()??0)*100)),_=B(()=>(l()??0)>=.5);let b=ce(null);async function m(){try{const n=await Ze.retentionDistribution();if(Array.isArray(n.endangered)&&n.endangered.length>0){C(b,n.endangered.length,!0);return}const v=n.distribution??[];let M=0;for(const i of v){const c=/^(\d+)/.exec(i.range);if(!c)continue;const p=Number.parseInt(c[1],10);Number.isFinite(p)&&p<30&&(M+=i.count??0)}C(b,M,!0)}catch{C(b,null)}}let d=ce(null);async function k(){var n;try{const v=await Ze.intentions("active");C(d,v.total??((n=v.intentions)==null?void 0:n.length)??0,!0)}catch{C(d,null)}}let D=ce(ut(Date.now()));const S=B(()=>{const n=u(),v=Nt(n,e(D)),M=v?ve(v)??e(D):null,i=M!==null?e(D)-M:null;return{isDreaming:jt(n,e(D)),recent:v,recentMsAgo:i,insights:Rt(v)}}),G=B(()=>Lt(u(),e(D))),L=B(()=>Vt(u(),e(D)));tt(()=>{m(),k();const n=setInterval(()=>{C(D,Date.now(),!0)},1e3),v=setInterval(()=>{m(),k()},6e4);return()=>{clearInterval(n),clearInterval(v)}});var N=Zt();let Z;var H=s(N),q=s(H),z=s(q);let ae;var ue=o(z,2);let we;t(q);var se=o(q,2),me=s(se,!0);t(se);var re=o(se,6);let ne;var Se=s(re);t(re),W(2),t(H);var ie=o(H,4),Ie=s(ie);{var fe=n=>{var v=Bt(),M=de(v),i=s(M,!0);t(M),W(2),I(()=>y(i,e(b))),h(n,v)},he=n=>{var v=Gt();W(2),h(n,v)},Le=n=>{var v=Ht();W(2),h(n,v)};V(Ie,n=>{e(b)!==null&&e(b)>0?n(fe):e(b)===0?n(he,1):n(Le,!1)})}t(ie);var J=o(ie,4),Ne=s(J);{var Re=n=>{var v=qt(),M=de(v);let i;var c=o(M,2);let p;var f=s(c,!0);t(c),W(2),I(()=>{i=R(M,1,"inline-flex h-2 w-2 rounded-full svelte-1kk3799",null,i,{"bg-node-pattern":e(d)>5,"animate-ping-slow":e(d)>5,"bg-muted":e(d)<=5}),p=R(c,1,"tabular-nums svelte-1kk3799",null,p,{"text-node-pattern":e(d)>5,"text-text":e(d)>0&&e(d)<=5,"text-muted":e(d)===0}),y(f,e(d))}),h(n,v)},je=n=>{var v=zt();h(n,v)};V(Ne,n=>{e(d)!==null?n(Re):n(je,!1)})}t(J);var ge=o(J,4),Ke=s(ge);{var be=n=>{var v=Pt(),M=o(de(v),4),i=s(M,!0);t(M);var c=o(M,2);{var p=f=>{var T=Ot(),F=o(de(T),2),K=s(F,!0);t(F),W(2),I(()=>y(K,e(S).insights)),h(f,T)};V(c,f=>{e(S).insights!==null&&f(p)})}I(f=>y(i,f),[()=>Kt(e(S).recentMsAgo)]),h(n,v)},$e=n=>{var v=Yt();h(n,v)};V(Ke,n=>{e(S).recent&&e(S).recentMsAgo!==null?n(be):n($e,!1)})}t(ge);var oe=o(ge,4),Me=o(s(oe),2);_e(Me,21,()=>e(G),Fe,(n,v)=>{var M=Wt();I(i=>nt(M,`height: ${i??""}%; opacity: ${e(v).count===0?.18:.5+e(v).ratio*.5};`),[()=>Math.max(10,e(v).ratio*100)]),h(n,M)}),t(Me),t(oe);var xe=o(oe,2);{var Ce=n=>{var v=Qt();W(2),h(n,v)};V(xe,n=>{e(S).isDreaming&&n(Ce)})}var Ae=o(xe,4);{var Ve=n=>{var v=Xt();h(n,v)};V(Ae,n=>{e(L)&&n(Ve)})}t(N),I(()=>{Z=R(N,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,Z,{"ambient-flash":e(L)}),ae=R(z,1,"absolute inline-flex h-full w-full animate-ping rounded-full opacity-75 svelte-1kk3799",null,ae,{"bg-recall":e(_),"bg-warning":!e(_)}),we=R(ue,1,"relative inline-flex h-2 w-2 rounded-full svelte-1kk3799",null,we,{"bg-recall":e(_),"bg-warning":!e(_)}),y(me,g()),ne=R(re,1,"svelte-1kk3799",null,ne,{"text-recall":e(_),"text-warning":!e(_)}),y(Se,`${e(A)??""}%`)}),h(a,N),Pe(),w()}const dt="vestige.theme",Je="vestige-theme-light",pe=it("dark"),ze=it(!0),Ue=Mt([pe,ze],([a,r])=>a==="auto"?r?"dark":"light":a);function Ut(a){return a==="dark"||a==="light"||a==="auto"}function ea(a){if(Ut(a)){pe.set(a);try{localStorage.setItem(dt,a)}catch{}}}function He(){const a=ot(pe);ea(a==="dark"?"light":a==="light"?"auto":"dark")}function ta(){if(document.getElementById(Je))return;const a=document.createElement("style");a.id=Je,a.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(a)}function et(a){document.documentElement.dataset.theme=a}let O=null,X=null,P=null,Y=null;function aa(){O&&X&&O.removeEventListener("change",X),Y==null||Y(),P==null||P(),O=null,X=null,Y=null,P=null,ta();let a="dark";try{const r=localStorage.getItem(dt);(r==="dark"||r==="light"||r==="auto")&&(a=r)}catch{}return pe.set(a),O=window.matchMedia("(prefers-color-scheme: dark)"),ze.set(O.matches),X=r=>ze.set(r.matches),O.addEventListener("change",X),et(ot(Ue)),Y=Ue.subscribe(et),P=pe.subscribe(()=>{}),()=>{O&&X&&O.removeEventListener("change",X),O=null,X=null,Y==null||Y(),P==null||P(),Y=null,P=null}}var sa=x('');function ra(a){const r=()=>j(pe,"$theme",l),[l,u]=ye(),g={dark:"Dark",light:"Light",auto:"Auto (system)"},$={dark:"light",light:"auto",auto:"dark"};let w=B(r),A=B(()=>$[e(w)]),_=B(()=>`Toggle theme: ${g[e(w)]} (click for ${g[e(A)]})`);var b=sa(),m=s(b),d=s(m);let k;var D=o(d,2);let S;var G=o(D,2);let L;t(m),t(b),I(()=>{te(b,"aria-label",e(_)),te(b,"title",e(_)),te(b,"data-mode",e(w)),k=R(d,0,"icon svelte-1cmi4dh",null,k,{active:e(w)==="dark"}),S=R(D,0,"icon svelte-1cmi4dh",null,S,{active:e(w)==="light"}),L=R(G,0,"icon svelte-1cmi4dh",null,L,{active:e(w)==="auto"})}),Q("click",b,function(...N){He==null||He.apply(this,N)}),h(a,b),u()}Ye(["click"]);var na=x(' '),ia=x('
'),oa=x(''),la=x(' '),da=x(''),ca=x('
No matches
'),va=x('
esc
'),pa=x('
',1);function La(a,r){Oe(r,!0);const l=()=>j(At,"$page",_),u=()=>j(yt,"$isConnected",_),g=()=>j(st,"$memoryCount",_),$=()=>j(rt,"$avgRetention",_),w=()=>j(_t,"$uptimeSeconds",_),A=()=>j(at,"$suppressedCount",_),[_,b]=ye();let m=ce(!1),d=ce(""),k=ce(void 0);tt(()=>{Xe.connect();const i=aa();function c(p){if((p.metaKey||p.ctrlKey)&&p.key==="k"){p.preventDefault(),C(m,!e(m)),C(d,""),e(m)&&requestAnimationFrame(()=>{var F;return(F=e(k))==null?void 0:F.focus()});return}if(p.key==="Escape"&&e(m)){C(m,!1);return}if(p.target instanceof HTMLInputElement||p.target instanceof HTMLTextAreaElement)return;if(p.key==="/"){p.preventDefault();const F=document.querySelector('input[type="text"]');F==null||F.focus();return}const T={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"}[p.key.toLowerCase()];T&&!p.metaKey&&!p.ctrlKey&&!p.altKey&&(p.preventDefault(),Qe(`${U}${T}`))}return window.addEventListener("keydown",c),()=>{Xe.disconnect(),window.removeEventListener("keydown",c),i()}});const D=[{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:","}],S=D.slice(0,5);function G(i,c){const p=c.startsWith(U)?c.slice(U.length)||"/":c;return i==="/graph"?p==="/"||p==="/graph":p.startsWith(i)}let L=B(()=>e(d)?D.filter(i=>i.label.toLowerCase().includes(e(d).toLowerCase())):D);function N(i){C(m,!1),C(d,""),Qe(`${U}${i}`)}var Z=pa(),H=o(de(Z),6),q=s(H),z=s(q),ae=o(z,2);_e(ae,21,()=>D,Fe,(i,c)=>{const p=B(()=>G(e(c).href,l().url.pathname));var f=na(),T=s(f),F=s(T,!0);t(T);var K=o(T,2),ke=s(K,!0);t(K);var De=o(K,2),E=s(De,!0);t(De),t(f),I(()=>{te(f,"href",`${U??""}${e(c).href??""}`),R(f,1,`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm + ${e(p)?"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"}`),y(F,e(c).icon),y(ke,e(c).label),y(E,e(c).shortcut)}),h(i,f)}),t(ae);var ue=o(ae,2),we=s(ue);t(ue);var se=o(ue,2),me=s(se),re=s(me),ne=o(re,2),Se=s(ne,!0);t(ne);var ie=o(ne,2),Ie=s(ie);ra(Ie),t(ie),t(me);var fe=o(me,2),he=s(fe),Le=s(he);t(he);var J=o(he,2),Ne=s(J);t(J);var Re=o(J,2);{var je=i=>{var c=ia(),p=s(c);t(c),I(f=>y(p,`up ${f??""}`),[()=>wt(w())]),h(i,c)};V(Re,i=>{w()>0&&i(je)})}t(fe);var ge=o(fe,2);{var Ke=i=>{var c=oa(),p=s(c);Tt(p),t(c),h(i,c)};V(ge,i=>{A()>0&&i(Ke)})}t(se),t(q);var be=o(q,2),$e=s(be);Jt($e,{});var oe=o($e,2),Me=s(oe);ft(Me,()=>r.children),t(oe),t(be);var xe=o(be,2),Ce=s(xe),Ae=s(Ce);_e(Ae,17,()=>S,Fe,(i,c)=>{const p=B(()=>G(e(c).href,l().url.pathname));var f=la(),T=s(f),F=s(T,!0);t(T);var K=o(T,2),ke=s(K,!0);t(K),t(f),I(()=>{te(f,"href",`${U??""}${e(c).href??""}`),R(f,1,`flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg transition-all min-w-[3.5rem] + ${e(p)?"text-synapse-glow":"text-muted"}`),y(F,e(c).icon),y(ke,e(c).label)}),h(i,f)});var Ve=o(Ae,2);t(Ce),t(xe),t(H);var n=o(H,2);St(n,{});var v=o(n,2);{var M=i=>{var c=va(),p=s(c),f=s(p),T=o(s(f),2);ht(T),bt(T,E=>C(k,E),()=>e(k)),W(2),t(f);var F=o(f,2),K=s(F);_e(K,17,()=>e(L),Fe,(E,le)=>{var Te=da(),Be=s(Te),ct=s(Be,!0);t(Be);var Ge=o(Be,2),vt=s(Ge,!0);t(Ge);var We=o(Ge,2),pt=s(We,!0);t(We),t(Te),I(()=>{y(ct,e(le).icon),y(vt,e(le).label),y(pt,e(le).shortcut)}),Q("click",Te,()=>N(e(le).href)),h(E,Te)});var ke=o(K,2);{var De=E=>{var le=ca();h(E,le)};V(ke,E=>{e(L).length===0&&E(De)})}t(F),t(p),t(c),Q("keydown",c,E=>{E.key==="Escape"&&C(m,!1)}),Q("click",c,E=>{E.target===E.currentTarget&&C(m,!1)}),Q("keydown",T,E=>{E.key==="Enter"&&e(L).length>0&&N(e(L)[0].href)}),gt(T,()=>e(d),E=>C(d,E)),h(i,c)};V(v,i=>{e(m)&&i(M)})}I(i=>{te(z,"href",`${U??""}/graph`),R(re,1,`w-2 h-2 rounded-full ${u()?"bg-recall animate-pulse-glow":"bg-decay"}`),y(Se,u()?"Connected":"Offline"),y(Le,`${g()??""} memories`),y(Ne,`${i??""}% retention`)},[()=>($()*100).toFixed(0)]),Q("click",we,()=>{C(m,!0),C(d,""),requestAnimationFrame(()=>{var i;return(i=e(k))==null?void 0:i.focus()})}),Q("click",Ve,()=>{C(m,!0),C(d,""),requestAnimationFrame(()=>{var i;return(i=e(k))==null?void 0:i.focus()})}),h(a,Z),Pe(),b()}Ye(["click","keydown"]);export{La as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js.br b/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js.br new file mode 100644 index 0000000..e2921e2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js.gz b/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js.gz new file mode 100644 index 0000000..c29e5e2 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/0.CFcocLwc.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js b/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js deleted file mode 100644 index e9e07cd..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js +++ /dev/null @@ -1,3 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as Ee}from"../chunks/CkyfbJUz.js";import{p as Me,d as o,f as Ke,t as K,a as Le,h as d,g as t,e as s,r as a,s as z,u as R,G as Te}from"../chunks/C9Z4nxhR.js";import{d as qe,a as h,s as v}from"../chunks/DP9qWekZ.js";import{i as ie}from"../chunks/C2oj68pw.js";import{e as H,i as V}from"../chunks/kH-DTQyy.js";import{a as g,f as _}from"../chunks/DPfxVJHQ.js";import{s as Fe}from"../chunks/D00YwZ1M.js";import{s as le,r as Se}from"../chunks/ZesQ8l8p.js";import{s as W}from"../chunks/Co2v30Gm.js";import{b as Ae}from"../chunks/P9ZHwQBL.js";import{b as Ne}from"../chunks/CR6HhlME.js";import{s as De,a as L}from"../chunks/DWr9YED7.js";import{s as Ge,g as de,b as O}from"../chunks/DunNqS1N.js";import{w as ce,a as Ie,i as je,m as ze}from"../chunks/BmeMLq0p.js";const Re=()=>{const x=Ge;return{page:{subscribe:x.page.subscribe},navigating:{subscribe:x.navigating.subscribe},updated:x.updated}},He={subscribe(x){return Re().page.subscribe(x)}};var Ve=_(' '),We=_(' '),Oe=_(''),Qe=_('
No matches
'),Be=_('
esc
'),Je=_('
',1);function ct(x,T){Me(T,!0);const Q=()=>L(He,"$page",k),B=()=>L(je,"$isConnected",k),pe=()=>L(ze,"$memoryCount",k),ve=()=>L(Ie,"$avgRetention",k),[k,me]=De();let c=z(!1),f=z(""),y=z(void 0);Ee(()=>{ce.connect();function r(e){if((e.metaKey||e.ctrlKey)&&e.key==="k"){e.preventDefault(),d(c,!t(c)),d(f,""),t(c)&&requestAnimationFrame(()=>{var n;return(n=t(y))==null?void 0:n.focus()});return}if(e.key==="Escape"&&t(c)){d(c,!1);return}if(e.target instanceof HTMLInputElement||e.target instanceof HTMLTextAreaElement)return;if(e.key==="/"){e.preventDefault();const n=document.querySelector('input[type="text"]');n==null||n.focus();return}const i={g:"/graph",m:"/memories",t:"/timeline",f:"/feed",e:"/explore",i:"/intentions",s:"/stats"}[e.key.toLowerCase()];i&&!e.metaKey&&!e.ctrlKey&&!e.altKey&&(e.preventDefault(),de(`${O}${i}`))}return window.addEventListener("keydown",r),()=>{ce.disconnect(),window.removeEventListener("keydown",r)}});const $=[{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:","}],fe=$.slice(0,5);function J(r,e){const p=e.startsWith(O)?e.slice(O.length)||"/":e;return r==="/graph"?p==="/"||p==="/graph":p.startsWith(r)}let C=R(()=>t(f)?$.filter(r=>r.label.toLowerCase().includes(t(f).toLowerCase())):$);function U(r){d(c,!1),d(f,""),de(r)}var X=Je(),q=o(Ke(X),6),F=s(q),S=o(s(F),2);H(S,21,()=>$,V,(r,e)=>{const p=R(()=>J(t(e).href,Q().url.pathname));var i=Ve(),n=s(i),u=s(n,!0);a(n);var m=o(n,2),w=s(m,!0);a(m);var E=o(m,2),l=s(E,!0);a(E),a(i),K(()=>{le(i,"href",t(e).href),W(i,1,`flex items-center gap-3 px-3 py-2.5 rounded-lg transition-all duration-200 text-sm - ${t(p)?"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(u,t(e).icon),v(w,t(e).label),v(l,t(e).shortcut)}),g(r,i)}),a(S);var A=o(S,2),xe=s(A);a(A);var Y=o(A,2),N=s(Y),Z=s(N),P=o(Z,2),ue=s(P,!0);a(P),a(N);var ee=o(N,2),D=s(ee),be=s(D);a(D);var te=o(D,2),he=s(te);a(te),a(ee),a(Y),a(F);var G=o(F,2),ae=s(G),ge=s(ae);Fe(ge,()=>T.children),a(ae),a(G);var se=o(G,2),re=s(se),ne=s(re);H(ne,17,()=>fe,V,(r,e)=>{const p=R(()=>J(t(e).href,Q().url.pathname));var i=We(),n=s(i),u=s(n,!0);a(n);var m=o(n,2),w=s(m,!0);a(m),a(i),K(()=>{le(i,"href",t(e).href),W(i,1,`flex flex-col items-center gap-0.5 px-3 py-2 rounded-lg transition-all min-w-[3.5rem] - ${t(p)?"text-synapse-glow":"text-muted"}`),v(u,t(e).icon),v(w,t(e).label)}),g(r,i)});var _e=o(ne,2);a(re),a(se),a(q);var ye=o(q,2);{var we=r=>{var e=Be(),p=s(e),i=s(p),n=o(s(i),2);Se(n),Ne(n,l=>d(y,l),()=>t(y)),Te(2),a(i);var u=o(i,2),m=s(u);H(m,17,()=>t(C),V,(l,b)=>{var M=Oe(),I=s(M),ke=s(I,!0);a(I);var j=o(I,2),$e=s(j,!0);a(j);var oe=o(j,2),Ce=s(oe,!0);a(oe),a(M),K(()=>{v(ke,t(b).icon),v($e,t(b).label),v(Ce,t(b).shortcut)}),h("click",M,()=>U(t(b).href)),g(l,M)});var w=o(m,2);{var E=l=>{var b=Qe();g(l,b)};ie(w,l=>{t(C).length===0&&l(E)})}a(u),a(p),a(e),h("keydown",e,l=>{l.key==="Escape"&&d(c,!1)}),h("click",e,l=>{l.target===l.currentTarget&&d(c,!1)}),h("keydown",n,l=>{l.key==="Enter"&&t(C).length>0&&U(t(C)[0].href)}),Ae(n,()=>t(f),l=>d(f,l)),g(r,e)};ie(ye,r=>{t(c)&&r(we)})}K(r=>{W(Z,1,`w-2 h-2 rounded-full ${B()?"bg-recall animate-pulse-glow":"bg-decay"}`),v(ue,B()?"Connected":"Offline"),v(be,`${pe()??""} memories`),v(he,`${r??""}% retention`)},[()=>(ve()*100).toFixed(0)]),h("click",xe,()=>{d(c,!0),d(f,""),requestAnimationFrame(()=>{var r;return(r=t(y))==null?void 0:r.focus()})}),h("click",_e,()=>{d(c,!0),d(f,""),requestAnimationFrame(()=>{var r;return(r=t(y))==null?void 0:r.focus()})}),g(x,X),Le(),me()}qe(["click","keydown"]);export{ct as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js.br b/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js.br deleted file mode 100644 index a2d870e..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js.gz b/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js.gz deleted file mode 100644 index 24f058a..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/0.ChrQNylP.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js b/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js new file mode 100644 index 0000000..f82681f --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{i as h}from"../chunks/Bz1l2A_1.js";import{p as g,f as d,t as l,a as v,d as _,e as s,r as o}from"../chunks/CvjSAYrz.js";import{s as p}from"../chunks/FzvEaXMa.js";import{a as x,f as $}from"../chunks/BsvCUYx-.js";import{p as m}from"../chunks/DJWRm1Ki.js";import{s as k}from"../chunks/S0ILvWpb.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.--qOmhsd.js.br b/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js.br new file mode 100644 index 0000000..1143018 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js.gz b/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js.gz new file mode 100644 index 0000000..443d0e7 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/1.--qOmhsd.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js b/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js deleted file mode 100644 index 76ea0a9..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{i as h}from"../chunks/_Va07L2l.js";import{p as g,f as d,t as l,a as v,d as _,e as s,r as o}from"../chunks/C9Z4nxhR.js";import{s as p}from"../chunks/DP9qWekZ.js";import{a as x,f as $}from"../chunks/DPfxVJHQ.js";import{s as k,p as m}from"../chunks/DunNqS1N.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=$("

",1);function B(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{B as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js.br b/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js.br deleted file mode 100644 index 2c9624f..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js.br +++ /dev/null @@ -1 +0,0 @@ -f`dͦ{))ڒCx;@hbߺ.*[qRi~ӭ!Hk^_׊`͍12BgQ߫0_ld&R("72xSOB6fpE>Lix>>nҔ[r:=Sɤi ^Ei`pE\q7`;}dCk4D.uH:d1?#I<:dh)a @[D~ (8F/s ǃ R6JnXP+ܰ \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js.gz b/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js.gz deleted file mode 100644 index 0c40894..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/1.CUEJZ2Iu.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js b/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js similarity index 51% rename from apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js rename to apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js index 2126237..4f9f471 100644 --- a/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js +++ b/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js @@ -1,12 +1,12 @@ -var Tc=Object.defineProperty;var wc=(i,t,e)=>t in i?Tc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var Wt=(i,t,e)=>wc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as Al,a as Rl}from"../chunks/CkyfbJUz.js";import{p as lr,a as cr,e as Dt,d as Bt,G as Cl,r as Rt,t as ln,g as q,u as ti,f as Pl,s as Ie,h as se,c as Ac}from"../chunks/C9Z4nxhR.js";import{s as _e,d as Dl,a as $e}from"../chunks/DP9qWekZ.js";import{i as wi}from"../chunks/C2oj68pw.js";import{e as $r,i as Jr}from"../chunks/kH-DTQyy.js";import{a as Be,f as qe,c as Rc}from"../chunks/DPfxVJHQ.js";import{s as Ve,r as Ll}from"../chunks/ZesQ8l8p.js";import{s as Cc}from"../chunks/Co2v30Gm.js";import{s as Ul}from"../chunks/BkopTN9z.js";import{b as Il}from"../chunks/P9ZHwQBL.js";import{b as Nl}from"../chunks/-jeO_JOJ.js";import{s as Pc,a as Dc}from"../chunks/DWr9YED7.js";import{b as Lc}from"../chunks/CR6HhlME.js";import{p as Qs}from"../chunks/Do8TgQ-j.js";import{N as Fl}from"../chunks/CZ45jJaw.js";import{a as Yi}from"../chunks/BcuCGYSa.js";import{e as Uc}from"../chunks/BmeMLq0p.js";/** +var Oc=Object.defineProperty;var Bc=(i,t,e)=>t in i?Oc(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var Bt=(i,t,e)=>Bc(i,typeof t!="symbol"?t+"":t,e);import"../chunks/Bzak7iHL.js";import{o as Hl,a as Vl}from"../chunks/CNjeV5xa.js";import{s as me,c as ha,h as Gt,g as H,p as Ms,aB as zc,a as Ss,e as yt,d as Tt,n as kc,r as St,t as nn,u as oi,f as Gl}from"../chunks/CvjSAYrz.js";import{s as fe,d as Wl,a as He}from"../chunks/FzvEaXMa.js";import{i as ti}from"../chunks/ciN1mm2W.js";import{e as mr,i as ua}from"../chunks/DTnG8poT.js";import{a as ye,f as Re,c as Hc}from"../chunks/BsvCUYx-.js";import{s as Me,r as da}from"../chunks/CNfQDikv.js";import{s as Cr}from"../chunks/DPl3NjBv.js";import{s as co}from"../chunks/Bhad70Ss.js";import{b as fa}from"../chunks/CVpUe0w3.js";import{b as Xl}from"../chunks/DMu1Byux.js";import{s as Vc,a as Gc}from"../chunks/D81f-o_I.js";import{b as bo}from"../chunks/DJWRm1Ki.js";import{b as Wc}from"../chunks/D3XWCg9-.js";import{p as gs}from"../chunks/B_YDQCB6.js";import{N as Yl}from"../chunks/DzfRjky4.js";import{i as Xc}from"../chunks/Bz1l2A_1.js";import{a as gi}from"../chunks/DNjM5a-l.js";import{e as Yc}from"../chunks/CtkE7HV2.js";/** * @license * Copyright 2010-2024 Three.js Authors * SPDX-License-Identifier: MIT - */const Xa="172",Di={ROTATE:0,DOLLY:1,PAN:2},Ri={ROTATE:0,PAN:1,DOLLY_PAN:2,DOLLY_ROTATE:3},Ic=0,uo=1,Nc=2,Ol=1,Fc=2,yn=3,zn=0,He=1,hn=2,bn=0,Li=1,ze=2,fo=3,po=4,Oc=5,Kn=100,Bc=101,zc=102,Hc=103,kc=104,Vc=200,Gc=201,Wc=202,Xc=203,Qr=204,ta=205,Yc=206,qc=207,jc=208,Zc=209,Kc=210,$c=211,Jc=212,Qc=213,th=214,ea=0,na=1,ia=2,Fi=3,sa=4,ra=5,aa=6,oa=7,Bl=0,eh=1,nh=2,On=0,ih=1,sh=2,rh=3,zl=4,ah=5,oh=6,lh=7,Hl=300,Oi=301,Bi=302,la=303,ca=304,hr=306,ha=1e3,Jn=1001,ua=1002,Ye=1003,ch=1004,ms=1005,dn=1006,gr=1007,Qn=1008,An=1009,kl=1010,Vl=1011,os=1012,Ya=1013,ei=1014,fn=1015,Tn=1016,qa=1017,ja=1018,zi=1020,Gl=35902,Wl=1021,Xl=1022,an=1023,Yl=1024,ql=1025,Ui=1026,Hi=1027,Za=1028,Ka=1029,jl=1030,$a=1031,Ja=1033,Xs=33776,Ys=33777,qs=33778,js=33779,da=35840,fa=35841,pa=35842,ma=35843,ga=36196,_a=37492,va=37496,xa=37808,Ma=37809,Sa=37810,ya=37811,Ea=37812,ba=37813,Ta=37814,wa=37815,Aa=37816,Ra=37817,Ca=37818,Pa=37819,Da=37820,La=37821,Zs=36492,Ua=36494,Ia=36495,Zl=36283,Na=36284,Fa=36285,Oa=36286,hh=3200,uh=3201,Kl=0,dh=1,Fn="",Je="srgb",ki="srgb-linear",tr="linear",re="srgb",oi=7680,mo=519,fh=512,ph=513,mh=514,$l=515,gh=516,_h=517,vh=518,xh=519,Ba=35044,go="300 es",En=2e3,er=2001;class ii{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]+Ae[i>>16&255]+Ae[i>>24&255]+"-"+Ae[t&255]+Ae[t>>8&255]+"-"+Ae[t>>16&15|64]+Ae[t>>24&255]+"-"+Ae[e&63|128]+Ae[e>>8&255]+"-"+Ae[e>>16&255]+Ae[e>>24&255]+Ae[n&255]+Ae[n>>8&255]+Ae[n>>16&255]+Ae[n>>24&255]).toLowerCase()}function qt(i,t,e){return Math.max(t,Math.min(e,i))}function Mh(i,t){return(i%t+t)%t}function _r(i,t,e){return(1-e)*i+e*t}function un(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 Sh={DEG2RAD:Ks};class vt{constructor(t=0,e=0){vt.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=qt(this.x,t.x,e.x),this.y=qt(this.y,t.y,e.y),this}clampScalar(t,e){return this.x=qt(this.x,t,e),this.y=qt(this.y,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(qt(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(qt(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],f=n[2],p=n[5],g=n[8],v=s[0],m=s[3],u=s[6],T=s[1],b=s[4],y=s[7],L=s[2],R=s[5],A=s[8];return r[0]=a*v+o*T+l*L,r[3]=a*m+o*b+l*R,r[6]=a*u+o*y+l*A,r[1]=c*v+h*T+d*L,r[4]=c*m+h*b+d*R,r[7]=c*u+h*y+d*A,r[2]=f*v+p*T+g*L,r[5]=f*m+p*b+g*R,r[8]=f*u+p*y+g*A,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,f=o*l-h*r,p=c*r-a*l,g=e*d+n*f+s*p;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]=f*v,t[4]=(h*e-s*l)*v,t[5]=(s*r-o*e)*v,t[6]=p*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(vr.makeScale(t,e)),this}rotate(t){return this.premultiply(vr.makeRotation(-t)),this}translate(t,e){return this.premultiply(vr.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 vr=new Ht;function Jl(i){for(let t=i.length-1;t>=0;--t)if(i[t]>=65535)return!0;return!1}function nr(i){return document.createElementNS("http://www.w3.org/1999/xhtml",i)}function yh(){const i=nr("canvas");return i.style.display="block",i}const _o={};function Ai(i){i in _o||(_o[i]=!0,console.warn(i))}function Eh(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 bh(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 Th(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 vo=new Ht().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),xo=new Ht().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function wh(){const i={enabled:!0,workingColorSpace:ki,spaces:{},convert:function(s,r,a){return this.enabled===!1||r===a||!r||!a||(this.spaces[r].transfer===re&&(s.r=wn(s.r),s.g=wn(s.g),s.b=wn(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=Ii(s.r),s.g=Ii(s.g),s.b=Ii(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===Fn?tr: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({[ki]:{primaries:t,whitePoint:n,transfer:tr,toXYZ:vo,fromXYZ:xo,luminanceCoefficients:e,workingColorSpaceConfig:{unpackColorSpace:Je},outputColorSpaceConfig:{drawingBufferColorSpace:Je}},[Je]:{primaries:t,whitePoint:n,transfer:re,toXYZ:vo,fromXYZ:xo,luminanceCoefficients:e,outputColorSpaceConfig:{drawingBufferColorSpace:Je}}}),i}const Jt=wh();function wn(i){return i<.04045?i*.0773993808:Math.pow(i*.9478672986+.0521327014,2.4)}function Ii(i){return i<.0031308?i*12.92:1.055*Math.pow(i,.41666)-.055}let li;class Ah{static getDataURL(t){if(/^data:/i.test(t.src)||typeof HTMLCanvasElement>"u")return t.src;let e;if(t instanceof HTMLCanvasElement)e=t;else{li===void 0&&(li=nr("canvas")),li.width=t.width,li.height=t.height;const n=li.getContext("2d");t instanceof ImageData?n.putImageData(t,0,0):n.drawImage(t,0,0,t.width,t.height),e=li}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=nr("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!==Hl)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case ha:t.x=t.x-Math.floor(t.x);break;case Jn:t.x=t.x<0?0:1;break;case ua: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 ha:t.y=t.y-Math.floor(t.y);break;case Jn:t.y=t.y<0?0:1;break;case ua: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++}}Ce.DEFAULT_IMAGE=null;Ce.DEFAULT_MAPPING=Hl;Ce.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],f=l[1],p=l[5],g=l[9],v=l[2],m=l[6],u=l[10];if(Math.abs(h-f)<.01&&Math.abs(d-v)<.01&&Math.abs(g-m)<.01){if(Math.abs(h+f)<.1&&Math.abs(d+v)<.1&&Math.abs(g+m)<.1&&Math.abs(c+p+u-3)<.1)return this.set(1,0,0,0),this;e=Math.PI;const b=(c+1)/2,y=(p+1)/2,L=(u+1)/2,R=(h+f)/4,A=(d+v)/4,U=(g+m)/4;return b>y&&b>L?b<.01?(n=0,s=.707106781,r=.707106781):(n=Math.sqrt(b),s=R/n,r=A/n):y>L?y<.01?(n=.707106781,s=0,r=.707106781):(s=Math.sqrt(y),n=R/s,r=U/s):L<.01?(n=.707106781,s=.707106781,r=0):(r=Math.sqrt(L),n=A/r,s=U/r),this.set(n,s,r,e),this}let T=Math.sqrt((m-g)*(m-g)+(d-v)*(d-v)+(f-h)*(f-h));return Math.abs(T)<.001&&(T=1),this.x=(m-g)/T,this.y=(d-v)/T,this.z=(f-h)/T,this.w=Math.acos((c+p+u-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=qt(this.x,t.x,e.x),this.y=qt(this.y,t.y,e.y),this.z=qt(this.z,t.z,e.z),this.w=qt(this.w,t.w,e.w),this}clampScalar(t,e){return this.x=qt(this.x,t,e),this.y=qt(this.y,t,e),this.z=qt(this.z,t,e),this.w=qt(this.w,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(qt(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 Ph extends ii{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:dn,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1},n);const r=new Ce(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-u*u;if(b>Number.EPSILON){const L=Math.sqrt(b),R=Math.atan2(L,u*T);m=Math.sin(m*R)/L,o=Math.sin(o*R)/L}const y=o*T;if(l=l*m+f*y,c=c*m+p*y,h=h*m+g*y,d=d*m+v*y,m===1-o){const L=1/Math.sqrt(l*l+c*c+h*h+d*d);l*=L,c*=L,h*=L,d*=L}}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],f=r[a+1],p=r[a+2],g=r[a+3];return t[e]=o*g+h*d+l*p-c*f,t[e+1]=l*g+h*f+c*d-o*p,t[e+2]=c*g+h*p+o*f-l*d,t[e+3]=h*g-o*d-l*f-c*p,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),f=l(n/2),p=l(s/2),g=l(r/2);switch(a){case"XYZ":this._x=f*h*d+c*p*g,this._y=c*p*d-f*h*g,this._z=c*h*g+f*p*d,this._w=c*h*d-f*p*g;break;case"YXZ":this._x=f*h*d+c*p*g,this._y=c*p*d-f*h*g,this._z=c*h*g-f*p*d,this._w=c*h*d+f*p*g;break;case"ZXY":this._x=f*h*d-c*p*g,this._y=c*p*d+f*h*g,this._z=c*h*g+f*p*d,this._w=c*h*d-f*p*g;break;case"ZYX":this._x=f*h*d-c*p*g,this._y=c*p*d+f*h*g,this._z=c*h*g-f*p*d,this._w=c*h*d+f*p*g;break;case"YZX":this._x=f*h*d+c*p*g,this._y=c*p*d+f*h*g,this._z=c*h*g-f*p*d,this._w=c*h*d-f*p*g;break;case"XZY":this._x=f*h*d-c*p*g,this._y=c*p*d-f*h*g,this._z=c*h*g+f*p*d,this._w=c*h*d+f*p*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],f=n+o+d;if(f>0){const p=.5/Math.sqrt(f+1);this._w=.25/p,this._x=(h-l)*p,this._y=(r-c)*p,this._z=(a-s)*p}else if(n>o&&n>d){const p=2*Math.sqrt(1+n-o-d);this._w=(h-l)/p,this._x=.25*p,this._y=(s+a)/p,this._z=(r+c)/p}else if(o>d){const p=2*Math.sqrt(1+o-n-d);this._w=(r-c)/p,this._x=(s+a)/p,this._y=.25*p,this._z=(l+h)/p}else{const p=2*Math.sqrt(1+d-n-o);this._w=(a-s)/p,this._x=(r+c)/p,this._y=(l+h)/p,this._z=.25*p}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(qt(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 p=1-e;return this._w=p*a+e*this._w,this._x=p*n+e*this._x,this._y=p*s+e*this._y,this._z=p*r+e*this._z,this.normalize(),this}const c=Math.sqrt(l),h=Math.atan2(c,o),d=Math.sin((1-e)*h)/c,f=Math.sin(e*h)/c;return this._w=a*d+this._w*f,this._x=n*d+this._x*f,this._y=s*d+this._y*f,this._z=r*d+this._z*f,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(Mo.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(Mo.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=qt(this.x,t.x,e.x),this.y=qt(this.y,t.y,e.y),this.z=qt(this.z,t.z,e.z),this}clampScalar(t,e){return this.x=qt(this.x,t,e),this.y=qt(this.y,t,e),this.z=qt(this.z,t,e),this}clampLength(t,e){const n=this.length();return this.divideScalar(n||1).multiplyScalar(qt(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 Mr.copy(this).projectOnVector(t),this.sub(Mr)}reflect(t){return this.sub(Mr.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(qt(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 Mr=new P,Mo=new ni;class si{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,nn),nn.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(qi),_s.subVectors(this.max,qi),ci.subVectors(t.a,qi),hi.subVectors(t.b,qi),ui.subVectors(t.c,qi),Rn.subVectors(hi,ci),Cn.subVectors(ui,hi),Gn.subVectors(ci,ui);let e=[0,-Rn.z,Rn.y,0,-Cn.z,Cn.y,0,-Gn.z,Gn.y,Rn.z,0,-Rn.x,Cn.z,0,-Cn.x,Gn.z,0,-Gn.x,-Rn.y,Rn.x,0,-Cn.y,Cn.x,0,-Gn.y,Gn.x,0];return!Sr(e,ci,hi,ui,_s)||(e=[1,0,0,0,1,0,0,0,1],!Sr(e,ci,hi,ui,_s))?!1:(vs.crossVectors(Rn,Cn),e=[vs.x,vs.y,vs.z],Sr(e,ci,hi,ui,_s))}clampPoint(t,e){return e.copy(t).clamp(this.min,this.max)}distanceToPoint(t){return this.clampPoint(t,nn).distanceTo(t)}getBoundingSphere(t){return this.isEmpty()?t.makeEmpty():(this.getCenter(t.center),t.radius=this.getSize(nn).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:(gn[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),gn[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),gn[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),gn[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),gn[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),gn[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),gn[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),gn[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(gn),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 gn=[new P,new P,new P,new P,new P,new P,new P,new P],nn=new P,gs=new si,ci=new P,hi=new P,ui=new P,Rn=new P,Cn=new P,Gn=new P,qi=new P,_s=new P,vs=new P,Wn=new P;function Sr(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 Lh=new si,ji=new P,yr=new P;class ri{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):Lh.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;ji.subVectors(t,this.center);const e=ji.lengthSq();if(e>this.radius*this.radius){const n=Math.sqrt(e),s=(n-this.radius)*.5;this.center.addScaledVector(ji,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):(yr.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(ji.copy(t.center).add(yr)),this.expandByPoint(ji.copy(t.center).sub(yr))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return new this.constructor().copy(this)}}const _n=new P,Er=new P,xs=new P,Pn=new P,br=new P,Ms=new P,Tr=new P;class cs{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,_n)),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=_n.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(_n.copy(this.origin).addScaledVector(this.direction,e),_n.distanceToSquared(t))}distanceSqToSegment(t,e,n,s){Er.copy(t).add(e).multiplyScalar(.5),xs.copy(e).sub(t).normalize(),Pn.copy(this.origin).sub(Er);const r=t.distanceTo(e)*.5,a=-this.direction.dot(xs),o=Pn.dot(this.direction),l=-Pn.dot(xs),c=Pn.lengthSq(),h=Math.abs(1-a*a);let d,f,p,g;if(h>0)if(d=a*l-o,f=a*o-l,g=r*h,d>=0)if(f>=-g)if(f<=g){const v=1/h;d*=v,f*=v,p=d*(d+a*f+2*o)+f*(a*d+f+2*l)+c}else f=r,d=Math.max(0,-(a*f+o)),p=-d*d+f*(f+2*l)+c;else f=-r,d=Math.max(0,-(a*f+o)),p=-d*d+f*(f+2*l)+c;else f<=-g?(d=Math.max(0,-(-a*r+o)),f=d>0?-r:Math.min(Math.max(-r,-l),r),p=-d*d+f*(f+2*l)+c):f<=g?(d=0,f=Math.min(Math.max(-r,-l),r),p=f*(f+2*l)+c):(d=Math.max(0,-(a*r+o)),f=d>0?r:Math.min(Math.max(-r,-l),r),p=-d*d+f*(f+2*l)+c);else f=a>0?-r:r,d=Math.max(0,-(a*f+o)),p=-d*d+f*(f+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,d),s&&s.copy(Er).addScaledVector(xs,f),p}intersectSphere(t,e){_n.subVectors(t.center,this.origin);const n=_n.dot(this.direction),s=_n.dot(_n)-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,f=this.origin;return c>=0?(n=(t.min.x-f.x)*c,s=(t.max.x-f.x)*c):(n=(t.max.x-f.x)*c,s=(t.min.x-f.x)*c),h>=0?(r=(t.min.y-f.y)*h,a=(t.max.y-f.y)*h):(r=(t.max.y-f.y)*h,a=(t.min.y-f.y)*h),n>a||r>s||((r>n||isNaN(n))&&(n=r),(a=0?(o=(t.min.z-f.z)*d,l=(t.max.z-f.z)*d):(o=(t.max.z-f.z)*d,l=(t.min.z-f.z)*d),n>l||o>s)||((o>n||n!==n)&&(n=o),(l=0?n:s,e)}intersectsBox(t){return this.intersectBox(t,_n)!==null}intersectTriangle(t,e,n,s,r){br.subVectors(e,t),Ms.subVectors(n,t),Tr.crossVectors(br,Ms);let a=this.direction.dot(Tr),o;if(a>0){if(s)return null;o=1}else if(a<0)o=-1,a=-a;else return null;Pn.subVectors(this.origin,t);const l=o*this.direction.dot(Ms.crossVectors(Pn,Ms));if(l<0)return null;const c=o*this.direction.dot(br.cross(Pn));if(c<0||l+c>a)return null;const h=-o*Pn.dot(Tr);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,f,p,g,v,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,f,p,g,v,m)}set(t,e,n,s,r,a,o,l,c,h,d,f,p,g,v,m){const u=this.elements;return u[0]=t,u[4]=e,u[8]=n,u[12]=s,u[1]=r,u[5]=a,u[9]=o,u[13]=l,u[2]=c,u[6]=h,u[10]=d,u[14]=f,u[3]=p,u[7]=g,u[11]=v,u[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/di.setFromMatrixColumn(t,0).length(),r=1/di.setFromMatrixColumn(t,1).length(),a=1/di.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 f=a*h,p=a*d,g=o*h,v=o*d;e[0]=l*h,e[4]=-l*d,e[8]=c,e[1]=p+g*c,e[5]=f-v*c,e[9]=-o*l,e[2]=v-f*c,e[6]=g+p*c,e[10]=a*l}else if(t.order==="YXZ"){const f=l*h,p=l*d,g=c*h,v=c*d;e[0]=f+v*o,e[4]=g*o-p,e[8]=a*c,e[1]=a*d,e[5]=a*h,e[9]=-o,e[2]=p*o-g,e[6]=v+f*o,e[10]=a*l}else if(t.order==="ZXY"){const f=l*h,p=l*d,g=c*h,v=c*d;e[0]=f-v*o,e[4]=-a*d,e[8]=g+p*o,e[1]=p+g*o,e[5]=a*h,e[9]=v-f*o,e[2]=-a*c,e[6]=o,e[10]=a*l}else if(t.order==="ZYX"){const f=a*h,p=a*d,g=o*h,v=o*d;e[0]=l*h,e[4]=g*c-p,e[8]=f*c+v,e[1]=l*d,e[5]=v*c+f,e[9]=p*c-g,e[2]=-c,e[6]=o*l,e[10]=a*l}else if(t.order==="YZX"){const f=a*l,p=a*c,g=o*l,v=o*c;e[0]=l*h,e[4]=v-f*d,e[8]=g*d+p,e[1]=d,e[5]=a*h,e[9]=-o*h,e[2]=-c*h,e[6]=p*d+g,e[10]=f-v*d}else if(t.order==="XZY"){const f=a*l,p=a*c,g=o*l,v=o*c;e[0]=l*h,e[4]=-d,e[8]=c*h,e[1]=f*d+v,e[5]=a*h,e[9]=p*d-g,e[2]=g*d-p,e[6]=o*h,e[10]=v*d+f}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(Uh,t,Ih)}lookAt(t,e,n){const s=this.elements;return Ge.subVectors(t,e),Ge.lengthSq()===0&&(Ge.z=1),Ge.normalize(),Dn.crossVectors(n,Ge),Dn.lengthSq()===0&&(Math.abs(n.z)===1?Ge.x+=1e-4:Ge.z+=1e-4,Ge.normalize(),Dn.crossVectors(n,Ge)),Dn.normalize(),Ss.crossVectors(Ge,Dn),s[0]=Dn.x,s[4]=Ss.x,s[8]=Ge.x,s[1]=Dn.y,s[5]=Ss.y,s[9]=Ge.y,s[2]=Dn.z,s[6]=Ss.z,s[10]=Ge.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],f=n[9],p=n[13],g=n[2],v=n[6],m=n[10],u=n[14],T=n[3],b=n[7],y=n[11],L=n[15],R=s[0],A=s[4],U=s[8],S=s[12],M=s[1],D=s[5],W=s[9],z=s[13],V=s[2],$=s[6],G=s[10],J=s[14],k=s[3],it=s[7],ut=s[11],yt=s[15];return r[0]=a*R+o*M+l*V+c*k,r[4]=a*A+o*D+l*$+c*it,r[8]=a*U+o*W+l*G+c*ut,r[12]=a*S+o*z+l*J+c*yt,r[1]=h*R+d*M+f*V+p*k,r[5]=h*A+d*D+f*$+p*it,r[9]=h*U+d*W+f*G+p*ut,r[13]=h*S+d*z+f*J+p*yt,r[2]=g*R+v*M+m*V+u*k,r[6]=g*A+v*D+m*$+u*it,r[10]=g*U+v*W+m*G+u*ut,r[14]=g*S+v*z+m*J+u*yt,r[3]=T*R+b*M+y*V+L*k,r[7]=T*A+b*D+y*$+L*it,r[11]=T*U+b*W+y*G+L*ut,r[15]=T*S+b*z+y*J+L*yt,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],f=t[10],p=t[14],g=t[3],v=t[7],m=t[11],u=t[15];return g*(+r*l*d-s*c*d-r*o*f+n*c*f+s*o*p-n*l*p)+v*(+e*l*p-e*c*f+r*a*f-s*a*p+s*c*h-r*l*h)+m*(+e*c*d-e*o*p-r*a*d+n*a*p+r*o*h-n*c*h)+u*(-s*o*h-e*l*d+e*o*f+s*a*d-n*a*f+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],f=t[10],p=t[11],g=t[12],v=t[13],m=t[14],u=t[15],T=d*m*c-v*f*c+v*l*p-o*m*p-d*l*u+o*f*u,b=g*f*c-h*m*c-g*l*p+a*m*p+h*l*u-a*f*u,y=h*v*c-g*d*c+g*o*p-a*v*p-h*o*u+a*d*u,L=g*d*l-h*v*l-g*o*f+a*v*f+h*o*m-a*d*m,R=e*T+n*b+s*y+r*L;if(R===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const A=1/R;return t[0]=T*A,t[1]=(v*f*r-d*m*r-v*s*p+n*m*p+d*s*u-n*f*u)*A,t[2]=(o*m*r-v*l*r+v*s*c-n*m*c-o*s*u+n*l*u)*A,t[3]=(d*l*r-o*f*r-d*s*c+n*f*c+o*s*p-n*l*p)*A,t[4]=b*A,t[5]=(h*m*r-g*f*r+g*s*p-e*m*p-h*s*u+e*f*u)*A,t[6]=(g*l*r-a*m*r-g*s*c+e*m*c+a*s*u-e*l*u)*A,t[7]=(a*f*r-h*l*r+h*s*c-e*f*c-a*s*p+e*l*p)*A,t[8]=y*A,t[9]=(g*d*r-h*v*r-g*n*p+e*v*p+h*n*u-e*d*u)*A,t[10]=(a*v*r-g*o*r+g*n*c-e*v*c-a*n*u+e*o*u)*A,t[11]=(h*o*r-a*d*r-h*n*c+e*d*c+a*n*p-e*o*p)*A,t[12]=L*A,t[13]=(h*v*s-g*d*s+g*n*f-e*v*f-h*n*m+e*d*m)*A,t[14]=(g*o*s-a*v*s-g*n*l+e*v*l+a*n*m-e*o*m)*A,t[15]=(a*d*s-h*o*s+h*n*l-e*d*l-a*n*f+e*o*f)*A,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,f=r*c,p=r*h,g=r*d,v=a*h,m=a*d,u=o*d,T=l*c,b=l*h,y=l*d,L=n.x,R=n.y,A=n.z;return s[0]=(1-(v+u))*L,s[1]=(p+y)*L,s[2]=(g-b)*L,s[3]=0,s[4]=(p-y)*R,s[5]=(1-(f+u))*R,s[6]=(m+T)*R,s[7]=0,s[8]=(g+b)*A,s[9]=(m-T)*A,s[10]=(1-(f+v))*A,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=di.set(s[0],s[1],s[2]).length();const a=di.set(s[4],s[5],s[6]).length(),o=di.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],sn.copy(this);const c=1/r,h=1/a,d=1/o;return sn.elements[0]*=c,sn.elements[1]*=c,sn.elements[2]*=c,sn.elements[4]*=h,sn.elements[5]*=h,sn.elements[6]*=h,sn.elements[8]*=d,sn.elements[9]*=d,sn.elements[10]*=d,e.setFromRotationMatrix(sn),n.x=r,n.y=a,n.z=o,this}makePerspective(t,e,n,s,r,a,o=En){const l=this.elements,c=2*r/(e-t),h=2*r/(n-s),d=(e+t)/(e-t),f=(n+s)/(n-s);let p,g;if(o===En)p=-(a+r)/(a-r),g=-2*a*r/(a-r);else if(o===er)p=-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]=f,l[13]=0,l[2]=0,l[6]=0,l[10]=p,l[14]=g,l[3]=0,l[7]=0,l[11]=-1,l[15]=0,this}makeOrthographic(t,e,n,s,r,a,o=En){const l=this.elements,c=1/(e-t),h=1/(n-s),d=1/(a-r),f=(e+t)*c,p=(n+s)*h;let g,v;if(o===En)g=(a+r)*d,v=-2*d;else if(o===er)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]=-f,l[1]=0,l[5]=2*h,l[9]=0,l[13]=-p,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 di=new P,sn=new ne,Uh=new P(0,0,0),Ih=new P(1,1,1),Dn=new P,Ss=new P,Ge=new P,So=new ne,yo=new ni;class pn{constructor(t=0,e=0,n=0,s=pn.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],f=s[6],p=s[10];switch(e){case"XYZ":this._y=Math.asin(qt(o,-1,1)),Math.abs(o)<.9999999?(this._x=Math.atan2(-h,p),this._z=Math.atan2(-a,r)):(this._x=Math.atan2(f,c),this._z=0);break;case"YXZ":this._x=Math.asin(-qt(h,-1,1)),Math.abs(h)<.9999999?(this._y=Math.atan2(o,p),this._z=Math.atan2(l,c)):(this._y=Math.atan2(-d,r),this._z=0);break;case"ZXY":this._x=Math.asin(qt(f,-1,1)),Math.abs(f)<.9999999?(this._y=Math.atan2(-d,p),this._z=Math.atan2(-a,c)):(this._y=0,this._z=Math.atan2(l,r));break;case"ZYX":this._y=Math.asin(-qt(d,-1,1)),Math.abs(d)<.9999999?(this._x=Math.atan2(f,p),this._z=Math.atan2(l,r)):(this._x=0,this._z=Math.atan2(-a,c));break;case"YZX":this._z=Math.asin(qt(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,p));break;case"XZY":this._z=Math.asin(-qt(a,-1,1)),Math.abs(a)<.9999999?(this._x=Math.atan2(f,c),this._y=Math.atan2(o,r)):(this._x=Math.atan2(-h,p),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 So.makeRotationFromQuaternion(t),this.setFromRotationMatrix(So,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return yo.setFromEuler(this),this.setFromQuaternion(yo,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}}pn.DEFAULT_ORDER="XYZ";class Qa{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),f.length>0&&(n.skeletons=f),p.length>0&&(n.animations=p),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){rn.subVectors(s,e),xn.subVectors(n,e),Ar.subVectors(t,e);const a=rn.dot(rn),o=rn.dot(xn),l=rn.dot(Ar),c=xn.dot(xn),h=xn.dot(Ar),d=a*c-o*o;if(d===0)return r.set(0,0,0),null;const f=1/d,p=(c*l-o*h)*f,g=(a*h-o*l)*f;return r.set(1-p-g,g,p)}static containsPoint(t,e,n,s){return this.getBarycoord(t,e,n,s,Mn)===null?!1:Mn.x>=0&&Mn.y>=0&&Mn.x+Mn.y<=1}static getInterpolation(t,e,n,s,r,a,o,l){return this.getBarycoord(t,e,n,s,Mn)===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,Mn.x),l.addScaledVector(a,Mn.y),l.addScaledVector(o,Mn.z),l)}static getInterpolatedAttribute(t,e,n,s,r,a){return Dr.setScalar(0),Lr.setScalar(0),Ur.setScalar(0),Dr.fromBufferAttribute(t,e),Lr.fromBufferAttribute(t,n),Ur.fromBufferAttribute(t,s),a.setScalar(0),a.addScaledVector(Dr,r.x),a.addScaledVector(Lr,r.y),a.addScaledVector(Ur,r.z),a}static isFrontFacing(t,e,n,s){return rn.subVectors(n,e),xn.subVectors(t,e),rn.cross(xn).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 rn.subVectors(this.c,this.b),xn.subVectors(this.a,this.b),rn.cross(xn).length()*.5}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return Qe.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return Qe.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,s,r){return Qe.getInterpolation(t,this.a,this.b,this.c,e,n,s,r)}containsPoint(t){return Qe.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return Qe.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;mi.subVectors(s,n),gi.subVectors(r,n),Rr.subVectors(t,n);const l=mi.dot(Rr),c=gi.dot(Rr);if(l<=0&&c<=0)return e.copy(n);Cr.subVectors(t,s);const h=mi.dot(Cr),d=gi.dot(Cr);if(h>=0&&d<=h)return e.copy(s);const f=l*d-h*c;if(f<=0&&l>=0&&h<=0)return a=l/(l-h),e.copy(n).addScaledVector(mi,a);Pr.subVectors(t,r);const p=mi.dot(Pr),g=gi.dot(Pr);if(g>=0&&p<=g)return e.copy(r);const v=p*c-l*g;if(v<=0&&c>=0&&g<=0)return o=c/(c-g),e.copy(n).addScaledVector(gi,o);const m=h*g-p*d;if(m<=0&&d-h>=0&&p-g>=0)return Ro.subVectors(r,s),o=(d-h)/(d-h+(p-g)),e.copy(s).addScaledVector(Ro,o);const u=1/(m+v+f);return a=v*u,o=f*u,e.copy(n).addScaledVector(mi,a).addScaledVector(gi,o)}equals(t){return t.a.equals(this.a)&&t.b.equals(this.b)&&t.c.equals(this.c)}}const ec={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},Ln={h:0,s:0,l:0},Es={h:0,s:0,l:0};function Ir(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 pt{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=Je){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(t&255)/255,Jt.toWorkingColorSpace(this,e),this}setRGB(t,e,n,s=Jt.workingColorSpace){return this.r=t,this.g=e,this.b=n,Jt.toWorkingColorSpace(this,s),this}setHSL(t,e,n,s=Jt.workingColorSpace){if(t=Mh(t,1),e=qt(e,0,1),n=qt(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=Ir(a,r,t+1/3),this.g=Ir(a,r,t),this.b=Ir(a,r,t-1/3)}return Jt.toWorkingColorSpace(this,s),this}setStyle(t,e=Je){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=Je){const n=ec[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=wn(t.r),this.g=wn(t.g),this.b=wn(t.b),this}copyLinearToSRGB(t){return this.r=Ii(t.r),this.g=Ii(t.g),this.b=Ii(t.b),this}convertSRGBToLinear(){return this.copySRGBToLinear(this),this}convertLinearToSRGB(){return this.copyLinearToSRGB(this),this}getHex(t=Je){return Jt.fromWorkingColorSpace(Re.copy(this),t),Math.round(qt(Re.r*255,0,255))*65536+Math.round(qt(Re.g*255,0,255))*256+Math.round(qt(Re.b*255,0,255))}getHexString(t=Je){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Jt.workingColorSpace){Jt.fromWorkingColorSpace(Re.copy(this),e);const n=Re.r,s=Re.g,r=Re.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!==Li&&(n.blending=this.blending),this.side!==zn&&(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!==Qr&&(n.blendSrc=this.blendSrc),this.blendDst!==ta&&(n.blendDst=this.blendDst),this.blendEquation!==Kn&&(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!==Fi&&(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!==mo&&(n.stencilFunc=this.stencilFunc),this.stencilRef!==0&&(n.stencilRef=this.stencilRef),this.stencilFuncMask!==255&&(n.stencilFuncMask=this.stencilFuncMask),this.stencilFail!==oi&&(n.stencilFail=this.stencilFail),this.stencilZFail!==oi&&(n.stencilZFail=this.stencilZFail),this.stencilZPass!==oi&&(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 ls extends Hn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new pt(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 pn,this.combine=Bl,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 ge=new P,bs=new vt;class de{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=Ba,this.updateRanges=[],this.gpuType=fn,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 si);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,f=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 f=0,p=d.length;f0){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))&&(Co.copy(r).invert(),Xn.copy(t.ray).applyMatrix4(Co),!(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,f=r.groups,p=r.drawRange;if(o!==null)if(Array.isArray(a))for(let g=0,v=f.length;ge.far?null:{distance:c,point:Ps.clone(),object:i}}function Ds(i,t,e,n,s,r,a,o,l,c){i.getVertexPosition(o,ws),i.getVertexPosition(l,As),i.getVertexPosition(c,Rs);const h=kh(i,t,e,n,ws,As,Rs,Do);if(h){const d=new P;Qe.getBarycoord(Do,ws,As,Rs,d),s&&(h.uv=Qe.getInterpolatedAttribute(s,o,l,c,d,new vt)),r&&(h.uv1=Qe.getInterpolatedAttribute(r,o,l,c,d,new vt)),a&&(h.normal=Qe.getInterpolatedAttribute(a,o,l,c,d,new P),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));const f={a:o,b:l,c,normal:new P,materialIndex:0};Qe.getNormal(ws,As,Rs,f.normal),h.face=f,h.barycoord=d}return h}class hs extends pe{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 f=0,p=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 Pe(c,3)),this.setAttribute("normal",new Pe(h,3)),this.setAttribute("uv",new Pe(d,2));function g(v,m,u,T,b,y,L,R,A,U,S){const M=y/A,D=L/U,W=y/2,z=L/2,V=R/2,$=A+1,G=U+1;let J=0,k=0;const it=new P;for(let ut=0;ut0?1:-1,h.push(it.x,it.y,it.z),d.push(Lt/A),d.push(1-ut/U),J+=1}}for(let ut=0;ut>8&255]+Ne[i>>16&255]+Ne[i>>24&255]+"-"+Ne[t&255]+Ne[t>>8&255]+"-"+Ne[t>>16&15|64]+Ne[t>>24&255]+"-"+Ne[e&63|128]+Ne[e>>8&255]+"-"+Ne[e>>16&255]+Ne[e>>24&255]+Ne[n&255]+Ne[n>>8&255]+Ne[n>>16&255]+Ne[n>>24&255]).toLowerCase()}function Yt(i,t,e){return Math.max(t,Math.min(e,i))}function Uh(i,t){return(i%t+t)%t}function Dr(i,t,e){return(1-e)*i+e*t}function gn(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 se(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 Ih={DEG2RAD:ur};class xt{constructor(t=0,e=0){xt.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 zt{constructor(t,e,n,s,r,a,o,l,c){zt.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],f=n[2],u=n[5],g=n[8],_=s[0],m=s[3],p=s[6],T=s[1],E=s[4],y=s[7],D=s[2],A=s[5],C=s[8];return r[0]=a*_+o*T+l*D,r[3]=a*m+o*E+l*A,r[6]=a*p+o*y+l*C,r[1]=c*_+h*T+d*D,r[4]=c*m+h*E+d*A,r[7]=c*p+h*y+d*C,r[2]=f*_+u*T+g*D,r[5]=f*m+u*E+g*A,r[8]=f*p+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,f=o*l-h*r,u=c*r-a*l,g=e*d+n*f+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]=f*_,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(Lr.makeScale(t,e)),this}rotate(t){return this.premultiply(Lr.makeRotation(-t)),this}translate(t,e){return this.premultiply(Lr.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 Lr=new zt;function lc(i){for(let t=i.length-1;t>=0;--t)if(i[t]>=65535)return!0;return!1}function vr(i){return document.createElementNS("http://www.w3.org/1999/xhtml",i)}function Nh(){const i=vr("canvas");return i.style.display="block",i}const Po={};function Fi(i){i in Po||(Po[i]=!0,console.warn(i))}function Fh(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 Oh(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 Bh(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 Do=new zt().set(.4123908,.3575843,.1804808,.212639,.7151687,.0721923,.0193308,.1191948,.9505322),Lo=new zt().set(3.2409699,-1.5373832,-.4986108,-.9692436,1.8759675,.0415551,.0556301,-.203977,1.0569715);function zh(){const i={enabled:!0,workingColorSpace:Ji,spaces:{},convert:function(s,r,a){return this.enabled===!1||r===a||!r||!a||(this.spaces[r].transfer===ie&&(s.r=Pn(s.r),s.g=Pn(s.g),s.b=Pn(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===ie&&(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===kn?gr: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:gr,toXYZ:Do,fromXYZ:Lo,luminanceCoefficients:e,workingColorSpaceConfig:{unpackColorSpace:sn},outputColorSpaceConfig:{drawingBufferColorSpace:sn}},[sn]:{primaries:t,whitePoint:n,transfer:ie,toXYZ:Do,fromXYZ:Lo,luminanceCoefficients:e,outputColorSpaceConfig:{drawingBufferColorSpace:sn}}}),i}const Jt=zh();function Pn(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 kh{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=vr("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=vr("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!==Kl)return t;if(t.applyMatrix3(this.matrix),t.x<0||t.x>1)switch(this.wrapS){case Ta:t.x=t.x-Math.floor(t.x);break;case ri:t.x=t.x<0?0:1;break;case wa: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 Ta:t.y=t.y-Math.floor(t.y);break;case ri:t.y=t.y<0?0:1;break;case wa: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++}}Ae.DEFAULT_IMAGE=null;Ae.DEFAULT_MAPPING=Kl;Ae.DEFAULT_ANISOTROPY=1;class ae{constructor(t=0,e=0,n=0,s=1){ae.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],f=l[1],u=l[5],g=l[9],_=l[2],m=l[6],p=l[10];if(Math.abs(h-f)<.01&&Math.abs(d-_)<.01&&Math.abs(g-m)<.01){if(Math.abs(h+f)<.1&&Math.abs(d+_)<.1&&Math.abs(g+m)<.1&&Math.abs(c+u+p-3)<.1)return this.set(1,0,0,0),this;e=Math.PI;const E=(c+1)/2,y=(u+1)/2,D=(p+1)/2,A=(h+f)/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=A/n,r=C/n):y>D?y<.01?(n=.707106781,s=0,r=.707106781):(s=Math.sqrt(y),n=A/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-_)+(f-h)*(f-h));return Math.abs(T)<.001&&(T=1),this.x=(m-g)/T,this.y=(d-_)/T,this.z=(f-h)/T,this.w=Math.acos((c+u+p-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 Gh extends ui{constructor(t=1,e=1,n={}){super(),this.isRenderTarget=!0,this.width=t,this.height=e,this.depth=1,this.scissor=new ae(0,0,t,e),this.scissorTest=!1,this.viewport=new ae(0,0,t,e);const s={width:t,height:e,depth:1};n=Object.assign({generateMipmaps:!1,internalFormat:null,minFilter:_n,depthBuffer:!0,stencilBuffer:!1,resolveDepthBuffer:!0,resolveStencilBuffer:!0,depthTexture:null,samples:0,count:1},n);const r=new Ae(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-p*p;if(E>Number.EPSILON){const D=Math.sqrt(E),A=Math.atan2(D,p*T);m=Math.sin(m*A)/D,o=Math.sin(o*A)/D}const y=o*T;if(l=l*m+f*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],f=r[a+1],u=r[a+2],g=r[a+3];return t[e]=o*g+h*d+l*u-c*f,t[e+1]=l*g+h*f+c*d-o*u,t[e+2]=c*g+h*u+o*f-l*d,t[e+3]=h*g-o*d-l*f-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),f=l(n/2),u=l(s/2),g=l(r/2);switch(a){case"XYZ":this._x=f*h*d+c*u*g,this._y=c*u*d-f*h*g,this._z=c*h*g+f*u*d,this._w=c*h*d-f*u*g;break;case"YXZ":this._x=f*h*d+c*u*g,this._y=c*u*d-f*h*g,this._z=c*h*g-f*u*d,this._w=c*h*d+f*u*g;break;case"ZXY":this._x=f*h*d-c*u*g,this._y=c*u*d+f*h*g,this._z=c*h*g+f*u*d,this._w=c*h*d-f*u*g;break;case"ZYX":this._x=f*h*d-c*u*g,this._y=c*u*d+f*h*g,this._z=c*h*g-f*u*d,this._w=c*h*d+f*u*g;break;case"YZX":this._x=f*h*d+c*u*g,this._y=c*u*d+f*h*g,this._z=c*h*g-f*u*d,this._w=c*h*d-f*u*g;break;case"XZY":this._x=f*h*d-c*u*g,this._y=c*u*d-f*h*g,this._z=c*h*g+f*u*d,this._w=c*h*d+f*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],f=n+o+d;if(f>0){const u=.5/Math.sqrt(f+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,f=Math.sin(e*h)/c;return this._w=a*d+this._w*f,this._x=n*d+this._x*f,this._y=s*d+this._y*f,this._z=r*d+this._z*f,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(Uo.setFromEuler(t))}applyAxisAngle(t,e){return this.applyQuaternion(Uo.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 Ir.copy(this).projectOnVector(t),this.sub(Ir)}reflect(t){return this.sub(Ir.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 Ir=new P,Uo=new hi;class di{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(is),Ls.subVectors(this.max,is),xi.subVectors(t.a,is),Mi.subVectors(t.b,is),Si.subVectors(t.c,is),Ln.subVectors(Mi,xi),Un.subVectors(Si,Mi),jn.subVectors(xi,Si);let e=[0,-Ln.z,Ln.y,0,-Un.z,Un.y,0,-jn.z,jn.y,Ln.z,0,-Ln.x,Un.z,0,-Un.x,jn.z,0,-jn.x,-Ln.y,Ln.x,0,-Un.y,Un.x,0,-jn.y,jn.x,0];return!Nr(e,xi,Mi,Si,Ls)||(e=[1,0,0,0,1,0,0,0,1],!Nr(e,xi,Mi,Si,Ls))?!1:(Us.crossVectors(Ln,Un),e=[Us.x,Us.y,Us.z],Nr(e,xi,Mi,Si,Ls))}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:(Mn[0].set(this.min.x,this.min.y,this.min.z).applyMatrix4(t),Mn[1].set(this.min.x,this.min.y,this.max.z).applyMatrix4(t),Mn[2].set(this.min.x,this.max.y,this.min.z).applyMatrix4(t),Mn[3].set(this.min.x,this.max.y,this.max.z).applyMatrix4(t),Mn[4].set(this.max.x,this.min.y,this.min.z).applyMatrix4(t),Mn[5].set(this.max.x,this.min.y,this.max.z).applyMatrix4(t),Mn[6].set(this.max.x,this.max.y,this.min.z).applyMatrix4(t),Mn[7].set(this.max.x,this.max.y,this.max.z).applyMatrix4(t),this.setFromPoints(Mn),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 Mn=[new P,new P,new P,new P,new P,new P,new P,new P],cn=new P,Ds=new di,xi=new P,Mi=new P,Si=new P,Ln=new P,Un=new P,jn=new P,is=new P,Ls=new P,Us=new P,Zn=new P;function Nr(i,t,e,n,s){for(let r=0,a=i.length-3;r<=a;r+=3){Zn.fromArray(i,r);const o=s.x*Math.abs(Zn.x)+s.y*Math.abs(Zn.y)+s.z*Math.abs(Zn.z),l=t.dot(Zn),c=e.dot(Zn),h=n.dot(Zn);if(Math.max(-Math.max(l,c,h),Math.min(l,c,h))>o)return!1}return!0}const Xh=new di,ss=new P,Fr=new P;class fi{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):Xh.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;ss.subVectors(t,this.center);const e=ss.lengthSq();if(e>this.radius*this.radius){const n=Math.sqrt(e),s=(n-this.radius)*.5;this.center.addScaledVector(ss,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):(Fr.subVectors(t.center,this.center).setLength(t.radius),this.expandByPoint(ss.copy(t.center).add(Fr)),this.expandByPoint(ss.copy(t.center).sub(Fr))),this)}equals(t){return t.center.equals(this.center)&&t.radius===this.radius}clone(){return new this.constructor().copy(this)}}const Sn=new P,Or=new P,Is=new P,In=new P,Br=new P,Ns=new P,zr=new P;class ys{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,Sn)),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=Sn.subVectors(t,this.origin).dot(this.direction);return e<0?this.origin.distanceToSquared(t):(Sn.copy(this.origin).addScaledVector(this.direction,e),Sn.distanceToSquared(t))}distanceSqToSegment(t,e,n,s){Or.copy(t).add(e).multiplyScalar(.5),Is.copy(e).sub(t).normalize(),In.copy(this.origin).sub(Or);const r=t.distanceTo(e)*.5,a=-this.direction.dot(Is),o=In.dot(this.direction),l=-In.dot(Is),c=In.lengthSq(),h=Math.abs(1-a*a);let d,f,u,g;if(h>0)if(d=a*l-o,f=a*o-l,g=r*h,d>=0)if(f>=-g)if(f<=g){const _=1/h;d*=_,f*=_,u=d*(d+a*f+2*o)+f*(a*d+f+2*l)+c}else f=r,d=Math.max(0,-(a*f+o)),u=-d*d+f*(f+2*l)+c;else f=-r,d=Math.max(0,-(a*f+o)),u=-d*d+f*(f+2*l)+c;else f<=-g?(d=Math.max(0,-(-a*r+o)),f=d>0?-r:Math.min(Math.max(-r,-l),r),u=-d*d+f*(f+2*l)+c):f<=g?(d=0,f=Math.min(Math.max(-r,-l),r),u=f*(f+2*l)+c):(d=Math.max(0,-(a*r+o)),f=d>0?r:Math.min(Math.max(-r,-l),r),u=-d*d+f*(f+2*l)+c);else f=a>0?-r:r,d=Math.max(0,-(a*f+o)),u=-d*d+f*(f+2*l)+c;return n&&n.copy(this.origin).addScaledVector(this.direction,d),s&&s.copy(Or).addScaledVector(Is,f),u}intersectSphere(t,e){Sn.subVectors(t.center,this.origin);const n=Sn.dot(this.direction),s=Sn.dot(Sn)-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,f=this.origin;return c>=0?(n=(t.min.x-f.x)*c,s=(t.max.x-f.x)*c):(n=(t.max.x-f.x)*c,s=(t.min.x-f.x)*c),h>=0?(r=(t.min.y-f.y)*h,a=(t.max.y-f.y)*h):(r=(t.max.y-f.y)*h,a=(t.min.y-f.y)*h),n>a||r>s||((r>n||isNaN(n))&&(n=r),(a=0?(o=(t.min.z-f.z)*d,l=(t.max.z-f.z)*d):(o=(t.max.z-f.z)*d,l=(t.min.z-f.z)*d),n>l||o>s)||((o>n||n!==n)&&(n=o),(l=0?n:s,e)}intersectsBox(t){return this.intersectBox(t,Sn)!==null}intersectTriangle(t,e,n,s,r){Br.subVectors(e,t),Ns.subVectors(n,t),zr.crossVectors(Br,Ns);let a=this.direction.dot(zr),o;if(a>0){if(s)return null;o=1}else if(a<0)o=-1,a=-a;else return null;In.subVectors(this.origin,t);const l=o*this.direction.dot(Ns.crossVectors(In,Ns));if(l<0)return null;const c=o*this.direction.dot(Br.cross(In));if(c<0||l+c>a)return null;const h=-o*In.dot(zr);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,f,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,f,u,g,_,m)}set(t,e,n,s,r,a,o,l,c,h,d,f,u,g,_,m){const p=this.elements;return p[0]=t,p[4]=e,p[8]=n,p[12]=s,p[1]=r,p[5]=a,p[9]=o,p[13]=l,p[2]=c,p[6]=h,p[10]=d,p[14]=f,p[3]=u,p[7]=g,p[11]=_,p[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 f=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]=f-_*c,e[9]=-o*l,e[2]=_-f*c,e[6]=g+u*c,e[10]=a*l}else if(t.order==="YXZ"){const f=l*h,u=l*d,g=c*h,_=c*d;e[0]=f+_*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]=_+f*o,e[10]=a*l}else if(t.order==="ZXY"){const f=l*h,u=l*d,g=c*h,_=c*d;e[0]=f-_*o,e[4]=-a*d,e[8]=g+u*o,e[1]=u+g*o,e[5]=a*h,e[9]=_-f*o,e[2]=-a*c,e[6]=o,e[10]=a*l}else if(t.order==="ZYX"){const f=a*h,u=a*d,g=o*h,_=o*d;e[0]=l*h,e[4]=g*c-u,e[8]=f*c+_,e[1]=l*d,e[5]=_*c+f,e[9]=u*c-g,e[2]=-c,e[6]=o*l,e[10]=a*l}else if(t.order==="YZX"){const f=a*l,u=a*c,g=o*l,_=o*c;e[0]=l*h,e[4]=_-f*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]=f-_*d}else if(t.order==="XZY"){const f=a*l,u=a*c,g=o*l,_=o*c;e[0]=l*h,e[4]=-d,e[8]=c*h,e[1]=f*d+_,e[5]=a*h,e[9]=u*d-g,e[2]=g*d-u,e[6]=o*h,e[10]=_*d+f}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(Yh,t,qh)}lookAt(t,e,n){const s=this.elements;return Ze.subVectors(t,e),Ze.lengthSq()===0&&(Ze.z=1),Ze.normalize(),Nn.crossVectors(n,Ze),Nn.lengthSq()===0&&(Math.abs(n.z)===1?Ze.x+=1e-4:Ze.z+=1e-4,Ze.normalize(),Nn.crossVectors(n,Ze)),Nn.normalize(),Fs.crossVectors(Ze,Nn),s[0]=Nn.x,s[4]=Fs.x,s[8]=Ze.x,s[1]=Nn.y,s[5]=Fs.y,s[9]=Ze.y,s[2]=Nn.z,s[6]=Fs.z,s[10]=Ze.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],f=n[9],u=n[13],g=n[2],_=n[6],m=n[10],p=n[14],T=n[3],E=n[7],y=n[11],D=n[15],A=s[0],C=s[4],I=s[8],S=s[12],M=s[1],w=s[5],Y=s[9],V=s[13],j=s[2],$=s[6],q=s[10],J=s[14],X=s[3],it=s[7],ft=s[11],Mt=s[15];return r[0]=a*A+o*M+l*j+c*X,r[4]=a*C+o*w+l*$+c*it,r[8]=a*I+o*Y+l*q+c*ft,r[12]=a*S+o*V+l*J+c*Mt,r[1]=h*A+d*M+f*j+u*X,r[5]=h*C+d*w+f*$+u*it,r[9]=h*I+d*Y+f*q+u*ft,r[13]=h*S+d*V+f*J+u*Mt,r[2]=g*A+_*M+m*j+p*X,r[6]=g*C+_*w+m*$+p*it,r[10]=g*I+_*Y+m*q+p*ft,r[14]=g*S+_*V+m*J+p*Mt,r[3]=T*A+E*M+y*j+D*X,r[7]=T*C+E*w+y*$+D*it,r[11]=T*I+E*Y+y*q+D*ft,r[15]=T*S+E*V+y*J+D*Mt,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],f=t[10],u=t[14],g=t[3],_=t[7],m=t[11],p=t[15];return g*(+r*l*d-s*c*d-r*o*f+n*c*f+s*o*u-n*l*u)+_*(+e*l*u-e*c*f+r*a*f-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)+p*(-s*o*h-e*l*d+e*o*f+s*a*d-n*a*f+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],f=t[10],u=t[11],g=t[12],_=t[13],m=t[14],p=t[15],T=d*m*c-_*f*c+_*l*u-o*m*u-d*l*p+o*f*p,E=g*f*c-h*m*c-g*l*u+a*m*u+h*l*p-a*f*p,y=h*_*c-g*d*c+g*o*u-a*_*u-h*o*p+a*d*p,D=g*d*l-h*_*l-g*o*f+a*_*f+h*o*m-a*d*m,A=e*T+n*E+s*y+r*D;if(A===0)return this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0);const C=1/A;return t[0]=T*C,t[1]=(_*f*r-d*m*r-_*s*u+n*m*u+d*s*p-n*f*p)*C,t[2]=(o*m*r-_*l*r+_*s*c-n*m*c-o*s*p+n*l*p)*C,t[3]=(d*l*r-o*f*r-d*s*c+n*f*c+o*s*u-n*l*u)*C,t[4]=E*C,t[5]=(h*m*r-g*f*r+g*s*u-e*m*u-h*s*p+e*f*p)*C,t[6]=(g*l*r-a*m*r-g*s*c+e*m*c+a*s*p-e*l*p)*C,t[7]=(a*f*r-h*l*r+h*s*c-e*f*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*p-e*d*p)*C,t[10]=(a*_*r-g*o*r+g*n*c-e*_*c-a*n*p+e*o*p)*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*f-e*_*f-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*f+e*o*f)*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,f=r*c,u=r*h,g=r*d,_=a*h,m=a*d,p=o*d,T=l*c,E=l*h,y=l*d,D=n.x,A=n.y,C=n.z;return s[0]=(1-(_+p))*D,s[1]=(u+y)*D,s[2]=(g-E)*D,s[3]=0,s[4]=(u-y)*A,s[5]=(1-(f+p))*A,s[6]=(m+T)*A,s[7]=0,s[8]=(g+E)*C,s[9]=(m-T)*C,s[10]=(1-(f+_))*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=An){const l=this.elements,c=2*r/(e-t),h=2*r/(n-s),d=(e+t)/(e-t),f=(n+s)/(n-s);let u,g;if(o===An)u=-(a+r)/(a-r),g=-2*a*r/(a-r);else if(o===_r)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]=f,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=An){const l=this.elements,c=1/(e-t),h=1/(n-s),d=1/(a-r),f=(e+t)*c,u=(n+s)*h;let g,_;if(o===An)g=(a+r)*d,_=-2*d;else if(o===_r)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]=-f,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,Yh=new P(0,0,0),qh=new P(1,1,1),Nn=new P,Fs=new P,Ze=new P,Io=new ne,No=new hi;class xn{constructor(t=0,e=0,n=0,s=xn.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],f=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(f,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(f,-1,1)),Math.abs(f)<.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(f,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(f,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 Io.makeRotationFromQuaternion(t),this.setFromRotationMatrix(Io,e,n)}setFromVector3(t,e=this._order){return this.set(t.x,t.y,t.z,e)}reorder(t){return No.setFromEuler(this),this.setFromQuaternion(No,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}}xn.DEFAULT_ORDER="XYZ";class xo{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),f.length>0&&(n.skeletons=f),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),En.subVectors(n,e),Hr.subVectors(t,e);const a=un.dot(un),o=un.dot(En),l=un.dot(Hr),c=En.dot(En),h=En.dot(Hr),d=a*c-o*o;if(d===0)return r.set(0,0,0),null;const f=1/d,u=(c*l-o*h)*f,g=(a*h-o*l)*f;return r.set(1-u-g,g,u)}static containsPoint(t,e,n,s){return this.getBarycoord(t,e,n,s,bn)===null?!1:bn.x>=0&&bn.y>=0&&bn.x+bn.y<=1}static getInterpolation(t,e,n,s,r,a,o,l){return this.getBarycoord(t,e,n,s,bn)===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,bn.x),l.addScaledVector(a,bn.y),l.addScaledVector(o,bn.z),l)}static getInterpolatedAttribute(t,e,n,s,r,a){return Xr.setScalar(0),Yr.setScalar(0),qr.setScalar(0),Xr.fromBufferAttribute(t,e),Yr.fromBufferAttribute(t,n),qr.fromBufferAttribute(t,s),a.setScalar(0),a.addScaledVector(Xr,r.x),a.addScaledVector(Yr,r.y),a.addScaledVector(qr,r.z),a}static isFrontFacing(t,e,n,s){return un.subVectors(n,e),En.subVectors(t,e),un.cross(En).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),En.subVectors(this.a,this.b),un.cross(En).length()*.5}getMidpoint(t){return t.addVectors(this.a,this.b).add(this.c).multiplyScalar(1/3)}getNormal(t){return rn.getNormal(this.a,this.b,this.c,t)}getPlane(t){return t.setFromCoplanarPoints(this.a,this.b,this.c)}getBarycoord(t,e){return rn.getBarycoord(t,this.a,this.b,this.c,e)}getInterpolation(t,e,n,s,r){return rn.getInterpolation(t,this.a,this.b,this.c,e,n,s,r)}containsPoint(t){return rn.containsPoint(t,this.a,this.b,this.c)}isFrontFacing(t){return rn.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),Vr.subVectors(t,n);const l=Ti.dot(Vr),c=wi.dot(Vr);if(l<=0&&c<=0)return e.copy(n);Gr.subVectors(t,s);const h=Ti.dot(Gr),d=wi.dot(Gr);if(h>=0&&d<=h)return e.copy(s);const f=l*d-h*c;if(f<=0&&l>=0&&h<=0)return a=l/(l-h),e.copy(n).addScaledVector(Ti,a);Wr.subVectors(t,r);const u=Ti.dot(Wr),g=wi.dot(Wr);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 Ho.subVectors(r,s),o=(d-h)/(d-h+(u-g)),e.copy(s).addScaledVector(Ho,o);const p=1/(m+_+f);return a=_*p,o=f*p,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 uc={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},Fn={h:0,s:0,l:0},Bs={h:0,s:0,l:0};function jr(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 st{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=sn){return t=Math.floor(t),this.r=(t>>16&255)/255,this.g=(t>>8&255)/255,this.b=(t&255)/255,Jt.toWorkingColorSpace(this,e),this}setRGB(t,e,n,s=Jt.workingColorSpace){return this.r=t,this.g=e,this.b=n,Jt.toWorkingColorSpace(this,s),this}setHSL(t,e,n,s=Jt.workingColorSpace){if(t=Uh(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=jr(a,r,t+1/3),this.g=jr(a,r,t),this.b=jr(a,r,t-1/3)}return Jt.toWorkingColorSpace(this,s),this}setStyle(t,e=sn){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=sn){const n=uc[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=Pn(t.r),this.g=Pn(t.g),this.b=Pn(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=sn){return Jt.fromWorkingColorSpace(Fe.copy(this),t),Math.round(Yt(Fe.r*255,0,255))*65536+Math.round(Yt(Fe.g*255,0,255))*256+Math.round(Yt(Fe.b*255,0,255))}getHexString(t=sn){return("000000"+this.getHex(t).toString(16)).slice(-6)}getHSL(t,e=Jt.workingColorSpace){Jt.fromWorkingColorSpace(Fe.copy(this),e);const n=Fe.r,s=Fe.g,r=Fe.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!==Gn&&(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!==pa&&(n.blendSrc=this.blendSrc),this.blendDst!==ma&&(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!==Ro&&(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 xs extends Wn{constructor(t){super(),this.isMeshBasicMaterial=!0,this.type="MeshBasicMaterial",this.color=new st(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 xn,this.combine=jl,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 xe=new P,zs=new xt;class ce{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=Qa,this.updateRanges=[],this.gpuType=vn,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 di);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,f=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 f=0,u=d.length;f0){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))&&(Vo.copy(r).invert(),Kn.copy(t.ray).applyMatrix4(Vo),!(n.boundingBox!==null&&Kn.intersectsBox(n.boundingBox)===!1)&&this._computeIntersections(t,e,Kn)))}_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,f=r.groups,u=r.drawRange;if(o!==null)if(Array.isArray(a))for(let g=0,_=f.length;g<_;g++){const m=f[g],p=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:Xs.clone(),object:i}}function Ys(i,t,e,n,s,r,a,o,l,c){i.getVertexPosition(o,Hs),i.getVertexPosition(l,Vs),i.getVertexPosition(c,Gs);const h=tu(i,t,e,n,Hs,Vs,Gs,Wo);if(h){const d=new P;rn.getBarycoord(Wo,Hs,Vs,Gs,d),s&&(h.uv=rn.getInterpolatedAttribute(s,o,l,c,d,new xt)),r&&(h.uv1=rn.getInterpolatedAttribute(r,o,l,c,d,new xt)),a&&(h.normal=rn.getInterpolatedAttribute(a,o,l,c,d,new P),h.normal.dot(n.direction)>0&&h.normal.multiplyScalar(-1));const f={a:o,b:l,c,normal:new P,materialIndex:0};rn.getNormal(Hs,Vs,Gs,f.normal),h.face=f,h.barycoord=d}return h}class Es 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 f=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,p,T,E,y,D,A,C,I,S){const M=y/C,w=D/I,Y=y/2,V=D/2,j=A/2,$=C+1,q=I+1;let J=0,X=0;const it=new P;for(let ft=0;ft0?1:-1,h.push(it.x,it.y,it.z),d.push(Nt/C),d.push(1-ft/I),J+=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 rc extends we{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new ne,this.projectionMatrix=new ne,this.projectionMatrixInverse=new ne,this.coordinateSystem=En}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 Un=new P,Lo=new vt,Uo=new vt;class Xe extends rc{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=za*2*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(Ks*.5*this.fov);return .5*this.getFilmHeight()/t}getEffectiveFOV(){return za*2*Math.atan(Math.tan(Ks*.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){Un.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(Un.x,Un.y).multiplyScalar(-t/Un.z),Un.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(Un.x,Un.y).multiplyScalar(-t/Un.z)}getViewSize(t,e){return this.getViewBounds(t,Lo,Uo),e.subVectors(Uo,Lo)}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(Ks*.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 vi=-90,xi=1;class Xh extends we{constructor(t,e,n){super(),this.type="CubeCamera",this.renderTarget=n,this.coordinateSystem=null,this.activeMipmapLevel=0;const s=new Xe(vi,xi,t,e);s.layers=this.layers,this.add(s);const r=new Xe(vi,xi,t,e);r.layers=this.layers,this.add(r);const a=new Xe(vi,xi,t,e);a.layers=this.layers,this.add(a);const o=new Xe(vi,xi,t,e);o.layers=this.layers,this.add(o);const l=new Xe(vi,xi,t,e);l.layers=this.layers,this.add(l);const c=new Xe(vi,xi,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===En)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(),f=t.getActiveCubeFace(),p=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,f,p),t.xr.enabled=g,n.texture.needsPMREMUpdate=!0}}class ac extends Ce{constructor(t,e,n,s,r,a,o,l,c,h){t=t!==void 0?t:[],e=e!==void 0?e:Oi,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 Yh extends on{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 ac(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:dn}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:` +}`;class Ve extends Wn{constructor(t){super(),this.isShaderMaterial=!0,this.type="ShaderMaterial",this.defines={},this.uniforms={},this.uniformsGroups=[],this.vertexShader=nu,this.fragmentShader=iu,this.linewidth=1,this.wireframe=!1,this.wireframeLinewidth=1,this.fog=!1,this.lights=!1,this.clipping=!1,this.forceSinglePass=!0,this.extensions={clipCullDistance:!1,multiDraw:!1},this.defaultAttributeValues={color:[1,1,1],uv:[0,0],uv1:[0,0]},this.index0AttributeName=void 0,this.uniformsNeedUpdate=!1,this.glslVersion=null,t!==void 0&&this.setValues(t)}copy(t){return super.copy(t),this.fragmentShader=t.fragmentShader,this.vertexShader=t.vertexShader,this.uniforms=Qi(t.uniforms),this.uniformsGroups=eu(t.uniformsGroups),this.defines=Object.assign({},t.defines),this.wireframe=t.wireframe,this.wireframeLinewidth=t.wireframeLinewidth,this.fog=t.fog,this.lights=t.lights,this.clipping=t.clipping,this.extensions=Object.assign({},t.extensions),this.glslVersion=t.glslVersion,this}toJSON(t){const e=super.toJSON(t);e.glslVersion=this.glslVersion,e.uniforms={};for(const s in this.uniforms){const a=this.uniforms[s].value;a&&a.isTexture?e.uniforms[s]={type:"t",value:a.toJSON(t).uuid}:a&&a.isColor?e.uniforms[s]={type:"c",value:a.getHex()}:a&&a.isVector2?e.uniforms[s]={type:"v2",value:a.toArray()}:a&&a.isVector3?e.uniforms[s]={type:"v3",value:a.toArray()}:a&&a.isVector4?e.uniforms[s]={type:"v4",value:a.toArray()}:a&&a.isMatrix3?e.uniforms[s]={type:"m3",value:a.toArray()}:a&&a.isMatrix4?e.uniforms[s]={type:"m4",value:a.toArray()}:e.uniforms[s]={value:a}}Object.keys(this.defines).length>0&&(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 mc extends Le{constructor(){super(),this.isCamera=!0,this.type="Camera",this.matrixWorldInverse=new ne,this.projectionMatrix=new ne,this.projectionMatrixInverse=new ne,this.coordinateSystem=An}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 On=new P,Xo=new xt,Yo=new xt;class $e extends mc{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=to*2*Math.atan(e),this.updateProjectionMatrix()}getFocalLength(){const t=Math.tan(ur*.5*this.fov);return .5*this.getFilmHeight()/t}getEffectiveFOV(){return to*2*Math.atan(Math.tan(ur*.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){On.set(-1,-1,.5).applyMatrix4(this.projectionMatrixInverse),e.set(On.x,On.y).multiplyScalar(-t/On.z),On.set(1,1,.5).applyMatrix4(this.projectionMatrixInverse),n.set(On.x,On.y).multiplyScalar(-t/On.z)}getViewSize(t,e){return this.getViewBounds(t,Xo,Yo),e.subVectors(Yo,Xo)}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(ur*.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 su extends Le{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===An)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===_r)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(),f=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,f,u),t.xr.enabled=g,n.texture.needsPMREMUpdate=!0}}class gc extends Ae{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 ru 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 gc(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:_n}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; @@ -41,13 +41,13 @@ var Tc=Object.defineProperty;var wc=(i,t,e)=>t in i?Tc(i,t,{enumerable:!0,config gl_FragColor = texture2D( tEquirect, sampleUV ); } - `},s=new hs(5,5,5),r=new Ne({name:"CubemapFromEquirect",uniforms:Vi(n.uniforms),vertexShader:n.vertexShader,fragmentShader:n.fragmentShader,side:He,blending:bn});r.uniforms.tEquirect.value=e;const a=new Me(s,r),o=e.minFilter;return e.minFilter===Qn&&(e.minFilter=dn),new Xh(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 ur{constructor(t,e=25e-5){this.isFogExp2=!0,this.name="",this.color=new pt(t),this.density=e}clone(){return new ur(this.color,this.density)}toJSON(){return{type:"FogExp2",name:this.name,color:this.color.getHex(),density:this.density}}}class qh extends we{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 pn,this.environmentIntensity=1,this.environmentRotation=new pn,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 jh{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=t!==void 0?t.length/e:0,this.usage=Ba,this.updateRanges=[],this.version=0,this.uuid=Bn()}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:$i.clone(),uv:Qe.getInterpolation($i,Ls,Qi,Us,Io,Or,No,new vt),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 Is(i,t,e,n,s,r){Ei.subVectors(i,e).addScalar(.5).multiply(n),s!==void 0?(Ji.x=r*Ei.x-s*Ei.y,Ji.y=s*Ei.x+r*Ei.y):Ji.copy(Ei),i.copy(t),i.x+=Ji.x,i.y+=Ji.y,i.applyMatrix4(oc)}class Zh extends Ce{constructor(t=null,e=1,n=1,s,r,a,o,l,c=Ye,h=Ye,d,f){super(null,a,o,l,c,h,s,r,d,f),this.isDataTexture=!0,this.image={data:t,width:e,height:n},this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}}class Fo extends de{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 bi=new ne,Oo=new ne,Ns=[],Bo=new si,Kh=new ne,ts=new Me,es=new ri;class $h extends Me{constructor(t,e,n){super(t,e),this.isInstancedMesh=!0,this.instanceMatrix=new Fo(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||Qh.getNormalMatrix(t),s=this.coplanarPoint(zr).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 ri,Fs=new P;class to{constructor(t=new Nn,e=new Nn,n=new Nn,s=new Nn,r=new Nn,a=new Nn){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=En){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],f=s[7],p=s[8],g=s[9],v=s[10],m=s[11],u=s[12],T=s[13],b=s[14],y=s[15];if(n[0].setComponents(l-r,f-c,m-p,y-u).normalize(),n[1].setComponents(l+r,f+c,m+p,y+u).normalize(),n[2].setComponents(l+a,f+h,m+g,y+T).normalize(),n[3].setComponents(l-a,f-h,m-g,y-T).normalize(),n[4].setComponents(l-o,f-d,m-v,y-b).normalize(),e===En)n[5].setComponents(l+o,f+d,m+v,y+b).normalize();else if(e===er)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,Fs.y=s.normal.y>0?t.max.y:t.min.y,Fs.z=s.normal.z>0?t.max.z:t.min.z,s.distanceToPoint(Fs)<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 rr extends Hn{constructor(t){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new pt(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 ar=new P,or=new P,zo=new ne,ns=new cs,Os=new ri,Hr=new P,Ho=new P;class ka extends we{constructor(t=new pe,e=new rr){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;Hr.applyMatrix4(i.matrixWorld);const l=t.ray.origin.distanceTo(Hr);if(!(lt.far))return{distance:l,point:Ho.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 pt(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 ko=new ne,Va=new cs,zs=new ri,Hs=new P;class rs extends we{constructor(t=new pe,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(),zs.copy(n.boundingSphere),zs.applyMatrix4(s),zs.radius+=r,t.ray.intersectsSphere(zs)===!1)return;ko.copy(s).invert(),Va.copy(t.ray).applyMatrix4(ko);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 f=Math.max(0,a.start),p=Math.min(c.count,a.start+a.count);for(let g=f,v=p;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 Ci extends we{constructor(){super(),this.isGroup=!0,this.type="Group"}}class tu extends Ce{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 lc extends Ce{constructor(t,e,n,s,r,a,o,l,c,h=Ui){if(h!==Ui&&h!==Hi)throw new Error("DepthTexture format must be either THREE.DepthFormat or THREE.DepthStencilFormat");n===void 0&&h===Ui&&(n=ei),n===void 0&&h===Hi&&(n=zi),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:Ye,this.minFilter=l!==void 0?l:Ye,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 us extends pe{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,f=e/l,p=[],g=[],v=[],m=[];for(let u=0;u0)&&p.push(b,y,R),(u!==n-1||l0&&(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 ou{constructor(t,e){this.isInterleavedBuffer=!0,this.array=t,this.stride=e,this.count=t!==void 0?t.length/e:0,this.usage=Qa,this.updateRanges=[],this.version=0,this.uuid=Vn()}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:os.clone(),uv:rn.getInterpolation(os,qs,cs,js,qo,$r,jo,new xt),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 Zs(i,t,e,n,s,r){Ui.subVectors(i,e).addScalar(.5).multiply(n),s!==void 0?(ls.x=r*Ui.x-s*Ui.y,ls.y=s*Ui.x+r*Ui.y):ls.copy(Ui),i.copy(t),i.x+=ls.x,i.y+=ls.y,i.applyMatrix4(_c)}class lu extends Ae{constructor(t=null,e=1,n=1,s,r,a,o,l,c=Je,h=Je,d,f){super(null,a,o,l,c,h,s,r,d,f),this.isDataTexture=!0,this.image={data:t,width:e,height:n},this.generateMipmaps=!1,this.flipY=!1,this.unpackAlignment=1}}class Zo extends ce{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,Ko=new ne,Ks=[],$o=new di,cu=new ne,hs=new Ee,us=new fi;class hu extends Ee{constructor(t,e,n){super(t,e),this.isInstancedMesh=!0,this.instanceMatrix=new Zo(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||du.getNormalMatrix(t),s=this.coplanarPoint(Jr).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 $n=new fi,$s=new P;class Mo{constructor(t=new zn,e=new zn,n=new zn,s=new zn,r=new zn,a=new zn){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=An){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],f=s[7],u=s[8],g=s[9],_=s[10],m=s[11],p=s[12],T=s[13],E=s[14],y=s[15];if(n[0].setComponents(l-r,f-c,m-u,y-p).normalize(),n[1].setComponents(l+r,f+c,m+u,y+p).normalize(),n[2].setComponents(l+a,f+h,m+g,y+T).normalize(),n[3].setComponents(l-a,f-h,m-g,y-T).normalize(),n[4].setComponents(l-o,f-d,m-_,y-E).normalize(),e===An)n[5].setComponents(l+o,f+d,m+_,y+E).normalize();else if(e===_r)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(),$n.copy(t.boundingSphere).applyMatrix4(t.matrixWorld);else{const e=t.geometry;e.boundingSphere===null&&e.computeBoundingSphere(),$n.copy(e.boundingSphere).applyMatrix4(t.matrixWorld)}return this.intersectsSphere($n)}intersectsSprite(t){return $n.center.set(0,0,0),$n.radius=.7071067811865476,$n.applyMatrix4(t.matrixWorld),this.intersectsSphere($n)}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,$s.y=s.normal.y>0?t.max.y:t.min.y,$s.z=s.normal.z>0?t.max.z:t.min.z,s.distanceToPoint($s)<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 Sr extends Wn{constructor(t){super(),this.isLineBasicMaterial=!0,this.type="LineBasicMaterial",this.color=new st(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 yr=new P,Er=new P,Jo=new ne,ds=new ys,Js=new fi,Qr=new P,Qo=new P;class eo extends Le{constructor(t=new ge,e=new Sr){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:Qo.clone().applyMatrix4(i.matrixWorld),index:s,face:null,faceIndex:null,barycoord:null,object:i}}class li extends Wn{constructor(t){super(),this.isPointsMaterial=!0,this.type="PointsMaterial",this.color=new st(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 tl=new ne,no=new ys,tr=new fi,er=new P;class Yi extends Le{constructor(t=new ge,e=new li){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(),tr.copy(n.boundingSphere),tr.applyMatrix4(s),tr.radius+=r,t.ray.intersectsSphere(tr)===!1)return;tl.copy(s).invert(),no.copy(t.ray).applyMatrix4(tl);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 f=Math.max(0,a.start),u=Math.min(c.count,a.start+a.count);for(let g=f,_=u;g<_;g++){const m=c.getX(g);er.fromBufferAttribute(d,m),el(er,m,l,s,t,e,this)}}else{const f=Math.max(0,a.start),u=Math.min(d.count,a.start+a.count);for(let g=f,_=u;g<_;g++)er.fromBufferAttribute(d,g),el(er,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 Le{constructor(){super(),this.isGroup=!0,this.type="Group"}}class vc extends Ae{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 xc extends Ae{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=ci),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 bs 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,f=e/l,u=[],g=[],_=[],m=[];for(let p=0;p0)&&u.push(E,y,A),(p!==n-1||lp.start-g.start);let f=0;for(let p=1;pu.start-g.start);let f=0;for(let u=1;ut in i?Tc(i,t,{enumerable:!0,config : cases.z; return clamp( threshold , 1.0e-6, 1.0 ); } -#endif`,mu=`#ifdef USE_ALPHAMAP +#endif`,Au=`#ifdef USE_ALPHAMAP diffuseColor.a *= texture2D( alphaMap, vAlphaMapUv ).g; -#endif`,gu=`#ifdef USE_ALPHAMAP +#endif`,Ru=`#ifdef USE_ALPHAMAP uniform sampler2D alphaMap; -#endif`,_u=`#ifdef USE_ALPHATEST +#endif`,Cu=`#ifdef USE_ALPHATEST #ifdef ALPHA_TO_COVERAGE diffuseColor.a = smoothstep( alphaTest, alphaTest + fwidth( diffuseColor.a ), diffuseColor.a ); if ( diffuseColor.a == 0.0 ) discard; #else if ( diffuseColor.a < alphaTest ) discard; #endif -#endif`,vu=`#ifdef USE_ALPHATEST +#endif`,Pu=`#ifdef USE_ALPHATEST uniform float alphaTest; -#endif`,xu=`#ifdef USE_AOMAP +#endif`,Du=`#ifdef USE_AOMAP float ambientOcclusion = ( texture2D( aoMap, vAoMapUv ).r - 1.0 ) * aoMapIntensity + 1.0; reflectedLight.indirectDiffuse *= ambientOcclusion; #if defined( USE_CLEARCOAT ) @@ -108,10 +108,10 @@ var Tc=Object.defineProperty;var wc=(i,t,e)=>t in i?Tc(i,t,{enumerable:!0,config float dotNV = saturate( dot( geometryNormal, geometryViewDir ) ); reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness ); #endif -#endif`,Mu=`#ifdef USE_AOMAP +#endif`,Lu=`#ifdef USE_AOMAP uniform sampler2D aoMap; uniform float aoMapIntensity; -#endif`,Su=`#ifdef USE_BATCHING +#endif`,Uu=`#ifdef USE_BATCHING #if ! defined( GL_ANGLE_multi_draw ) #define gl_DrawID _gl_DrawID uniform int _gl_DrawID; @@ -145,15 +145,15 @@ var Tc=Object.defineProperty;var wc=(i,t,e)=>t in i?Tc(i,t,{enumerable:!0,config int y = j / size; return texelFetch( batchingColorTexture, ivec2( x, y ), 0 ).rgb; } -#endif`,yu=`#ifdef USE_BATCHING +#endif`,Iu=`#ifdef USE_BATCHING mat4 batchingMatrix = getBatchingMatrix( getIndirectIndex( gl_DrawID ) ); -#endif`,Eu=`vec3 transformed = vec3( position ); +#endif`,Nu=`vec3 transformed = vec3( position ); #ifdef USE_ALPHAHASH vPosition = vec3( position ); -#endif`,bu=`vec3 objectNormal = vec3( normal ); +#endif`,Fu=`vec3 objectNormal = vec3( normal ); #ifdef USE_TANGENT vec3 objectTangent = vec3( tangent.xyz ); -#endif`,Tu=`float G_BlinnPhong_Implicit( ) { +#endif`,Ou=`float G_BlinnPhong_Implicit( ) { return 0.25; } float D_BlinnPhong( const in float shininess, const in float dotNH ) { @@ -167,7 +167,7 @@ vec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in ve float G = G_BlinnPhong_Implicit( ); float D = D_BlinnPhong( shininess, dotNH ); return F * ( G * D ); -} // validated`,wu=`#ifdef USE_IRIDESCENCE +} // validated`,Bu=`#ifdef USE_IRIDESCENCE const mat3 XYZ_TO_REC709 = mat3( 3.2404542, -0.9692660, 0.0556434, -1.5371385, 1.8760108, -0.2040259, @@ -230,7 +230,7 @@ vec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in ve } return max( I, vec3( 0.0 ) ); } -#endif`,Au=`#ifdef USE_BUMPMAP +#endif`,zu=`#ifdef USE_BUMPMAP uniform sampler2D bumpMap; uniform float bumpScale; vec2 dHdxy_fwd() { @@ -251,7 +251,7 @@ vec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in ve vec3 vGrad = sign( fDet ) * ( dHdxy.x * R1 + dHdxy.y * R2 ); return normalize( abs( fDet ) * surf_norm - vGrad ); } -#endif`,Ru=`#if NUM_CLIPPING_PLANES > 0 +#endif`,ku=`#if NUM_CLIPPING_PLANES > 0 vec4 plane; #ifdef ALPHA_TO_COVERAGE float distanceToPlane, distanceGradient; @@ -297,26 +297,26 @@ vec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in ve if ( clipped ) discard; #endif #endif -#endif`,Cu=`#if NUM_CLIPPING_PLANES > 0 +#endif`,Hu=`#if NUM_CLIPPING_PLANES > 0 varying vec3 vClipPosition; uniform vec4 clippingPlanes[ NUM_CLIPPING_PLANES ]; -#endif`,Pu=`#if NUM_CLIPPING_PLANES > 0 +#endif`,Vu=`#if NUM_CLIPPING_PLANES > 0 varying vec3 vClipPosition; -#endif`,Du=`#if NUM_CLIPPING_PLANES > 0 +#endif`,Gu=`#if NUM_CLIPPING_PLANES > 0 vClipPosition = - mvPosition.xyz; -#endif`,Lu=`#if defined( USE_COLOR_ALPHA ) +#endif`,Wu=`#if defined( USE_COLOR_ALPHA ) diffuseColor *= vColor; #elif defined( USE_COLOR ) diffuseColor.rgb *= vColor; -#endif`,Uu=`#if defined( USE_COLOR_ALPHA ) +#endif`,Xu=`#if defined( USE_COLOR_ALPHA ) varying vec4 vColor; #elif defined( USE_COLOR ) varying vec3 vColor; -#endif`,Iu=`#if defined( USE_COLOR_ALPHA ) +#endif`,Yu=`#if defined( USE_COLOR_ALPHA ) varying vec4 vColor; #elif defined( USE_COLOR ) || defined( USE_INSTANCING_COLOR ) || defined( USE_BATCHING_COLOR ) varying vec3 vColor; -#endif`,Nu=`#if defined( USE_COLOR_ALPHA ) +#endif`,qu=`#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 ); @@ -330,7 +330,7 @@ vec3 BRDF_BlinnPhong( const in vec3 lightDir, const in vec3 viewDir, const in ve #ifdef USE_BATCHING_COLOR vec3 batchingColor = getBatchingColor( getIndirectIndex( gl_DrawID ) ); vColor.xyz *= batchingColor.xyz; -#endif`,Fu=`#define PI 3.141592653589793 +#endif`,ju=`#define PI 3.141592653589793 #define PI2 6.283185307179586 #define PI_HALF 1.5707963267948966 #define RECIPROCAL_PI 0.3183098861837907 @@ -404,7 +404,7 @@ vec3 F_Schlick( const in vec3 f0, const in float f90, const in float dotVH ) { 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`,Ou=`#ifdef ENVMAP_TYPE_CUBE_UV +} // validated`,Zu=`#ifdef ENVMAP_TYPE_CUBE_UV #define cubeUV_minMipLevel 4.0 #define cubeUV_minTileSize 16.0 float getFace( vec3 direction ) { @@ -497,7 +497,7 @@ float F_Schlick( const in float f0, const in float f90, const in float dotVH ) { return vec4( mix( color0, color1, mipF ), 1.0 ); } } -#endif`,Bu=`vec3 transformedNormal = objectNormal; +#endif`,Ku=`vec3 transformedNormal = objectNormal; #ifdef USE_TANGENT vec3 transformedTangent = objectTangent; #endif @@ -526,21 +526,21 @@ transformedNormal = normalMatrix * transformedNormal; #ifdef FLIP_SIDED transformedTangent = - transformedTangent; #endif -#endif`,zu=`#ifdef USE_DISPLACEMENTMAP +#endif`,$u=`#ifdef USE_DISPLACEMENTMAP uniform sampler2D displacementMap; uniform float displacementScale; uniform float displacementBias; -#endif`,Hu=`#ifdef USE_DISPLACEMENTMAP +#endif`,Ju=`#ifdef USE_DISPLACEMENTMAP transformed += normalize( objectNormal ) * ( texture2D( displacementMap, vDisplacementMapUv ).x * displacementScale + displacementBias ); -#endif`,ku=`#ifdef USE_EMISSIVEMAP +#endif`,Qu=`#ifdef USE_EMISSIVEMAP vec4 emissiveColor = texture2D( emissiveMap, vEmissiveMapUv ); #ifdef DECODE_VIDEO_TEXTURE_EMISSIVE emissiveColor = sRGBTransferEOTF( emissiveColor ); #endif totalEmissiveRadiance *= emissiveColor.rgb; -#endif`,Vu=`#ifdef USE_EMISSIVEMAP +#endif`,td=`#ifdef USE_EMISSIVEMAP uniform sampler2D emissiveMap; -#endif`,Gu="gl_FragColor = linearToOutputTexel( gl_FragColor );",Wu=`vec4 LinearTransferOETF( in vec4 value ) { +#endif`,ed="gl_FragColor = linearToOutputTexel( gl_FragColor );",nd=`vec4 LinearTransferOETF( in vec4 value ) { return value; } vec4 sRGBTransferEOTF( in vec4 value ) { @@ -548,7 +548,7 @@ vec4 sRGBTransferEOTF( in vec4 value ) { } 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 ); -}`,Xu=`#ifdef USE_ENVMAP +}`,id=`#ifdef USE_ENVMAP #ifdef ENV_WORLDPOS vec3 cameraToFrag; if ( isOrthographic ) { @@ -577,7 +577,7 @@ vec4 sRGBTransferOETF( in vec4 value ) { #elif defined( ENVMAP_BLENDING_ADD ) outgoingLight += envColor.xyz * specularStrength * reflectivity; #endif -#endif`,Yu=`#ifdef USE_ENVMAP +#endif`,sd=`#ifdef USE_ENVMAP uniform float envMapIntensity; uniform float flipEnvMap; uniform mat3 envMapRotation; @@ -587,7 +587,7 @@ vec4 sRGBTransferOETF( in vec4 value ) { uniform sampler2D envMap; #endif -#endif`,qu=`#ifdef USE_ENVMAP +#endif`,rd=`#ifdef USE_ENVMAP uniform float reflectivity; #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) #define ENV_WORLDPOS @@ -598,7 +598,7 @@ vec4 sRGBTransferOETF( in vec4 value ) { #else varying vec3 vReflect; #endif -#endif`,ju=`#ifdef USE_ENVMAP +#endif`,ad=`#ifdef USE_ENVMAP #if defined( USE_BUMPMAP ) || defined( USE_NORMALMAP ) || defined( PHONG ) || defined( LAMBERT ) #define ENV_WORLDPOS #endif @@ -609,7 +609,7 @@ vec4 sRGBTransferOETF( in vec4 value ) { varying vec3 vReflect; uniform float refractionRatio; #endif -#endif`,Zu=`#ifdef USE_ENVMAP +#endif`,od=`#ifdef USE_ENVMAP #ifdef ENV_WORLDPOS vWorldPosition = worldPosition.xyz; #else @@ -626,18 +626,18 @@ vec4 sRGBTransferOETF( in vec4 value ) { vReflect = refract( cameraToVertex, worldNormal, refractionRatio ); #endif #endif -#endif`,Ku=`#ifdef USE_FOG +#endif`,ld=`#ifdef USE_FOG vFogDepth = - mvPosition.z; -#endif`,$u=`#ifdef USE_FOG +#endif`,cd=`#ifdef USE_FOG varying float vFogDepth; -#endif`,Ju=`#ifdef USE_FOG +#endif`,hd=`#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`,Qu=`#ifdef USE_FOG +#endif`,ud=`#ifdef USE_FOG uniform vec3 fogColor; varying float vFogDepth; #ifdef FOG_EXP2 @@ -646,7 +646,7 @@ vec4 sRGBTransferOETF( in vec4 value ) { uniform float fogNear; uniform float fogFar; #endif -#endif`,td=`#ifdef USE_GRADIENTMAP +#endif`,dd=`#ifdef USE_GRADIENTMAP uniform sampler2D gradientMap; #endif vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { @@ -658,12 +658,12 @@ vec3 getGradientIrradiance( vec3 normal, vec3 lightDirection ) { 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 -}`,ed=`#ifdef USE_LIGHTMAP +}`,fd=`#ifdef USE_LIGHTMAP uniform sampler2D lightMap; uniform float lightMapIntensity; -#endif`,nd=`LambertMaterial material; +#endif`,pd=`LambertMaterial material; material.diffuseColor = diffuseColor.rgb; -material.specularStrength = specularStrength;`,id=`varying vec3 vViewPosition; +material.specularStrength = specularStrength;`,md=`varying vec3 vViewPosition; struct LambertMaterial { vec3 diffuseColor; float specularStrength; @@ -677,7 +677,7 @@ void RE_IndirectDiffuse_Lambert( const in vec3 irradiance, const in vec3 geometr reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); } #define RE_Direct RE_Direct_Lambert -#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,sd=`uniform bool receiveShadow; +#define RE_IndirectDiffuse RE_IndirectDiffuse_Lambert`,gd=`uniform bool receiveShadow; uniform vec3 ambientLightColor; #if defined( USE_LIGHT_PROBES ) uniform vec3 lightProbe[ 9 ]; @@ -793,7 +793,7 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight ); return irradiance; } -#endif`,rd=`#ifdef USE_ENVMAP +#endif`,_d=`#ifdef USE_ENVMAP vec3 getIBLIrradiance( const in vec3 normal ) { #ifdef ENVMAP_TYPE_CUBE_UV vec3 worldNormal = inverseTransformDirection( normal, viewMatrix ); @@ -826,8 +826,8 @@ float getSpotAttenuation( const in float coneCosine, const in float penumbraCosi #endif } #endif -#endif`,ad=`ToonMaterial material; -material.diffuseColor = diffuseColor.rgb;`,od=`varying vec3 vViewPosition; +#endif`,vd=`ToonMaterial material; +material.diffuseColor = diffuseColor.rgb;`,xd=`varying vec3 vViewPosition; struct ToonMaterial { vec3 diffuseColor; }; @@ -839,11 +839,11 @@ void RE_IndirectDiffuse_Toon( const in vec3 irradiance, const in vec3 geometryPo reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); } #define RE_Direct RE_Direct_Toon -#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,ld=`BlinnPhongMaterial material; +#define RE_IndirectDiffuse RE_IndirectDiffuse_Toon`,Md=`BlinnPhongMaterial material; material.diffuseColor = diffuseColor.rgb; material.specularColor = specular; material.specularShininess = shininess; -material.specularStrength = specularStrength;`,cd=`varying vec3 vViewPosition; +material.specularStrength = specularStrength;`,Sd=`varying vec3 vViewPosition; struct BlinnPhongMaterial { vec3 diffuseColor; vec3 specularColor; @@ -860,7 +860,7 @@ void RE_IndirectDiffuse_BlinnPhong( const in vec3 irradiance, const in vec3 geom reflectedLight.indirectDiffuse += irradiance * BRDF_Lambert( material.diffuseColor ); } #define RE_Direct RE_Direct_BlinnPhong -#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,hd=`PhysicalMaterial material; +#define RE_IndirectDiffuse RE_IndirectDiffuse_BlinnPhong`,yd=`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 ); @@ -946,7 +946,7 @@ material.roughness = min( material.roughness, 1.0 ); 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`,ud=`struct PhysicalMaterial { +#endif`,Ed=`struct PhysicalMaterial { vec3 diffuseColor; float roughness; vec3 specularColor; @@ -1247,7 +1247,7 @@ void RE_IndirectSpecular_Physical( const in vec3 radiance, const in vec3 irradia #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 ); -}`,dd=` +}`,bd=` vec3 geometryPosition = - vViewPosition; vec3 geometryNormal = normal; vec3 geometryViewDir = ( isOrthographic ) ? vec3( 0, 0, 1 ) : normalize( vViewPosition ); @@ -1362,7 +1362,7 @@ IncidentLight directLight; #if defined( RE_IndirectSpecular ) vec3 radiance = vec3( 0.0 ); vec3 clearcoatRadiance = vec3( 0.0 ); -#endif`,fd=`#if defined( RE_IndirectDiffuse ) +#endif`,Td=`#if defined( RE_IndirectDiffuse ) #ifdef USE_LIGHTMAP vec4 lightMapTexel = texture2D( lightMap, vLightMapUv ); vec3 lightMapIrradiance = lightMapTexel.rgb * lightMapIntensity; @@ -1381,32 +1381,32 @@ IncidentLight directLight; #ifdef USE_CLEARCOAT clearcoatRadiance += getIBLRadiance( geometryViewDir, geometryClearcoatNormal, material.clearcoatRoughness ); #endif -#endif`,pd=`#if defined( RE_IndirectDiffuse ) +#endif`,wd=`#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`,md=`#if defined( USE_LOGDEPTHBUF ) +#endif`,Ad=`#if defined( USE_LOGDEPTHBUF ) gl_FragDepth = vIsPerspective == 0.0 ? gl_FragCoord.z : log2( vFragDepth ) * logDepthBufFC * 0.5; -#endif`,gd=`#if defined( USE_LOGDEPTHBUF ) +#endif`,Rd=`#if defined( USE_LOGDEPTHBUF ) uniform float logDepthBufFC; varying float vFragDepth; varying float vIsPerspective; -#endif`,_d=`#ifdef USE_LOGDEPTHBUF +#endif`,Cd=`#ifdef USE_LOGDEPTHBUF varying float vFragDepth; varying float vIsPerspective; -#endif`,vd=`#ifdef USE_LOGDEPTHBUF +#endif`,Pd=`#ifdef USE_LOGDEPTHBUF vFragDepth = 1.0 + gl_Position.w; vIsPerspective = float( isPerspectiveMatrix( projectionMatrix ) ); -#endif`,xd=`#ifdef USE_MAP +#endif`,Dd=`#ifdef USE_MAP vec4 sampledDiffuseColor = texture2D( map, vMapUv ); #ifdef DECODE_VIDEO_TEXTURE sampledDiffuseColor = sRGBTransferEOTF( sampledDiffuseColor ); #endif diffuseColor *= sampledDiffuseColor; -#endif`,Md=`#ifdef USE_MAP +#endif`,Ld=`#ifdef USE_MAP uniform sampler2D map; -#endif`,Sd=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) +#endif`,Ud=`#if defined( USE_MAP ) || defined( USE_ALPHAMAP ) #if defined( USE_POINTS_UV ) vec2 uv = vUv; #else @@ -1418,7 +1418,7 @@ IncidentLight directLight; #endif #ifdef USE_ALPHAMAP diffuseColor.a *= texture2D( alphaMap, uv ).g; -#endif`,yd=`#if defined( USE_POINTS_UV ) +#endif`,Id=`#if defined( USE_POINTS_UV ) varying vec2 vUv; #else #if defined( USE_MAP ) || defined( USE_ALPHAMAP ) @@ -1430,19 +1430,19 @@ IncidentLight directLight; #endif #ifdef USE_ALPHAMAP uniform sampler2D alphaMap; -#endif`,Ed=`float metalnessFactor = metalness; +#endif`,Nd=`float metalnessFactor = metalness; #ifdef USE_METALNESSMAP vec4 texelMetalness = texture2D( metalnessMap, vMetalnessMapUv ); metalnessFactor *= texelMetalness.b; -#endif`,bd=`#ifdef USE_METALNESSMAP +#endif`,Fd=`#ifdef USE_METALNESSMAP uniform sampler2D metalnessMap; -#endif`,Td=`#ifdef USE_INSTANCING_MORPH +#endif`,Od=`#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`,wd=`#if defined( USE_MORPHCOLORS ) +#endif`,Bd=`#if defined( USE_MORPHCOLORS ) vColor *= morphTargetBaseInfluence; for ( int i = 0; i < MORPHTARGETS_COUNT; i ++ ) { #if defined( USE_COLOR_ALPHA ) @@ -1451,12 +1451,12 @@ IncidentLight directLight; if ( morphTargetInfluences[ i ] != 0.0 ) vColor += getMorph( gl_VertexID, i, 2 ).rgb * morphTargetInfluences[ i ]; #endif } -#endif`,Ad=`#ifdef USE_MORPHNORMALS +#endif`,zd=`#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`,Rd=`#ifdef USE_MORPHTARGETS +#endif`,kd=`#ifdef USE_MORPHTARGETS #ifndef USE_INSTANCING_MORPH uniform float morphTargetBaseInfluence; uniform float morphTargetInfluences[ MORPHTARGETS_COUNT ]; @@ -1470,12 +1470,12 @@ IncidentLight directLight; ivec3 morphUV = ivec3( x, y, morphTargetIndex ); return texelFetch( morphTargetsTexture, morphUV, 0 ); } -#endif`,Cd=`#ifdef USE_MORPHTARGETS +#endif`,Hd=`#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`,Pd=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; +#endif`,Vd=`float faceDirection = gl_FrontFacing ? 1.0 : - 1.0; #ifdef FLAT_SHADED vec3 fdx = dFdx( vViewPosition ); vec3 fdy = dFdy( vViewPosition ); @@ -1516,7 +1516,7 @@ IncidentLight directLight; tbn2[1] *= faceDirection; #endif #endif -vec3 nonPerturbedNormal = normal;`,Dd=`#ifdef USE_NORMALMAP_OBJECTSPACE +vec3 nonPerturbedNormal = normal;`,Gd=`#ifdef USE_NORMALMAP_OBJECTSPACE normal = texture2D( normalMap, vNormalMapUv ).xyz * 2.0 - 1.0; #ifdef FLIP_SIDED normal = - normal; @@ -1531,25 +1531,25 @@ vec3 nonPerturbedNormal = normal;`,Dd=`#ifdef USE_NORMALMAP_OBJECTSPACE normal = normalize( tbn * mapN ); #elif defined( USE_BUMPMAP ) normal = perturbNormalArb( - vViewPosition, normal, dHdxy_fwd(), faceDirection ); -#endif`,Ld=`#ifndef FLAT_SHADED +#endif`,Wd=`#ifndef FLAT_SHADED varying vec3 vNormal; #ifdef USE_TANGENT varying vec3 vTangent; varying vec3 vBitangent; #endif -#endif`,Ud=`#ifndef FLAT_SHADED +#endif`,Xd=`#ifndef FLAT_SHADED varying vec3 vNormal; #ifdef USE_TANGENT varying vec3 vTangent; varying vec3 vBitangent; #endif -#endif`,Id=`#ifndef FLAT_SHADED +#endif`,Yd=`#ifndef FLAT_SHADED vNormal = normalize( transformedNormal ); #ifdef USE_TANGENT vTangent = normalize( transformedTangent ); vBitangent = normalize( cross( vNormal, vTangent ) * tangent.w ); #endif -#endif`,Nd=`#ifdef USE_NORMALMAP +#endif`,qd=`#ifdef USE_NORMALMAP uniform sampler2D normalMap; uniform vec2 normalScale; #endif @@ -1571,13 +1571,13 @@ vec3 nonPerturbedNormal = normal;`,Dd=`#ifdef USE_NORMALMAP_OBJECTSPACE float scale = ( det == 0.0 ) ? 0.0 : inversesqrt( det ); return mat3( T * scale, B * scale, N ); } -#endif`,Fd=`#ifdef USE_CLEARCOAT +#endif`,jd=`#ifdef USE_CLEARCOAT vec3 clearcoatNormal = nonPerturbedNormal; -#endif`,Od=`#ifdef USE_CLEARCOAT_NORMALMAP +#endif`,Zd=`#ifdef USE_CLEARCOAT_NORMALMAP vec3 clearcoatMapN = texture2D( clearcoatNormalMap, vClearcoatNormalMapUv ).xyz * 2.0 - 1.0; clearcoatMapN.xy *= clearcoatNormalScale; clearcoatNormal = normalize( tbn2 * clearcoatMapN ); -#endif`,Bd=`#ifdef USE_CLEARCOATMAP +#endif`,Kd=`#ifdef USE_CLEARCOATMAP uniform sampler2D clearcoatMap; #endif #ifdef USE_CLEARCOAT_NORMALMAP @@ -1586,18 +1586,18 @@ vec3 nonPerturbedNormal = normal;`,Dd=`#ifdef USE_NORMALMAP_OBJECTSPACE #endif #ifdef USE_CLEARCOAT_ROUGHNESSMAP uniform sampler2D clearcoatRoughnessMap; -#endif`,zd=`#ifdef USE_IRIDESCENCEMAP +#endif`,$d=`#ifdef USE_IRIDESCENCEMAP uniform sampler2D iridescenceMap; #endif #ifdef USE_IRIDESCENCE_THICKNESSMAP uniform sampler2D iridescenceThicknessMap; -#endif`,Hd=`#ifdef OPAQUE +#endif`,Jd=`#ifdef OPAQUE diffuseColor.a = 1.0; #endif #ifdef USE_TRANSMISSION diffuseColor.a *= material.transmissionAlpha; #endif -gl_FragColor = vec4( outgoingLight, diffuseColor.a );`,kd=`vec3 packNormalToRGB( const in vec3 normal ) { +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 ) { @@ -1666,9 +1666,9 @@ float viewZToPerspectiveDepth( const in float viewZ, const in float near, const } float perspectiveDepthToViewZ( const in float depth, const in float near, const in float far ) { return ( near * far ) / ( ( far - near ) * depth - far ); -}`,Vd=`#ifdef PREMULTIPLIED_ALPHA +}`,tf=`#ifdef PREMULTIPLIED_ALPHA gl_FragColor.rgb *= gl_FragColor.a; -#endif`,Gd=`vec4 mvPosition = vec4( transformed, 1.0 ); +#endif`,ef=`vec4 mvPosition = vec4( transformed, 1.0 ); #ifdef USE_BATCHING mvPosition = batchingMatrix * mvPosition; #endif @@ -1676,22 +1676,22 @@ float perspectiveDepthToViewZ( const in float depth, const in float near, const mvPosition = instanceMatrix * mvPosition; #endif mvPosition = modelViewMatrix * mvPosition; -gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING +gl_Position = projectionMatrix * mvPosition;`,nf=`#ifdef DITHERING gl_FragColor.rgb = dithering( gl_FragColor.rgb ); -#endif`,Xd=`#ifdef DITHERING +#endif`,sf=`#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`,Yd=`float roughnessFactor = roughness; +#endif`,rf=`float roughnessFactor = roughness; #ifdef USE_ROUGHNESSMAP vec4 texelRoughness = texture2D( roughnessMap, vRoughnessMapUv ); roughnessFactor *= texelRoughness.g; -#endif`,qd=`#ifdef USE_ROUGHNESSMAP +#endif`,af=`#ifdef USE_ROUGHNESSMAP uniform sampler2D roughnessMap; -#endif`,jd=`#if NUM_SPOT_LIGHT_COORDS > 0 +#endif`,of=`#if NUM_SPOT_LIGHT_COORDS > 0 varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; #endif #if NUM_SPOT_LIGHT_MAPS > 0 @@ -1877,7 +1877,7 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING } return mix( 1.0, shadow, shadowIntensity ); } -#endif`,Zd=`#if NUM_SPOT_LIGHT_COORDS > 0 +#endif`,lf=`#if NUM_SPOT_LIGHT_COORDS > 0 uniform mat4 spotLightMatrix[ NUM_SPOT_LIGHT_COORDS ]; varying vec4 vSpotLightCoord[ NUM_SPOT_LIGHT_COORDS ]; #endif @@ -1918,7 +1918,7 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING }; uniform PointLightShadow pointLightShadows[ NUM_POINT_LIGHT_SHADOWS ]; #endif -#endif`,Kd=`#if ( defined( USE_SHADOWMAP ) && ( NUM_DIR_LIGHT_SHADOWS > 0 || NUM_POINT_LIGHT_SHADOWS > 0 ) ) || ( NUM_SPOT_LIGHT_COORDS > 0 ) +#endif`,cf=`#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 @@ -1950,7 +1950,7 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING vSpotLightCoord[ i ] = spotLightMatrix[ i ] * shadowWorldPosition; } #pragma unroll_loop_end -#endif`,$d=`float getShadowMask() { +#endif`,hf=`float getShadowMask() { float shadow = 1.0; #ifdef USE_SHADOWMAP #if NUM_DIR_LIGHT_SHADOWS > 0 @@ -1982,12 +1982,12 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING #endif #endif return shadow; -}`,Jd=`#ifdef USE_SKINNING +}`,uf=`#ifdef USE_SKINNING mat4 boneMatX = getBoneMatrix( skinIndex.x ); mat4 boneMatY = getBoneMatrix( skinIndex.y ); mat4 boneMatZ = getBoneMatrix( skinIndex.z ); mat4 boneMatW = getBoneMatrix( skinIndex.w ); -#endif`,Qd=`#ifdef USE_SKINNING +#endif`,df=`#ifdef USE_SKINNING uniform mat4 bindMatrix; uniform mat4 bindMatrixInverse; uniform highp sampler2D boneTexture; @@ -2002,7 +2002,7 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING vec4 v4 = texelFetch( boneTexture, ivec2( x + 3, y ), 0 ); return mat4( v1, v2, v3, v4 ); } -#endif`,tf=`#ifdef USE_SKINNING +#endif`,ff=`#ifdef USE_SKINNING vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 ); vec4 skinned = vec4( 0.0 ); skinned += boneMatX * skinVertex * skinWeight.x; @@ -2010,7 +2010,7 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING skinned += boneMatZ * skinVertex * skinWeight.z; skinned += boneMatW * skinVertex * skinWeight.w; transformed = ( bindMatrixInverse * skinned ).xyz; -#endif`,ef=`#ifdef USE_SKINNING +#endif`,pf=`#ifdef USE_SKINNING mat4 skinMatrix = mat4( 0.0 ); skinMatrix += skinWeight.x * boneMatX; skinMatrix += skinWeight.y * boneMatY; @@ -2021,17 +2021,17 @@ gl_Position = projectionMatrix * mvPosition;`,Wd=`#ifdef DITHERING #ifdef USE_TANGENT objectTangent = vec4( skinMatrix * vec4( objectTangent, 0.0 ) ).xyz; #endif -#endif`,nf=`float specularStrength; +#endif`,mf=`float specularStrength; #ifdef USE_SPECULARMAP vec4 texelSpecular = texture2D( specularMap, vSpecularMapUv ); specularStrength = texelSpecular.r; #else specularStrength = 1.0; -#endif`,sf=`#ifdef USE_SPECULARMAP +#endif`,gf=`#ifdef USE_SPECULARMAP uniform sampler2D specularMap; -#endif`,rf=`#if defined( TONE_MAPPING ) +#endif`,_f=`#if defined( TONE_MAPPING ) gl_FragColor.rgb = toneMapping( gl_FragColor.rgb ); -#endif`,af=`#ifndef saturate +#endif`,vf=`#ifndef saturate #define saturate( a ) clamp( a, 0.0, 1.0 ) #endif uniform float toneMappingExposure; @@ -2128,7 +2128,7 @@ vec3 NeutralToneMapping( vec3 color ) { float g = 1. - 1. / ( Desaturation * ( peak - newPeak ) + 1. ); return mix( color, vec3( newPeak ), g ); } -vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISSION +vec3 CustomToneMapping( vec3 color ) { return color; }`,xf=`#ifdef USE_TRANSMISSION material.transmission = transmission; material.transmissionAlpha = 1.0; material.thickness = thickness; @@ -2149,7 +2149,7 @@ vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISS material.attenuationColor, material.attenuationDistance ); material.transmissionAlpha = mix( material.transmissionAlpha, transmitted.a, material.transmission ); totalDiffuse = mix( totalDiffuse, transmitted.rgb, material.transmission ); -#endif`,lf=`#ifdef USE_TRANSMISSION +#endif`,Mf=`#ifdef USE_TRANSMISSION uniform float transmission; uniform float thickness; uniform float attenuationDistance; @@ -2275,7 +2275,7 @@ vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISS float transmittanceFactor = ( transmittance.r + transmittance.g + transmittance.b ) / 3.0; return vec4( ( 1.0 - F ) * attenuatedColor, 1.0 - ( 1.0 - transmittedLight.a ) * transmittanceFactor ); } -#endif`,cf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) +#endif`,Sf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) varying vec2 vUv; #endif #ifdef USE_MAP @@ -2345,7 +2345,7 @@ vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISS #ifdef USE_THICKNESSMAP uniform mat3 thicknessMapTransform; varying vec2 vThicknessMapUv; -#endif`,hf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) +#endif`,yf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) varying vec2 vUv; #endif #ifdef USE_MAP @@ -2439,7 +2439,7 @@ vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISS #ifdef USE_THICKNESSMAP uniform mat3 thicknessMapTransform; varying vec2 vThicknessMapUv; -#endif`,uf=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) +#endif`,Ef=`#if defined( USE_UV ) || defined( USE_ANISOTROPY ) vUv = vec3( uv, 1 ).xy; #endif #ifdef USE_MAP @@ -2510,7 +2510,7 @@ vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISS #endif #ifdef USE_THICKNESSMAP vThicknessMapUv = ( thicknessMapTransform * vec3( THICKNESSMAP_UV, 1 ) ).xy; -#endif`,df=`#if defined( USE_ENVMAP ) || defined( DISTANCE ) || defined ( USE_SHADOWMAP ) || defined ( USE_TRANSMISSION ) || NUM_SPOT_LIGHT_COORDS > 0 +#endif`,bf=`#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; @@ -2519,12 +2519,12 @@ vec3 CustomToneMapping( vec3 color ) { return color; }`,of=`#ifdef USE_TRANSMISS worldPosition = instanceMatrix * worldPosition; #endif worldPosition = modelMatrix * worldPosition; -#endif`;const ff=`varying vec2 vUv; +#endif`;const Tf=`varying vec2 vUv; uniform mat3 uvTransform; void main() { vUv = ( uvTransform * vec3( uv, 1 ) ).xy; gl_Position = vec4( position.xy, 1.0, 1.0 ); -}`,pf=`uniform sampler2D t2D; +}`,wf=`uniform sampler2D t2D; uniform float backgroundIntensity; varying vec2 vUv; void main() { @@ -2536,14 +2536,14 @@ void main() { gl_FragColor = texColor; #include #include -}`,mf=`varying vec3 vWorldDirection; +}`,Af=`varying vec3 vWorldDirection; #include void main() { vWorldDirection = transformDirection( position, modelMatrix ); #include #include gl_Position.z = gl_Position.w; -}`,gf=`#ifdef ENVMAP_TYPE_CUBE +}`,Rf=`#ifdef ENVMAP_TYPE_CUBE uniform samplerCube envMap; #elif defined( ENVMAP_TYPE_CUBE_UV ) uniform sampler2D envMap; @@ -2566,14 +2566,14 @@ void main() { gl_FragColor = texColor; #include #include -}`,_f=`varying vec3 vWorldDirection; +}`,Cf=`varying vec3 vWorldDirection; #include void main() { vWorldDirection = transformDirection( position, modelMatrix ); #include #include gl_Position.z = gl_Position.w; -}`,vf=`uniform samplerCube tCube; +}`,Pf=`uniform samplerCube tCube; uniform float tFlip; uniform float opacity; varying vec3 vWorldDirection; @@ -2583,7 +2583,7 @@ void main() { gl_FragColor.a *= opacity; #include #include -}`,xf=`#include +}`,Df=`#include #include #include #include @@ -2610,7 +2610,7 @@ void main() { #include #include vHighPrecisionZW = gl_Position.zw; -}`,Mf=`#if DEPTH_PACKING == 3200 +}`,Lf=`#if DEPTH_PACKING == 3200 uniform float opacity; #endif #include @@ -2644,7 +2644,7 @@ void main() { #elif DEPTH_PACKING == 3203 gl_FragColor = vec4( packDepthToRG( fragCoordZ ), 0.0, 1.0 ); #endif -}`,Sf=`#define DISTANCE +}`,Uf=`#define DISTANCE varying vec3 vWorldPosition; #include #include @@ -2671,7 +2671,7 @@ void main() { #include #include vWorldPosition = worldPosition.xyz; -}`,yf=`#define DISTANCE +}`,If=`#define DISTANCE uniform vec3 referencePosition; uniform float nearDistance; uniform float farDistance; @@ -2695,13 +2695,13 @@ void main () { dist = ( dist - nearDistance ) / ( farDistance - nearDistance ); dist = saturate( dist ); gl_FragColor = packDepthToRGBA( dist ); -}`,Ef=`varying vec3 vWorldDirection; +}`,Nf=`varying vec3 vWorldDirection; #include void main() { vWorldDirection = transformDirection( position, modelMatrix ); #include #include -}`,bf=`uniform sampler2D tEquirect; +}`,Ff=`uniform sampler2D tEquirect; varying vec3 vWorldDirection; #include void main() { @@ -2710,7 +2710,7 @@ void main() { gl_FragColor = texture2D( tEquirect, sampleUV ); #include #include -}`,Tf=`uniform float scale; +}`,Of=`uniform float scale; attribute float lineDistance; varying float vLineDistance; #include @@ -2732,7 +2732,7 @@ void main() { #include #include #include -}`,wf=`uniform vec3 diffuse; +}`,Bf=`uniform vec3 diffuse; uniform float opacity; uniform float dashSize; uniform float totalSize; @@ -2760,7 +2760,7 @@ void main() { #include #include #include -}`,Af=`#include +}`,zf=`#include #include #include #include @@ -2792,7 +2792,7 @@ void main() { #include #include #include -}`,Rf=`uniform vec3 diffuse; +}`,kf=`uniform vec3 diffuse; uniform float opacity; #ifndef FLAT_SHADED varying vec3 vNormal; @@ -2840,7 +2840,7 @@ void main() { #include #include #include -}`,Cf=`#define LAMBERT +}`,Hf=`#define LAMBERT varying vec3 vViewPosition; #include #include @@ -2879,7 +2879,7 @@ void main() { #include #include #include -}`,Pf=`#define LAMBERT +}`,Vf=`#define LAMBERT uniform vec3 diffuse; uniform vec3 emissive; uniform float opacity; @@ -2936,7 +2936,7 @@ void main() { #include #include #include -}`,Df=`#define MATCAP +}`,Gf=`#define MATCAP varying vec3 vViewPosition; #include #include @@ -2970,7 +2970,7 @@ void main() { #include #include vViewPosition = - mvPosition.xyz; -}`,Lf=`#define MATCAP +}`,Wf=`#define MATCAP uniform vec3 diffuse; uniform float opacity; uniform sampler2D matcap; @@ -3016,7 +3016,7 @@ void main() { #include #include #include -}`,Uf=`#define NORMAL +}`,Xf=`#define NORMAL #if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) varying vec3 vViewPosition; #endif @@ -3049,7 +3049,7 @@ void main() { #if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) vViewPosition = - mvPosition.xyz; #endif -}`,If=`#define NORMAL +}`,Yf=`#define NORMAL uniform float opacity; #if defined( FLAT_SHADED ) || defined( USE_BUMPMAP ) || defined( USE_NORMALMAP_TANGENTSPACE ) varying vec3 vViewPosition; @@ -3071,7 +3071,7 @@ void main() { #ifdef OPAQUE gl_FragColor.a = 1.0; #endif -}`,Nf=`#define PHONG +}`,qf=`#define PHONG varying vec3 vViewPosition; #include #include @@ -3110,7 +3110,7 @@ void main() { #include #include #include -}`,Ff=`#define PHONG +}`,jf=`#define PHONG uniform vec3 diffuse; uniform vec3 emissive; uniform vec3 specular; @@ -3169,7 +3169,7 @@ void main() { #include #include #include -}`,Of=`#define STANDARD +}`,Zf=`#define STANDARD varying vec3 vViewPosition; #ifdef USE_TRANSMISSION varying vec3 vWorldPosition; @@ -3212,7 +3212,7 @@ void main() { #ifdef USE_TRANSMISSION vWorldPosition = worldPosition.xyz; #endif -}`,Bf=`#define STANDARD +}`,Kf=`#define STANDARD #ifdef PHYSICAL #define IOR #define USE_SPECULAR @@ -3337,7 +3337,7 @@ void main() { #include #include #include -}`,zf=`#define TOON +}`,$f=`#define TOON varying vec3 vViewPosition; #include #include @@ -3374,7 +3374,7 @@ void main() { #include #include #include -}`,Hf=`#define TOON +}`,Jf=`#define TOON uniform vec3 diffuse; uniform vec3 emissive; uniform float opacity; @@ -3427,7 +3427,7 @@ void main() { #include #include #include -}`,kf=`uniform float size; +}`,Qf=`uniform float size; uniform float scale; #include #include @@ -3458,7 +3458,7 @@ void main() { #include #include #include -}`,Vf=`uniform vec3 diffuse; +}`,tp=`uniform vec3 diffuse; uniform float opacity; #include #include @@ -3483,7 +3483,7 @@ void main() { #include #include #include -}`,Gf=`#include +}`,ep=`#include #include #include #include @@ -3506,7 +3506,7 @@ void main() { #include #include #include -}`,Wf=`uniform vec3 color; +}`,np=`uniform vec3 color; uniform float opacity; #include #include @@ -3522,7 +3522,7 @@ void main() { #include #include #include -}`,Xf=`uniform float rotation; +}`,ip=`uniform float rotation; uniform vec2 center; #include #include @@ -3546,7 +3546,7 @@ void main() { #include #include #include -}`,Yf=`uniform vec3 diffuse; +}`,sp=`uniform vec3 diffuse; uniform float opacity; #include #include @@ -3571,7 +3571,7 @@ void main() { #include #include #include -}`,Vt={alphahash_fragment:fu,alphahash_pars_fragment:pu,alphamap_fragment:mu,alphamap_pars_fragment:gu,alphatest_fragment:_u,alphatest_pars_fragment:vu,aomap_fragment:xu,aomap_pars_fragment:Mu,batching_pars_vertex:Su,batching_vertex:yu,begin_vertex:Eu,beginnormal_vertex:bu,bsdfs:Tu,iridescence_fragment:wu,bumpmap_pars_fragment:Au,clipping_planes_fragment:Ru,clipping_planes_pars_fragment:Cu,clipping_planes_pars_vertex:Pu,clipping_planes_vertex:Du,color_fragment:Lu,color_pars_fragment:Uu,color_pars_vertex:Iu,color_vertex:Nu,common:Fu,cube_uv_reflection_fragment:Ou,defaultnormal_vertex:Bu,displacementmap_pars_vertex:zu,displacementmap_vertex:Hu,emissivemap_fragment:ku,emissivemap_pars_fragment:Vu,colorspace_fragment:Gu,colorspace_pars_fragment:Wu,envmap_fragment:Xu,envmap_common_pars_fragment:Yu,envmap_pars_fragment:qu,envmap_pars_vertex:ju,envmap_physical_pars_fragment:rd,envmap_vertex:Zu,fog_vertex:Ku,fog_pars_vertex:$u,fog_fragment:Ju,fog_pars_fragment:Qu,gradientmap_pars_fragment:td,lightmap_pars_fragment:ed,lights_lambert_fragment:nd,lights_lambert_pars_fragment:id,lights_pars_begin:sd,lights_toon_fragment:ad,lights_toon_pars_fragment:od,lights_phong_fragment:ld,lights_phong_pars_fragment:cd,lights_physical_fragment:hd,lights_physical_pars_fragment:ud,lights_fragment_begin:dd,lights_fragment_maps:fd,lights_fragment_end:pd,logdepthbuf_fragment:md,logdepthbuf_pars_fragment:gd,logdepthbuf_pars_vertex:_d,logdepthbuf_vertex:vd,map_fragment:xd,map_pars_fragment:Md,map_particle_fragment:Sd,map_particle_pars_fragment:yd,metalnessmap_fragment:Ed,metalnessmap_pars_fragment:bd,morphinstance_vertex:Td,morphcolor_vertex:wd,morphnormal_vertex:Ad,morphtarget_pars_vertex:Rd,morphtarget_vertex:Cd,normal_fragment_begin:Pd,normal_fragment_maps:Dd,normal_pars_fragment:Ld,normal_pars_vertex:Ud,normal_vertex:Id,normalmap_pars_fragment:Nd,clearcoat_normal_fragment_begin:Fd,clearcoat_normal_fragment_maps:Od,clearcoat_pars_fragment:Bd,iridescence_pars_fragment:zd,opaque_fragment:Hd,packing:kd,premultiplied_alpha_fragment:Vd,project_vertex:Gd,dithering_fragment:Wd,dithering_pars_fragment:Xd,roughnessmap_fragment:Yd,roughnessmap_pars_fragment:qd,shadowmap_pars_fragment:jd,shadowmap_pars_vertex:Zd,shadowmap_vertex:Kd,shadowmask_pars_fragment:$d,skinbase_vertex:Jd,skinning_pars_vertex:Qd,skinning_vertex:tf,skinnormal_vertex:ef,specularmap_fragment:nf,specularmap_pars_fragment:sf,tonemapping_fragment:rf,tonemapping_pars_fragment:af,transmission_fragment:of,transmission_pars_fragment:lf,uv_pars_fragment:cf,uv_pars_vertex:hf,uv_vertex:uf,worldpos_vertex:df,background_vert:ff,background_frag:pf,backgroundCube_vert:mf,backgroundCube_frag:gf,cube_vert:_f,cube_frag:vf,depth_vert:xf,depth_frag:Mf,distanceRGBA_vert:Sf,distanceRGBA_frag:yf,equirect_vert:Ef,equirect_frag:bf,linedashed_vert:Tf,linedashed_frag:wf,meshbasic_vert:Af,meshbasic_frag:Rf,meshlambert_vert:Cf,meshlambert_frag:Pf,meshmatcap_vert:Df,meshmatcap_frag:Lf,meshnormal_vert:Uf,meshnormal_frag:If,meshphong_vert:Nf,meshphong_frag:Ff,meshphysical_vert:Of,meshphysical_frag:Bf,meshtoon_vert:zf,meshtoon_frag:Hf,points_vert:kf,points_frag:Vf,shadow_vert:Gf,shadow_frag:Wf,sprite_vert:Xf,sprite_frag:Yf},st={common:{diffuse:{value:new pt(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 vt(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 pt(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 pt(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 pt(16777215)},opacity:{value:1},center:{value:new vt(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new Ht},alphaMap:{value:null},alphaMapTransform:{value:new Ht},alphaTest:{value:0}}},cn={basic:{uniforms:Ue([st.common,st.specularmap,st.envmap,st.aomap,st.lightmap,st.fog]),vertexShader:Vt.meshbasic_vert,fragmentShader:Vt.meshbasic_frag},lambert:{uniforms:Ue([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 pt(0)}}]),vertexShader:Vt.meshlambert_vert,fragmentShader:Vt.meshlambert_frag},phong:{uniforms:Ue([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 pt(0)},specular:{value:new pt(1118481)},shininess:{value:30}}]),vertexShader:Vt.meshphong_vert,fragmentShader:Vt.meshphong_frag},standard:{uniforms:Ue([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 pt(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Vt.meshphysical_vert,fragmentShader:Vt.meshphysical_frag},toon:{uniforms:Ue([st.common,st.aomap,st.lightmap,st.emissivemap,st.bumpmap,st.normalmap,st.displacementmap,st.gradientmap,st.fog,st.lights,{emissive:{value:new pt(0)}}]),vertexShader:Vt.meshtoon_vert,fragmentShader:Vt.meshtoon_frag},matcap:{uniforms:Ue([st.common,st.bumpmap,st.normalmap,st.displacementmap,st.fog,{matcap:{value:null}}]),vertexShader:Vt.meshmatcap_vert,fragmentShader:Vt.meshmatcap_frag},points:{uniforms:Ue([st.points,st.fog]),vertexShader:Vt.points_vert,fragmentShader:Vt.points_frag},dashed:{uniforms:Ue([st.common,st.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Vt.linedashed_vert,fragmentShader:Vt.linedashed_frag},depth:{uniforms:Ue([st.common,st.displacementmap]),vertexShader:Vt.depth_vert,fragmentShader:Vt.depth_frag},normal:{uniforms:Ue([st.common,st.bumpmap,st.normalmap,st.displacementmap,{opacity:{value:1}}]),vertexShader:Vt.meshnormal_vert,fragmentShader:Vt.meshnormal_frag},sprite:{uniforms:Ue([st.sprite,st.fog]),vertexShader:Vt.sprite_vert,fragmentShader:Vt.sprite_frag},background:{uniforms:{uvTransform:{value:new Ht},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Vt.background_vert,fragmentShader:Vt.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new Ht}},vertexShader:Vt.backgroundCube_vert,fragmentShader:Vt.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Vt.cube_vert,fragmentShader:Vt.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Vt.equirect_vert,fragmentShader:Vt.equirect_frag},distanceRGBA:{uniforms:Ue([st.common,st.displacementmap,{referencePosition:{value:new P},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Vt.distanceRGBA_vert,fragmentShader:Vt.distanceRGBA_frag},shadow:{uniforms:Ue([st.lights,st.fog,{color:{value:new pt(0)},opacity:{value:1}}]),vertexShader:Vt.shadow_vert,fragmentShader:Vt.shadow_frag}};cn.physical={uniforms:Ue([cn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new Ht},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new Ht},clearcoatNormalScale:{value:new vt(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 pt(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 vt},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new Ht},attenuationDistance:{value:0},attenuationColor:{value:new pt(0)},specularColor:{value:new pt(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new Ht},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new Ht},anisotropyVector:{value:new vt},anisotropyMap:{value:null},anisotropyMapTransform:{value:new Ht}}]),vertexShader:Vt.meshphysical_vert,fragmentShader:Vt.meshphysical_frag};const ks={r:0,b:0,g:0},qn=new pn,qf=new ne;function jf(i,t,e,n,s,r,a){const o=new pt(0);let l=r===!0?0:1,c,h,d=null,f=0,p=null;function g(b){let y=b.isScene===!0?b.background:null;return y&&y.isTexture&&(y=(b.backgroundBlurriness>0?e:t).get(y)),y}function v(b){let y=!1;const L=g(b);L===null?u(o,l):L&&L.isColor&&(u(L,1),y=!0);const R=i.xr.getEnvironmentBlendMode();R==="additive"?n.buffers.color.setClear(0,0,0,1,a):R==="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(b,y){const L=g(y);L&&(L.isCubeTexture||L.mapping===hr)?(h===void 0&&(h=new Me(new hs(1,1,1),new Ne({name:"BackgroundCubeMaterial",uniforms:Vi(cn.backgroundCube.uniforms),vertexShader:cn.backgroundCube.vertexShader,fragmentShader:cn.backgroundCube.fragmentShader,side:He,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(R,A,U){this.matrixWorld.copyPosition(U.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,L.isCubeTexture&&L.isRenderTargetTexture===!1&&(qn.y*=-1,qn.z*=-1),h.material.uniforms.envMap.value=L,h.material.uniforms.flipEnvMap.value=L.isCubeTexture&&L.isRenderTargetTexture===!1?-1:1,h.material.uniforms.backgroundBlurriness.value=y.backgroundBlurriness,h.material.uniforms.backgroundIntensity.value=y.backgroundIntensity,h.material.uniforms.backgroundRotation.value.setFromMatrix4(qf.makeRotationFromEuler(qn)),h.material.toneMapped=Jt.getTransfer(L.colorSpace)!==re,(d!==L||f!==L.version||p!==i.toneMapping)&&(h.material.needsUpdate=!0,d=L,f=L.version,p=i.toneMapping),h.layers.enableAll(),b.unshift(h,h.geometry,h.material,0,0,null)):L&&L.isTexture&&(c===void 0&&(c=new Me(new us(2,2),new Ne({name:"BackgroundMaterial",uniforms:Vi(cn.background.uniforms),vertexShader:cn.background.vertexShader,fragmentShader:cn.background.fragmentShader,side:zn,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=L,c.material.uniforms.backgroundIntensity.value=y.backgroundIntensity,c.material.toneMapped=Jt.getTransfer(L.colorSpace)!==re,L.matrixAutoUpdate===!0&&L.updateMatrix(),c.material.uniforms.uvTransform.value.copy(L.matrix),(d!==L||f!==L.version||p!==i.toneMapping)&&(c.material.needsUpdate=!0,d=L,f=L.version,p=i.toneMapping),c.layers.enableAll(),b.unshift(c,c.geometry,c.material,0,0,null))}function u(b,y){b.getRGB(ks,sc(i)),n.buffers.color.setClear(ks.r,ks.g,ks.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(b,y=1){o.set(b),l=y,u(o,l)},getClearAlpha:function(){return l},setClearAlpha:function(b){l=b,u(o,l)},render:v,addToRenderList:m,dispose:T}}function Zf(i,t){const e=i.getParameter(i.MAX_VERTEX_ATTRIBS),n={},s=f(null);let r=s,a=!1;function o(M,D,W,z,V){let $=!1;const G=d(z,W,D);r!==G&&(r=G,c(r.object)),$=p(M,z,W,V),$&&g(M,z,W,V),V!==null&&t.update(V,i.ELEMENT_ARRAY_BUFFER),($||a)&&(a=!1,y(M,D,W,z),V!==null&&i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,t.get(V).buffer))}function l(){return i.createVertexArray()}function c(M){return i.bindVertexArray(M)}function h(M){return i.deleteVertexArray(M)}function d(M,D,W){const z=W.wireframe===!0;let V=n[M.id];V===void 0&&(V={},n[M.id]=V);let $=V[D.id];$===void 0&&($={},V[D.id]=$);let G=$[z];return G===void 0&&(G=f(l()),$[z]=G),G}function f(M){const D=[],W=[],z=[];for(let V=0;V=0){const ut=V[k];let yt=$[k];if(yt===void 0&&(k==="instanceMatrix"&&M.instanceMatrix&&(yt=M.instanceMatrix),k==="instanceColor"&&M.instanceColor&&(yt=M.instanceColor)),ut===void 0||ut.attribute!==yt||yt&&ut.data!==yt.data)return!0;G++}return r.attributesNum!==G||r.index!==z}function g(M,D,W,z){const V={},$=D.attributes;let G=0;const J=W.getAttributes();for(const k in J)if(J[k].location>=0){let ut=$[k];ut===void 0&&(k==="instanceMatrix"&&M.instanceMatrix&&(ut=M.instanceMatrix),k==="instanceColor"&&M.instanceColor&&(ut=M.instanceColor));const yt={};yt.attribute=ut,ut&&ut.data&&(yt.data=ut.data),V[k]=yt,G++}r.attributes=V,r.attributesNum=G,r.index=z}function v(){const M=r.newAttributes;for(let D=0,W=M.length;D=0){let it=V[J];if(it===void 0&&(J==="instanceMatrix"&&M.instanceMatrix&&(it=M.instanceMatrix),J==="instanceColor"&&M.instanceColor&&(it=M.instanceColor)),it!==void 0){const ut=it.normalized,yt=it.itemSize,Lt=t.get(it);if(Lt===void 0)continue;const jt=Lt.buffer,Y=Lt.type,et=Lt.bytesPerElement,xt=Y===i.INT||Y===i.UNSIGNED_INT||it.gpuType===Ya;if(it.isInterleavedBufferAttribute){const at=it.data,wt=at.stride,Ut=it.offset;if(at.isInstancedInterleavedBuffer){for(let Gt=0;Gt0&&i.getShaderPrecisionFormat(i.FRAGMENT_SHADER,i.HIGH_FLOAT).precision>0)return"highp";A="mediump"}return A==="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,f=e.reverseDepthBuffer===!0&&t.has("EXT_clip_control"),p=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),u=i.getParameter(i.MAX_VERTEX_ATTRIBS),T=i.getParameter(i.MAX_VERTEX_UNIFORM_VECTORS),b=i.getParameter(i.MAX_VARYING_VECTORS),y=i.getParameter(i.MAX_FRAGMENT_UNIFORM_VECTORS),L=g>0,R=i.getParameter(i.MAX_SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:r,getMaxPrecision:l,textureFormatReadable:a,textureTypeReadable:o,precision:c,logarithmicDepthBuffer:d,reverseDepthBuffer:f,maxTextures:p,maxVertexTextures:g,maxTextureSize:v,maxCubemapSize:m,maxAttributes:u,maxVertexUniforms:T,maxVaryings:b,maxFragmentUniforms:y,vertexTextures:L,maxSamples:R}}function Jf(i){const t=this;let e=null,n=0,s=!1,r=!1;const a=new Nn,o=new Ht,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(d,f){const p=d.length!==0||f||n!==0||s;return s=f,n=d.length,p},this.beginShadows=function(){r=!0,h(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(d,f){e=h(d,f,0)},this.setState=function(d,f,p){const g=d.clippingPlanes,v=d.clipIntersection,m=d.clipShadows,u=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 y=u.clippingState||null;l.value=y,y=h(g,f,b,p);for(let L=0;L!==b;++L)y[L]=e[L];u.clippingState=y,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,f,p,g){const v=d!==null?d.length:0;let m=null;if(v!==0){if(m=l.value,g!==!0||m===null){const u=p+v*4,T=f.matrixWorldInverse;o.getNormalMatrix(T),(m===null||m.length0){const c=new Yh(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 Pi=4,Jo=[.125,.215,.35,.446,.526,.582],$n=20,Gr=new hc,Qo=new pt;let Wr=null,Xr=0,Yr=0,qr=!1;const Zn=(1+Math.sqrt(5))/2,Ti=1/Zn,tl=[new P(-Zn,Ti,0),new P(Zn,Ti,0),new P(-Ti,0,Zn),new P(Ti,0,Zn),new P(0,Zn,-Ti),new P(0,Zn,Ti),new P(-1,1,-1),new P(1,1,-1),new P(-1,1,1),new P(1,1,1)];class el{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){Wr=this._renderer.getRenderTarget(),Xr=this._renderer.getActiveCubeFace(),Yr=this._renderer.getActiveMipmapLevel(),qr=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=sl(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=il(),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=f,h.autoClear=d,t.background=m}_textureToCubeUV(t,e){const n=this._renderer,s=t.mapping===Oi||t.mapping===Bi;s?(this._cubemapMaterial===null&&(this._cubemapMaterial=sl()),this._cubemapMaterial.uniforms.flipEnvMap.value=t.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=il());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;Vs(e,0,0,3*l,2*l),n.setRenderTarget(e),n.render(a,Gr)}_applyPMREM(t){const e=this._renderer,n=e.autoClear;e.autoClear=!1;const s=this._lodPlanes.length;for(let r=1;r$n&&console.warn(`sigmaRadians, ${r}, is too large and will clip, as it requested ${m} samples when the maximum is set to ${$n}`);const u=[];let T=0;for(let A=0;A<$n;++A){const U=A/v,S=Math.exp(-U*U/2);u.push(S),A===0?T+=S:Ab-Pi?s-b+Pi:0),R=4*(this._cubeSize-y);Vs(e,L,R,3*y,2*y),l.setRenderTarget(e),l.render(d,Gr)}}function tp(i){const t=[],e=[],n=[];let s=i;const r=i-Pi+1+Jo.length;for(let a=0;ai-Pi?l=Jo[a-i+Pi-1]:a===0&&(l=0),n.push(l);const c=1/(o-2),h=-c,d=1+c,f=[h,h,d,h,d,d,h,h,d,d,h,d],p=6,g=6,v=3,m=2,u=1,T=new Float32Array(v*g*p),b=new Float32Array(m*g*p),y=new Float32Array(u*g*p);for(let R=0;R2?0:-1,S=[A,U,0,A+2/3,U,0,A+2/3,U+1,0,A,U,0,A+2/3,U+1,0,A,U+1,0];T.set(S,v*g*R),b.set(f,m*g*R);const M=[R,R,R,R,R,R];y.set(M,u*g*R)}const L=new pe;L.setAttribute("position",new de(T,v)),L.setAttribute("uv",new de(b,m)),L.setAttribute("faceIndex",new de(y,u)),t.push(L),s>Pi&&s--}return{lodPlanes:t,sizeLods:e,sigmas:n}}function nl(i,t,e){const n=new on(i,t,e);return n.texture.mapping=hr,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function Vs(i,t,e,n,s){i.viewport.set(t,e,n,s),i.scissor.set(t,e,n,s)}function ep(i,t,e){const n=new Float32Array($n),s=new P(0,1,0);return new Ne({name:"SphericalGaussianBlur",defines:{n:$n,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:no(),fragmentShader:` +}`,Vt={alphahash_fragment:Tu,alphahash_pars_fragment:wu,alphamap_fragment:Au,alphamap_pars_fragment:Ru,alphatest_fragment:Cu,alphatest_pars_fragment:Pu,aomap_fragment:Du,aomap_pars_fragment:Lu,batching_pars_vertex:Uu,batching_vertex:Iu,begin_vertex:Nu,beginnormal_vertex:Fu,bsdfs:Ou,iridescence_fragment:Bu,bumpmap_pars_fragment:zu,clipping_planes_fragment:ku,clipping_planes_pars_fragment:Hu,clipping_planes_pars_vertex:Vu,clipping_planes_vertex:Gu,color_fragment:Wu,color_pars_fragment:Xu,color_pars_vertex:Yu,color_vertex:qu,common:ju,cube_uv_reflection_fragment:Zu,defaultnormal_vertex:Ku,displacementmap_pars_vertex:$u,displacementmap_vertex:Ju,emissivemap_fragment:Qu,emissivemap_pars_fragment:td,colorspace_fragment:ed,colorspace_pars_fragment:nd,envmap_fragment:id,envmap_common_pars_fragment:sd,envmap_pars_fragment:rd,envmap_pars_vertex:ad,envmap_physical_pars_fragment:_d,envmap_vertex:od,fog_vertex:ld,fog_pars_vertex:cd,fog_fragment:hd,fog_pars_fragment:ud,gradientmap_pars_fragment:dd,lightmap_pars_fragment:fd,lights_lambert_fragment:pd,lights_lambert_pars_fragment:md,lights_pars_begin:gd,lights_toon_fragment:vd,lights_toon_pars_fragment:xd,lights_phong_fragment:Md,lights_phong_pars_fragment:Sd,lights_physical_fragment:yd,lights_physical_pars_fragment:Ed,lights_fragment_begin:bd,lights_fragment_maps:Td,lights_fragment_end:wd,logdepthbuf_fragment:Ad,logdepthbuf_pars_fragment:Rd,logdepthbuf_pars_vertex:Cd,logdepthbuf_vertex:Pd,map_fragment:Dd,map_pars_fragment:Ld,map_particle_fragment:Ud,map_particle_pars_fragment:Id,metalnessmap_fragment:Nd,metalnessmap_pars_fragment:Fd,morphinstance_vertex:Od,morphcolor_vertex:Bd,morphnormal_vertex:zd,morphtarget_pars_vertex:kd,morphtarget_vertex:Hd,normal_fragment_begin:Vd,normal_fragment_maps:Gd,normal_pars_fragment:Wd,normal_pars_vertex:Xd,normal_vertex:Yd,normalmap_pars_fragment:qd,clearcoat_normal_fragment_begin:jd,clearcoat_normal_fragment_maps:Zd,clearcoat_pars_fragment:Kd,iridescence_pars_fragment:$d,opaque_fragment:Jd,packing:Qd,premultiplied_alpha_fragment:tf,project_vertex:ef,dithering_fragment:nf,dithering_pars_fragment:sf,roughnessmap_fragment:rf,roughnessmap_pars_fragment:af,shadowmap_pars_fragment:of,shadowmap_pars_vertex:lf,shadowmap_vertex:cf,shadowmask_pars_fragment:hf,skinbase_vertex:uf,skinning_pars_vertex:df,skinning_vertex:ff,skinnormal_vertex:pf,specularmap_fragment:mf,specularmap_pars_fragment:gf,tonemapping_fragment:_f,tonemapping_pars_fragment:vf,transmission_fragment:xf,transmission_pars_fragment:Mf,uv_pars_fragment:Sf,uv_pars_vertex:yf,uv_vertex:Ef,worldpos_vertex:bf,background_vert:Tf,background_frag:wf,backgroundCube_vert:Af,backgroundCube_frag:Rf,cube_vert:Cf,cube_frag:Pf,depth_vert:Df,depth_frag:Lf,distanceRGBA_vert:Uf,distanceRGBA_frag:If,equirect_vert:Nf,equirect_frag:Ff,linedashed_vert:Of,linedashed_frag:Bf,meshbasic_vert:zf,meshbasic_frag:kf,meshlambert_vert:Hf,meshlambert_frag:Vf,meshmatcap_vert:Gf,meshmatcap_frag:Wf,meshnormal_vert:Xf,meshnormal_frag:Yf,meshphong_vert:qf,meshphong_frag:jf,meshphysical_vert:Zf,meshphysical_frag:Kf,meshtoon_vert:$f,meshtoon_frag:Jf,points_vert:Qf,points_frag:tp,shadow_vert:ep,shadow_frag:np,sprite_vert:ip,sprite_frag:sp},rt={common:{diffuse:{value:new st(16777215)},opacity:{value:1},map:{value:null},mapTransform:{value:new zt},alphaMap:{value:null},alphaMapTransform:{value:new zt},alphaTest:{value:0}},specularmap:{specularMap:{value:null},specularMapTransform:{value:new zt}},envmap:{envMap:{value:null},envMapRotation:{value:new zt},flipEnvMap:{value:-1},reflectivity:{value:1},ior:{value:1.5},refractionRatio:{value:.98}},aomap:{aoMap:{value:null},aoMapIntensity:{value:1},aoMapTransform:{value:new zt}},lightmap:{lightMap:{value:null},lightMapIntensity:{value:1},lightMapTransform:{value:new zt}},bumpmap:{bumpMap:{value:null},bumpMapTransform:{value:new zt},bumpScale:{value:1}},normalmap:{normalMap:{value:null},normalMapTransform:{value:new zt},normalScale:{value:new xt(1,1)}},displacementmap:{displacementMap:{value:null},displacementMapTransform:{value:new zt},displacementScale:{value:1},displacementBias:{value:0}},emissivemap:{emissiveMap:{value:null},emissiveMapTransform:{value:new zt}},metalnessmap:{metalnessMap:{value:null},metalnessMapTransform:{value:new zt}},roughnessmap:{roughnessMap:{value:null},roughnessMapTransform:{value:new zt}},gradientmap:{gradientMap:{value:null}},fog:{fogDensity:{value:25e-5},fogNear:{value:1},fogFar:{value:2e3},fogColor:{value:new st(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 st(16777215)},opacity:{value:1},size:{value:1},scale:{value:1},map:{value:null},alphaMap:{value:null},alphaMapTransform:{value:new zt},alphaTest:{value:0},uvTransform:{value:new zt}},sprite:{diffuse:{value:new st(16777215)},opacity:{value:1},center:{value:new xt(.5,.5)},rotation:{value:0},map:{value:null},mapTransform:{value:new zt},alphaMap:{value:null},alphaMapTransform:{value:new zt},alphaTest:{value:0}}},pn={basic:{uniforms:ke([rt.common,rt.specularmap,rt.envmap,rt.aomap,rt.lightmap,rt.fog]),vertexShader:Vt.meshbasic_vert,fragmentShader:Vt.meshbasic_frag},lambert:{uniforms:ke([rt.common,rt.specularmap,rt.envmap,rt.aomap,rt.lightmap,rt.emissivemap,rt.bumpmap,rt.normalmap,rt.displacementmap,rt.fog,rt.lights,{emissive:{value:new st(0)}}]),vertexShader:Vt.meshlambert_vert,fragmentShader:Vt.meshlambert_frag},phong:{uniforms:ke([rt.common,rt.specularmap,rt.envmap,rt.aomap,rt.lightmap,rt.emissivemap,rt.bumpmap,rt.normalmap,rt.displacementmap,rt.fog,rt.lights,{emissive:{value:new st(0)},specular:{value:new st(1118481)},shininess:{value:30}}]),vertexShader:Vt.meshphong_vert,fragmentShader:Vt.meshphong_frag},standard:{uniforms:ke([rt.common,rt.envmap,rt.aomap,rt.lightmap,rt.emissivemap,rt.bumpmap,rt.normalmap,rt.displacementmap,rt.roughnessmap,rt.metalnessmap,rt.fog,rt.lights,{emissive:{value:new st(0)},roughness:{value:1},metalness:{value:0},envMapIntensity:{value:1}}]),vertexShader:Vt.meshphysical_vert,fragmentShader:Vt.meshphysical_frag},toon:{uniforms:ke([rt.common,rt.aomap,rt.lightmap,rt.emissivemap,rt.bumpmap,rt.normalmap,rt.displacementmap,rt.gradientmap,rt.fog,rt.lights,{emissive:{value:new st(0)}}]),vertexShader:Vt.meshtoon_vert,fragmentShader:Vt.meshtoon_frag},matcap:{uniforms:ke([rt.common,rt.bumpmap,rt.normalmap,rt.displacementmap,rt.fog,{matcap:{value:null}}]),vertexShader:Vt.meshmatcap_vert,fragmentShader:Vt.meshmatcap_frag},points:{uniforms:ke([rt.points,rt.fog]),vertexShader:Vt.points_vert,fragmentShader:Vt.points_frag},dashed:{uniforms:ke([rt.common,rt.fog,{scale:{value:1},dashSize:{value:1},totalSize:{value:2}}]),vertexShader:Vt.linedashed_vert,fragmentShader:Vt.linedashed_frag},depth:{uniforms:ke([rt.common,rt.displacementmap]),vertexShader:Vt.depth_vert,fragmentShader:Vt.depth_frag},normal:{uniforms:ke([rt.common,rt.bumpmap,rt.normalmap,rt.displacementmap,{opacity:{value:1}}]),vertexShader:Vt.meshnormal_vert,fragmentShader:Vt.meshnormal_frag},sprite:{uniforms:ke([rt.sprite,rt.fog]),vertexShader:Vt.sprite_vert,fragmentShader:Vt.sprite_frag},background:{uniforms:{uvTransform:{value:new zt},t2D:{value:null},backgroundIntensity:{value:1}},vertexShader:Vt.background_vert,fragmentShader:Vt.background_frag},backgroundCube:{uniforms:{envMap:{value:null},flipEnvMap:{value:-1},backgroundBlurriness:{value:0},backgroundIntensity:{value:1},backgroundRotation:{value:new zt}},vertexShader:Vt.backgroundCube_vert,fragmentShader:Vt.backgroundCube_frag},cube:{uniforms:{tCube:{value:null},tFlip:{value:-1},opacity:{value:1}},vertexShader:Vt.cube_vert,fragmentShader:Vt.cube_frag},equirect:{uniforms:{tEquirect:{value:null}},vertexShader:Vt.equirect_vert,fragmentShader:Vt.equirect_frag},distanceRGBA:{uniforms:ke([rt.common,rt.displacementmap,{referencePosition:{value:new P},nearDistance:{value:1},farDistance:{value:1e3}}]),vertexShader:Vt.distanceRGBA_vert,fragmentShader:Vt.distanceRGBA_frag},shadow:{uniforms:ke([rt.lights,rt.fog,{color:{value:new st(0)},opacity:{value:1}}]),vertexShader:Vt.shadow_vert,fragmentShader:Vt.shadow_frag}};pn.physical={uniforms:ke([pn.standard.uniforms,{clearcoat:{value:0},clearcoatMap:{value:null},clearcoatMapTransform:{value:new zt},clearcoatNormalMap:{value:null},clearcoatNormalMapTransform:{value:new zt},clearcoatNormalScale:{value:new xt(1,1)},clearcoatRoughness:{value:0},clearcoatRoughnessMap:{value:null},clearcoatRoughnessMapTransform:{value:new zt},dispersion:{value:0},iridescence:{value:0},iridescenceMap:{value:null},iridescenceMapTransform:{value:new zt},iridescenceIOR:{value:1.3},iridescenceThicknessMinimum:{value:100},iridescenceThicknessMaximum:{value:400},iridescenceThicknessMap:{value:null},iridescenceThicknessMapTransform:{value:new zt},sheen:{value:0},sheenColor:{value:new st(0)},sheenColorMap:{value:null},sheenColorMapTransform:{value:new zt},sheenRoughness:{value:1},sheenRoughnessMap:{value:null},sheenRoughnessMapTransform:{value:new zt},transmission:{value:0},transmissionMap:{value:null},transmissionMapTransform:{value:new zt},transmissionSamplerSize:{value:new xt},transmissionSamplerMap:{value:null},thickness:{value:0},thicknessMap:{value:null},thicknessMapTransform:{value:new zt},attenuationDistance:{value:0},attenuationColor:{value:new st(0)},specularColor:{value:new st(1,1,1)},specularColorMap:{value:null},specularColorMapTransform:{value:new zt},specularIntensity:{value:1},specularIntensityMap:{value:null},specularIntensityMapTransform:{value:new zt},anisotropyVector:{value:new xt},anisotropyMap:{value:null},anisotropyMapTransform:{value:new zt}}]),vertexShader:Vt.meshphysical_vert,fragmentShader:Vt.meshphysical_frag};const nr={r:0,b:0,g:0},Jn=new xn,rp=new ne;function ap(i,t,e,n,s,r,a){const o=new st(0);let l=r===!0?0:1,c,h,d=null,f=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?p(o,l):D&&D.isColor&&(p(D,1),y=!0);const A=i.xr.getEnvironmentBlendMode();A==="additive"?n.buffers.color.setClear(0,0,0,1,a):A==="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===br)?(h===void 0&&(h=new Ee(new Es(1,1,1),new Ve({name:"BackgroundCubeMaterial",uniforms:Qi(pn.backgroundCube.uniforms),vertexShader:pn.backgroundCube.vertexShader,fragmentShader:pn.backgroundCube.fragmentShader,side:Ye,depthTest:!1,depthWrite:!1,fog:!1})),h.geometry.deleteAttribute("normal"),h.geometry.deleteAttribute("uv"),h.onBeforeRender=function(A,C,I){this.matrixWorld.copyPosition(I.matrixWorld)},Object.defineProperty(h.material,"envMap",{get:function(){return this.uniforms.envMap.value}}),s.update(h)),Jn.copy(y.backgroundRotation),Jn.x*=-1,Jn.y*=-1,Jn.z*=-1,D.isCubeTexture&&D.isRenderTargetTexture===!1&&(Jn.y*=-1,Jn.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(rp.makeRotationFromEuler(Jn)),h.material.toneMapped=Jt.getTransfer(D.colorSpace)!==ie,(d!==D||f!==D.version||u!==i.toneMapping)&&(h.material.needsUpdate=!0,d=D,f=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 Ee(new bs(2,2),new Ve({name:"BackgroundMaterial",uniforms:Qi(pn.background.uniforms),vertexShader:pn.background.vertexShader,fragmentShader:pn.background.fragmentShader,side:Gn,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=Jt.getTransfer(D.colorSpace)!==ie,D.matrixAutoUpdate===!0&&D.updateMatrix(),c.material.uniforms.uvTransform.value.copy(D.matrix),(d!==D||f!==D.version||u!==i.toneMapping)&&(c.material.needsUpdate=!0,d=D,f=D.version,u=i.toneMapping),c.layers.enableAll(),E.unshift(c,c.geometry,c.material,0,0,null))}function p(E,y){E.getRGB(nr,pc(i)),n.buffers.color.setClear(nr.r,nr.g,nr.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,p(o,l)},getClearAlpha:function(){return l},setClearAlpha:function(E){l=E,p(o,l)},render:_,addToRenderList:m,dispose:T}}function op(i,t){const e=i.getParameter(i.MAX_VERTEX_ATTRIBS),n={},s=f(null);let r=s,a=!1;function o(M,w,Y,V,j){let $=!1;const q=d(V,Y,w);r!==q&&(r=q,c(r.object)),$=u(M,V,Y,j),$&&g(M,V,Y,j),j!==null&&t.update(j,i.ELEMENT_ARRAY_BUFFER),($||a)&&(a=!1,y(M,w,Y,V),j!==null&&i.bindBuffer(i.ELEMENT_ARRAY_BUFFER,t.get(j).buffer))}function l(){return i.createVertexArray()}function c(M){return i.bindVertexArray(M)}function h(M){return i.deleteVertexArray(M)}function d(M,w,Y){const V=Y.wireframe===!0;let j=n[M.id];j===void 0&&(j={},n[M.id]=j);let $=j[w.id];$===void 0&&($={},j[w.id]=$);let q=$[V];return q===void 0&&(q=f(l()),$[V]=q),q}function f(M){const w=[],Y=[],V=[];for(let j=0;j=0){const ft=j[X];let Mt=$[X];if(Mt===void 0&&(X==="instanceMatrix"&&M.instanceMatrix&&(Mt=M.instanceMatrix),X==="instanceColor"&&M.instanceColor&&(Mt=M.instanceColor)),ft===void 0||ft.attribute!==Mt||Mt&&ft.data!==Mt.data)return!0;q++}return r.attributesNum!==q||r.index!==V}function g(M,w,Y,V){const j={},$=w.attributes;let q=0;const J=Y.getAttributes();for(const X in J)if(J[X].location>=0){let ft=$[X];ft===void 0&&(X==="instanceMatrix"&&M.instanceMatrix&&(ft=M.instanceMatrix),X==="instanceColor"&&M.instanceColor&&(ft=M.instanceColor));const Mt={};Mt.attribute=ft,ft&&ft.data&&(Mt.data=ft.data),j[X]=Mt,q++}r.attributes=j,r.attributesNum=q,r.index=V}function _(){const M=r.newAttributes;for(let w=0,Y=M.length;w=0){let it=j[J];if(it===void 0&&(J==="instanceMatrix"&&M.instanceMatrix&&(it=M.instanceMatrix),J==="instanceColor"&&M.instanceColor&&(it=M.instanceColor)),it!==void 0){const ft=it.normalized,Mt=it.itemSize,Nt=t.get(it);if(Nt===void 0)continue;const Wt=Nt.buffer,Z=Nt.type,nt=Nt.bytesPerElement,_t=Z===i.INT||Z===i.UNSIGNED_INT||it.gpuType===uo;if(it.isInterleavedBufferAttribute){const at=it.data,wt=at.stride,Pt=it.offset;if(at.isInstancedInterleavedBuffer){for(let kt=0;kt0&&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,f=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),p=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,A=i.getParameter(i.MAX_SAMPLES);return{isWebGL2:!0,getMaxAnisotropy:r,getMaxPrecision:l,textureFormatReadable:a,textureTypeReadable:o,precision:c,logarithmicDepthBuffer:d,reverseDepthBuffer:f,maxTextures:u,maxVertexTextures:g,maxTextureSize:_,maxCubemapSize:m,maxAttributes:p,maxVertexUniforms:T,maxVaryings:E,maxFragmentUniforms:y,vertexTextures:D,maxSamples:A}}function hp(i){const t=this;let e=null,n=0,s=!1,r=!1;const a=new zn,o=new zt,l={value:null,needsUpdate:!1};this.uniform=l,this.numPlanes=0,this.numIntersection=0,this.init=function(d,f){const u=d.length!==0||f||n!==0||s;return s=f,n=d.length,u},this.beginShadows=function(){r=!0,h(null)},this.endShadows=function(){r=!1},this.setGlobalState=function(d,f){e=h(d,f,0)},this.setState=function(d,f,u){const g=d.clippingPlanes,_=d.clipIntersection,m=d.clipShadows,p=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=p.clippingState||null;l.value=y,y=h(g,f,E,u);for(let D=0;D!==E;++D)y[D]=e[D];p.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,f,u,g){const _=d!==null?d.length:0;let m=null;if(_!==0){if(m=l.value,g!==!0||m===null){const p=u+_*4,T=f.matrixWorldInverse;o.getNormalMatrix(T),(m===null||m.length0){const c=new ru(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,ul=[.125,.215,.35,.446,.526,.582],si=20,na=new Sc,dl=new st;let ia=null,sa=0,ra=0,aa=!1;const ei=(1+Math.sqrt(5))/2,Ni=1/ei,fl=[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 pl{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){ia=this._renderer.getRenderTarget(),sa=this._renderer.getActiveCubeFace(),ra=this._renderer.getActiveMipmapLevel(),aa=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=_l(),this._compileMaterial(this._cubemapMaterial))}compileEquirectangularShader(){this._equirectMaterial===null&&(this._equirectMaterial=gl(),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=f,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=_l()),this._cubemapMaterial.uniforms.flipEnvMap.value=t.isRenderTargetTexture===!1?-1:1):this._equirectMaterial===null&&(this._equirectMaterial=gl());const r=s?this._cubemapMaterial:this._equirectMaterial,a=new Ee(this._lodPlanes[0],r),o=r.uniforms;o.envMap.value=t;const l=this._cubeSize;ir(e,0,0,3*l,2*l),n.setRenderTarget(e),n.render(a,na)}_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 p=[];let T=0;for(let C=0;CE-ki?s-E+ki:0),A=4*(this._cubeSize-y);ir(e,D,A,3*y,2*y),l.setRenderTarget(e),l.render(d,na)}}function dp(i){const t=[],e=[],n=[];let s=i;const r=i-ki+1+ul.length;for(let a=0;ai-ki?l=ul[a-i+ki-1]:a===0&&(l=0),n.push(l);const c=1/(o-2),h=-c,d=1+c,f=[h,h,d,h,d,d,h,h,d,d,h,d],u=6,g=6,_=3,m=2,p=1,T=new Float32Array(_*g*u),E=new Float32Array(m*g*u),y=new Float32Array(p*g*u);for(let A=0;A2?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*A),E.set(f,m*g*A);const M=[A,A,A,A,A,A];y.set(M,p*g*A)}const D=new ge;D.setAttribute("position",new ce(T,_)),D.setAttribute("uv",new ce(E,m)),D.setAttribute("faceIndex",new ce(y,p)),t.push(D),s>ki&&s--}return{lodPlanes:t,sizeLods:e,sigmas:n}}function ml(i,t,e){const n=new fn(i,t,e);return n.texture.mapping=br,n.texture.name="PMREM.cubeUv",n.scissorTest=!0,n}function ir(i,t,e,n,s){i.viewport.set(t,e,n,s),i.scissor.set(t,e,n,s)}function fp(i,t,e){const n=new Float32Array(si),s=new P(0,1,0);return new Ve({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:yo(),fragmentShader:` precision mediump float; precision mediump int; @@ -3631,7 +3631,7 @@ void main() { } } - `,blending:bn,depthTest:!1,depthWrite:!1})}function il(){return new Ne({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:no(),fragmentShader:` + `,blending:Rn,depthTest:!1,depthWrite:!1})}function gl(){return new Ve({name:"EquirectangularToCubeUV",uniforms:{envMap:{value:null}},vertexShader:yo(),fragmentShader:` precision mediump float; precision mediump int; @@ -3650,7 +3650,7 @@ void main() { gl_FragColor = vec4( texture2D ( envMap, uv ).rgb, 1.0 ); } - `,blending:bn,depthTest:!1,depthWrite:!1})}function sl(){return new Ne({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:no(),fragmentShader:` + `,blending:Rn,depthTest:!1,depthWrite:!1})}function _l(){return new Ve({name:"CubemapToCubeUV",uniforms:{envMap:{value:null},flipEnvMap:{value:-1}},vertexShader:yo(),fragmentShader:` precision mediump float; precision mediump int; @@ -3666,7 +3666,7 @@ void main() { gl_FragColor = textureCube( envMap, vec3( flipEnvMap * vOutputDirection.x, vOutputDirection.yz ) ); } - `,blending:bn,depthTest:!1,depthWrite:!1})}function no(){return` + `,blending:Rn,depthTest:!1,depthWrite:!1})}function yo(){return` precision mediump float; precision mediump int; @@ -3721,17 +3721,17 @@ void main() { gl_Position = vec4( position, 1.0 ); } - `}function np(i){let t=new WeakMap,e=null;function n(o){if(o&&o.isTexture){const l=o.mapping,c=l===la||l===ca,h=l===Oi||l===Bi;if(c||h){let d=t.get(o);const f=d!==void 0?d.texture.pmremVersion:0;if(o.isRenderTargetTexture&&o.pmremVersion!==f)return e===null&&(e=new el(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 p=o.image;return c&&p&&p.height>0||h&&p&&s(p)?(e===null&&(e=new el(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&&(L=Math.ceil(y/t.maxTextureSize),y=t.maxTextureSize);const R=new Float32Array(y*L*4*d),A=new tc(R,y,L,d);A.type=fn,A.needsUpdate=!0;const U=b*4;for(let M=0;M0)return i;const s=t*e;let r=al[s];if(r===void 0&&(r=new Float32Array(s),al[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;e0||h&&u&&s(u)?(e===null&&(e=new pl(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 A=new Float32Array(y*D*4*d),C=new hc(A,y,D,d);C.type=vn,C.needsUpdate=!0;const I=E*4;for(let M=0;M0)return i;const s=t*e;let r=xl[s];if(r===void 0&&(r=new Float32Array(s),xl[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 be(i,t){if(i.length!==t.length)return!1;for(let e=0,n=i.length;e":" "} ${o}: ${e[a]}`)}return n.join(` -`)}const fl=new Ht;function nm(i){Jt._getMatrix(fl,Jt.workingColorSpace,i);const t=`mat3( ${fl.elements.map(e=>e.toFixed(4))} )`;switch(Jt.getTransfer(i)){case tr:return[t,"LinearTransferOETF"];case re:return[t,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",i),[t,"LinearTransferOETF"]}}function pl(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()+` +`)}const wl=new zt;function pm(i){Jt._getMatrix(wl,Jt.workingColorSpace,i);const t=`mat3( ${wl.elements.map(e=>e.toFixed(4))} )`;switch(Jt.getTransfer(i)){case gr:return[t,"LinearTransferOETF"];case ie:return[t,"sRGBTransferOETF"];default:return console.warn("THREE.WebGLProgram: Unsupported color space: ",i),[t,"LinearTransferOETF"]}}function Al(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+` -`+em(i.getShaderSource(t),a)}else return s}function im(i,t){const e=nm(t);return[`vec4 ${i}( vec4 value ) {`,` return ${e[1]}( vec4( value.rgb * ${e[0]}, value.a ) );`,"}"].join(` -`)}function sm(i,t){let e;switch(t){case ih:e="Linear";break;case sh:e="Reinhard";break;case rh:e="Cineon";break;case zl:e="ACESFilmic";break;case oh:e="AgX";break;case lh:e="Neutral";break;case ah:e="Custom";break;default:console.warn("THREE.WebGLProgram: Unsupported toneMapping:",t),e="Linear"}return"vec3 "+i+"( vec3 color ) { return "+e+"ToneMapping( color ); }"}const Gs=new P;function rm(){Jt.getLuminanceCoefficients(Gs);const i=Gs.x.toFixed(4),t=Gs.y.toFixed(4),e=Gs.z.toFixed(4);return["float luminance( const in vec3 rgb ) {",` const vec3 weights = vec3( ${i}, ${t}, ${e} );`," return dot( weights, rgb );","}"].join(` -`)}function am(i){return[i.extensionClipCullDistance?"#extension GL_ANGLE_clip_cull_distance : require":"",i.extensionMultiDraw?"#extension GL_ANGLE_multi_draw : require":""].filter(ss).join(` -`)}function om(i){const t=[];for(const e in i){const n=i[e];n!==!1&&t.push("#define "+e+" "+n)}return t.join(` -`)}function lm(i,t){const e={},n=i.getProgramParameter(t,i.ACTIVE_ATTRIBUTES);for(let s=0;s/gm;function Wa(i){return i.replace(cm,um)}const hm=new Map;function um(i,t){let e=Vt[t];if(e===void 0){const n=hm.get(t);if(n!==void 0)e=Vt[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 Wa(e)}const dm=/#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 _l(i){return i.replace(dm,fm)}function fm(i,t,e,n){let s="";for(let r=parseInt(t);r/gm;function so(i){return i.replace(Sm,Em)}const ym=new Map;function Em(i,t){let e=Vt[t];if(e===void 0){const n=ym.get(t);if(n!==void 0)e=Vt[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 so(e)}const bm=/#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 Pl(i){return i.replace(bm,Tm)}function Tm(i,t,e,n){let s="";for(let r=parseInt(t);r0&&(m+=` -`),u=["#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g].filter(ss).join(` -`),u.length>0&&(u+=` -`)):(m=[vl(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(ss).join(` -`),u=[vl(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:"",f?"#define CUBEUV_TEXEL_WIDTH "+f.texelWidth:"",f?"#define CUBEUV_TEXEL_HEIGHT "+f.texelHeight:"",f?"#define CUBEUV_MAX_MIP "+f.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!==On?"#define TONE_MAPPING":"",e.toneMapping!==On?Vt.tonemapping_pars_fragment:"",e.toneMapping!==On?sm("toneMapping",e.toneMapping):"",e.dithering?"#define DITHERING":"",e.opaque?"#define OPAQUE":"",Vt.colorspace_pars_fragment,im("linearToOutputTexel",e.outputColorSpace),rm(),e.useDepthPacking?"#define DEPTH_PACKING "+e.depthPacking:"",` -`].filter(ss).join(` -`)),a=Wa(a),a=ml(a,e),a=gl(a,e),o=Wa(o),o=ml(o,e),o=gl(o,e),a=_l(a),o=_l(o),e.isRawShaderMaterial!==!0&&(T=`#version 300 es -`,m=[p,"#define attribute in","#define varying out","#define texture2D texture"].join(` +`),p=["#define SHADER_TYPE "+e.shaderType,"#define SHADER_NAME "+e.shaderName,g].filter(ms).join(` +`),p.length>0&&(p+=` +`)):(m=[Dl(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(ms).join(` +`),p=[Dl(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:"",f?"#define CUBEUV_TEXEL_WIDTH "+f.texelWidth:"",f?"#define CUBEUV_TEXEL_HEIGHT "+f.texelHeight:"",f?"#define CUBEUV_MAX_MIP "+f.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!==Hn?"#define TONE_MAPPING":"",e.toneMapping!==Hn?Vt.tonemapping_pars_fragment:"",e.toneMapping!==Hn?gm("toneMapping",e.toneMapping):"",e.dithering?"#define DITHERING":"",e.opaque?"#define OPAQUE":"",Vt.colorspace_pars_fragment,mm("linearToOutputTexel",e.outputColorSpace),_m(),e.useDepthPacking?"#define DEPTH_PACKING "+e.depthPacking:"",` +`].filter(ms).join(` +`)),a=so(a),a=Rl(a,e),a=Cl(a,e),o=so(o),o=Rl(o,e),o=Cl(o,e),a=Pl(a),o=Pl(o),e.isRawShaderMaterial!==!0&&(T=`#version 300 es +`,m=[u,"#define attribute in","#define varying out","#define texture2D texture"].join(` `)+` -`+m,u=["#define varying in",e.glslVersion===go?"":"layout(location = 0) out highp vec4 pc_fragColor;",e.glslVersion===go?"":"#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(` +`+m,p=["#define varying in",e.glslVersion===Co?"":"layout(location = 0) out highp vec4 pc_fragColor;",e.glslVersion===Co?"":"#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(` `)+` -`+u);const b=T+m+a,y=T+u+o,L=dl(s,s.VERTEX_SHADER,b),R=dl(s,s.FRAGMENT_SHADER,y);s.attachShader(v,L),s.attachShader(v,R),e.index0AttributeName!==void 0?s.bindAttribLocation(v,0,e.index0AttributeName):e.morphTargets===!0&&s.bindAttribLocation(v,0,"position"),s.linkProgram(v);function A(D){if(i.debug.checkShaderErrors){const W=s.getProgramInfoLog(v).trim(),z=s.getShaderInfoLog(L).trim(),V=s.getShaderInfoLog(R).trim();let $=!0,G=!0;if(s.getProgramParameter(v,s.LINK_STATUS)===!1)if($=!1,typeof i.debug.onShaderError=="function")i.debug.onShaderError(s,v,L,R);else{const J=pl(s,L,"vertex"),k=pl(s,R,"fragment");console.error("THREE.WebGLProgram: Shader Error "+s.getError()+" - VALIDATE_STATUS "+s.getProgramParameter(v,s.VALIDATE_STATUS)+` +`+p);const E=T+m+a,y=T+p+o,D=Tl(s,s.VERTEX_SHADER,E),A=Tl(s,s.FRAGMENT_SHADER,y);s.attachShader(_,D),s.attachShader(_,A),e.index0AttributeName!==void 0?s.bindAttribLocation(_,0,e.index0AttributeName):e.morphTargets===!0&&s.bindAttribLocation(_,0,"position"),s.linkProgram(_);function C(w){if(i.debug.checkShaderErrors){const Y=s.getProgramInfoLog(_).trim(),V=s.getShaderInfoLog(D).trim(),j=s.getShaderInfoLog(A).trim();let $=!0,q=!0;if(s.getProgramParameter(_,s.LINK_STATUS)===!1)if($=!1,typeof i.debug.onShaderError=="function")i.debug.onShaderError(s,_,D,A);else{const J=Al(s,D,"vertex"),X=Al(s,A,"fragment");console.error("THREE.WebGLProgram: Shader Error "+s.getError()+" - VALIDATE_STATUS "+s.getProgramParameter(_,s.VALIDATE_STATUS)+` -Material Name: `+D.name+` -Material Type: `+D.type+` +Material Name: `+w.name+` +Material Type: `+w.type+` -Program Info Log: `+W+` +Program Info Log: `+Y+` `+J+` -`+k)}else W!==""?console.warn("THREE.WebGLProgram: Program Info Log:",W):(z===""||V==="")&&(G=!1);G&&(D.diagnostics={runnable:$,programLog:W,vertexShader:{log:z,prefix:m},fragmentShader:{log:V,prefix:u}})}s.deleteShader(L),s.deleteShader(R),U=new $s(s,v),S=lm(s,v)}let U;this.getUniforms=function(){return U===void 0&&A(this),U};let S;this.getAttributes=function(){return S===void 0&&A(this),S};let M=e.rendererExtensionParallelShaderCompile===!1;return this.isReady=function(){return M===!1&&(M=s.getProgramParameter(v,Qp)),M},this.destroy=function(){n.releaseStatesOfProgram(this),s.deleteProgram(v),this.program=void 0},this.type=e.shaderType,this.name=e.shaderName,this.id=tm++,this.cacheKey=t,this.usedTimes=1,this.program=v,this.vertexShader=L,this.fragmentShader=R,this}let Mm=0;class Sm{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 ym(t),e.set(t,n)),n}}class ym{constructor(t){this.id=Mm++,this.code=t,this.usedTimes=0}}function Em(i,t,e,n,s,r,a){const o=new Qa,l=new Sm,c=new Set,h=[],d=s.logarithmicDepthBuffer,f=s.vertexTextures;let p=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(S){return c.add(S),S===0?"uv":`uv${S}`}function m(S,M,D,W,z){const V=W.fog,$=z.geometry,G=S.isMeshStandardMaterial?W.environment:null,J=(S.isMeshStandardMaterial?e:t).get(S.envMap||G),k=J&&J.mapping===hr?J.image.height:null,it=g[S.type];S.precision!==null&&(p=s.getMaxPrecision(S.precision),p!==S.precision&&console.warn("THREE.WebGLProgram.getParameters:",S.precision,"not supported, using",p,"instead."));const ut=$.morphAttributes.position||$.morphAttributes.normal||$.morphAttributes.color,yt=ut!==void 0?ut.length:0;let Lt=0;$.morphAttributes.position!==void 0&&(Lt=1),$.morphAttributes.normal!==void 0&&(Lt=2),$.morphAttributes.color!==void 0&&(Lt=3);let jt,Y,et,xt;if(it){const Qt=cn[it];jt=Qt.vertexShader,Y=Qt.fragmentShader}else jt=S.vertexShader,Y=S.fragmentShader,l.update(S),et=l.getVertexShaderID(S),xt=l.getFragmentShaderID(S);const at=i.getRenderTarget(),wt=i.state.buffers.depth.getReversed(),Ut=z.isInstancedMesh===!0,Gt=z.isBatchedMesh===!0,ce=!!S.map,rt=!!S.matcap,Ct=!!J,w=!!S.aoMap,ve=!!S.lightMap,Ft=!!S.bumpMap,kt=!!S.normalMap,Mt=!!S.displacementMap,ie=!!S.emissiveMap,Et=!!S.metalnessMap,E=!!S.roughnessMap,_=S.anisotropy>0,F=S.clearcoat>0,j=S.dispersion>0,K=S.iridescence>0,X=S.sheen>0,St=S.transmission>0,ot=_&&!!S.anisotropyMap,dt=F&&!!S.clearcoatMap,Zt=F&&!!S.clearcoatNormalMap,tt=F&&!!S.clearcoatRoughnessMap,mt=K&&!!S.iridescenceMap,bt=K&&!!S.iridescenceThicknessMap,Pt=X&&!!S.sheenColorMap,ft=X&&!!S.sheenRoughnessMap,Yt=!!S.specularMap,zt=!!S.specularColorMap,Xt=!!S.specularIntensityMap,C=St&&!!S.transmissionMap,nt=St&&!!S.thicknessMap,H=!!S.gradientMap,Z=!!S.alphaMap,ht=S.alphaTest>0,lt=!!S.alphaHash,Ot=!!S.extensions;let he=On;S.toneMapped&&(at===null||at.isXRRenderTarget===!0)&&(he=i.toneMapping);const Ee={shaderID:it,shaderType:S.type,shaderName:S.name,vertexShader:jt,fragmentShader:Y,defines:S.defines,customVertexShaderID:et,customFragmentShaderID:xt,isRawShaderMaterial:S.isRawShaderMaterial===!0,glslVersion:S.glslVersion,precision:p,batching:Gt,batchingColor:Gt&&z._colorsTexture!==null,instancing:Ut,instancingColor:Ut&&z.instanceColor!==null,instancingMorph:Ut&&z.morphTexture!==null,supportsVertexTextures:f,outputColorSpace:at===null?i.outputColorSpace:at.isXRRenderTarget===!0?at.texture.colorSpace:ki,alphaToCoverage:!!S.alphaToCoverage,map:ce,matcap:rt,envMap:Ct,envMapMode:Ct&&J.mapping,envMapCubeUVHeight:k,aoMap:w,lightMap:ve,bumpMap:Ft,normalMap:kt,displacementMap:f&&Mt,emissiveMap:ie,normalMapObjectSpace:kt&&S.normalMapType===dh,normalMapTangentSpace:kt&&S.normalMapType===Kl,metalnessMap:Et,roughnessMap:E,anisotropy:_,anisotropyMap:ot,clearcoat:F,clearcoatMap:dt,clearcoatNormalMap:Zt,clearcoatRoughnessMap:tt,dispersion:j,iridescence:K,iridescenceMap:mt,iridescenceThicknessMap:bt,sheen:X,sheenColorMap:Pt,sheenRoughnessMap:ft,specularMap:Yt,specularColorMap:zt,specularIntensityMap:Xt,transmission:St,transmissionMap:C,thicknessMap:nt,gradientMap:H,opaque:S.transparent===!1&&S.blending===Li&&S.alphaToCoverage===!1,alphaMap:Z,alphaTest:ht,alphaHash:lt,combine:S.combine,mapUv:ce&&v(S.map.channel),aoMapUv:w&&v(S.aoMap.channel),lightMapUv:ve&&v(S.lightMap.channel),bumpMapUv:Ft&&v(S.bumpMap.channel),normalMapUv:kt&&v(S.normalMap.channel),displacementMapUv:Mt&&v(S.displacementMap.channel),emissiveMapUv:ie&&v(S.emissiveMap.channel),metalnessMapUv:Et&&v(S.metalnessMap.channel),roughnessMapUv:E&&v(S.roughnessMap.channel),anisotropyMapUv:ot&&v(S.anisotropyMap.channel),clearcoatMapUv:dt&&v(S.clearcoatMap.channel),clearcoatNormalMapUv:Zt&&v(S.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:tt&&v(S.clearcoatRoughnessMap.channel),iridescenceMapUv:mt&&v(S.iridescenceMap.channel),iridescenceThicknessMapUv:bt&&v(S.iridescenceThicknessMap.channel),sheenColorMapUv:Pt&&v(S.sheenColorMap.channel),sheenRoughnessMapUv:ft&&v(S.sheenRoughnessMap.channel),specularMapUv:Yt&&v(S.specularMap.channel),specularColorMapUv:zt&&v(S.specularColorMap.channel),specularIntensityMapUv:Xt&&v(S.specularIntensityMap.channel),transmissionMapUv:C&&v(S.transmissionMap.channel),thicknessMapUv:nt&&v(S.thicknessMap.channel),alphaMapUv:Z&&v(S.alphaMap.channel),vertexTangents:!!$.attributes.tangent&&(kt||_),vertexColors:S.vertexColors,vertexAlphas:S.vertexColors===!0&&!!$.attributes.color&&$.attributes.color.itemSize===4,pointsUvs:z.isPoints===!0&&!!$.attributes.uv&&(ce||Z),fog:!!V,useFog:S.fog===!0,fogExp2:!!V&&V.isFogExp2,flatShading:S.flatShading===!0,sizeAttenuation:S.sizeAttenuation===!0,logarithmicDepthBuffer:d,reverseDepthBuffer:wt,skinning:z.isSkinnedMesh===!0,morphTargets:$.morphAttributes.position!==void 0,morphNormals:$.morphAttributes.normal!==void 0,morphColors:$.morphAttributes.color!==void 0,morphTargetsCount:yt,morphTextureStride:Lt,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&&D.length>0,shadowMapType:i.shadowMap.type,toneMapping:he,decodeVideoTexture:ce&&S.map.isVideoTexture===!0&&Jt.getTransfer(S.map.colorSpace)===re,decodeVideoTextureEmissive:ie&&S.emissiveMap.isVideoTexture===!0&&Jt.getTransfer(S.emissiveMap.colorSpace)===re,premultipliedAlpha:S.premultipliedAlpha,doubleSided:S.side===hn,flipSided:S.side===He,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||Gt)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:S.customProgramCacheKey()};return Ee.vertexUv1s=c.has(1),Ee.vertexUv2s=c.has(2),Ee.vertexUv3s=c.has(3),c.clear(),Ee}function u(S){const M=[];if(S.shaderID?M.push(S.shaderID):(M.push(S.customVertexShaderID),M.push(S.customFragmentShaderID)),S.defines!==void 0)for(const D in S.defines)M.push(D),M.push(S.defines[D]);return S.isRawShaderMaterial===!1&&(T(M,S),b(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 b(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 D;if(M){const W=cn[M];D=ir.clone(W.uniforms)}else D=S.uniforms;return D}function L(S,M){let D;for(let W=0,z=h.length;W0?n.push(u):p.transparent===!0?s.push(u):e.push(u)}function l(d,f,p,g,v,m){const u=a(d,f,p,g,v,m);p.transmission>0?n.unshift(u):p.transparent===!0?s.unshift(u):e.unshift(u)}function c(d,f){e.length>1&&e.sort(d||Tm),n.length>1&&n.sort(f||xl),s.length>1&&s.sort(f||xl)}function h(){for(let d=t,f=i.length;d=r.length?(a=new Ml,r.push(a)):a=r[s],a}function e(){i=new WeakMap}return{get:t,dispose:e}}function Am(){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 pt};break;case"SpotLight":e={position:new P,direction:new P,color:new pt,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":e={position:new P,color:new pt,distance:0,decay:0};break;case"HemisphereLight":e={direction:new P,skyColor:new pt,groundColor:new pt};break;case"RectAreaLight":e={color:new pt,position:new P,halfWidth:new P,halfHeight:new P};break}return i[t.id]=e,e}}}function Rm(){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 vt};break;case"SpotLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new vt};break;case"PointLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new vt,shadowCameraNear:1,shadowCameraFar:1e3};break}return i[t.id]=e,e}}}let Cm=0;function Pm(i,t){return(t.castShadow?2:0)-(i.castShadow?2:0)+(t.map?1:0)-(i.map?1:0)}function Dm(i){const t=new Am,e=Rm(),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,f=0;for(let S=0;S<9;S++)n.probe[S].set(0,0,0);let p=0,g=0,v=0,m=0,u=0,T=0,b=0,y=0,L=0,R=0,A=0;c.sort(Pm);for(let S=0,M=c.length;S0&&(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]=f;const U=n.hash;(U.directionalLength!==p||U.pointLength!==g||U.spotLength!==v||U.rectAreaLength!==m||U.hemiLength!==u||U.numDirectionalShadows!==T||U.numPointShadows!==b||U.numSpotShadows!==y||U.numSpotMaps!==L||U.numLightProbes!==A)&&(n.directional.length=p,n.spot.length=v,n.rectArea.length=m,n.point.length=g,n.hemi.length=u,n.directionalShadow.length=T,n.directionalShadowMap.length=T,n.pointShadow.length=b,n.pointShadowMap.length=b,n.spotShadow.length=y,n.spotShadowMap.length=y,n.directionalShadowMatrix.length=T,n.pointShadowMatrix.length=b,n.spotLightMatrix.length=y+L-R,n.spotLightMap.length=L,n.numSpotLightShadowsWithMaps=R,n.numLightProbes=A,U.directionalLength=p,U.pointLength=g,U.spotLength=v,U.rectAreaLength=m,U.hemiLength=u,U.numDirectionalShadows=T,U.numPointShadows=b,U.numSpotShadows=y,U.numSpotMaps=L,U.numLightProbes=A,n.version=Cm++)}function l(c,h){let d=0,f=0,p=0,g=0,v=0;const m=h.matrixWorldInverse;for(let u=0,T=c.length;u=a.length?(o=new Sl(i),a.push(o)):o=a[r],o}function n(){t=new WeakMap}return{get:e,dispose:n}}const Um=`void main() { +`+X)}else Y!==""?console.warn("THREE.WebGLProgram: Program Info Log:",Y):(V===""||j==="")&&(q=!1);q&&(w.diagnostics={runnable:$,programLog:Y,vertexShader:{log:V,prefix:m},fragmentShader:{log:j,prefix:p}})}s.deleteShader(D),s.deleteShader(A),I=new dr(s,_),S=Mm(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(_,um)),M},this.destroy=function(){n.releaseStatesOfProgram(this),s.deleteProgram(_),this.program=void 0},this.type=e.shaderType,this.name=e.shaderName,this.id=dm++,this.cacheKey=t,this.usedTimes=1,this.program=_,this.vertexShader=D,this.fragmentShader=A,this}let Lm=0;class Um{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 Im(t),e.set(t,n)),n}}class Im{constructor(t){this.id=Lm++,this.code=t,this.usedTimes=0}}function Nm(i,t,e,n,s,r,a){const o=new xo,l=new Um,c=new Set,h=[],d=s.logarithmicDepthBuffer,f=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,w,Y,V){const j=Y.fog,$=V.geometry,q=S.isMeshStandardMaterial?Y.environment:null,J=(S.isMeshStandardMaterial?e:t).get(S.envMap||q),X=J&&J.mapping===br?J.image.height:null,it=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 ft=$.morphAttributes.position||$.morphAttributes.normal||$.morphAttributes.color,Mt=ft!==void 0?ft.length:0;let Nt=0;$.morphAttributes.position!==void 0&&(Nt=1),$.morphAttributes.normal!==void 0&&(Nt=2),$.morphAttributes.color!==void 0&&(Nt=3);let Wt,Z,nt,_t;if(it){const Kt=pn[it];Wt=Kt.vertexShader,Z=Kt.fragmentShader}else Wt=S.vertexShader,Z=S.fragmentShader,l.update(S),nt=l.getVertexShaderID(S),_t=l.getFragmentShaderID(S);const at=i.getRenderTarget(),wt=i.state.buffers.depth.getReversed(),Pt=V.isInstancedMesh===!0,kt=V.isBatchedMesh===!0,le=!!S.map,Ht=!!S.matcap,de=!!J,R=!!S.aoMap,Ue=!!S.lightMap,qt=!!S.bumpMap,jt=!!S.normalMap,bt=!!S.displacementMap,oe=!!S.emissiveMap,Et=!!S.metalnessMap,b=!!S.roughnessMap,v=S.anisotropy>0,O=S.clearcoat>0,F=S.dispersion>0,k=S.iridescence>0,G=S.sheen>0,ot=S.transmission>0,Q=v&&!!S.anisotropyMap,lt=O&&!!S.clearcoatMap,Ft=O&&!!S.clearcoatNormalMap,tt=O&&!!S.clearcoatRoughnessMap,mt=k&&!!S.iridescenceMap,At=k&&!!S.iridescenceThicknessMap,Dt=G&&!!S.sheenColorMap,pt=G&&!!S.sheenRoughnessMap,Xt=!!S.specularMap,Ot=!!S.specularColorMap,Qt=!!S.specularIntensityMap,L=ot&&!!S.transmissionMap,ct=ot&&!!S.thicknessMap,W=!!S.gradientMap,K=!!S.alphaMap,ht=S.alphaTest>0,ut=!!S.alphaHash,Ut=!!S.extensions;let he=Hn;S.toneMapped&&(at===null||at.isXRRenderTarget===!0)&&(he=i.toneMapping);const _e={shaderID:it,shaderType:S.type,shaderName:S.name,vertexShader:Wt,fragmentShader:Z,defines:S.defines,customVertexShaderID:nt,customFragmentShaderID:_t,isRawShaderMaterial:S.isRawShaderMaterial===!0,glslVersion:S.glslVersion,precision:u,batching:kt,batchingColor:kt&&V._colorsTexture!==null,instancing:Pt,instancingColor:Pt&&V.instanceColor!==null,instancingMorph:Pt&&V.morphTexture!==null,supportsVertexTextures:f,outputColorSpace:at===null?i.outputColorSpace:at.isXRRenderTarget===!0?at.texture.colorSpace:Ji,alphaToCoverage:!!S.alphaToCoverage,map:le,matcap:Ht,envMap:de,envMapMode:de&&J.mapping,envMapCubeUVHeight:X,aoMap:R,lightMap:Ue,bumpMap:qt,normalMap:jt,displacementMap:f&&bt,emissiveMap:oe,normalMapObjectSpace:jt&&S.normalMapType===Th,normalMapTangentSpace:jt&&S.normalMapType===ac,metalnessMap:Et,roughnessMap:b,anisotropy:v,anisotropyMap:Q,clearcoat:O,clearcoatMap:lt,clearcoatNormalMap:Ft,clearcoatRoughnessMap:tt,dispersion:F,iridescence:k,iridescenceMap:mt,iridescenceThicknessMap:At,sheen:G,sheenColorMap:Dt,sheenRoughnessMap:pt,specularMap:Xt,specularColorMap:Ot,specularIntensityMap:Qt,transmission:ot,transmissionMap:L,thicknessMap:ct,gradientMap:W,opaque:S.transparent===!1&&S.blending===Vi&&S.alphaToCoverage===!1,alphaMap:K,alphaTest:ht,alphaHash:ut,combine:S.combine,mapUv:le&&_(S.map.channel),aoMapUv:R&&_(S.aoMap.channel),lightMapUv:Ue&&_(S.lightMap.channel),bumpMapUv:qt&&_(S.bumpMap.channel),normalMapUv:jt&&_(S.normalMap.channel),displacementMapUv:bt&&_(S.displacementMap.channel),emissiveMapUv:oe&&_(S.emissiveMap.channel),metalnessMapUv:Et&&_(S.metalnessMap.channel),roughnessMapUv:b&&_(S.roughnessMap.channel),anisotropyMapUv:Q&&_(S.anisotropyMap.channel),clearcoatMapUv:lt&&_(S.clearcoatMap.channel),clearcoatNormalMapUv:Ft&&_(S.clearcoatNormalMap.channel),clearcoatRoughnessMapUv:tt&&_(S.clearcoatRoughnessMap.channel),iridescenceMapUv:mt&&_(S.iridescenceMap.channel),iridescenceThicknessMapUv:At&&_(S.iridescenceThicknessMap.channel),sheenColorMapUv:Dt&&_(S.sheenColorMap.channel),sheenRoughnessMapUv:pt&&_(S.sheenRoughnessMap.channel),specularMapUv:Xt&&_(S.specularMap.channel),specularColorMapUv:Ot&&_(S.specularColorMap.channel),specularIntensityMapUv:Qt&&_(S.specularIntensityMap.channel),transmissionMapUv:L&&_(S.transmissionMap.channel),thicknessMapUv:ct&&_(S.thicknessMap.channel),alphaMapUv:K&&_(S.alphaMap.channel),vertexTangents:!!$.attributes.tangent&&(jt||v),vertexColors:S.vertexColors,vertexAlphas:S.vertexColors===!0&&!!$.attributes.color&&$.attributes.color.itemSize===4,pointsUvs:V.isPoints===!0&&!!$.attributes.uv&&(le||K),fog:!!j,useFog:S.fog===!0,fogExp2:!!j&&j.isFogExp2,flatShading:S.flatShading===!0,sizeAttenuation:S.sizeAttenuation===!0,logarithmicDepthBuffer:d,reverseDepthBuffer:wt,skinning:V.isSkinnedMesh===!0,morphTargets:$.morphAttributes.position!==void 0,morphNormals:$.morphAttributes.normal!==void 0,morphColors:$.morphAttributes.color!==void 0,morphTargetsCount:Mt,morphTextureStride:Nt,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&&w.length>0,shadowMapType:i.shadowMap.type,toneMapping:he,decodeVideoTexture:le&&S.map.isVideoTexture===!0&&Jt.getTransfer(S.map.colorSpace)===ie,decodeVideoTextureEmissive:oe&&S.emissiveMap.isVideoTexture===!0&&Jt.getTransfer(S.emissiveMap.colorSpace)===ie,premultipliedAlpha:S.premultipliedAlpha,doubleSided:S.side===mn,flipSided:S.side===Ye,useDepthPacking:S.depthPacking>=0,depthPacking:S.depthPacking||0,index0AttributeName:S.index0AttributeName,extensionClipCullDistance:Ut&&S.extensions.clipCullDistance===!0&&n.has("WEBGL_clip_cull_distance"),extensionMultiDraw:(Ut&&S.extensions.multiDraw===!0||kt)&&n.has("WEBGL_multi_draw"),rendererExtensionParallelShaderCompile:n.has("KHR_parallel_shader_compile"),customProgramCacheKey:S.customProgramCacheKey()};return _e.vertexUv1s=c.has(1),_e.vertexUv2s=c.has(2),_e.vertexUv3s=c.has(3),c.clear(),_e}function p(S){const M=[];if(S.shaderID?M.push(S.shaderID):(M.push(S.customVertexShaderID),M.push(S.customFragmentShaderID)),S.defines!==void 0)for(const w in S.defines)M.push(w),M.push(S.defines[w]);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 w;if(M){const Y=pn[M];w=xr.clone(Y.uniforms)}else w=S.uniforms;return w}function D(S,M){let w;for(let Y=0,V=h.length;Y0?n.push(p):u.transparent===!0?s.push(p):e.push(p)}function l(d,f,u,g,_,m){const p=a(d,f,u,g,_,m);u.transmission>0?n.unshift(p):u.transparent===!0?s.unshift(p):e.unshift(p)}function c(d,f){e.length>1&&e.sort(d||Om),n.length>1&&n.sort(f||Ll),s.length>1&&s.sort(f||Ll)}function h(){for(let d=t,f=i.length;d=r.length?(a=new Ul,r.push(a)):a=r[s],a}function e(){i=new WeakMap}return{get:t,dispose:e}}function zm(){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 st};break;case"SpotLight":e={position:new P,direction:new P,color:new st,distance:0,coneCos:0,penumbraCos:0,decay:0};break;case"PointLight":e={position:new P,color:new st,distance:0,decay:0};break;case"HemisphereLight":e={direction:new P,skyColor:new st,groundColor:new st};break;case"RectAreaLight":e={color:new st,position:new P,halfWidth:new P,halfHeight:new P};break}return i[t.id]=e,e}}}function km(){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 xt};break;case"SpotLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new xt};break;case"PointLight":e={shadowIntensity:1,shadowBias:0,shadowNormalBias:0,shadowRadius:1,shadowMapSize:new xt,shadowCameraNear:1,shadowCameraFar:1e3};break}return i[t.id]=e,e}}}let Hm=0;function Vm(i,t){return(t.castShadow?2:0)-(i.castShadow?2:0)+(t.map?1:0)-(i.map?1:0)}function Gm(i){const t=new zm,e=km(),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,f=0;for(let S=0;S<9;S++)n.probe[S].set(0,0,0);let u=0,g=0,_=0,m=0,p=0,T=0,E=0,y=0,D=0,A=0,C=0;c.sort(Vm);for(let S=0,M=c.length;S0&&(i.has("OES_texture_float_linear")===!0?(n.rectAreaLTC1=rt.LTC_FLOAT_1,n.rectAreaLTC2=rt.LTC_FLOAT_2):(n.rectAreaLTC1=rt.LTC_HALF_1,n.rectAreaLTC2=rt.LTC_HALF_2)),n.ambient[0]=h,n.ambient[1]=d,n.ambient[2]=f;const I=n.hash;(I.directionalLength!==u||I.pointLength!==g||I.spotLength!==_||I.rectAreaLength!==m||I.hemiLength!==p||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=p,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-A,n.spotLightMap.length=D,n.numSpotLightShadowsWithMaps=A,n.numLightProbes=C,I.directionalLength=u,I.pointLength=g,I.spotLength=_,I.rectAreaLength=m,I.hemiLength=p,I.numDirectionalShadows=T,I.numPointShadows=E,I.numSpotShadows=y,I.numSpotMaps=D,I.numLightProbes=C,n.version=Hm++)}function l(c,h){let d=0,f=0,u=0,g=0,_=0;const m=h.matrixWorldInverse;for(let p=0,T=c.length;p=a.length?(o=new Il(i),a.push(o)):o=a[r],o}function n(){t=new WeakMap}return{get:e,dispose:n}}const Xm=`void main() { gl_Position = vec4( position, 1.0 ); -}`,Im=`uniform sampler2D shadow_pass; +}`,Ym=`uniform sampler2D shadow_pass; uniform vec2 resolution; uniform float radius; #include @@ -3800,12 +3800,12 @@ void main() { squared_mean = squared_mean / samples; float std_dev = sqrt( squared_mean - mean * mean ); gl_FragColor = pack2HalfToRGBA( vec2( mean, std_dev ) ); -}`;function Nm(i,t,e){let n=new to;const s=new vt,r=new vt,a=new le,o=new nu({depthPacking:uh}),l=new iu,c={},h=e.maxTextureSize,d={[zn]:He,[He]:zn,[hn]:hn},f=new Ne({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new vt},radius:{value:4}},vertexShader:Um,fragmentShader:Im}),p=f.clone();p.defines.HORIZONTAL_PASS=1;const g=new pe;g.setAttribute("position",new de(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const v=new Me(g,f),m=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=Ol;let u=this.type;this.render=function(R,A,U){if(m.enabled===!1||m.autoUpdate===!1&&m.needsUpdate===!1||R.length===0)return;const S=i.getRenderTarget(),M=i.getActiveCubeFace(),D=i.getActiveMipmapLevel(),W=i.state;W.setBlending(bn),W.buffers.color.setClear(1,1,1,1),W.buffers.depth.setTest(!0),W.setScissorTest(!1);const z=u!==yn&&this.type===yn,V=u===yn&&this.type!==yn;for(let $=0,G=R.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||V===!0){const yt=this.type!==yn?{minFilter:Ye,magFilter:Ye}:{};k.map!==null&&k.map.dispose(),k.map=new on(s.x,s.y,yt),k.map.texture.name=J.name+".shadowMap",k.camera.updateProjectionMatrix()}i.setRenderTarget(k.map),i.clear();const ut=k.getViewportCount();for(let yt=0;yt0||A.map&&A.alphaTest>0){const W=M.uuid,z=A.uuid;let V=c[W];V===void 0&&(V={},c[W]=V);let $=V[z];$===void 0&&($=M.clone(),V[z]=$,A.addEventListener("dispose",L)),M=$}if(M.visible=A.visible,M.wireframe=A.wireframe,S===yn?M.side=A.shadowSide!==null?A.shadowSide:A.side:M.side=A.shadowSide!==null?A.shadowSide:d[A.side],M.alphaMap=A.alphaMap,M.alphaTest=A.alphaTest,M.map=A.map,M.clipShadows=A.clipShadows,M.clippingPlanes=A.clippingPlanes,M.clipIntersection=A.clipIntersection,M.displacementMap=A.displacementMap,M.displacementScale=A.displacementScale,M.displacementBias=A.displacementBias,M.wireframeLinewidth=A.wireframeLinewidth,M.linewidth=A.linewidth,U.isPointLight===!0&&M.isMeshDistanceMaterial===!0){const W=i.properties.get(M);W.light=U}return M}function y(R,A,U,S,M){if(R.visible===!1)return;if(R.layers.test(A.layers)&&(R.isMesh||R.isLine||R.isPoints)&&(R.castShadow||R.receiveShadow&&M===yn)&&(!R.frustumCulled||n.intersectsObject(R))){R.modelViewMatrix.multiplyMatrices(U.matrixWorldInverse,R.matrixWorld);const z=t.update(R),V=R.material;if(Array.isArray(V)){const $=z.groups;for(let G=0,J=$.length;G=1):k.indexOf("OpenGL ES")!==-1&&(J=parseFloat(/^OpenGL ES (\d)/.exec(k)[1]),G=J>=2);let it=null,ut={};const yt=i.getParameter(i.SCISSOR_BOX),Lt=i.getParameter(i.VIEWPORT),jt=new le().fromArray(yt),Y=new le().fromArray(Lt);function et(C,nt,H,Z){const ht=new Uint8Array(4),lt=i.createTexture();i.bindTexture(C,lt),i.texParameteri(C,i.TEXTURE_MIN_FILTER,i.NEAREST),i.texParameteri(C,i.TEXTURE_MAG_FILTER,i.NEAREST);for(let Ot=0;Ot"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new vt,h=new WeakMap;let d;const f=new WeakMap;let p=!1;try{p=typeof OffscreenCanvas<"u"&&new OffscreenCanvas(1,1).getContext("2d")!==null}catch{}function g(E,_){return p?new OffscreenCanvas(E,_):nr("canvas")}function v(E,_,F){let j=1;const K=Et(E);if((K.width>F||K.height>F)&&(j=F/Math.max(K.width,K.height)),j<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 X=Math.floor(j*K.width),St=Math.floor(j*K.height);d===void 0&&(d=g(X,St));const ot=_?g(X,St):d;return ot.width=X,ot.height=St,ot.getContext("2d").drawImage(E,0,0,X,St),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+K.width+"x"+K.height+") to ("+X+"x"+St+")."),ot}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 u(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,j,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 X=_;if(_===i.RED&&(F===i.FLOAT&&(X=i.R32F),F===i.HALF_FLOAT&&(X=i.R16F),F===i.UNSIGNED_BYTE&&(X=i.R8)),_===i.RED_INTEGER&&(F===i.UNSIGNED_BYTE&&(X=i.R8UI),F===i.UNSIGNED_SHORT&&(X=i.R16UI),F===i.UNSIGNED_INT&&(X=i.R32UI),F===i.BYTE&&(X=i.R8I),F===i.SHORT&&(X=i.R16I),F===i.INT&&(X=i.R32I)),_===i.RG&&(F===i.FLOAT&&(X=i.RG32F),F===i.HALF_FLOAT&&(X=i.RG16F),F===i.UNSIGNED_BYTE&&(X=i.RG8)),_===i.RG_INTEGER&&(F===i.UNSIGNED_BYTE&&(X=i.RG8UI),F===i.UNSIGNED_SHORT&&(X=i.RG16UI),F===i.UNSIGNED_INT&&(X=i.RG32UI),F===i.BYTE&&(X=i.RG8I),F===i.SHORT&&(X=i.RG16I),F===i.INT&&(X=i.RG32I)),_===i.RGB_INTEGER&&(F===i.UNSIGNED_BYTE&&(X=i.RGB8UI),F===i.UNSIGNED_SHORT&&(X=i.RGB16UI),F===i.UNSIGNED_INT&&(X=i.RGB32UI),F===i.BYTE&&(X=i.RGB8I),F===i.SHORT&&(X=i.RGB16I),F===i.INT&&(X=i.RGB32I)),_===i.RGBA_INTEGER&&(F===i.UNSIGNED_BYTE&&(X=i.RGBA8UI),F===i.UNSIGNED_SHORT&&(X=i.RGBA16UI),F===i.UNSIGNED_INT&&(X=i.RGBA32UI),F===i.BYTE&&(X=i.RGBA8I),F===i.SHORT&&(X=i.RGBA16I),F===i.INT&&(X=i.RGBA32I)),_===i.RGB&&F===i.UNSIGNED_INT_5_9_9_9_REV&&(X=i.RGB9_E5),_===i.RGBA){const St=K?tr:Jt.getTransfer(j);F===i.FLOAT&&(X=i.RGBA32F),F===i.HALF_FLOAT&&(X=i.RGBA16F),F===i.UNSIGNED_BYTE&&(X=St===re?i.SRGB8_ALPHA8:i.RGBA8),F===i.UNSIGNED_SHORT_4_4_4_4&&(X=i.RGBA4),F===i.UNSIGNED_SHORT_5_5_5_1&&(X=i.RGB5_A1)}return(X===i.R16F||X===i.R32F||X===i.RG16F||X===i.RG32F||X===i.RGBA16F||X===i.RGBA32F)&&t.get("EXT_color_buffer_float"),X}function y(E,_){let F;return E?_===null||_===ei||_===zi?F=i.DEPTH24_STENCIL8:_===fn?F=i.DEPTH32F_STENCIL8:_===os&&(F=i.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):_===null||_===ei||_===zi?F=i.DEPTH_COMPONENT24:_===fn?F=i.DEPTH_COMPONENT32F:_===os&&(F=i.DEPTH_COMPONENT16),F}function L(E,_){return m(E)===!0||E.isFramebufferTexture&&E.minFilter!==Ye&&E.minFilter!==dn?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 R(E){const _=E.target;_.removeEventListener("dispose",R),U(_),_.isVideoTexture&&h.delete(_)}function A(E){const _=E.target;_.removeEventListener("dispose",A),M(_)}function U(E){const _=n.get(E);if(_.__webglInit===void 0)return;const F=E.source,j=f.get(F);if(j){const K=j[_.__cacheKey];K.usedTimes--,K.usedTimes===0&&S(E),Object.keys(j).length===0&&f.delete(F)}n.remove(E)}function S(E){const _=n.get(E);i.deleteTexture(_.__webglTexture);const F=E.source,j=f.get(F);delete j[_.__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 j=0;j<6;j++){if(Array.isArray(_.__webglFramebuffer[j]))for(let K=0;K<_.__webglFramebuffer[j].length;K++)i.deleteFramebuffer(_.__webglFramebuffer[j][K]);else i.deleteFramebuffer(_.__webglFramebuffer[j]);_.__webglDepthbuffer&&i.deleteRenderbuffer(_.__webglDepthbuffer[j])}else{if(Array.isArray(_.__webglFramebuffer))for(let j=0;j<_.__webglFramebuffer.length;j++)i.deleteFramebuffer(_.__webglFramebuffer[j]);else i.deleteFramebuffer(_.__webglFramebuffer);if(_.__webglDepthbuffer&&i.deleteRenderbuffer(_.__webglDepthbuffer),_.__webglMultisampledFramebuffer&&i.deleteFramebuffer(_.__webglMultisampledFramebuffer),_.__webglColorRenderbuffer)for(let j=0;j<_.__webglColorRenderbuffer.length;j++)_.__webglColorRenderbuffer[j]&&i.deleteRenderbuffer(_.__webglColorRenderbuffer[j]);_.__webglDepthRenderbuffer&&i.deleteRenderbuffer(_.__webglDepthRenderbuffer)}const F=E.textures;for(let j=0,K=F.length;j=s.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+E+" texture units while this GPU supports only "+s.maxTextures),D+=1,E}function V(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&&Mt(E),E.isRenderTargetTexture===!1&&E.version>0&&F.__version!==E.version){const j=E.image;if(j===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(j.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 G(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 J(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){et(F,E,_);return}e.bindTexture(i.TEXTURE_CUBE_MAP,F.__webglTexture,i.TEXTURE0+_)}const it={[ha]:i.REPEAT,[Jn]:i.CLAMP_TO_EDGE,[ua]:i.MIRRORED_REPEAT},ut={[Ye]:i.NEAREST,[ch]:i.NEAREST_MIPMAP_NEAREST,[ms]:i.NEAREST_MIPMAP_LINEAR,[dn]:i.LINEAR,[gr]:i.LINEAR_MIPMAP_NEAREST,[Qn]:i.LINEAR_MIPMAP_LINEAR},yt={[fh]:i.NEVER,[xh]:i.ALWAYS,[ph]:i.LESS,[$l]:i.LEQUAL,[mh]:i.EQUAL,[vh]:i.GEQUAL,[gh]:i.GREATER,[_h]:i.NOTEQUAL};function Lt(E,_){if(_.type===fn&&t.has("OES_texture_float_linear")===!1&&(_.magFilter===dn||_.magFilter===gr||_.magFilter===ms||_.magFilter===Qn||_.minFilter===dn||_.minFilter===gr||_.minFilter===ms||_.minFilter===Qn)&&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,ut[_.magFilter]),i.texParameteri(E,i.TEXTURE_MIN_FILTER,ut[_.minFilter]),_.compareFunction&&(i.texParameteri(E,i.TEXTURE_COMPARE_MODE,i.COMPARE_REF_TO_TEXTURE),i.texParameteri(E,i.TEXTURE_COMPARE_FUNC,yt[_.compareFunction])),t.has("EXT_texture_filter_anisotropic")===!0){if(_.magFilter===Ye||_.minFilter!==ms&&_.minFilter!==Qn||_.type===fn&&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 jt(E,_){let F=!1;E.__webglInit===void 0&&(E.__webglInit=!0,_.addEventListener("dispose",R));const j=_.source;let K=f.get(j);K===void 0&&(K={},f.set(j,K));const X=V(_);if(X!==E.__cacheKey){K[X]===void 0&&(K[X]={texture:i.createTexture(),usedTimes:0},a.memory.textures++,F=!0),K[X].usedTimes++;const St=K[E.__cacheKey];St!==void 0&&(K[E.__cacheKey].usedTimes--,St.usedTimes===0&&S(_)),E.__cacheKey=X,E.__webglTexture=K[X].texture}return F}function Y(E,_,F){let j=i.TEXTURE_2D;(_.isDataArrayTexture||_.isCompressedArrayTexture)&&(j=i.TEXTURE_2D_ARRAY),_.isData3DTexture&&(j=i.TEXTURE_3D);const K=jt(E,_),X=_.source;e.bindTexture(j,E.__webglTexture,i.TEXTURE0+F);const St=n.get(X);if(X.version!==St.__version||K===!0){e.activeTexture(i.TEXTURE0+F);const ot=Jt.getPrimaries(Jt.workingColorSpace),dt=_.colorSpace===Fn?null:Jt.getPrimaries(_.colorSpace),Zt=_.colorSpace===Fn||ot===dt?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,Zt);let tt=v(_.image,!1,s.maxTextureSize);tt=ie(_,tt);const mt=r.convert(_.format,_.colorSpace),bt=r.convert(_.type);let Pt=b(_.internalFormat,mt,bt,_.colorSpace,_.isVideoTexture);Lt(j,_);let ft;const Yt=_.mipmaps,zt=_.isVideoTexture!==!0,Xt=St.__version===void 0||K===!0,C=X.dataReady,nt=L(_,tt);if(_.isDepthTexture)Pt=y(_.format===Hi,_.type),Xt&&(zt?e.texStorage2D(i.TEXTURE_2D,1,Pt,tt.width,tt.height):e.texImage2D(i.TEXTURE_2D,0,Pt,tt.width,tt.height,0,mt,bt,null));else if(_.isDataTexture)if(Yt.length>0){zt&&Xt&&e.texStorage2D(i.TEXTURE_2D,nt,Pt,Yt[0].width,Yt[0].height);for(let H=0,Z=Yt.length;H0){const ht=$o(ft.width,ft.height,_.format,_.type);for(const lt of _.layerUpdates){const Ot=ft.data.subarray(lt*ht/ft.data.BYTES_PER_ELEMENT,(lt+1)*ht/ft.data.BYTES_PER_ELEMENT);e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,H,0,0,lt,ft.width,ft.height,1,mt,Ot)}_.clearLayerUpdates()}else e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,H,0,0,0,ft.width,ft.height,tt.depth,mt,ft.data)}else e.compressedTexImage3D(i.TEXTURE_2D_ARRAY,H,Pt,ft.width,ft.height,tt.depth,0,ft.data,0,0);else console.warn("THREE.WebGLRenderer: Attempt to load unsupported compressed texture format in .uploadTexture()");else zt?C&&e.texSubImage3D(i.TEXTURE_2D_ARRAY,H,0,0,0,ft.width,ft.height,tt.depth,mt,bt,ft.data):e.texImage3D(i.TEXTURE_2D_ARRAY,H,Pt,ft.width,ft.height,tt.depth,0,mt,bt,ft.data)}else{zt&&Xt&&e.texStorage2D(i.TEXTURE_2D,nt,Pt,Yt[0].width,Yt[0].height);for(let H=0,Z=Yt.length;H0){const H=$o(tt.width,tt.height,_.format,_.type);for(const Z of _.layerUpdates){const ht=tt.data.subarray(Z*H/tt.data.BYTES_PER_ELEMENT,(Z+1)*H/tt.data.BYTES_PER_ELEMENT);e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,Z,tt.width,tt.height,1,mt,bt,ht)}_.clearLayerUpdates()}else e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,0,tt.width,tt.height,tt.depth,mt,bt,tt.data)}else e.texImage3D(i.TEXTURE_2D_ARRAY,0,Pt,tt.width,tt.height,tt.depth,0,mt,bt,tt.data);else if(_.isData3DTexture)zt?(Xt&&e.texStorage3D(i.TEXTURE_3D,nt,Pt,tt.width,tt.height,tt.depth),C&&e.texSubImage3D(i.TEXTURE_3D,0,0,0,0,tt.width,tt.height,tt.depth,mt,bt,tt.data)):e.texImage3D(i.TEXTURE_3D,0,Pt,tt.width,tt.height,tt.depth,0,mt,bt,tt.data);else if(_.isFramebufferTexture){if(Xt)if(zt)e.texStorage2D(i.TEXTURE_2D,nt,Pt,tt.width,tt.height);else{let H=tt.width,Z=tt.height;for(let ht=0;ht>=1,Z>>=1}}else if(Yt.length>0){if(zt&&Xt){const H=Et(Yt[0]);e.texStorage2D(i.TEXTURE_2D,nt,Pt,H.width,H.height)}for(let H=0,Z=Yt.length;H0&&nt++;const Z=Et(mt[0]);e.texStorage2D(i.TEXTURE_CUBE_MAP,nt,Yt,Z.width,Z.height)}for(let Z=0;Z<6;Z++)if(tt){zt?C&&e.texSubImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+Z,0,0,0,mt[Z].width,mt[Z].height,Pt,ft,mt[Z].data):e.texImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+Z,0,Yt,mt[Z].width,mt[Z].height,0,Pt,ft,mt[Z].data);for(let ht=0;ht>X),bt=Math.max(1,_.height>>X);K===i.TEXTURE_3D||K===i.TEXTURE_2D_ARRAY?e.texImage3D(K,X,dt,mt,bt,_.depth,0,St,ot,null):e.texImage2D(K,X,dt,mt,bt,0,St,ot,null)}e.bindFramebuffer(i.FRAMEBUFFER,E),kt(_)?o.framebufferTexture2DMultisampleEXT(i.FRAMEBUFFER,j,K,tt.__webglTexture,0,Ft(_)):(K===i.TEXTURE_2D||K>=i.TEXTURE_CUBE_MAP_POSITIVE_X&&K<=i.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&i.framebufferTexture2D(i.FRAMEBUFFER,j,K,tt.__webglTexture,X),e.bindFramebuffer(i.FRAMEBUFFER,null)}function at(E,_,F){if(i.bindRenderbuffer(i.RENDERBUFFER,E),_.depthBuffer){const j=_.depthTexture,K=j&&j.isDepthTexture?j.type:null,X=y(_.stencilBuffer,K),St=_.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,ot=Ft(_);kt(_)?o.renderbufferStorageMultisampleEXT(i.RENDERBUFFER,ot,X,_.width,_.height):F?i.renderbufferStorageMultisample(i.RENDERBUFFER,ot,X,_.width,_.height):i.renderbufferStorage(i.RENDERBUFFER,X,_.width,_.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,St,i.RENDERBUFFER,E)}else{const j=_.textures;for(let K=0;K{delete _.__boundDepthTexture,delete _.__depthDisposeCallback,j.removeEventListener("dispose",K)};j.addEventListener("dispose",K),_.__depthDisposeCallback=K}_.__boundDepthTexture=j}if(E.depthTexture&&!_.__autoAllocateDepthBuffer){if(F)throw new Error("target.depthTexture not supported in Cube render targets");wt(_.__webglFramebuffer,E)}else if(F){_.__webglDepthbuffer=[];for(let j=0;j<6;j++)if(e.bindFramebuffer(i.FRAMEBUFFER,_.__webglFramebuffer[j]),_.__webglDepthbuffer[j]===void 0)_.__webglDepthbuffer[j]=i.createRenderbuffer(),at(_.__webglDepthbuffer[j],E,!1);else{const K=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,X=_.__webglDepthbuffer[j];i.bindRenderbuffer(i.RENDERBUFFER,X),i.framebufferRenderbuffer(i.FRAMEBUFFER,K,i.RENDERBUFFER,X)}}else if(e.bindFramebuffer(i.FRAMEBUFFER,_.__webglFramebuffer),_.__webglDepthbuffer===void 0)_.__webglDepthbuffer=i.createRenderbuffer(),at(_.__webglDepthbuffer,E,!1);else{const j=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,K=_.__webglDepthbuffer;i.bindRenderbuffer(i.RENDERBUFFER,K),i.framebufferRenderbuffer(i.FRAMEBUFFER,j,i.RENDERBUFFER,K)}e.bindFramebuffer(i.FRAMEBUFFER,null)}function Gt(E,_,F){const j=n.get(E);_!==void 0&&xt(j.__webglFramebuffer,E,E.texture,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,0),F!==void 0&&Ut(E)}function ce(E){const _=E.texture,F=n.get(E),j=n.get(_);E.addEventListener("dispose",A);const K=E.textures,X=E.isWebGLCubeRenderTarget===!0,St=K.length>1;if(St||(j.__webglTexture===void 0&&(j.__webglTexture=i.createTexture()),j.__version=_.version,a.memory.textures++),X){F.__webglFramebuffer=[];for(let ot=0;ot<6;ot++)if(_.mipmaps&&_.mipmaps.length>0){F.__webglFramebuffer[ot]=[];for(let dt=0;dt<_.mipmaps.length;dt++)F.__webglFramebuffer[ot][dt]=i.createFramebuffer()}else F.__webglFramebuffer[ot]=i.createFramebuffer()}else{if(_.mipmaps&&_.mipmaps.length>0){F.__webglFramebuffer=[];for(let ot=0;ot<_.mipmaps.length;ot++)F.__webglFramebuffer[ot]=i.createFramebuffer()}else F.__webglFramebuffer=i.createFramebuffer();if(St)for(let ot=0,dt=K.length;ot0&&kt(E)===!1){F.__webglMultisampledFramebuffer=i.createFramebuffer(),F.__webglColorRenderbuffer=[],e.bindFramebuffer(i.FRAMEBUFFER,F.__webglMultisampledFramebuffer);for(let ot=0;ot0)for(let dt=0;dt<_.mipmaps.length;dt++)xt(F.__webglFramebuffer[ot][dt],E,_,i.COLOR_ATTACHMENT0,i.TEXTURE_CUBE_MAP_POSITIVE_X+ot,dt);else xt(F.__webglFramebuffer[ot],E,_,i.COLOR_ATTACHMENT0,i.TEXTURE_CUBE_MAP_POSITIVE_X+ot,0);m(_)&&u(i.TEXTURE_CUBE_MAP),e.unbindTexture()}else if(St){for(let ot=0,dt=K.length;ot0)for(let dt=0;dt<_.mipmaps.length;dt++)xt(F.__webglFramebuffer[dt],E,_,i.COLOR_ATTACHMENT0,ot,dt);else xt(F.__webglFramebuffer,E,_,i.COLOR_ATTACHMENT0,ot,0);m(_)&&u(ot),e.unbindTexture()}E.depthBuffer&&Ut(E)}function rt(E){const _=E.textures;for(let F=0,j=_.length;F0){if(kt(E)===!1){const _=E.textures,F=E.width,j=E.height;let K=i.COLOR_BUFFER_BIT;const X=E.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,St=n.get(E),ot=_.length>1;if(ot)for(let dt=0;dt<_.length;dt++)e.bindFramebuffer(i.FRAMEBUFFER,St.__webglMultisampledFramebuffer),i.framebufferRenderbuffer(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0+dt,i.RENDERBUFFER,null),e.bindFramebuffer(i.FRAMEBUFFER,St.__webglFramebuffer),i.framebufferTexture2D(i.DRAW_FRAMEBUFFER,i.COLOR_ATTACHMENT0+dt,i.TEXTURE_2D,null,0);e.bindFramebuffer(i.READ_FRAMEBUFFER,St.__webglMultisampledFramebuffer),e.bindFramebuffer(i.DRAW_FRAMEBUFFER,St.__webglFramebuffer);for(let dt=0;dt<_.length;dt++){if(E.resolveDepthBuffer&&(E.depthBuffer&&(K|=i.DEPTH_BUFFER_BIT),E.stencilBuffer&&E.resolveStencilBuffer&&(K|=i.STENCIL_BUFFER_BIT)),ot){i.framebufferRenderbuffer(i.READ_FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.RENDERBUFFER,St.__webglColorRenderbuffer[dt]);const Zt=n.get(_[dt]).__webglTexture;i.framebufferTexture2D(i.DRAW_FRAMEBUFFER,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,Zt,0)}i.blitFramebuffer(0,0,F,j,0,0,F,j,K,i.NEAREST),l===!0&&(Ct.length=0,w.length=0,Ct.push(i.COLOR_ATTACHMENT0+dt),E.depthBuffer&&E.resolveDepthBuffer===!1&&(Ct.push(X),w.push(X),i.invalidateFramebuffer(i.DRAW_FRAMEBUFFER,w)),i.invalidateFramebuffer(i.READ_FRAMEBUFFER,Ct))}if(e.bindFramebuffer(i.READ_FRAMEBUFFER,null),e.bindFramebuffer(i.DRAW_FRAMEBUFFER,null),ot)for(let dt=0;dt<_.length;dt++){e.bindFramebuffer(i.FRAMEBUFFER,St.__webglMultisampledFramebuffer),i.framebufferRenderbuffer(i.FRAMEBUFFER,i.COLOR_ATTACHMENT0+dt,i.RENDERBUFFER,St.__webglColorRenderbuffer[dt]);const Zt=n.get(_[dt]).__webglTexture;e.bindFramebuffer(i.FRAMEBUFFER,St.__webglFramebuffer),i.framebufferTexture2D(i.DRAW_FRAMEBUFFER,i.COLOR_ATTACHMENT0+dt,i.TEXTURE_2D,Zt,0)}e.bindFramebuffer(i.DRAW_FRAMEBUFFER,St.__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 Ft(E){return Math.min(s.maxSamples,E.samples)}function kt(E){const _=n.get(E);return E.samples>0&&t.has("WEBGL_multisampled_render_to_texture")===!0&&_.__useRenderToTexture!==!1}function Mt(E){const _=a.render.frame;h.get(E)!==_&&(h.set(E,_),E.update())}function ie(E,_){const F=E.colorSpace,j=E.format,K=E.type;return E.isCompressedTexture===!0||E.isVideoTexture===!0||F!==ki&&F!==Fn&&(Jt.getTransfer(F)===re?(j!==an||K!==An)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",F)),_}function Et(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=G,this.setTexture3D=J,this.setTextureCube=k,this.rebindTextures=Gt,this.setupRenderTarget=ce,this.updateRenderTargetMipmap=rt,this.updateMultisampleRenderTarget=ve,this.setupDepthRenderbuffer=Ut,this.setupFrameBufferTexture=xt,this.useMultisampledRTT=kt}function zm(i,t){function e(n,s=Fn){let r;const a=Jt.getTransfer(s);if(n===An)return i.UNSIGNED_BYTE;if(n===qa)return i.UNSIGNED_SHORT_4_4_4_4;if(n===ja)return i.UNSIGNED_SHORT_5_5_5_1;if(n===Gl)return i.UNSIGNED_INT_5_9_9_9_REV;if(n===kl)return i.BYTE;if(n===Vl)return i.SHORT;if(n===os)return i.UNSIGNED_SHORT;if(n===Ya)return i.INT;if(n===ei)return i.UNSIGNED_INT;if(n===fn)return i.FLOAT;if(n===Tn)return i.HALF_FLOAT;if(n===Wl)return i.ALPHA;if(n===Xl)return i.RGB;if(n===an)return i.RGBA;if(n===Yl)return i.LUMINANCE;if(n===ql)return i.LUMINANCE_ALPHA;if(n===Ui)return i.DEPTH_COMPONENT;if(n===Hi)return i.DEPTH_STENCIL;if(n===Za)return i.RED;if(n===Ka)return i.RED_INTEGER;if(n===jl)return i.RG;if(n===$a)return i.RG_INTEGER;if(n===Ja)return i.RGBA_INTEGER;if(n===Xs||n===Ys||n===qs||n===js)if(a===re)if(r=t.get("WEBGL_compressed_texture_s3tc_srgb"),r!==null){if(n===Xs)return r.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===Ys)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===qs)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===js)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(r=t.get("WEBGL_compressed_texture_s3tc"),r!==null){if(n===Xs)return r.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===Ys)return r.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===qs)return r.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===js)return r.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===da||n===fa||n===pa||n===ma)if(r=t.get("WEBGL_compressed_texture_pvrtc"),r!==null){if(n===da)return r.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===fa)return r.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===pa)return r.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===ma)return r.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===ga||n===_a||n===va)if(r=t.get("WEBGL_compressed_texture_etc"),r!==null){if(n===ga||n===_a)return a===re?r.COMPRESSED_SRGB8_ETC2:r.COMPRESSED_RGB8_ETC2;if(n===va)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:r.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(n===xa||n===Ma||n===Sa||n===ya||n===Ea||n===ba||n===Ta||n===wa||n===Aa||n===Ra||n===Ca||n===Pa||n===Da||n===La)if(r=t.get("WEBGL_compressed_texture_astc"),r!==null){if(n===xa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:r.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===Ma)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:r.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Sa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:r.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===ya)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:r.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===Ea)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:r.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===ba)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:r.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===Ta)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:r.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===wa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:r.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===Aa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:r.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===Ra)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:r.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===Ca)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:r.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===Pa)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:r.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===Da)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:r.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===La)return a===re?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:r.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===Zs||n===Ua||n===Ia)if(r=t.get("EXT_texture_compression_bptc"),r!==null){if(n===Zs)return a===re?r.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:r.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===Ua)return r.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===Ia)return r.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===Zl||n===Na||n===Fa||n===Oa)if(r=t.get("EXT_texture_compression_rgtc"),r!==null){if(n===Zs)return r.COMPRESSED_RED_RGTC1_EXT;if(n===Na)return r.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===Fa)return r.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===Oa)return r.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT}else return null;return n===zi?i.UNSIGNED_INT_24_8:i[n]!==void 0?i[n]:null}return{convert:e}}const Hm={type:"move"};class Zr{constructor(){this._targetRay=null,this._grip=null,this._hand=null}getHandSpace(){return this._hand===null&&(this._hand=new Ci,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 Ci,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 Ci,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 v of t.hand.values()){const m=e.getJointPose(v,n),u=this._getHandJoint(c,v);m!==null&&(u.matrix.fromArray(m.transform.matrix),u.matrix.decompose(u.position,u.rotation,u.scale),u.matrixWorldNeedsUpdate=!0,u.jointRadius=m.radius),u.visible=m!==null}const h=c.joints["index-finger-tip"],d=c.joints["thumb-tip"],f=h.position.distanceTo(d.position),p=.02,g=.005;c.inputState.pinching&&f>p+g?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!c.inputState.pinching&&f<=p-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(Hm)))}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 Ci;n.matrixAutoUpdate=!1,n.visible=!1,t.joints[e.jointName]=n,t.add(n)}return t.joints[e.jointName]}}const km=` +}`;function qm(i,t,e){let n=new Mo;const s=new xt,r=new xt,a=new ae,o=new pu({depthPacking:bh}),l=new mu,c={},h=e.maxTextureSize,d={[Gn]:Ye,[Ye]:Gn,[mn]:mn},f=new Ve({defines:{VSM_SAMPLES:8},uniforms:{shadow_pass:{value:null},resolution:{value:new xt},radius:{value:4}},vertexShader:Xm,fragmentShader:Ym}),u=f.clone();u.defines.HORIZONTAL_PASS=1;const g=new ge;g.setAttribute("position",new ce(new Float32Array([-1,-1,.5,3,-1,.5,-1,3,.5]),3));const _=new Ee(g,f),m=this;this.enabled=!1,this.autoUpdate=!0,this.needsUpdate=!1,this.type=ql;let p=this.type;this.render=function(A,C,I){if(m.enabled===!1||m.autoUpdate===!1&&m.needsUpdate===!1||A.length===0)return;const S=i.getRenderTarget(),M=i.getActiveCubeFace(),w=i.getActiveMipmapLevel(),Y=i.state;Y.setBlending(Rn),Y.buffers.color.setClear(1,1,1,1),Y.buffers.depth.setTest(!0),Y.setScissorTest(!1);const V=p!==wn&&this.type===wn,j=p===wn&&this.type!==wn;for(let $=0,q=A.length;$h||s.y>h)&&(s.x>h&&(r.x=Math.floor(h/it.x),s.x=r.x*it.x,X.mapSize.x=r.x),s.y>h&&(r.y=Math.floor(h/it.y),s.y=r.y*it.y,X.mapSize.y=r.y)),X.map===null||V===!0||j===!0){const Mt=this.type!==wn?{minFilter:Je,magFilter:Je}:{};X.map!==null&&X.map.dispose(),X.map=new fn(s.x,s.y,Mt),X.map.texture.name=J.name+".shadowMap",X.camera.updateProjectionMatrix()}i.setRenderTarget(X.map),i.clear();const ft=X.getViewportCount();for(let Mt=0;Mt0||C.map&&C.alphaTest>0){const Y=M.uuid,V=C.uuid;let j=c[Y];j===void 0&&(j={},c[Y]=j);let $=j[V];$===void 0&&($=M.clone(),j[V]=$,C.addEventListener("dispose",D)),M=$}if(M.visible=C.visible,M.wireframe=C.wireframe,S===wn?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 Y=i.properties.get(M);Y.light=I}return M}function y(A,C,I,S,M){if(A.visible===!1)return;if(A.layers.test(C.layers)&&(A.isMesh||A.isLine||A.isPoints)&&(A.castShadow||A.receiveShadow&&M===wn)&&(!A.frustumCulled||n.intersectsObject(A))){A.modelViewMatrix.multiplyMatrices(I.matrixWorldInverse,A.matrixWorld);const V=t.update(A),j=A.material;if(Array.isArray(j)){const $=V.groups;for(let q=0,J=$.length;q=1):X.indexOf("OpenGL ES")!==-1&&(J=parseFloat(/^OpenGL ES (\d)/.exec(X)[1]),q=J>=2);let it=null,ft={};const Mt=i.getParameter(i.SCISSOR_BOX),Nt=i.getParameter(i.VIEWPORT),Wt=new ae().fromArray(Mt),Z=new ae().fromArray(Nt);function nt(L,ct,W,K){const ht=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 Ut=0;Ut"u"?!1:/OculusBrowser/g.test(navigator.userAgent),c=new xt,h=new WeakMap;let d;const f=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):vr("canvas")}function _(b,v,O){let F=1;const k=Et(b);if((k.width>O||k.height>O)&&(F=O/Math.max(k.width,k.height)),F<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 G=Math.floor(F*k.width),ot=Math.floor(F*k.height);d===void 0&&(d=g(G,ot));const Q=v?g(G,ot):d;return Q.width=G,Q.height=ot,Q.getContext("2d").drawImage(b,0,0,G,ot),console.warn("THREE.WebGLRenderer: Texture has been resized from ("+k.width+"x"+k.height+") to ("+G+"x"+ot+")."),Q}else return"data"in b&&console.warn("THREE.WebGLRenderer: Image in DataTexture is too big ("+k.width+"x"+k.height+")."),b;return b}function m(b){return b.generateMipmaps}function p(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,O,F,k=!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 G=v;if(v===i.RED&&(O===i.FLOAT&&(G=i.R32F),O===i.HALF_FLOAT&&(G=i.R16F),O===i.UNSIGNED_BYTE&&(G=i.R8)),v===i.RED_INTEGER&&(O===i.UNSIGNED_BYTE&&(G=i.R8UI),O===i.UNSIGNED_SHORT&&(G=i.R16UI),O===i.UNSIGNED_INT&&(G=i.R32UI),O===i.BYTE&&(G=i.R8I),O===i.SHORT&&(G=i.R16I),O===i.INT&&(G=i.R32I)),v===i.RG&&(O===i.FLOAT&&(G=i.RG32F),O===i.HALF_FLOAT&&(G=i.RG16F),O===i.UNSIGNED_BYTE&&(G=i.RG8)),v===i.RG_INTEGER&&(O===i.UNSIGNED_BYTE&&(G=i.RG8UI),O===i.UNSIGNED_SHORT&&(G=i.RG16UI),O===i.UNSIGNED_INT&&(G=i.RG32UI),O===i.BYTE&&(G=i.RG8I),O===i.SHORT&&(G=i.RG16I),O===i.INT&&(G=i.RG32I)),v===i.RGB_INTEGER&&(O===i.UNSIGNED_BYTE&&(G=i.RGB8UI),O===i.UNSIGNED_SHORT&&(G=i.RGB16UI),O===i.UNSIGNED_INT&&(G=i.RGB32UI),O===i.BYTE&&(G=i.RGB8I),O===i.SHORT&&(G=i.RGB16I),O===i.INT&&(G=i.RGB32I)),v===i.RGBA_INTEGER&&(O===i.UNSIGNED_BYTE&&(G=i.RGBA8UI),O===i.UNSIGNED_SHORT&&(G=i.RGBA16UI),O===i.UNSIGNED_INT&&(G=i.RGBA32UI),O===i.BYTE&&(G=i.RGBA8I),O===i.SHORT&&(G=i.RGBA16I),O===i.INT&&(G=i.RGBA32I)),v===i.RGB&&O===i.UNSIGNED_INT_5_9_9_9_REV&&(G=i.RGB9_E5),v===i.RGBA){const ot=k?gr:Jt.getTransfer(F);O===i.FLOAT&&(G=i.RGBA32F),O===i.HALF_FLOAT&&(G=i.RGBA16F),O===i.UNSIGNED_BYTE&&(G=ot===ie?i.SRGB8_ALPHA8:i.RGBA8),O===i.UNSIGNED_SHORT_4_4_4_4&&(G=i.RGBA4),O===i.UNSIGNED_SHORT_5_5_5_1&&(G=i.RGB5_A1)}return(G===i.R16F||G===i.R32F||G===i.RG16F||G===i.RG32F||G===i.RGBA16F||G===i.RGBA32F)&&t.get("EXT_color_buffer_float"),G}function y(b,v){let O;return b?v===null||v===ci||v===Ki?O=i.DEPTH24_STENCIL8:v===vn?O=i.DEPTH32F_STENCIL8:v===vs&&(O=i.DEPTH24_STENCIL8,console.warn("DepthTexture: 16 bit depth attachment is not supported with stencil. Using 24-bit attachment.")):v===null||v===ci||v===Ki?O=i.DEPTH_COMPONENT24:v===vn?O=i.DEPTH_COMPONENT32F:v===vs&&(O=i.DEPTH_COMPONENT16),O}function D(b,v){return m(b)===!0||b.isFramebufferTexture&&b.minFilter!==Je&&b.minFilter!==_n?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 A(b){const v=b.target;v.removeEventListener("dispose",A),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 O=b.source,F=f.get(O);if(F){const k=F[v.__cacheKey];k.usedTimes--,k.usedTimes===0&&S(b),Object.keys(F).length===0&&f.delete(O)}n.remove(b)}function S(b){const v=n.get(b);i.deleteTexture(v.__webglTexture);const O=b.source,F=f.get(O);delete F[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 F=0;F<6;F++){if(Array.isArray(v.__webglFramebuffer[F]))for(let k=0;k=s.maxTextures&&console.warn("THREE.WebGLTextures: Trying to use "+b+" texture units while this GPU supports only "+s.maxTextures),w+=1,b}function j(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 $(b,v){const O=n.get(b);if(b.isVideoTexture&&bt(b),b.isRenderTargetTexture===!1&&b.version>0&&O.__version!==b.version){const F=b.image;if(F===null)console.warn("THREE.WebGLRenderer: Texture marked for update but no image data found.");else if(F.complete===!1)console.warn("THREE.WebGLRenderer: Texture marked for update but image is incomplete");else{Z(O,b,v);return}}e.bindTexture(i.TEXTURE_2D,O.__webglTexture,i.TEXTURE0+v)}function q(b,v){const O=n.get(b);if(b.version>0&&O.__version!==b.version){Z(O,b,v);return}e.bindTexture(i.TEXTURE_2D_ARRAY,O.__webglTexture,i.TEXTURE0+v)}function J(b,v){const O=n.get(b);if(b.version>0&&O.__version!==b.version){Z(O,b,v);return}e.bindTexture(i.TEXTURE_3D,O.__webglTexture,i.TEXTURE0+v)}function X(b,v){const O=n.get(b);if(b.version>0&&O.__version!==b.version){nt(O,b,v);return}e.bindTexture(i.TEXTURE_CUBE_MAP,O.__webglTexture,i.TEXTURE0+v)}const it={[Ta]:i.REPEAT,[ri]:i.CLAMP_TO_EDGE,[wa]:i.MIRRORED_REPEAT},ft={[Je]:i.NEAREST,[yh]:i.NEAREST_MIPMAP_NEAREST,[Ps]:i.NEAREST_MIPMAP_LINEAR,[_n]:i.LINEAR,[Pr]:i.LINEAR_MIPMAP_NEAREST,[ai]:i.LINEAR_MIPMAP_LINEAR},Mt={[wh]:i.NEVER,[Lh]:i.ALWAYS,[Ah]:i.LESS,[oc]:i.LEQUAL,[Rh]:i.EQUAL,[Dh]:i.GEQUAL,[Ch]:i.GREATER,[Ph]:i.NOTEQUAL};function Nt(b,v){if(v.type===vn&&t.has("OES_texture_float_linear")===!1&&(v.magFilter===_n||v.magFilter===Pr||v.magFilter===Ps||v.magFilter===ai||v.minFilter===_n||v.minFilter===Pr||v.minFilter===Ps||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,it[v.wrapS]),i.texParameteri(b,i.TEXTURE_WRAP_T,it[v.wrapT]),(b===i.TEXTURE_3D||b===i.TEXTURE_2D_ARRAY)&&i.texParameteri(b,i.TEXTURE_WRAP_R,it[v.wrapR]),i.texParameteri(b,i.TEXTURE_MAG_FILTER,ft[v.magFilter]),i.texParameteri(b,i.TEXTURE_MIN_FILTER,ft[v.minFilter]),v.compareFunction&&(i.texParameteri(b,i.TEXTURE_COMPARE_MODE,i.COMPARE_REF_TO_TEXTURE),i.texParameteri(b,i.TEXTURE_COMPARE_FUNC,Mt[v.compareFunction])),t.has("EXT_texture_filter_anisotropic")===!0){if(v.magFilter===Je||v.minFilter!==Ps&&v.minFilter!==ai||v.type===vn&&t.has("OES_texture_float_linear")===!1)return;if(v.anisotropy>1||n.get(v).__currentAnisotropy){const O=t.get("EXT_texture_filter_anisotropic");i.texParameterf(b,O.TEXTURE_MAX_ANISOTROPY_EXT,Math.min(v.anisotropy,s.getMaxAnisotropy())),n.get(v).__currentAnisotropy=v.anisotropy}}}function Wt(b,v){let O=!1;b.__webglInit===void 0&&(b.__webglInit=!0,v.addEventListener("dispose",A));const F=v.source;let k=f.get(F);k===void 0&&(k={},f.set(F,k));const G=j(v);if(G!==b.__cacheKey){k[G]===void 0&&(k[G]={texture:i.createTexture(),usedTimes:0},a.memory.textures++,O=!0),k[G].usedTimes++;const ot=k[b.__cacheKey];ot!==void 0&&(k[b.__cacheKey].usedTimes--,ot.usedTimes===0&&S(v)),b.__cacheKey=G,b.__webglTexture=k[G].texture}return O}function Z(b,v,O){let F=i.TEXTURE_2D;(v.isDataArrayTexture||v.isCompressedArrayTexture)&&(F=i.TEXTURE_2D_ARRAY),v.isData3DTexture&&(F=i.TEXTURE_3D);const k=Wt(b,v),G=v.source;e.bindTexture(F,b.__webglTexture,i.TEXTURE0+O);const ot=n.get(G);if(G.version!==ot.__version||k===!0){e.activeTexture(i.TEXTURE0+O);const Q=Jt.getPrimaries(Jt.workingColorSpace),lt=v.colorSpace===kn?null:Jt.getPrimaries(v.colorSpace),Ft=v.colorSpace===kn||Q===lt?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,Ft);let tt=_(v.image,!1,s.maxTextureSize);tt=oe(v,tt);const mt=r.convert(v.format,v.colorSpace),At=r.convert(v.type);let Dt=E(v.internalFormat,mt,At,v.colorSpace,v.isVideoTexture);Nt(F,v);let pt;const Xt=v.mipmaps,Ot=v.isVideoTexture!==!0,Qt=ot.__version===void 0||k===!0,L=G.dataReady,ct=D(v,tt);if(v.isDepthTexture)Dt=y(v.format===$i,v.type),Qt&&(Ot?e.texStorage2D(i.TEXTURE_2D,1,Dt,tt.width,tt.height):e.texImage2D(i.TEXTURE_2D,0,Dt,tt.width,tt.height,0,mt,At,null));else if(v.isDataTexture)if(Xt.length>0){Ot&&Qt&&e.texStorage2D(i.TEXTURE_2D,ct,Dt,Xt[0].width,Xt[0].height);for(let W=0,K=Xt.length;W0){const ht=hl(pt.width,pt.height,v.format,v.type);for(const ut of v.layerUpdates){const Ut=pt.data.subarray(ut*ht/pt.data.BYTES_PER_ELEMENT,(ut+1)*ht/pt.data.BYTES_PER_ELEMENT);e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,W,0,0,ut,pt.width,pt.height,1,mt,Ut)}v.clearLayerUpdates()}else e.compressedTexSubImage3D(i.TEXTURE_2D_ARRAY,W,0,0,0,pt.width,pt.height,tt.depth,mt,pt.data)}else e.compressedTexImage3D(i.TEXTURE_2D_ARRAY,W,Dt,pt.width,pt.height,tt.depth,0,pt.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,W,0,0,0,pt.width,pt.height,tt.depth,mt,At,pt.data):e.texImage3D(i.TEXTURE_2D_ARRAY,W,Dt,pt.width,pt.height,tt.depth,0,mt,At,pt.data)}else{Ot&&Qt&&e.texStorage2D(i.TEXTURE_2D,ct,Dt,Xt[0].width,Xt[0].height);for(let W=0,K=Xt.length;W0){const W=hl(tt.width,tt.height,v.format,v.type);for(const K of v.layerUpdates){const ht=tt.data.subarray(K*W/tt.data.BYTES_PER_ELEMENT,(K+1)*W/tt.data.BYTES_PER_ELEMENT);e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,K,tt.width,tt.height,1,mt,At,ht)}v.clearLayerUpdates()}else e.texSubImage3D(i.TEXTURE_2D_ARRAY,0,0,0,0,tt.width,tt.height,tt.depth,mt,At,tt.data)}else e.texImage3D(i.TEXTURE_2D_ARRAY,0,Dt,tt.width,tt.height,tt.depth,0,mt,At,tt.data);else if(v.isData3DTexture)Ot?(Qt&&e.texStorage3D(i.TEXTURE_3D,ct,Dt,tt.width,tt.height,tt.depth),L&&e.texSubImage3D(i.TEXTURE_3D,0,0,0,0,tt.width,tt.height,tt.depth,mt,At,tt.data)):e.texImage3D(i.TEXTURE_3D,0,Dt,tt.width,tt.height,tt.depth,0,mt,At,tt.data);else if(v.isFramebufferTexture){if(Qt)if(Ot)e.texStorage2D(i.TEXTURE_2D,ct,Dt,tt.width,tt.height);else{let W=tt.width,K=tt.height;for(let ht=0;ht>=1,K>>=1}}else if(Xt.length>0){if(Ot&&Qt){const W=Et(Xt[0]);e.texStorage2D(i.TEXTURE_2D,ct,Dt,W.width,W.height)}for(let W=0,K=Xt.length;W0&&ct++;const K=Et(mt[0]);e.texStorage2D(i.TEXTURE_CUBE_MAP,ct,Xt,K.width,K.height)}for(let K=0;K<6;K++)if(tt){Ot?L&&e.texSubImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+K,0,0,0,mt[K].width,mt[K].height,Dt,pt,mt[K].data):e.texImage2D(i.TEXTURE_CUBE_MAP_POSITIVE_X+K,0,Xt,mt[K].width,mt[K].height,0,Dt,pt,mt[K].data);for(let ht=0;ht>G),At=Math.max(1,v.height>>G);k===i.TEXTURE_3D||k===i.TEXTURE_2D_ARRAY?e.texImage3D(k,G,lt,mt,At,v.depth,0,ot,Q,null):e.texImage2D(k,G,lt,mt,At,0,ot,Q,null)}e.bindFramebuffer(i.FRAMEBUFFER,b),jt(v)?o.framebufferTexture2DMultisampleEXT(i.FRAMEBUFFER,F,k,tt.__webglTexture,0,qt(v)):(k===i.TEXTURE_2D||k>=i.TEXTURE_CUBE_MAP_POSITIVE_X&&k<=i.TEXTURE_CUBE_MAP_NEGATIVE_Z)&&i.framebufferTexture2D(i.FRAMEBUFFER,F,k,tt.__webglTexture,G),e.bindFramebuffer(i.FRAMEBUFFER,null)}function at(b,v,O){if(i.bindRenderbuffer(i.RENDERBUFFER,b),v.depthBuffer){const F=v.depthTexture,k=F&&F.isDepthTexture?F.type:null,G=y(v.stencilBuffer,k),ot=v.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,Q=qt(v);jt(v)?o.renderbufferStorageMultisampleEXT(i.RENDERBUFFER,Q,G,v.width,v.height):O?i.renderbufferStorageMultisample(i.RENDERBUFFER,Q,G,v.width,v.height):i.renderbufferStorage(i.RENDERBUFFER,G,v.width,v.height),i.framebufferRenderbuffer(i.FRAMEBUFFER,ot,i.RENDERBUFFER,b)}else{const F=v.textures;for(let k=0;k{delete v.__boundDepthTexture,delete v.__depthDisposeCallback,F.removeEventListener("dispose",k)};F.addEventListener("dispose",k),v.__depthDisposeCallback=k}v.__boundDepthTexture=F}if(b.depthTexture&&!v.__autoAllocateDepthBuffer){if(O)throw new Error("target.depthTexture not supported in Cube render targets");wt(v.__webglFramebuffer,b)}else if(O){v.__webglDepthbuffer=[];for(let F=0;F<6;F++)if(e.bindFramebuffer(i.FRAMEBUFFER,v.__webglFramebuffer[F]),v.__webglDepthbuffer[F]===void 0)v.__webglDepthbuffer[F]=i.createRenderbuffer(),at(v.__webglDepthbuffer[F],b,!1);else{const k=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,G=v.__webglDepthbuffer[F];i.bindRenderbuffer(i.RENDERBUFFER,G),i.framebufferRenderbuffer(i.FRAMEBUFFER,k,i.RENDERBUFFER,G)}}else if(e.bindFramebuffer(i.FRAMEBUFFER,v.__webglFramebuffer),v.__webglDepthbuffer===void 0)v.__webglDepthbuffer=i.createRenderbuffer(),at(v.__webglDepthbuffer,b,!1);else{const F=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,k=v.__webglDepthbuffer;i.bindRenderbuffer(i.RENDERBUFFER,k),i.framebufferRenderbuffer(i.FRAMEBUFFER,F,i.RENDERBUFFER,k)}e.bindFramebuffer(i.FRAMEBUFFER,null)}function kt(b,v,O){const F=n.get(b);v!==void 0&&_t(F.__webglFramebuffer,b,b.texture,i.COLOR_ATTACHMENT0,i.TEXTURE_2D,0),O!==void 0&&Pt(b)}function le(b){const v=b.texture,O=n.get(b),F=n.get(v);b.addEventListener("dispose",C);const k=b.textures,G=b.isWebGLCubeRenderTarget===!0,ot=k.length>1;if(ot||(F.__webglTexture===void 0&&(F.__webglTexture=i.createTexture()),F.__version=v.version,a.memory.textures++),G){O.__webglFramebuffer=[];for(let Q=0;Q<6;Q++)if(v.mipmaps&&v.mipmaps.length>0){O.__webglFramebuffer[Q]=[];for(let lt=0;lt0){O.__webglFramebuffer=[];for(let Q=0;Q0&&jt(b)===!1){O.__webglMultisampledFramebuffer=i.createFramebuffer(),O.__webglColorRenderbuffer=[],e.bindFramebuffer(i.FRAMEBUFFER,O.__webglMultisampledFramebuffer);for(let Q=0;Q0)for(let lt=0;lt0)for(let lt=0;lt0){if(jt(b)===!1){const v=b.textures,O=b.width,F=b.height;let k=i.COLOR_BUFFER_BIT;const G=b.stencilBuffer?i.DEPTH_STENCIL_ATTACHMENT:i.DEPTH_ATTACHMENT,ot=n.get(b),Q=v.length>1;if(Q)for(let lt=0;lt0&&t.has("WEBGL_multisampled_render_to_texture")===!0&&v.__useRenderToTexture!==!1}function bt(b){const v=a.render.frame;h.get(b)!==v&&(h.set(b,v),b.update())}function oe(b,v){const O=b.colorSpace,F=b.format,k=b.type;return b.isCompressedTexture===!0||b.isVideoTexture===!0||O!==Ji&&O!==kn&&(Jt.getTransfer(O)===ie?(F!==dn||k!==Dn)&&console.warn("THREE.WebGLTextures: sRGB encoded textures have to use RGBAFormat and UnsignedByteType."):console.error("THREE.WebGLTextures: Unsupported texture color space:",O)),v}function Et(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=V,this.resetTextureUnits=Y,this.setTexture2D=$,this.setTexture2DArray=q,this.setTexture3D=J,this.setTextureCube=X,this.rebindTextures=kt,this.setupRenderTarget=le,this.updateRenderTargetMipmap=Ht,this.updateMultisampleRenderTarget=Ue,this.setupDepthRenderbuffer=Pt,this.setupFrameBufferTexture=_t,this.useMultisampledRTT=jt}function $m(i,t){function e(n,s=kn){let r;const a=Jt.getTransfer(s);if(n===Dn)return i.UNSIGNED_BYTE;if(n===fo)return i.UNSIGNED_SHORT_4_4_4_4;if(n===po)return i.UNSIGNED_SHORT_5_5_5_1;if(n===Ql)return i.UNSIGNED_INT_5_9_9_9_REV;if(n===$l)return i.BYTE;if(n===Jl)return i.SHORT;if(n===vs)return i.UNSIGNED_SHORT;if(n===uo)return i.INT;if(n===ci)return i.UNSIGNED_INT;if(n===vn)return i.FLOAT;if(n===Cn)return i.HALF_FLOAT;if(n===tc)return i.ALPHA;if(n===ec)return i.RGB;if(n===dn)return i.RGBA;if(n===nc)return i.LUMINANCE;if(n===ic)return i.LUMINANCE_ALPHA;if(n===Gi)return i.DEPTH_COMPONENT;if(n===$i)return i.DEPTH_STENCIL;if(n===mo)return i.RED;if(n===go)return i.RED_INTEGER;if(n===sc)return i.RG;if(n===_o)return i.RG_INTEGER;if(n===vo)return i.RGBA_INTEGER;if(n===ar||n===or||n===lr||n===cr)if(a===ie)if(r=t.get("WEBGL_compressed_texture_s3tc_srgb"),r!==null){if(n===ar)return r.COMPRESSED_SRGB_S3TC_DXT1_EXT;if(n===or)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;if(n===lr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;if(n===cr)return r.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT}else return null;else if(r=t.get("WEBGL_compressed_texture_s3tc"),r!==null){if(n===ar)return r.COMPRESSED_RGB_S3TC_DXT1_EXT;if(n===or)return r.COMPRESSED_RGBA_S3TC_DXT1_EXT;if(n===lr)return r.COMPRESSED_RGBA_S3TC_DXT3_EXT;if(n===cr)return r.COMPRESSED_RGBA_S3TC_DXT5_EXT}else return null;if(n===Aa||n===Ra||n===Ca||n===Pa)if(r=t.get("WEBGL_compressed_texture_pvrtc"),r!==null){if(n===Aa)return r.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;if(n===Ra)return r.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;if(n===Ca)return r.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;if(n===Pa)return r.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG}else return null;if(n===Da||n===La||n===Ua)if(r=t.get("WEBGL_compressed_texture_etc"),r!==null){if(n===Da||n===La)return a===ie?r.COMPRESSED_SRGB8_ETC2:r.COMPRESSED_RGB8_ETC2;if(n===Ua)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:r.COMPRESSED_RGBA8_ETC2_EAC}else return null;if(n===Ia||n===Na||n===Fa||n===Oa||n===Ba||n===za||n===ka||n===Ha||n===Va||n===Ga||n===Wa||n===Xa||n===Ya||n===qa)if(r=t.get("WEBGL_compressed_texture_astc"),r!==null){if(n===Ia)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR:r.COMPRESSED_RGBA_ASTC_4x4_KHR;if(n===Na)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR:r.COMPRESSED_RGBA_ASTC_5x4_KHR;if(n===Fa)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR:r.COMPRESSED_RGBA_ASTC_5x5_KHR;if(n===Oa)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR:r.COMPRESSED_RGBA_ASTC_6x5_KHR;if(n===Ba)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR:r.COMPRESSED_RGBA_ASTC_6x6_KHR;if(n===za)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR:r.COMPRESSED_RGBA_ASTC_8x5_KHR;if(n===ka)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR:r.COMPRESSED_RGBA_ASTC_8x6_KHR;if(n===Ha)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR:r.COMPRESSED_RGBA_ASTC_8x8_KHR;if(n===Va)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR:r.COMPRESSED_RGBA_ASTC_10x5_KHR;if(n===Ga)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR:r.COMPRESSED_RGBA_ASTC_10x6_KHR;if(n===Wa)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR:r.COMPRESSED_RGBA_ASTC_10x8_KHR;if(n===Xa)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR:r.COMPRESSED_RGBA_ASTC_10x10_KHR;if(n===Ya)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR:r.COMPRESSED_RGBA_ASTC_12x10_KHR;if(n===qa)return a===ie?r.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR:r.COMPRESSED_RGBA_ASTC_12x12_KHR}else return null;if(n===hr||n===ja||n===Za)if(r=t.get("EXT_texture_compression_bptc"),r!==null){if(n===hr)return a===ie?r.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT:r.COMPRESSED_RGBA_BPTC_UNORM_EXT;if(n===ja)return r.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;if(n===Za)return r.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT}else return null;if(n===rc||n===Ka||n===$a||n===Ja)if(r=t.get("EXT_texture_compression_rgtc"),r!==null){if(n===hr)return r.COMPRESSED_RED_RGTC1_EXT;if(n===Ka)return r.COMPRESSED_SIGNED_RED_RGTC1_EXT;if(n===$a)return r.COMPRESSED_RED_GREEN_RGTC2_EXT;if(n===Ja)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 Jm={type:"move"};class la{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),p=this._getHandJoint(c,_);m!==null&&(p.matrix.fromArray(m.transform.matrix),p.matrix.decompose(p.position,p.rotation,p.scale),p.matrixWorldNeedsUpdate=!0,p.jointRadius=m.radius),p.visible=m!==null}const h=c.joints["index-finger-tip"],d=c.joints["thumb-tip"],f=h.position.distanceTo(d.position),u=.02,g=.005;c.inputState.pinching&&f>u+g?(c.inputState.pinching=!1,this.dispatchEvent({type:"pinchend",handedness:t.handedness,target:this})):!c.inputState.pinching&&f<=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(Jm)))}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 Qm=` void main() { gl_Position = vec4( position, 1.0 ); -}`,Vm=` +}`,tg=` uniform sampler2DArray depthColor; uniform float depthWidth; uniform float depthHeight; @@ -3824,7 +3824,7 @@ void main() { } -}`;class Gm{constructor(){this.texture=null,this.mesh=null,this.depthNear=0,this.depthFar=0}init(t,e,n){if(this.texture===null){const s=new Ce,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 Ne({vertexShader:km,fragmentShader:Vm,uniforms:{depthColor:{value:this.texture},depthWidth:{value:e.z},depthHeight:{value:e.w}}});this.mesh=new Me(new us(20,20),n)}return this.mesh}reset(){this.texture=null,this.mesh=null}getDepthTexture(){return this.texture}}class Wm extends ii{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,f=null,p=null,g=null;const v=new Gm,m=e.getContextAttributes();let u=null,T=null;const b=[],y=[],L=new vt;let R=null;const A=new Xe;A.viewport=new le;const U=new Xe;U.viewport=new le;const S=[A,U],M=new ou;let D=null,W=null;this.cameraAutoUpdate=!0,this.enabled=!1,this.isPresenting=!1,this.getController=function(Y){let et=b[Y];return et===void 0&&(et=new Zr,b[Y]=et),et.getTargetRaySpace()},this.getControllerGrip=function(Y){let et=b[Y];return et===void 0&&(et=new Zr,b[Y]=et),et.getGripSpace()},this.getHand=function(Y){let et=b[Y];return et===void 0&&(et=new Zr,b[Y]=et),et.getHandSpace()};function z(Y){const et=y.indexOf(Y.inputSource);if(et===-1)return;const xt=b[et];xt!==void 0&&(xt.update(Y.inputSource,Y.frame,c||a),xt.dispatchEvent({type:Y.type,data:Y.inputSource}))}function V(){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",V),s.removeEventListener("inputsourceschange",$);for(let Y=0;Y=0&&(y[at]=null,b[at].disconnect(xt))}for(let et=0;et=y.length){y.push(xt),at=Ut;break}else if(y[Ut]===null){y[Ut]=xt,at=Ut;break}if(at===-1)break}const wt=b[at];wt&&wt.connect(xt)}}const G=new P,J=new P;function k(Y,et,xt){G.setFromMatrixPosition(et.matrixWorld),J.setFromMatrixPosition(xt.matrixWorld);const at=G.distanceTo(J),wt=et.projectionMatrix.elements,Ut=xt.projectionMatrix.elements,Gt=wt[14]/(wt[10]-1),ce=wt[14]/(wt[10]+1),rt=(wt[9]+1)/wt[5],Ct=(wt[9]-1)/wt[5],w=(wt[8]-1)/wt[0],ve=(Ut[8]+1)/Ut[0],Ft=Gt*w,kt=Gt*ve,Mt=at/(-w+ve),ie=Mt*-w;if(et.matrixWorld.decompose(Y.position,Y.quaternion,Y.scale),Y.translateX(ie),Y.translateZ(Mt),Y.matrixWorld.compose(Y.position,Y.quaternion,Y.scale),Y.matrixWorldInverse.copy(Y.matrixWorld).invert(),wt[10]===-1)Y.projectionMatrix.copy(et.projectionMatrix),Y.projectionMatrixInverse.copy(et.projectionMatrixInverse);else{const Et=Gt+Mt,E=ce+Mt,_=Ft-ie,F=kt+(at-ie),j=rt*ce/E*Et,K=Ct*ce/E*Et;Y.projectionMatrix.makePerspective(_,F,j,K,Et,E),Y.projectionMatrixInverse.copy(Y.projectionMatrix).invert()}}function it(Y,et){et===null?Y.matrixWorld.copy(Y.matrix):Y.matrixWorld.multiplyMatrices(et.matrixWorld,Y.matrix),Y.matrixWorldInverse.copy(Y.matrixWorld).invert()}this.updateCamera=function(Y){if(s===null)return;let et=Y.near,xt=Y.far;v.texture!==null&&(v.depthNear>0&&(et=v.depthNear),v.depthFar>0&&(xt=v.depthFar)),M.near=U.near=A.near=et,M.far=U.far=A.far=xt,(D!==M.near||W!==M.far)&&(s.updateRenderState({depthNear:M.near,depthFar:M.far}),D=M.near,W=M.far),A.layers.mask=Y.layers.mask|2,U.layers.mask=Y.layers.mask|4,M.layers.mask=A.layers.mask|U.layers.mask;const at=Y.parent,wt=M.cameras;it(M,at);for(let Ut=0;Ut0&&(m.alphaTest.value=u.alphaTest);const T=t.get(u),b=T.envMap,y=T.envMapRotation;b&&(m.envMap.value=b,jn.copy(y),jn.x*=-1,jn.y*=-1,jn.z*=-1,b.isCubeTexture&&b.isRenderTargetTexture===!1&&(jn.y*=-1,jn.z*=-1),m.envMapRotation.value.setFromMatrix4(Xm.makeRotationFromEuler(jn)),m.flipEnvMap.value=b.isCubeTexture&&b.isRenderTargetTexture===!1?-1:1,m.reflectivity.value=u.reflectivity,m.ior.value=u.ior,m.refractionRatio.value=u.refractionRatio),u.lightMap&&(m.lightMap.value=u.lightMap,m.lightMapIntensity.value=u.lightMapIntensity,e(u.lightMap,m.lightMapTransform)),u.aoMap&&(m.aoMap.value=u.aoMap,m.aoMapIntensity.value=u.aoMapIntensity,e(u.aoMap,m.aoMapTransform))}function a(m,u){m.diffuse.value.copy(u.color),m.opacity.value=u.opacity,u.map&&(m.map.value=u.map,e(u.map,m.mapTransform))}function o(m,u){m.dashSize.value=u.dashSize,m.totalSize.value=u.dashSize+u.gapSize,m.scale.value=u.scale}function l(m,u,T,b){m.diffuse.value.copy(u.color),m.opacity.value=u.opacity,m.size.value=u.size*T,m.scale.value=b*.5,u.map&&(m.map.value=u.map,e(u.map,m.uvTransform)),u.alphaMap&&(m.alphaMap.value=u.alphaMap,e(u.alphaMap,m.alphaMapTransform)),u.alphaTest>0&&(m.alphaTest.value=u.alphaTest)}function c(m,u){m.diffuse.value.copy(u.color),m.opacity.value=u.opacity,m.rotation.value=u.rotation,u.map&&(m.map.value=u.map,e(u.map,m.mapTransform)),u.alphaMap&&(m.alphaMap.value=u.alphaMap,e(u.alphaMap,m.alphaMapTransform)),u.alphaTest>0&&(m.alphaTest.value=u.alphaTest)}function h(m,u){m.specular.value.copy(u.specular),m.shininess.value=Math.max(u.shininess,1e-4)}function d(m,u){u.gradientMap&&(m.gradientMap.value=u.gradientMap)}function f(m,u){m.metalness.value=u.metalness,u.metalnessMap&&(m.metalnessMap.value=u.metalnessMap,e(u.metalnessMap,m.metalnessMapTransform)),m.roughness.value=u.roughness,u.roughnessMap&&(m.roughnessMap.value=u.roughnessMap,e(u.roughnessMap,m.roughnessMapTransform)),u.envMap&&(m.envMapIntensity.value=u.envMapIntensity)}function p(m,u,T){m.ior.value=u.ior,u.sheen>0&&(m.sheenColor.value.copy(u.sheenColor).multiplyScalar(u.sheen),m.sheenRoughness.value=u.sheenRoughness,u.sheenColorMap&&(m.sheenColorMap.value=u.sheenColorMap,e(u.sheenColorMap,m.sheenColorMapTransform)),u.sheenRoughnessMap&&(m.sheenRoughnessMap.value=u.sheenRoughnessMap,e(u.sheenRoughnessMap,m.sheenRoughnessMapTransform))),u.clearcoat>0&&(m.clearcoat.value=u.clearcoat,m.clearcoatRoughness.value=u.clearcoatRoughness,u.clearcoatMap&&(m.clearcoatMap.value=u.clearcoatMap,e(u.clearcoatMap,m.clearcoatMapTransform)),u.clearcoatRoughnessMap&&(m.clearcoatRoughnessMap.value=u.clearcoatRoughnessMap,e(u.clearcoatRoughnessMap,m.clearcoatRoughnessMapTransform)),u.clearcoatNormalMap&&(m.clearcoatNormalMap.value=u.clearcoatNormalMap,e(u.clearcoatNormalMap,m.clearcoatNormalMapTransform),m.clearcoatNormalScale.value.copy(u.clearcoatNormalScale),u.side===He&&m.clearcoatNormalScale.value.negate())),u.dispersion>0&&(m.dispersion.value=u.dispersion),u.iridescence>0&&(m.iridescence.value=u.iridescence,m.iridescenceIOR.value=u.iridescenceIOR,m.iridescenceThicknessMinimum.value=u.iridescenceThicknessRange[0],m.iridescenceThicknessMaximum.value=u.iridescenceThicknessRange[1],u.iridescenceMap&&(m.iridescenceMap.value=u.iridescenceMap,e(u.iridescenceMap,m.iridescenceMapTransform)),u.iridescenceThicknessMap&&(m.iridescenceThicknessMap.value=u.iridescenceThicknessMap,e(u.iridescenceThicknessMap,m.iridescenceThicknessMapTransform))),u.transmission>0&&(m.transmission.value=u.transmission,m.transmissionSamplerMap.value=T.texture,m.transmissionSamplerSize.value.set(T.width,T.height),u.transmissionMap&&(m.transmissionMap.value=u.transmissionMap,e(u.transmissionMap,m.transmissionMapTransform)),m.thickness.value=u.thickness,u.thicknessMap&&(m.thicknessMap.value=u.thicknessMap,e(u.thicknessMap,m.thicknessMapTransform)),m.attenuationDistance.value=u.attenuationDistance,m.attenuationColor.value.copy(u.attenuationColor)),u.anisotropy>0&&(m.anisotropyVector.value.set(u.anisotropy*Math.cos(u.anisotropyRotation),u.anisotropy*Math.sin(u.anisotropyRotation)),u.anisotropyMap&&(m.anisotropyMap.value=u.anisotropyMap,e(u.anisotropyMap,m.anisotropyMapTransform))),m.specularIntensity.value=u.specularIntensity,m.specularColor.value.copy(u.specularColor),u.specularColorMap&&(m.specularColorMap.value=u.specularColorMap,e(u.specularColorMap,m.specularColorMapTransform)),u.specularIntensityMap&&(m.specularIntensityMap.value=u.specularIntensityMap,e(u.specularIntensityMap,m.specularIntensityMapTransform))}function g(m,u){u.matcap&&(m.matcap.value=u.matcap)}function v(m,u){const T=t.get(u).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 y=b.program;n.uniformBlockBinding(T,y)}function c(T,b){let y=s[T.id];y===void 0&&(g(T),y=h(T),s[T.id]=y,T.addEventListener("dispose",m));const L=b.program;n.updateUBOMapping(T,L);const R=t.render.frame;r[T.id]!==R&&(f(T),r[T.id]=R)}function h(T){const b=d();T.__bindingPointIndex=b;const y=i.createBuffer(),L=T.__size,R=T.usage;return i.bindBuffer(i.UNIFORM_BUFFER,y),i.bufferData(i.UNIFORM_BUFFER,L,R),i.bindBuffer(i.UNIFORM_BUFFER,null),i.bindBufferBase(i.UNIFORM_BUFFER,b,y),y}function d(){for(let T=0;T0&&(y+=L-R),T.__size=y,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 y=a.indexOf(b.__bindingPointIndex);a.splice(y,1),i.deleteBuffer(s[b.id]),delete s[b.id],delete r[b.id]}function u(){for(const T in s)i.deleteBuffer(s[T]);a=[],s={},r={}}return{bind:l,update:c,dispose:u}}class jm{constructor(t={}){const{canvas:e=yh(),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:f=!1}=t;this.isWebGLRenderer=!0;let p;if(n!==null){if(typeof WebGLRenderingContext<"u"&&n instanceof WebGLRenderingContext)throw new Error("THREE.WebGLRenderer: WebGL 1 is not supported since r163.");p=n.getContextAttributes().alpha}else p=a;const g=new Uint32Array(4),v=new Int32Array(4);let m=null,u=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=Je,this.toneMapping=On,this.toneMappingExposure=1;const y=this;let L=!1,R=0,A=0,U=null,S=-1,M=null;const D=new le,W=new le;let z=null;const V=new pt(0);let $=0,G=e.width,J=e.height,k=1,it=null,ut=null;const yt=new le(0,0,G,J),Lt=new le(0,0,G,J);let jt=!1;const Y=new to;let et=!1,xt=!1;this.transmissionResolutionScale=1;const at=new ne,wt=new ne,Ut=new P,Gt=new le,ce={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};let rt=!1;function Ct(){return U===null?k:1}let w=n;function ve(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${Xa}`),e.addEventListener("webglcontextlost",Z,!1),e.addEventListener("webglcontextrestored",ht,!1),e.addEventListener("webglcontextcreationerror",lt,!1),w===null){const I="webgl2";if(w=ve(I,x),w===null)throw ve(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 Ft,kt,Mt,ie,Et,E,_,F,j,K,X,St,ot,dt,Zt,tt,mt,bt,Pt,ft,Yt,zt,Xt,C;function nt(){Ft=new ip(w),Ft.init(),zt=new zm(w,Ft),kt=new $f(w,Ft,t,zt),Mt=new Om(w,Ft),kt.reverseDepthBuffer&&f&&Mt.buffers.depth.setReversed(!0),ie=new ap(w),Et=new bm,E=new Bm(w,Ft,Mt,Et,kt,zt,ie),_=new Qf(y),F=new np(y),j=new du(w),Xt=new Zf(w,j),K=new sp(w,j,ie,Xt),X=new lp(w,K,j,ie),Pt=new op(w,kt,E),tt=new Jf(Et),St=new Em(y,_,F,Ft,kt,Xt,tt),ot=new Ym(y,Et),dt=new wm,Zt=new Lm(Ft),bt=new jf(y,_,F,Mt,X,p,l),mt=new Nm(y,X,kt),C=new qm(w,ie,kt,Mt),ft=new Kf(w,Ft,ie),Yt=new rp(w,Ft,ie),ie.programs=St.programs,y.capabilities=kt,y.extensions=Ft,y.properties=Et,y.renderLists=dt,y.shadowMap=mt,y.state=Mt,y.info=ie}nt();const H=new Wm(y,w);this.xr=H,this.getContext=function(){return w},this.getContextAttributes=function(){return w.getContextAttributes()},this.forceContextLoss=function(){const x=Ft.get("WEBGL_lose_context");x&&x.loseContext()},this.forceContextRestore=function(){const x=Ft.get("WEBGL_lose_context");x&&x.restoreContext()},this.getPixelRatio=function(){return k},this.setPixelRatio=function(x){x!==void 0&&(k=x,this.setSize(G,J,!1))},this.getSize=function(x){return x.set(G,J)},this.setSize=function(x,I,O=!0){if(H.isPresenting){console.warn("THREE.WebGLRenderer: Can't change size while VR device is presenting.");return}G=x,J=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(G*k,J*k).floor()},this.setDrawingBufferSize=function(x,I,O){G=x,J=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(D)},this.getViewport=function(x){return x.copy(yt)},this.setViewport=function(x,I,O,B){x.isVector4?yt.set(x.x,x.y,x.z,x.w):yt.set(x,I,O,B),Mt.viewport(D.copy(yt).multiplyScalar(k).round())},this.getScissor=function(x){return x.copy(Lt)},this.setScissor=function(x,I,O,B){x.isVector4?Lt.set(x.x,x.y,x.z,x.w):Lt.set(x,I,O,B),Mt.scissor(W.copy(Lt).multiplyScalar(k).round())},this.getScissorTest=function(){return jt},this.setScissorTest=function(x){Mt.setScissorTest(jt=x)},this.setOpaqueSort=function(x){it=x},this.setTransparentSort=function(x){ut=x},this.getClearColor=function(x){return x.copy(bt.getClearColor())},this.setClearColor=function(){bt.setClearColor.apply(bt,arguments)},this.getClearAlpha=function(){return bt.getClearAlpha()},this.setClearAlpha=function(){bt.setClearAlpha.apply(bt,arguments)},this.clear=function(x=!0,I=!0,O=!0){let B=0;if(x){let N=!1;if(U!==null){const Q=U.texture.format;N=Q===Ja||Q===$a||Q===Ka}if(N){const Q=U.texture.type,ct=Q===An||Q===ei||Q===os||Q===zi||Q===qa||Q===ja,gt=bt.getClearColor(),_t=bt.getClearAlpha(),It=gt.r,Nt=gt.g,Tt=gt.b;ct?(g[0]=It,g[1]=Nt,g[2]=Tt,g[3]=_t,w.clearBufferuiv(w.COLOR,0,g)):(v[0]=It,v[1]=Nt,v[2]=Tt,v[3]=_t,w.clearBufferiv(w.COLOR,0,v))}else B|=w.COLOR_BUFFER_BIT}I&&(B|=w.DEPTH_BUFFER_BIT),O&&(B|=w.STENCIL_BUFFER_BIT,this.state.buffers.stencil.setMask(4294967295)),w.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",ht,!1),e.removeEventListener("webglcontextcreationerror",lt,!1),bt.dispose(),dt.dispose(),Zt.dispose(),Et.dispose(),_.dispose(),F.dispose(),X.dispose(),Xt.dispose(),C.dispose(),St.dispose(),H.dispose(),H.removeEventListener("sessionstart",so),H.removeEventListener("sessionend",ro),kn.stop()};function Z(x){x.preventDefault(),console.log("THREE.WebGLRenderer: Context Lost."),L=!0}function ht(){console.log("THREE.WebGLRenderer: Context Restored."),L=!1;const x=ie.autoReset,I=mt.enabled,O=mt.autoUpdate,B=mt.needsUpdate,N=mt.type;nt(),ie.autoReset=x,mt.enabled=I,mt.autoUpdate=O,mt.needsUpdate=B,mt.type=N}function lt(x){console.error("THREE.WebGLRenderer: A WebGL context could not be created. Reason: ",x.statusMessage)}function Ot(x){const I=x.target;I.removeEventListener("dispose",Ot),he(I)}function he(x){Ee(x),Et.remove(x)}function Ee(x){const I=Et.get(x).programs;I!==void 0&&(I.forEach(function(O){St.releaseProgram(O)}),x.isShaderMaterial&&St.releaseShaderCache(x))}this.renderBufferDirect=function(x,I,O,B,N,Q){I===null&&(I=ce);const ct=N.isMesh&&N.matrixWorld.determinant()<0,gt=xc(x,I,O,B,N);Mt.setMaterial(B,ct);let _t=O.index,It=1;if(B.wireframe===!0){if(_t=K.getWireframeAttribute(O),_t===void 0)return;It=2}const Nt=O.drawRange,Tt=O.attributes.position;let Kt=Nt.start*It,te=(Nt.start+Nt.count)*It;Q!==null&&(Kt=Math.max(Kt,Q.start*It),te=Math.min(te,(Q.start+Q.count)*It)),_t!==null?(Kt=Math.max(Kt,0),te=Math.min(te,_t.count)):Tt!=null&&(Kt=Math.max(Kt,0),te=Math.min(te,Tt.count));const me=te-Kt;if(me<0||me===1/0)return;Xt.setup(N,B,gt,O,_t);let fe,$t=ft;if(_t!==null&&(fe=j.get(_t),$t=Yt,$t.setIndex(fe)),N.isMesh)B.wireframe===!0?(Mt.setLineWidth(B.wireframeLinewidth*Ct()),$t.setMode(w.LINES)):$t.setMode(w.TRIANGLES);else if(N.isLine){let At=B.linewidth;At===void 0&&(At=1),Mt.setLineWidth(At*Ct()),N.isLineSegments?$t.setMode(w.LINES):N.isLineLoop?$t.setMode(w.LINE_LOOP):$t.setMode(w.LINE_STRIP)}else N.isPoints?$t.setMode(w.POINTS):N.isSprite&&$t.setMode(w.TRIANGLES);if(N.isBatchedMesh)if(N._multiDrawInstances!==null)$t.renderMultiDrawInstances(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount,N._multiDrawInstances);else if(Ft.get("WEBGL_multi_draw"))$t.renderMultiDraw(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount);else{const At=N._multiDrawStarts,Te=N._multiDrawCounts,ee=N._multiDrawCount,en=_t?j.get(_t).bytesPerElement:1,ai=Et.get(B).currentProgram.getUniforms();for(let ke=0;ke{function Q(){if(B.forEach(function(ct){Et.get(ct).currentProgram.isReady()&&B.delete(ct)}),B.size===0){N(x);return}setTimeout(Q,10)}Ft.get("KHR_parallel_shader_compile")!==null?Q():setTimeout(Q,10)})};let tn=null;function mn(x){tn&&tn(x)}function so(){kn.stop()}function ro(){kn.start()}const kn=new uc;kn.setAnimationLoop(mn),typeof self<"u"&&kn.setContext(self),this.setAnimationLoop=function(x){tn=x,H.setAnimationLoop(x),x===null?kn.stop():kn.start()},H.addEventListener("sessionstart",so),H.addEventListener("sessionend",ro),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(L===!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(y,x,I,U),u=Zt.get(x,b.length),u.init(I),b.push(u),wt.multiplyMatrices(I.projectionMatrix,I.matrixWorldInverse),Y.setFromProjectionMatrix(wt),xt=this.localClippingEnabled,et=tt.init(this.clippingPlanes,xt),m=dt.get(x,T.length),m.init(),T.push(m),H.enabled===!0&&H.isPresenting===!0){const Q=y.xr.getDepthSensingMesh();Q!==null&&pr(Q,I,-1/0,y.sortObjects)}pr(x,I,0,y.sortObjects),m.finish(),y.sortObjects===!0&&m.sort(it,ut),rt=H.enabled===!1||H.isPresenting===!1||H.hasDepthSensing()===!1,rt&&bt.addToRenderList(m,x),this.info.render.frame++,et===!0&&tt.beginShadows();const O=u.state.shadowsArray;mt.render(O,x,I),et===!0&&tt.endShadows(),this.info.autoReset===!0&&this.info.reset();const B=m.opaque,N=m.transmissive;if(u.setupLights(),I.isArrayCamera){const Q=I.cameras;if(N.length>0)for(let ct=0,gt=Q.length;ct0&&oo(B,N,x,I),rt&&bt.render(x),ao(m,x,I);U!==null&&A===0&&(E.updateMultisampleRenderTarget(U),E.updateRenderTargetMipmap(U)),x.isScene===!0&&x.onAfterRender(y,x,I),Xt.resetDefaultState(),S=-1,M=null,b.pop(),b.length>0?(u=b[b.length-1],et===!0&&tt.setGlobalState(y.clippingPlanes,u.state.camera)):u=null,T.pop(),T.length>0?m=T[T.length-1]:m=null};function pr(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)u.pushLight(x),x.castShadow&&u.pushShadow(x);else if(x.isSprite){if(!x.frustumCulled||Y.intersectsSprite(x)){B&&Gt.setFromMatrixPosition(x.matrixWorld).applyMatrix4(wt);const ct=X.update(x),gt=x.material;gt.visible&&m.push(x,ct,gt,O,Gt.z,null)}}else if((x.isMesh||x.isLine||x.isPoints)&&(!x.frustumCulled||Y.intersectsObject(x))){const ct=X.update(x),gt=x.material;if(B&&(x.boundingSphere!==void 0?(x.boundingSphere===null&&x.computeBoundingSphere(),Gt.copy(x.boundingSphere.center)):(ct.boundingSphere===null&&ct.computeBoundingSphere(),Gt.copy(ct.boundingSphere.center)),Gt.applyMatrix4(x.matrixWorld).applyMatrix4(wt)),Array.isArray(gt)){const _t=ct.groups;for(let It=0,Nt=_t.length;It0&&fs(N,I,O),Q.length>0&&fs(Q,I,O),ct.length>0&&fs(ct,I,O),Mt.buffers.depth.setTest(!0),Mt.buffers.depth.setMask(!0),Mt.buffers.color.setMask(!0),Mt.setPolygonOffset(!1)}function oo(x,I,O,B){if((O.isScene===!0?O.overrideMaterial:null)!==null)return;u.state.transmissionRenderTarget[B.id]===void 0&&(u.state.transmissionRenderTarget[B.id]=new on(1,1,{generateMipmaps:!0,type:Ft.has("EXT_color_buffer_half_float")||Ft.has("EXT_color_buffer_float")?Tn:An,minFilter:Qn,samples:4,stencilBuffer:r,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:Jt.workingColorSpace}));const Q=u.state.transmissionRenderTarget[B.id],ct=B.viewport||D;Q.setSize(ct.z*y.transmissionResolutionScale,ct.w*y.transmissionResolutionScale);const gt=y.getRenderTarget();y.setRenderTarget(Q),y.getClearColor(V),$=y.getClearAlpha(),$<1&&y.setClearColor(16777215,.5),y.clear(),rt&&bt.render(O);const _t=y.toneMapping;y.toneMapping=On;const It=B.viewport;if(B.viewport!==void 0&&(B.viewport=void 0),u.setupLightsView(B),et===!0&&tt.setGlobalState(y.clippingPlanes,B),fs(x,O,B),E.updateMultisampleRenderTarget(Q),E.updateRenderTargetMipmap(Q),Ft.has("WEBGL_multisampled_render_to_texture")===!1){let Nt=!1;for(let Tt=0,Kt=I.length;Tt0),Tt=!!O.morphAttributes.position,Kt=!!O.morphAttributes.normal,te=!!O.morphAttributes.color;let me=On;B.toneMapped&&(U===null||U.isXRRenderTarget===!0)&&(me=y.toneMapping);const fe=O.morphAttributes.position||O.morphAttributes.normal||O.morphAttributes.color,$t=fe!==void 0?fe.length:0,At=Et.get(B),Te=u.state.lights;if(et===!0&&(xt===!0||x!==M)){const De=x===M&&B.id===S;tt.setState(B,x,De)}let ee=!1;B.version===At.__version?(At.needsLights&&At.lightsStateVersion!==Te.state.version||At.outputColorSpace!==gt||N.isBatchedMesh&&At.batching===!1||!N.isBatchedMesh&&At.batching===!0||N.isBatchedMesh&&At.batchingColor===!0&&N.colorTexture===null||N.isBatchedMesh&&At.batchingColor===!1&&N.colorTexture!==null||N.isInstancedMesh&&At.instancing===!1||!N.isInstancedMesh&&At.instancing===!0||N.isSkinnedMesh&&At.skinning===!1||!N.isSkinnedMesh&&At.skinning===!0||N.isInstancedMesh&&At.instancingColor===!0&&N.instanceColor===null||N.isInstancedMesh&&At.instancingColor===!1&&N.instanceColor!==null||N.isInstancedMesh&&At.instancingMorph===!0&&N.morphTexture===null||N.isInstancedMesh&&At.instancingMorph===!1&&N.morphTexture!==null||At.envMap!==_t||B.fog===!0&&At.fog!==Q||At.numClippingPlanes!==void 0&&(At.numClippingPlanes!==tt.numPlanes||At.numIntersection!==tt.numIntersection)||At.vertexAlphas!==It||At.vertexTangents!==Nt||At.morphTargets!==Tt||At.morphNormals!==Kt||At.morphColors!==te||At.toneMapping!==me||At.morphTargetsCount!==$t)&&(ee=!0):(ee=!0,At.__version=B.version);let en=At.currentProgram;ee===!0&&(en=ps(B,I,N));let ai=!1,ke=!1,Xi=!1;const ue=en.getUniforms(),je=At.uniforms;if(Mt.useProgram(en.program)&&(ai=!0,ke=!0,Xi=!0),B.id!==S&&(S=B.id,ke=!0),ai||M!==x){Mt.buffers.depth.getReversed()?(at.copy(x.projectionMatrix),bh(at),Th(at),ue.setValue(w,"projectionMatrix",at)):ue.setValue(w,"projectionMatrix",x.projectionMatrix),ue.setValue(w,"viewMatrix",x.matrixWorldInverse);const Fe=ue.map.cameraPosition;Fe!==void 0&&Fe.setValue(w,Ut.setFromMatrixPosition(x.matrixWorld)),kt.logarithmicDepthBuffer&&ue.setValue(w,"logDepthBufFC",2/(Math.log(x.far+1)/Math.LN2)),(B.isMeshPhongMaterial||B.isMeshToonMaterial||B.isMeshLambertMaterial||B.isMeshBasicMaterial||B.isMeshStandardMaterial||B.isShaderMaterial)&&ue.setValue(w,"isOrthographic",x.isOrthographicCamera===!0),M!==x&&(M=x,ke=!0,Xi=!0)}if(N.isSkinnedMesh){ue.setOptional(w,N,"bindMatrix"),ue.setOptional(w,N,"bindMatrixInverse");const De=N.skeleton;De&&(De.boneTexture===null&&De.computeBoneTexture(),ue.setValue(w,"boneTexture",De.boneTexture,E))}N.isBatchedMesh&&(ue.setOptional(w,N,"batchingTexture"),ue.setValue(w,"batchingTexture",N._matricesTexture,E),ue.setOptional(w,N,"batchingIdTexture"),ue.setValue(w,"batchingIdTexture",N._indirectTexture,E),ue.setOptional(w,N,"batchingColorTexture"),N._colorsTexture!==null&&ue.setValue(w,"batchingColorTexture",N._colorsTexture,E));const Ze=O.morphAttributes;if((Ze.position!==void 0||Ze.normal!==void 0||Ze.color!==void 0)&&Pt.update(N,O,en),(ke||At.receiveShadow!==N.receiveShadow)&&(At.receiveShadow=N.receiveShadow,ue.setValue(w,"receiveShadow",N.receiveShadow)),B.isMeshGouraudMaterial&&B.envMap!==null&&(je.envMap.value=_t,je.flipEnvMap.value=_t.isCubeTexture&&_t.isRenderTargetTexture===!1?-1:1),B.isMeshStandardMaterial&&B.envMap===null&&I.environment!==null&&(je.envMapIntensity.value=I.environmentIntensity),ke&&(ue.setValue(w,"toneMappingExposure",y.toneMappingExposure),At.needsLights&&Mc(je,Xi),Q&&B.fog===!0&&ot.refreshFogUniforms(je,Q),ot.refreshMaterialUniforms(je,B,k,J,u.state.transmissionRenderTarget[x.id]),$s.upload(w,co(At),je,E)),B.isShaderMaterial&&B.uniformsNeedUpdate===!0&&($s.upload(w,co(At),je,E),B.uniformsNeedUpdate=!1),B.isSpriteMaterial&&ue.setValue(w,"center",N.center),ue.setValue(w,"modelViewMatrix",N.modelViewMatrix),ue.setValue(w,"normalMatrix",N.normalMatrix),ue.setValue(w,"modelMatrix",N.matrixWorld),B.isShaderMaterial||B.isRawShaderMaterial){const De=B.uniformsGroups;for(let Fe=0,mr=De.length;Fe0&&E.useMultisampledRTT(x)===!1?N=Et.get(x).__webglMultisampledFramebuffer:Array.isArray(Nt)?N=Nt[O]:N=Nt,D.copy(x.viewport),W.copy(x.scissor),z=x.scissorTest}else D.copy(yt).multiplyScalar(k).floor(),W.copy(Lt).multiplyScalar(k).floor(),z=jt;if(O!==0&&(N=yc),Mt.bindFramebuffer(w.FRAMEBUFFER,N)&&B&&Mt.drawBuffers(x,N),Mt.viewport(D),Mt.scissor(W),Mt.setScissorTest(z),Q){const _t=Et.get(x.texture);w.framebufferTexture2D(w.FRAMEBUFFER,w.COLOR_ATTACHMENT0,w.TEXTURE_CUBE_MAP_POSITIVE_X+I,_t.__webglTexture,O)}else if(ct){const _t=Et.get(x.texture),It=I;w.framebufferTextureLayer(w.FRAMEBUFFER,w.COLOR_ATTACHMENT0,_t.__webglTexture,O,It)}else if(x!==null&&O!==0){const _t=Et.get(x.texture);w.framebufferTexture2D(w.FRAMEBUFFER,w.COLOR_ATTACHMENT0,w.TEXTURE_2D,_t.__webglTexture,O)}S=-1},this.readRenderTargetPixels=function(x,I,O,B,N,Q,ct){if(!(x&&x.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let gt=Et.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&ct!==void 0&&(gt=gt[ct]),gt){Mt.bindFramebuffer(w.FRAMEBUFFER,gt);try{const _t=x.texture,It=_t.format,Nt=_t.type;if(!kt.textureFormatReadable(It)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!kt.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&&w.readPixels(I,O,B,N,zt.convert(It),zt.convert(Nt),Q)}finally{const _t=U!==null?Et.get(U).__webglFramebuffer:null;Mt.bindFramebuffer(w.FRAMEBUFFER,_t)}}},this.readRenderTargetPixelsAsync=async function(x,I,O,B,N,Q,ct){if(!(x&&x.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let gt=Et.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&ct!==void 0&&(gt=gt[ct]),gt){const _t=x.texture,It=_t.format,Nt=_t.type;if(!kt.textureFormatReadable(It))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!kt.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){Mt.bindFramebuffer(w.FRAMEBUFFER,gt);const Tt=w.createBuffer();w.bindBuffer(w.PIXEL_PACK_BUFFER,Tt),w.bufferData(w.PIXEL_PACK_BUFFER,Q.byteLength,w.STREAM_READ),w.readPixels(I,O,B,N,zt.convert(It),zt.convert(Nt),0);const Kt=U!==null?Et.get(U).__webglFramebuffer:null;Mt.bindFramebuffer(w.FRAMEBUFFER,Kt);const te=w.fenceSync(w.SYNC_GPU_COMMANDS_COMPLETE,0);return w.flush(),await Eh(w,te,4),w.bindBuffer(w.PIXEL_PACK_BUFFER,Tt),w.getBufferSubData(w.PIXEL_PACK_BUFFER,0,Q),w.deleteBuffer(Tt),w.deleteSync(te),Q}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&&(Ai("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),Q=Math.floor(x.image.height*B),ct=I!==null?I.x:0,gt=I!==null?I.y:0;E.setTexture2D(x,0),w.copyTexSubImage2D(w.TEXTURE_2D,O,0,0,ct,gt,N,Q),Mt.unbindTexture()};const Ec=w.createFramebuffer(),bc=w.createFramebuffer();this.copyTextureToTexture=function(x,I,O=null,B=null,N=0,Q=null){x.isTexture!==!0&&(Ai("WebGLRenderer: copyTextureToTexture function signature has changed."),B=arguments[0]||null,x=arguments[1],I=arguments[2],Q=arguments[3]||0,O=null),Q===null&&(N!==0?(Ai("WebGLRenderer: copyTextureToTexture function signature has changed to support src and dst mipmap levels."),Q=N,N=0):Q=0);let ct,gt,_t,It,Nt,Tt,Kt,te,me;const fe=x.isCompressedTexture?x.mipmaps[Q]:x.image;if(O!==null)ct=O.max.x-O.min.x,gt=O.max.y-O.min.y,_t=O.isBox3?O.max.z-O.min.z:1,It=O.min.x,Nt=O.min.y,Tt=O.isBox3?O.min.z:0;else{const Ze=Math.pow(2,-N);ct=Math.floor(fe.width*Ze),gt=Math.floor(fe.height*Ze),x.isDataArrayTexture?_t=fe.depth:x.isData3DTexture?_t=Math.floor(fe.depth*Ze):_t=1,It=0,Nt=0,Tt=0}B!==null?(Kt=B.x,te=B.y,me=B.z):(Kt=0,te=0,me=0);const $t=zt.convert(I.format),At=zt.convert(I.type);let Te;I.isData3DTexture?(E.setTexture3D(I,0),Te=w.TEXTURE_3D):I.isDataArrayTexture||I.isCompressedArrayTexture?(E.setTexture2DArray(I,0),Te=w.TEXTURE_2D_ARRAY):(E.setTexture2D(I,0),Te=w.TEXTURE_2D),w.pixelStorei(w.UNPACK_FLIP_Y_WEBGL,I.flipY),w.pixelStorei(w.UNPACK_PREMULTIPLY_ALPHA_WEBGL,I.premultiplyAlpha),w.pixelStorei(w.UNPACK_ALIGNMENT,I.unpackAlignment);const ee=w.getParameter(w.UNPACK_ROW_LENGTH),en=w.getParameter(w.UNPACK_IMAGE_HEIGHT),ai=w.getParameter(w.UNPACK_SKIP_PIXELS),ke=w.getParameter(w.UNPACK_SKIP_ROWS),Xi=w.getParameter(w.UNPACK_SKIP_IMAGES);w.pixelStorei(w.UNPACK_ROW_LENGTH,fe.width),w.pixelStorei(w.UNPACK_IMAGE_HEIGHT,fe.height),w.pixelStorei(w.UNPACK_SKIP_PIXELS,It),w.pixelStorei(w.UNPACK_SKIP_ROWS,Nt),w.pixelStorei(w.UNPACK_SKIP_IMAGES,Tt);const ue=x.isDataArrayTexture||x.isData3DTexture,je=I.isDataArrayTexture||I.isData3DTexture;if(x.isDepthTexture){const Ze=Et.get(x),De=Et.get(I),Fe=Et.get(Ze.__renderTarget),mr=Et.get(De.__renderTarget);Mt.bindFramebuffer(w.READ_FRAMEBUFFER,Fe.__webglFramebuffer),Mt.bindFramebuffer(w.DRAW_FRAMEBUFFER,mr.__webglFramebuffer);for(let Vn=0;Vn<_t;Vn++)ue&&(w.framebufferTextureLayer(w.READ_FRAMEBUFFER,w.COLOR_ATTACHMENT0,Et.get(x).__webglTexture,N,Tt+Vn),w.framebufferTextureLayer(w.DRAW_FRAMEBUFFER,w.COLOR_ATTACHMENT0,Et.get(I).__webglTexture,Q,me+Vn)),w.blitFramebuffer(It,Nt,ct,gt,Kt,te,ct,gt,w.DEPTH_BUFFER_BIT,w.NEAREST);Mt.bindFramebuffer(w.READ_FRAMEBUFFER,null),Mt.bindFramebuffer(w.DRAW_FRAMEBUFFER,null)}else if(N!==0||x.isRenderTargetTexture||Et.has(x)){const Ze=Et.get(x),De=Et.get(I);Mt.bindFramebuffer(w.READ_FRAMEBUFFER,Ec),Mt.bindFramebuffer(w.DRAW_FRAMEBUFFER,bc);for(let Fe=0;Fe<_t;Fe++)ue?w.framebufferTextureLayer(w.READ_FRAMEBUFFER,w.COLOR_ATTACHMENT0,Ze.__webglTexture,N,Tt+Fe):w.framebufferTexture2D(w.READ_FRAMEBUFFER,w.COLOR_ATTACHMENT0,w.TEXTURE_2D,Ze.__webglTexture,N),je?w.framebufferTextureLayer(w.DRAW_FRAMEBUFFER,w.COLOR_ATTACHMENT0,De.__webglTexture,Q,me+Fe):w.framebufferTexture2D(w.DRAW_FRAMEBUFFER,w.COLOR_ATTACHMENT0,w.TEXTURE_2D,De.__webglTexture,Q),N!==0?w.blitFramebuffer(It,Nt,ct,gt,Kt,te,ct,gt,w.COLOR_BUFFER_BIT,w.NEAREST):je?w.copyTexSubImage3D(Te,Q,Kt,te,me+Fe,It,Nt,ct,gt):w.copyTexSubImage2D(Te,Q,Kt,te,It,Nt,ct,gt);Mt.bindFramebuffer(w.READ_FRAMEBUFFER,null),Mt.bindFramebuffer(w.DRAW_FRAMEBUFFER,null)}else je?x.isDataTexture||x.isData3DTexture?w.texSubImage3D(Te,Q,Kt,te,me,ct,gt,_t,$t,At,fe.data):I.isCompressedArrayTexture?w.compressedTexSubImage3D(Te,Q,Kt,te,me,ct,gt,_t,$t,fe.data):w.texSubImage3D(Te,Q,Kt,te,me,ct,gt,_t,$t,At,fe):x.isDataTexture?w.texSubImage2D(w.TEXTURE_2D,Q,Kt,te,ct,gt,$t,At,fe.data):x.isCompressedTexture?w.compressedTexSubImage2D(w.TEXTURE_2D,Q,Kt,te,fe.width,fe.height,$t,fe.data):w.texSubImage2D(w.TEXTURE_2D,Q,Kt,te,ct,gt,$t,At,fe);w.pixelStorei(w.UNPACK_ROW_LENGTH,ee),w.pixelStorei(w.UNPACK_IMAGE_HEIGHT,en),w.pixelStorei(w.UNPACK_SKIP_PIXELS,ai),w.pixelStorei(w.UNPACK_SKIP_ROWS,ke),w.pixelStorei(w.UNPACK_SKIP_IMAGES,Xi),Q===0&&I.generateMipmaps&&w.generateMipmap(Te),Mt.unbindTexture()},this.copyTextureToTexture3D=function(x,I,O=null,B=null,N=0){return x.isTexture!==!0&&(Ai("WebGLRenderer: copyTextureToTexture3D function signature has changed."),O=arguments[0]||null,B=arguments[1]||null,x=arguments[2],I=arguments[3],N=arguments[4]||0),Ai('WebGLRenderer: copyTextureToTexture3D function has been deprecated. Use "copyTextureToTexture" instead.'),this.copyTextureToTexture(x,I,O,B,N)},this.initRenderTarget=function(x){Et.get(x).__webglFramebuffer===void 0&&E.setupRenderTarget(x)},this.initTexture=function(x){x.isCubeTexture?E.setTextureCube(x,0):x.isData3DTexture?E.setTexture3D(x,0):x.isDataArrayTexture||x.isCompressedArrayTexture?E.setTexture2DArray(x,0):E.setTexture2D(x,0),Mt.unbindTexture()},this.resetState=function(){R=0,A=0,U=null,Mt.reset(),Xt.reset()},typeof __THREE_DEVTOOLS__<"u"&&__THREE_DEVTOOLS__.dispatchEvent(new CustomEvent("observe",{detail:this}))}get coordinateSystem(){return En}get outputColorSpace(){return this._outputColorSpace}set outputColorSpace(t){this._outputColorSpace=t;const e=this.getContext();e.drawingBufferColorspace=Jt._getDrawingBufferColorSpace(t),e.unpackColorSpace=Jt._getUnpackColorSpace()}}const yl={type:"change"},io={type:"start"},gc={type:"end"},Ws=new cs,El=new Nn,Zm=Math.cos(70*Sh.DEG2RAD),xe=new P,Oe=2*Math.PI,oe={NONE:-1,ROTATE:0,DOLLY:1,PAN:2,TOUCH_ROTATE:3,TOUCH_PAN:4,TOUCH_DOLLY_PAN:5,TOUCH_DOLLY_ROTATE:6},Kr=1e-6;class Km extends hu{constructor(t,e=null){super(t,e),this.state=oe.NONE,this.enabled=!0,this.target=new P,this.cursor=new P,this.minDistance=0,this.maxDistance=1/0,this.minZoom=0,this.maxZoom=1/0,this.minTargetRadius=0,this.maxTargetRadius=1/0,this.minPolarAngle=0,this.maxPolarAngle=Math.PI,this.minAzimuthAngle=-1/0,this.maxAzimuthAngle=1/0,this.enableDamping=!1,this.dampingFactor=.05,this.enableZoom=!0,this.zoomSpeed=1,this.enableRotate=!0,this.rotateSpeed=1,this.keyRotateSpeed=1,this.enablePan=!0,this.panSpeed=1,this.screenSpacePanning=!0,this.keyPanSpeed=7,this.zoomToCursor=!1,this.autoRotate=!1,this.autoRotateSpeed=2,this.keys={LEFT:"ArrowLeft",UP:"ArrowUp",RIGHT:"ArrowRight",BOTTOM:"ArrowDown"},this.mouseButtons={LEFT:Di.ROTATE,MIDDLE:Di.DOLLY,RIGHT:Di.PAN},this.touches={ONE:Ri.ROTATE,TWO:Ri.DOLLY_PAN},this.target0=this.target.clone(),this.position0=this.object.position.clone(),this.zoom0=this.object.zoom,this._domElementKeyEvents=null,this._lastPosition=new P,this._lastQuaternion=new ni,this._lastTargetPosition=new P,this._quat=new ni().setFromUnitVectors(t.up,new P(0,1,0)),this._quatInverse=this._quat.clone().invert(),this._spherical=new Ko,this._sphericalDelta=new Ko,this._scale=1,this._panOffset=new P,this._rotateStart=new vt,this._rotateEnd=new vt,this._rotateDelta=new vt,this._panStart=new vt,this._panEnd=new vt,this._panDelta=new vt,this._dollyStart=new vt,this._dollyEnd=new vt,this._dollyDelta=new vt,this._dollyDirection=new P,this._mouse=new vt,this._performCursorZoom=!1,this._pointers=[],this._pointerPositions={},this._controlActive=!1,this._onPointerMove=Jm.bind(this),this._onPointerDown=$m.bind(this),this._onPointerUp=Qm.bind(this),this._onContextMenu=ag.bind(this),this._onMouseWheel=ng.bind(this),this._onKeyDown=ig.bind(this),this._onTouchStart=sg.bind(this),this._onTouchMove=rg.bind(this),this._onMouseDown=tg.bind(this),this._onMouseMove=eg.bind(this),this._interceptControlDown=og.bind(this),this._interceptControlUp=lg.bind(this),this.domElement!==null&&this.connect(),this.update()}connect(){this.domElement.addEventListener("pointerdown",this._onPointerDown),this.domElement.addEventListener("pointercancel",this._onPointerUp),this.domElement.addEventListener("contextmenu",this._onContextMenu),this.domElement.addEventListener("wheel",this._onMouseWheel,{passive:!1}),this.domElement.getRootNode().addEventListener("keydown",this._interceptControlDown,{passive:!0,capture:!0}),this.domElement.style.touchAction="none"}disconnect(){this.domElement.removeEventListener("pointerdown",this._onPointerDown),this.domElement.removeEventListener("pointermove",this._onPointerMove),this.domElement.removeEventListener("pointerup",this._onPointerUp),this.domElement.removeEventListener("pointercancel",this._onPointerUp),this.domElement.removeEventListener("wheel",this._onMouseWheel),this.domElement.removeEventListener("contextmenu",this._onContextMenu),this.stopListenToKeyEvents(),this.domElement.getRootNode().removeEventListener("keydown",this._interceptControlDown,{capture:!0}),this.domElement.style.touchAction="auto"}dispose(){this.disconnect()}getPolarAngle(){return this._spherical.phi}getAzimuthalAngle(){return this._spherical.theta}getDistance(){return this.object.position.distanceTo(this.target)}listenToKeyEvents(t){t.addEventListener("keydown",this._onKeyDown),this._domElementKeyEvents=t}stopListenToKeyEvents(){this._domElementKeyEvents!==null&&(this._domElementKeyEvents.removeEventListener("keydown",this._onKeyDown),this._domElementKeyEvents=null)}saveState(){this.target0.copy(this.target),this.position0.copy(this.object.position),this.zoom0=this.object.zoom}reset(){this.target.copy(this.target0),this.object.position.copy(this.position0),this.object.zoom=this.zoom0,this.object.updateProjectionMatrix(),this.dispatchEvent(yl),this.update(),this.state=oe.NONE}update(t=null){const e=this.object.position;xe.copy(e).sub(this.target),xe.applyQuaternion(this._quat),this._spherical.setFromVector3(xe),this.autoRotate&&this.state===oe.NONE&&this._rotateLeft(this._getAutoRotationAngle(t)),this.enableDamping?(this._spherical.theta+=this._sphericalDelta.theta*this.dampingFactor,this._spherical.phi+=this._sphericalDelta.phi*this.dampingFactor):(this._spherical.theta+=this._sphericalDelta.theta,this._spherical.phi+=this._sphericalDelta.phi);let n=this.minAzimuthAngle,s=this.maxAzimuthAngle;isFinite(n)&&isFinite(s)&&(n<-Math.PI?n+=Oe:n>Math.PI&&(n-=Oe),s<-Math.PI?s+=Oe:s>Math.PI&&(s-=Oe),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 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=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):(Ws.origin.copy(this.object.position),Ws.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(Ws.direction))Kr||8*(1-this._lastQuaternion.dot(this.object.quaternion))>Kr||this._lastTargetPosition.distanceToSquared(this.target)>Kr?(this.dispatchEvent(yl),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(t){return t!==null?Oe/60*this.autoRotateSpeed*t:Oe/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(Oe*this._rotateDelta.x/e.clientHeight),this._rotateUp(Oe*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(Oe*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(-Oe*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(Oe*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(-Oe*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(Oe*this._rotateDelta.x/e.clientHeight),this._rotateUp(Oe*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=0&&(y[at]=null,E[at].disconnect(_t))}for(let nt=0;nt=y.length){y.push(_t),at=Pt;break}else if(y[Pt]===null){y[Pt]=_t,at=Pt;break}if(at===-1)break}const wt=E[at];wt&&wt.connect(_t)}}const q=new P,J=new P;function X(Z,nt,_t){q.setFromMatrixPosition(nt.matrixWorld),J.setFromMatrixPosition(_t.matrixWorld);const at=q.distanceTo(J),wt=nt.projectionMatrix.elements,Pt=_t.projectionMatrix.elements,kt=wt[14]/(wt[10]-1),le=wt[14]/(wt[10]+1),Ht=(wt[9]+1)/wt[5],de=(wt[9]-1)/wt[5],R=(wt[8]-1)/wt[0],Ue=(Pt[8]+1)/Pt[0],qt=kt*R,jt=kt*Ue,bt=at/(-R+Ue),oe=bt*-R;if(nt.matrixWorld.decompose(Z.position,Z.quaternion,Z.scale),Z.translateX(oe),Z.translateZ(bt),Z.matrixWorld.compose(Z.position,Z.quaternion,Z.scale),Z.matrixWorldInverse.copy(Z.matrixWorld).invert(),wt[10]===-1)Z.projectionMatrix.copy(nt.projectionMatrix),Z.projectionMatrixInverse.copy(nt.projectionMatrixInverse);else{const Et=kt+bt,b=le+bt,v=qt-oe,O=jt+(at-oe),F=Ht*le/b*Et,k=de*le/b*Et;Z.projectionMatrix.makePerspective(v,O,F,k,Et,b),Z.projectionMatrixInverse.copy(Z.projectionMatrix).invert()}}function it(Z,nt){nt===null?Z.matrixWorld.copy(Z.matrix):Z.matrixWorld.multiplyMatrices(nt.matrixWorld,Z.matrix),Z.matrixWorldInverse.copy(Z.matrixWorld).invert()}this.updateCamera=function(Z){if(s===null)return;let nt=Z.near,_t=Z.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,(w!==M.near||Y!==M.far)&&(s.updateRenderState({depthNear:M.near,depthFar:M.far}),w=M.near,Y=M.far),C.layers.mask=Z.layers.mask|2,I.layers.mask=Z.layers.mask|4,M.layers.mask=C.layers.mask|I.layers.mask;const at=Z.parent,wt=M.cameras;it(M,at);for(let Pt=0;Pt0&&(m.alphaTest.value=p.alphaTest);const T=t.get(p),E=T.envMap,y=T.envMapRotation;E&&(m.envMap.value=E,Qn.copy(y),Qn.x*=-1,Qn.y*=-1,Qn.z*=-1,E.isCubeTexture&&E.isRenderTargetTexture===!1&&(Qn.y*=-1,Qn.z*=-1),m.envMapRotation.value.setFromMatrix4(ig.makeRotationFromEuler(Qn)),m.flipEnvMap.value=E.isCubeTexture&&E.isRenderTargetTexture===!1?-1:1,m.reflectivity.value=p.reflectivity,m.ior.value=p.ior,m.refractionRatio.value=p.refractionRatio),p.lightMap&&(m.lightMap.value=p.lightMap,m.lightMapIntensity.value=p.lightMapIntensity,e(p.lightMap,m.lightMapTransform)),p.aoMap&&(m.aoMap.value=p.aoMap,m.aoMapIntensity.value=p.aoMapIntensity,e(p.aoMap,m.aoMapTransform))}function a(m,p){m.diffuse.value.copy(p.color),m.opacity.value=p.opacity,p.map&&(m.map.value=p.map,e(p.map,m.mapTransform))}function o(m,p){m.dashSize.value=p.dashSize,m.totalSize.value=p.dashSize+p.gapSize,m.scale.value=p.scale}function l(m,p,T,E){m.diffuse.value.copy(p.color),m.opacity.value=p.opacity,m.size.value=p.size*T,m.scale.value=E*.5,p.map&&(m.map.value=p.map,e(p.map,m.uvTransform)),p.alphaMap&&(m.alphaMap.value=p.alphaMap,e(p.alphaMap,m.alphaMapTransform)),p.alphaTest>0&&(m.alphaTest.value=p.alphaTest)}function c(m,p){m.diffuse.value.copy(p.color),m.opacity.value=p.opacity,m.rotation.value=p.rotation,p.map&&(m.map.value=p.map,e(p.map,m.mapTransform)),p.alphaMap&&(m.alphaMap.value=p.alphaMap,e(p.alphaMap,m.alphaMapTransform)),p.alphaTest>0&&(m.alphaTest.value=p.alphaTest)}function h(m,p){m.specular.value.copy(p.specular),m.shininess.value=Math.max(p.shininess,1e-4)}function d(m,p){p.gradientMap&&(m.gradientMap.value=p.gradientMap)}function f(m,p){m.metalness.value=p.metalness,p.metalnessMap&&(m.metalnessMap.value=p.metalnessMap,e(p.metalnessMap,m.metalnessMapTransform)),m.roughness.value=p.roughness,p.roughnessMap&&(m.roughnessMap.value=p.roughnessMap,e(p.roughnessMap,m.roughnessMapTransform)),p.envMap&&(m.envMapIntensity.value=p.envMapIntensity)}function u(m,p,T){m.ior.value=p.ior,p.sheen>0&&(m.sheenColor.value.copy(p.sheenColor).multiplyScalar(p.sheen),m.sheenRoughness.value=p.sheenRoughness,p.sheenColorMap&&(m.sheenColorMap.value=p.sheenColorMap,e(p.sheenColorMap,m.sheenColorMapTransform)),p.sheenRoughnessMap&&(m.sheenRoughnessMap.value=p.sheenRoughnessMap,e(p.sheenRoughnessMap,m.sheenRoughnessMapTransform))),p.clearcoat>0&&(m.clearcoat.value=p.clearcoat,m.clearcoatRoughness.value=p.clearcoatRoughness,p.clearcoatMap&&(m.clearcoatMap.value=p.clearcoatMap,e(p.clearcoatMap,m.clearcoatMapTransform)),p.clearcoatRoughnessMap&&(m.clearcoatRoughnessMap.value=p.clearcoatRoughnessMap,e(p.clearcoatRoughnessMap,m.clearcoatRoughnessMapTransform)),p.clearcoatNormalMap&&(m.clearcoatNormalMap.value=p.clearcoatNormalMap,e(p.clearcoatNormalMap,m.clearcoatNormalMapTransform),m.clearcoatNormalScale.value.copy(p.clearcoatNormalScale),p.side===Ye&&m.clearcoatNormalScale.value.negate())),p.dispersion>0&&(m.dispersion.value=p.dispersion),p.iridescence>0&&(m.iridescence.value=p.iridescence,m.iridescenceIOR.value=p.iridescenceIOR,m.iridescenceThicknessMinimum.value=p.iridescenceThicknessRange[0],m.iridescenceThicknessMaximum.value=p.iridescenceThicknessRange[1],p.iridescenceMap&&(m.iridescenceMap.value=p.iridescenceMap,e(p.iridescenceMap,m.iridescenceMapTransform)),p.iridescenceThicknessMap&&(m.iridescenceThicknessMap.value=p.iridescenceThicknessMap,e(p.iridescenceThicknessMap,m.iridescenceThicknessMapTransform))),p.transmission>0&&(m.transmission.value=p.transmission,m.transmissionSamplerMap.value=T.texture,m.transmissionSamplerSize.value.set(T.width,T.height),p.transmissionMap&&(m.transmissionMap.value=p.transmissionMap,e(p.transmissionMap,m.transmissionMapTransform)),m.thickness.value=p.thickness,p.thicknessMap&&(m.thicknessMap.value=p.thicknessMap,e(p.thicknessMap,m.thicknessMapTransform)),m.attenuationDistance.value=p.attenuationDistance,m.attenuationColor.value.copy(p.attenuationColor)),p.anisotropy>0&&(m.anisotropyVector.value.set(p.anisotropy*Math.cos(p.anisotropyRotation),p.anisotropy*Math.sin(p.anisotropyRotation)),p.anisotropyMap&&(m.anisotropyMap.value=p.anisotropyMap,e(p.anisotropyMap,m.anisotropyMapTransform))),m.specularIntensity.value=p.specularIntensity,m.specularColor.value.copy(p.specularColor),p.specularColorMap&&(m.specularColorMap.value=p.specularColorMap,e(p.specularColorMap,m.specularColorMapTransform)),p.specularIntensityMap&&(m.specularIntensityMap.value=p.specularIntensityMap,e(p.specularIntensityMap,m.specularIntensityMapTransform))}function g(m,p){p.matcap&&(m.matcap.value=p.matcap)}function _(m,p){const T=t.get(p).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 rg(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 A=t.render.frame;r[T.id]!==A&&(f(T),r[T.id]=A)}function h(T){const E=d();T.__bindingPointIndex=E;const y=i.createBuffer(),D=T.__size,A=T.usage;return i.bindBuffer(i.UNIFORM_BUFFER,y),i.bufferData(i.UNIFORM_BUFFER,D,A),i.bindBuffer(i.UNIFORM_BUFFER,null),i.bindBufferBase(i.UNIFORM_BUFFER,E,y),y}function d(){for(let T=0;T0&&(y+=D-A),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 p(){for(const T in s)i.deleteBuffer(s[T]);a=[],s={},r={}}return{bind:l,update:c,dispose:p}}class ag{constructor(t={}){const{canvas:e=Nh(),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:f=!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,p=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=sn,this.toneMapping=Hn,this.toneMappingExposure=1;const y=this;let D=!1,A=0,C=0,I=null,S=-1,M=null;const w=new ae,Y=new ae;let V=null;const j=new st(0);let $=0,q=e.width,J=e.height,X=1,it=null,ft=null;const Mt=new ae(0,0,q,J),Nt=new ae(0,0,q,J);let Wt=!1;const Z=new Mo;let nt=!1,_t=!1;this.transmissionResolutionScale=1;const at=new ne,wt=new ne,Pt=new P,kt=new ae,le={background:null,fog:null,environment:null,overrideMaterial:null,isScene:!0};let Ht=!1;function de(){return I===null?X:1}let R=n;function Ue(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${ho}`),e.addEventListener("webglcontextlost",K,!1),e.addEventListener("webglcontextrestored",ht,!1),e.addEventListener("webglcontextcreationerror",ut,!1),R===null){const U="webgl2";if(R=Ue(U,x),R===null)throw Ue(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,bt,oe,Et,b,v,O,F,k,G,ot,Q,lt,Ft,tt,mt,At,Dt,pt,Xt,Ot,Qt,L;function ct(){qt=new mp(R),qt.init(),Ot=new $m(R,qt),jt=new cp(R,qt,t,Ot),bt=new Zm(R,qt),jt.reverseDepthBuffer&&f&&bt.buffers.depth.setReversed(!0),oe=new vp(R),Et=new Fm,b=new Km(R,qt,bt,Et,jt,Ot,oe),v=new up(y),O=new pp(y),F=new bu(R),Qt=new op(R,F),k=new gp(R,F,oe,Qt),G=new Mp(R,k,F,oe),Dt=new xp(R,jt,b),tt=new hp(Et),ot=new Nm(y,v,O,qt,jt,Qt,tt),Q=new sg(y,Et),lt=new Bm,Ft=new Wm(qt),At=new ap(y,v,O,bt,G,u,l),mt=new qm(y,G,jt),L=new rg(R,oe,jt,bt),pt=new lp(R,qt,oe),Xt=new _p(R,qt,oe),oe.programs=ot.programs,y.capabilities=jt,y.extensions=qt,y.properties=Et,y.renderLists=lt,y.shadowMap=mt,y.state=bt,y.info=oe}ct();const W=new ng(y,R);this.xr=W,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 X},this.setPixelRatio=function(x){x!==void 0&&(X=x,this.setSize(q,J,!1))},this.getSize=function(x){return x.set(q,J)},this.setSize=function(x,U,B=!0){if(W.isPresenting){console.warn("THREE.WebGLRenderer: Can't change size while VR device is presenting.");return}q=x,J=U,e.width=Math.floor(x*X),e.height=Math.floor(U*X),B===!0&&(e.style.width=x+"px",e.style.height=U+"px"),this.setViewport(0,0,x,U)},this.getDrawingBufferSize=function(x){return x.set(q*X,J*X).floor()},this.setDrawingBufferSize=function(x,U,B){q=x,J=U,X=B,e.width=Math.floor(x*B),e.height=Math.floor(U*B),this.setViewport(0,0,x,U)},this.getCurrentViewport=function(x){return x.copy(w)},this.getViewport=function(x){return x.copy(Mt)},this.setViewport=function(x,U,B,z){x.isVector4?Mt.set(x.x,x.y,x.z,x.w):Mt.set(x,U,B,z),bt.viewport(w.copy(Mt).multiplyScalar(X).round())},this.getScissor=function(x){return x.copy(Nt)},this.setScissor=function(x,U,B,z){x.isVector4?Nt.set(x.x,x.y,x.z,x.w):Nt.set(x,U,B,z),bt.scissor(Y.copy(Nt).multiplyScalar(X).round())},this.getScissorTest=function(){return Wt},this.setScissorTest=function(x){bt.setScissorTest(Wt=x)},this.setOpaqueSort=function(x){it=x},this.setTransparentSort=function(x){ft=x},this.getClearColor=function(x){return x.copy(At.getClearColor())},this.setClearColor=function(){At.setClearColor.apply(At,arguments)},this.getClearAlpha=function(){return At.getClearAlpha()},this.setClearAlpha=function(){At.setClearAlpha.apply(At,arguments)},this.clear=function(x=!0,U=!0,B=!0){let z=0;if(x){let N=!1;if(I!==null){const et=I.texture.format;N=et===vo||et===_o||et===go}if(N){const et=I.texture.type,dt=et===Dn||et===ci||et===vs||et===Ki||et===fo||et===po,gt=At.getClearColor(),vt=At.getClearAlpha(),Lt=gt.r,It=gt.g,Rt=gt.b;dt?(g[0]=Lt,g[1]=It,g[2]=Rt,g[3]=vt,R.clearBufferuiv(R.COLOR,0,g)):(_[0]=Lt,_[1]=It,_[2]=Rt,_[3]=vt,R.clearBufferiv(R.COLOR,0,_))}else z|=R.COLOR_BUFFER_BIT}U&&(z|=R.DEPTH_BUFFER_BIT),B&&(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",K,!1),e.removeEventListener("webglcontextrestored",ht,!1),e.removeEventListener("webglcontextcreationerror",ut,!1),At.dispose(),lt.dispose(),Ft.dispose(),Et.dispose(),v.dispose(),O.dispose(),G.dispose(),Qt.dispose(),L.dispose(),ot.dispose(),W.dispose(),W.removeEventListener("sessionstart",Ge),W.removeEventListener("sessionend",Ie),Ce.stop()};function K(x){x.preventDefault(),console.log("THREE.WebGLRenderer: Context Lost."),D=!0}function ht(){console.log("THREE.WebGLRenderer: Context Restored."),D=!1;const x=oe.autoReset,U=mt.enabled,B=mt.autoUpdate,z=mt.needsUpdate,N=mt.type;ct(),oe.autoReset=x,mt.enabled=U,mt.autoUpdate=B,mt.needsUpdate=z,mt.type=N}function ut(x){console.error("THREE.WebGLRenderer: A WebGL context could not be created. Reason: ",x.statusMessage)}function Ut(x){const U=x.target;U.removeEventListener("dispose",Ut),he(U)}function he(x){_e(x),Et.remove(x)}function _e(x){const U=Et.get(x).programs;U!==void 0&&(U.forEach(function(B){ot.releaseProgram(B)}),x.isShaderMaterial&&ot.releaseShaderCache(x))}this.renderBufferDirect=function(x,U,B,z,N,et){U===null&&(U=le);const dt=N.isMesh&&N.matrixWorld.determinant()<0,gt=Dc(x,U,B,z,N);bt.setMaterial(z,dt);let vt=B.index,Lt=1;if(z.wireframe===!0){if(vt=k.getWireframeAttribute(B),vt===void 0)return;Lt=2}const It=B.drawRange,Rt=B.attributes.position;let Zt=It.start*Lt,te=(It.start+It.count)*Lt;et!==null&&(Zt=Math.max(Zt,et.start*Lt),te=Math.min(te,(et.start+et.count)*Lt)),vt!==null?(Zt=Math.max(Zt,0),te=Math.min(te,vt.count)):Rt!=null&&(Zt=Math.max(Zt,0),te=Math.min(te,Rt.count));const ve=te-Zt;if(ve<0||ve===1/0)return;Qt.setup(N,z,gt,B,vt);let pe,$t=pt;if(vt!==null&&(pe=F.get(vt),$t=Xt,$t.setIndex(pe)),N.isMesh)z.wireframe===!0?(bt.setLineWidth(z.wireframeLinewidth*de()),$t.setMode(R.LINES)):$t.setMode(R.TRIANGLES);else if(N.isLine){let Ct=z.linewidth;Ct===void 0&&(Ct=1),bt.setLineWidth(Ct*de()),N.isLineSegments?$t.setMode(R.LINES):N.isLineLoop?$t.setMode(R.LINE_LOOP):$t.setMode(R.LINE_STRIP)}else N.isPoints?$t.setMode(R.POINTS):N.isSprite&&$t.setMode(R.TRIANGLES);if(N.isBatchedMesh)if(N._multiDrawInstances!==null)$t.renderMultiDrawInstances(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount,N._multiDrawInstances);else if(qt.get("WEBGL_multi_draw"))$t.renderMultiDraw(N._multiDrawStarts,N._multiDrawCounts,N._multiDrawCount);else{const Ct=N._multiDrawStarts,Pe=N._multiDrawCounts,ee=N._multiDrawCount,ln=vt?F.get(vt).bytesPerElement:1,mi=Et.get(z).currentProgram.getUniforms();for(let je=0;je{function et(){if(z.forEach(function(dt){Et.get(dt).currentProgram.isReady()&&z.delete(dt)}),z.size===0){N(x);return}setTimeout(et,10)}qt.get("KHR_parallel_shader_compile")!==null?et():setTimeout(et,10)})};let qe=null;function an(x){qe&&qe(x)}function Ge(){Ce.stop()}function Ie(){Ce.start()}const Ce=new yc;Ce.setAnimationLoop(an),typeof self<"u"&&Ce.setContext(self),this.setAnimationLoop=function(x){qe=x,W.setAnimationLoop(x),x===null?Ce.stop():Ce.start()},W.addEventListener("sessionstart",Ge),W.addEventListener("sessionend",Ie),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(),W.enabled===!0&&W.isPresenting===!0&&(W.cameraAutoUpdate===!0&&W.updateCamera(U),U=W.getCamera()),x.isScene===!0&&x.onBeforeRender(y,x,U,I),p=Ft.get(x,E.length),p.init(U),E.push(p),wt.multiplyMatrices(U.projectionMatrix,U.matrixWorldInverse),Z.setFromProjectionMatrix(wt),_t=this.localClippingEnabled,nt=tt.init(this.clippingPlanes,_t),m=lt.get(x,T.length),m.init(),T.push(m),W.enabled===!0&&W.isPresenting===!0){const et=y.xr.getDepthSensingMesh();et!==null&&on(et,U,-1/0,y.sortObjects)}on(x,U,0,y.sortObjects),m.finish(),y.sortObjects===!0&&m.sort(it,ft),Ht=W.enabled===!1||W.isPresenting===!1||W.hasDepthSensing()===!1,Ht&&At.addToRenderList(m,x),this.info.render.frame++,nt===!0&&tt.beginShadows();const B=p.state.shadowsArray;mt.render(B,x,U),nt===!0&&tt.endShadows(),this.info.autoReset===!0&&this.info.reset();const z=m.opaque,N=m.transmissive;if(p.setupLights(),U.isArrayCamera){const et=U.cameras;if(N.length>0)for(let dt=0,gt=et.length;dt0&&ws(z,N,x,U),Ht&&At.render(x),pi(m,x,U);I!==null&&C===0&&(b.updateMultisampleRenderTarget(I),b.updateRenderTargetMipmap(I)),x.isScene===!0&&x.onAfterRender(y,x,U),Qt.resetDefaultState(),S=-1,M=null,E.pop(),E.length>0?(p=E[E.length-1],nt===!0&&tt.setGlobalState(y.clippingPlanes,p.state.camera)):p=null,T.pop(),T.length>0?m=T[T.length-1]:m=null};function on(x,U,B,z){if(x.visible===!1)return;if(x.layers.test(U.layers)){if(x.isGroup)B=x.renderOrder;else if(x.isLOD)x.autoUpdate===!0&&x.update(U);else if(x.isLight)p.pushLight(x),x.castShadow&&p.pushShadow(x);else if(x.isSprite){if(!x.frustumCulled||Z.intersectsSprite(x)){z&&kt.setFromMatrixPosition(x.matrixWorld).applyMatrix4(wt);const dt=G.update(x),gt=x.material;gt.visible&&m.push(x,dt,gt,B,kt.z,null)}}else if((x.isMesh||x.isLine||x.isPoints)&&(!x.frustumCulled||Z.intersectsObject(x))){const dt=G.update(x),gt=x.material;if(z&&(x.boundingSphere!==void 0?(x.boundingSphere===null&&x.computeBoundingSphere(),kt.copy(x.boundingSphere.center)):(dt.boundingSphere===null&&dt.computeBoundingSphere(),kt.copy(dt.boundingSphere.center)),kt.applyMatrix4(x.matrixWorld).applyMatrix4(wt)),Array.isArray(gt)){const vt=dt.groups;for(let Lt=0,It=vt.length;Lt0&&Xn(N,U,B),et.length>0&&Xn(et,U,B),dt.length>0&&Xn(dt,U,B),bt.buffers.depth.setTest(!0),bt.buffers.depth.setMask(!0),bt.buffers.color.setMask(!0),bt.setPolygonOffset(!1)}function ws(x,U,B,z){if((B.isScene===!0?B.overrideMaterial:null)!==null)return;p.state.transmissionRenderTarget[z.id]===void 0&&(p.state.transmissionRenderTarget[z.id]=new fn(1,1,{generateMipmaps:!0,type:qt.has("EXT_color_buffer_half_float")||qt.has("EXT_color_buffer_float")?Cn:Dn,minFilter:ai,samples:4,stencilBuffer:r,resolveDepthBuffer:!1,resolveStencilBuffer:!1,colorSpace:Jt.workingColorSpace}));const et=p.state.transmissionRenderTarget[z.id],dt=z.viewport||w;et.setSize(dt.z*y.transmissionResolutionScale,dt.w*y.transmissionResolutionScale);const gt=y.getRenderTarget();y.setRenderTarget(et),y.getClearColor(j),$=y.getClearAlpha(),$<1&&y.setClearColor(16777215,.5),y.clear(),Ht&&At.render(B);const vt=y.toneMapping;y.toneMapping=Hn;const Lt=z.viewport;if(z.viewport!==void 0&&(z.viewport=void 0),p.setupLightsView(z),nt===!0&&tt.setGlobalState(y.clippingPlanes,z),Xn(x,B,z),b.updateMultisampleRenderTarget(et),b.updateRenderTargetMipmap(et),qt.has("WEBGL_multisampled_render_to_texture")===!1){let It=!1;for(let Rt=0,Zt=U.length;Rt0),Rt=!!B.morphAttributes.position,Zt=!!B.morphAttributes.normal,te=!!B.morphAttributes.color;let ve=Hn;z.toneMapped&&(I===null||I.isXRRenderTarget===!0)&&(ve=y.toneMapping);const pe=B.morphAttributes.position||B.morphAttributes.normal||B.morphAttributes.color,$t=pe!==void 0?pe.length:0,Ct=Et.get(z),Pe=p.state.lights;if(nt===!0&&(_t===!0||x!==M)){const Be=x===M&&z.id===S;tt.setState(z,x,Be)}let ee=!1;z.version===Ct.__version?(Ct.needsLights&&Ct.lightsStateVersion!==Pe.state.version||Ct.outputColorSpace!==gt||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!==vt||z.fog===!0&&Ct.fog!==et||Ct.numClippingPlanes!==void 0&&(Ct.numClippingPlanes!==tt.numPlanes||Ct.numIntersection!==tt.numIntersection)||Ct.vertexAlphas!==Lt||Ct.vertexTangents!==It||Ct.morphTargets!==Rt||Ct.morphNormals!==Zt||Ct.morphColors!==te||Ct.toneMapping!==ve||Ct.morphTargetsCount!==$t)&&(ee=!0):(ee=!0,Ct.__version=z.version);let ln=Ct.currentProgram;ee===!0&&(ln=Yn(z,U,N));let mi=!1,je=!1,ns=!1;const ue=ln.getUniforms(),Qe=Ct.uniforms;if(bt.useProgram(ln.program)&&(mi=!0,je=!0,ns=!0),z.id!==S&&(S=z.id,je=!0),mi||M!==x){bt.buffers.depth.getReversed()?(at.copy(x.projectionMatrix),Oh(at),Bh(at),ue.setValue(R,"projectionMatrix",at)):ue.setValue(R,"projectionMatrix",x.projectionMatrix),ue.setValue(R,"viewMatrix",x.matrixWorldInverse);const We=ue.map.cameraPosition;We!==void 0&&We.setValue(R,Pt.setFromMatrixPosition(x.matrixWorld)),jt.logarithmicDepthBuffer&&ue.setValue(R,"logDepthBufFC",2/(Math.log(x.far+1)/Math.LN2)),(z.isMeshPhongMaterial||z.isMeshToonMaterial||z.isMeshLambertMaterial||z.isMeshBasicMaterial||z.isMeshStandardMaterial||z.isShaderMaterial)&&ue.setValue(R,"isOrthographic",x.isOrthographicCamera===!0),M!==x&&(M=x,je=!0,ns=!0)}if(N.isSkinnedMesh){ue.setOptional(R,N,"bindMatrix"),ue.setOptional(R,N,"bindMatrixInverse");const Be=N.skeleton;Be&&(Be.boneTexture===null&&Be.computeBoneTexture(),ue.setValue(R,"boneTexture",Be.boneTexture,b))}N.isBatchedMesh&&(ue.setOptional(R,N,"batchingTexture"),ue.setValue(R,"batchingTexture",N._matricesTexture,b),ue.setOptional(R,N,"batchingIdTexture"),ue.setValue(R,"batchingIdTexture",N._indirectTexture,b),ue.setOptional(R,N,"batchingColorTexture"),N._colorsTexture!==null&&ue.setValue(R,"batchingColorTexture",N._colorsTexture,b));const tn=B.morphAttributes;if((tn.position!==void 0||tn.normal!==void 0||tn.color!==void 0)&&Dt.update(N,B,ln),(je||Ct.receiveShadow!==N.receiveShadow)&&(Ct.receiveShadow=N.receiveShadow,ue.setValue(R,"receiveShadow",N.receiveShadow)),z.isMeshGouraudMaterial&&z.envMap!==null&&(Qe.envMap.value=vt,Qe.flipEnvMap.value=vt.isCubeTexture&&vt.isRenderTargetTexture===!1?-1:1),z.isMeshStandardMaterial&&z.envMap===null&&U.environment!==null&&(Qe.envMapIntensity.value=U.environmentIntensity),je&&(ue.setValue(R,"toneMappingExposure",y.toneMappingExposure),Ct.needsLights&&Lc(Qe,ns),et&&z.fog===!0&&Q.refreshFogUniforms(Qe,et),Q.refreshMaterialUniforms(Qe,z,X,J,p.state.transmissionRenderTarget[x.id]),dr.upload(R,Rs(Ct),Qe,b)),z.isShaderMaterial&&z.uniformsNeedUpdate===!0&&(dr.upload(R,Rs(Ct),Qe,b),z.uniformsNeedUpdate=!1),z.isSpriteMaterial&&ue.setValue(R,"center",N.center),ue.setValue(R,"modelViewMatrix",N.modelViewMatrix),ue.setValue(R,"normalMatrix",N.normalMatrix),ue.setValue(R,"modelMatrix",N.matrixWorld),z.isShaderMaterial||z.isRawShaderMaterial){const Be=z.uniformsGroups;for(let We=0,Rr=Be.length;We0&&b.useMultisampledRTT(x)===!1?N=Et.get(x).__webglMultisampledFramebuffer:Array.isArray(It)?N=It[B]:N=It,w.copy(x.viewport),Y.copy(x.scissor),V=x.scissorTest}else w.copy(Mt).multiplyScalar(X).floor(),Y.copy(Nt).multiplyScalar(X).floor(),V=Wt;if(B!==0&&(N=Ic),bt.bindFramebuffer(R.FRAMEBUFFER,N)&&z&&bt.drawBuffers(x,N),bt.viewport(w),bt.scissor(Y),bt.setScissorTest(V),et){const vt=Et.get(x.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_CUBE_MAP_POSITIVE_X+U,vt.__webglTexture,B)}else if(dt){const vt=Et.get(x.texture),Lt=U;R.framebufferTextureLayer(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,vt.__webglTexture,B,Lt)}else if(x!==null&&B!==0){const vt=Et.get(x.texture);R.framebufferTexture2D(R.FRAMEBUFFER,R.COLOR_ATTACHMENT0,R.TEXTURE_2D,vt.__webglTexture,B)}S=-1},this.readRenderTargetPixels=function(x,U,B,z,N,et,dt){if(!(x&&x.isWebGLRenderTarget)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");return}let gt=Et.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&dt!==void 0&&(gt=gt[dt]),gt){bt.bindFramebuffer(R.FRAMEBUFFER,gt);try{const vt=x.texture,Lt=vt.format,It=vt.type;if(!jt.textureFormatReadable(Lt)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in RGBA or implementation defined format.");return}if(!jt.textureTypeReadable(It)){console.error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not in UnsignedByteType or implementation defined type.");return}U>=0&&U<=x.width-z&&B>=0&&B<=x.height-N&&R.readPixels(U,B,z,N,Ot.convert(Lt),Ot.convert(It),et)}finally{const vt=I!==null?Et.get(I).__webglFramebuffer:null;bt.bindFramebuffer(R.FRAMEBUFFER,vt)}}},this.readRenderTargetPixelsAsync=async function(x,U,B,z,N,et,dt){if(!(x&&x.isWebGLRenderTarget))throw new Error("THREE.WebGLRenderer.readRenderTargetPixels: renderTarget is not THREE.WebGLRenderTarget.");let gt=Et.get(x).__webglFramebuffer;if(x.isWebGLCubeRenderTarget&&dt!==void 0&&(gt=gt[dt]),gt){const vt=x.texture,Lt=vt.format,It=vt.type;if(!jt.textureFormatReadable(Lt))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in RGBA or implementation defined format.");if(!jt.textureTypeReadable(It))throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: renderTarget is not in UnsignedByteType or implementation defined type.");if(U>=0&&U<=x.width-z&&B>=0&&B<=x.height-N){bt.bindFramebuffer(R.FRAMEBUFFER,gt);const Rt=R.createBuffer();R.bindBuffer(R.PIXEL_PACK_BUFFER,Rt),R.bufferData(R.PIXEL_PACK_BUFFER,et.byteLength,R.STREAM_READ),R.readPixels(U,B,z,N,Ot.convert(Lt),Ot.convert(It),0);const Zt=I!==null?Et.get(I).__webglFramebuffer:null;bt.bindFramebuffer(R.FRAMEBUFFER,Zt);const te=R.fenceSync(R.SYNC_GPU_COMMANDS_COMPLETE,0);return R.flush(),await Fh(R,te,4),R.bindBuffer(R.PIXEL_PACK_BUFFER,Rt),R.getBufferSubData(R.PIXEL_PACK_BUFFER,0,et),R.deleteBuffer(Rt),R.deleteSync(te),et}else throw new Error("THREE.WebGLRenderer.readRenderTargetPixelsAsync: requested read bounds are out of range.")}},this.copyFramebufferToTexture=function(x,U=null,B=0){x.isTexture!==!0&&(Fi("WebGLRenderer: copyFramebufferToTexture function signature has changed."),U=arguments[0]||null,x=arguments[1]);const z=Math.pow(2,-B),N=Math.floor(x.image.width*z),et=Math.floor(x.image.height*z),dt=U!==null?U.x:0,gt=U!==null?U.y:0;b.setTexture2D(x,0),R.copyTexSubImage2D(R.TEXTURE_2D,B,0,0,dt,gt,N,et),bt.unbindTexture()};const Nc=R.createFramebuffer(),Fc=R.createFramebuffer();this.copyTextureToTexture=function(x,U,B=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,B=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 dt,gt,vt,Lt,It,Rt,Zt,te,ve;const pe=x.isCompressedTexture?x.mipmaps[et]:x.image;if(B!==null)dt=B.max.x-B.min.x,gt=B.max.y-B.min.y,vt=B.isBox3?B.max.z-B.min.z:1,Lt=B.min.x,It=B.min.y,Rt=B.isBox3?B.min.z:0;else{const tn=Math.pow(2,-N);dt=Math.floor(pe.width*tn),gt=Math.floor(pe.height*tn),x.isDataArrayTexture?vt=pe.depth:x.isData3DTexture?vt=Math.floor(pe.depth*tn):vt=1,Lt=0,It=0,Rt=0}z!==null?(Zt=z.x,te=z.y,ve=z.z):(Zt=0,te=0,ve=0);const $t=Ot.convert(U.format),Ct=Ot.convert(U.type);let Pe;U.isData3DTexture?(b.setTexture3D(U,0),Pe=R.TEXTURE_3D):U.isDataArrayTexture||U.isCompressedArrayTexture?(b.setTexture2DArray(U,0),Pe=R.TEXTURE_2D_ARRAY):(b.setTexture2D(U,0),Pe=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),je=R.getParameter(R.UNPACK_SKIP_ROWS),ns=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,Lt),R.pixelStorei(R.UNPACK_SKIP_ROWS,It),R.pixelStorei(R.UNPACK_SKIP_IMAGES,Rt);const ue=x.isDataArrayTexture||x.isData3DTexture,Qe=U.isDataArrayTexture||U.isData3DTexture;if(x.isDepthTexture){const tn=Et.get(x),Be=Et.get(U),We=Et.get(tn.__renderTarget),Rr=Et.get(Be.__renderTarget);bt.bindFramebuffer(R.READ_FRAMEBUFFER,We.__webglFramebuffer),bt.bindFramebuffer(R.DRAW_FRAMEBUFFER,Rr.__webglFramebuffer);for(let qn=0;qnMath.PI&&(n-=Xe),s<-Math.PI?s+=Xe:s>Math.PI&&(s-=Xe),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(Se.setFromSpherical(this._spherical),Se.applyQuaternion(this._quatInverse),e.copy(this.target).add(Se),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=Se.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=Se.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):(rr.origin.copy(this.object.position),rr.direction.set(0,0,-1).transformDirection(this.object.matrix),Math.abs(this.object.up.dot(rr.direction))ca||8*(1-this._lastQuaternion.dot(this.object.quaternion))>ca||this._lastTargetPosition.distanceToSquared(this.target)>ca?(this.dispatchEvent(Nl),this._lastPosition.copy(this.object.position),this._lastQuaternion.copy(this.object.quaternion),this._lastTargetPosition.copy(this.target),!0):!1}_getAutoRotationAngle(t){return t!==null?Xe/60*this.autoRotateSpeed*t:Xe/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){Se.setFromMatrixColumn(e,0),Se.multiplyScalar(-t),this._panOffset.add(Se)}_panUp(t,e){this.screenSpacePanning===!0?Se.setFromMatrixColumn(e,1):(Se.setFromMatrixColumn(e,0),Se.crossVectors(this.object.up,Se)),Se.multiplyScalar(t),this._panOffset.add(Se)}_pan(t,e){const n=this.domElement;if(this.object.isPerspectiveCamera){const s=this.object.position;Se.copy(s).sub(this.target);let r=Se.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(Xe*this._rotateDelta.x/e.clientHeight),this._rotateUp(Xe*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(Xe*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(-Xe*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(Xe*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(-Xe*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(Xe*this._rotateDelta.x/e.clientHeight),this._rotateUp(Xe*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{var e;(t instanceof Me||t instanceof $h)&&((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 xg{constructor(t){Wt(this,"positions");Wt(this,"velocities");Wt(this,"running",!0);Wt(this,"step",0);Wt(this,"repulsionStrength",500);Wt(this,"attractionStrength",.01);Wt(this,"dampening",.9);Wt(this,"baseMaxSteps",300);Wt(this,"maxSteps",300);Wt(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;s0?.15+t.retention*.2:0,blending:ze}),h=new Br(c);h.scale.set(s*4*n,s*4*n,1),h.position.copy(e),h.userData={isGlow:!0,nodeId:t.id},this.glowMap.set(t.id,h),this.group.add(h);const d=t.label||t.type,f=this.createTextSprite(d,"#e2e8f0");return f.position.copy(e),f.position.y+=s*2+1.5,f.userData={isLabel:!0,nodeId:t.id,offset:s*2+1.5},this.group.add(f),this.labelSprites.set(t.id,f),{mesh:l,glow:h,label:f,size:s}}addNode(t,e){const n=(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,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");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="bold 28px -apple-system, BlinkMacSystemFont, sans-serif",s.textAlign="center",s.textBaseline="middle",s.shadowColor="rgba(0, 0, 0, 0.8)",s.shadowBlur=6,s.shadowOffsetX=0,s.shadowOffsetY=2,s.fillStyle=e,s.fillText(r,n.width/2,n.height/2);const a=new tu(n);a.needsUpdate=!0;const o=new Ha({map:a,transparent:!0,opacity:0,depthTest:!1,sizeAttenuation:!0}),l=new Br(o);return l.scale.set(12,1.5,1),l}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=Tl(c);if(l.mesh.scale.setScalar(Math.max(.001,h)),l.frame>=5){const d=Math.min((l.frame-5)/5,1),f=l.glow.material;f.opacity=d*.25;const p=l.targetScale*4*h;l.glow.scale.set(p,p,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-Mg(c),d=Math.max(.001,l.originalScale*h);l.mesh.scale.setScalar(d);const f=d*4;l.glow.scale.set(f,f,1);const p=l.mesh.material;p.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)*Tl(c),d=this.meshMap.get(l.id);d&&d.scale.setScalar(h);const f=this.glowMap.get(l.id);if(f){const p=h*4;f.scale.set(p,p,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(f=>f.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 p=.3+c.retention*.5+Math.sin(t*(.8+c.retention*.7))*.1*c.retention;d.emissiveIntensity=p}}),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,f=l===this.hoveredNode||l===this.selectedNode?1:h<40?.9:h<80?.9*(1-(h-40)/40):0;d.opacity+=(f-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 Br&&((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 yg(i){return 1-Math.pow(1-i,3)}class Eg{constructor(){Wt(this,"group");Wt(this,"growingEdges",[]);Wt(this,"dissolvingEdges",[]);this.group=new Ci}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 pe().setFromPoints(a),l=new rr({color:4868730,transparent:!0,opacity:Math.min(.1+n.weight*.5,.6),blending:ze}),c=new ka(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 pe().setFromPoints(r),o=new rr({color:4868730,transparent:!0,opacity:0,blending:ze}),l=new ka(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=yg(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*.5,n.frame>=n.totalFrames&&(c.opacity=.5,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,.5*(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 bg{constructor(t){Wt(this,"starField");Wt(this,"neuralParticles");this.starField=this.createStarField(),this.neuralParticles=this.createNeuralParticles(),t.add(this.starField),t.add(this.neuralParticles)}createStarField(){const e=new pe,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 de(n,3)),e.setAttribute("size",new de(s,1));const r=new Ni({color:6514417,size:.5,transparent:!0,opacity:.4,sizeAttenuation:!0,blending:ze});return new rs(e,r)}createNeuralParticles(){const e=new pe,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 de(n,3)),e.setAttribute("color",new de(s,3));const r=new Ni({size:.3,vertexColors:!0,transparent:!0,opacity:.4,blending:ze,sizeAttenuation:!0});return new rs(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 f=0;f=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);if(h>=a-o&&h<=a+o){r.pulsedNodes.add(c),this.addPulse(c,.8,new pt(65489),.03);const d=t.get(c);d&&d.scale.multiplyScalar(1.3)}})}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 Sn={bloomStrength:.8,rotateSpeed:.3,fogColor:328976,fogDensity:.008,nebulaIntensity:0,chromaticIntensity:.002,vignetteRadius:.9,breatheAmplitude:1},In={bloomStrength:1.8,rotateSpeed:.08,fogColor:656672,fogDensity:.006,nebulaIntensity:1,chromaticIntensity:.005,vignetteRadius:.7,breatheAmplitude:2};class wg{constructor(){Wt(this,"active",!1);Wt(this,"transition",0);Wt(this,"transitionSpeed",.008);Wt(this,"current");Wt(this,"auroraHue",0);this.current={...Sn}}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(Sn.bloomStrength,In.bloomStrength,o),this.current.rotateSpeed=this.lerp(Sn.rotateSpeed,In.rotateSpeed,o),this.current.fogDensity=this.lerp(Sn.fogDensity,In.fogDensity,o),this.current.nebulaIntensity=this.lerp(Sn.nebulaIntensity,In.nebulaIntensity,o),this.current.chromaticIntensity=this.lerp(Sn.chromaticIntensity,In.chromaticIntensity,o),this.current.vignetteRadius=this.lerp(Sn.vignetteRadius,In.vignetteRadius,o),this.current.breatheAmplitude=this.lerp(Sn.breatheAmplitude,In.breatheAmplitude,o),e.strength=this.current.bloomStrength,n.autoRotateSpeed=this.current.rotateSpeed;const l=new pt(Sn.fogColor),c=new pt(In.fogColor),h=l.clone().lerp(c,o);if(t.fog=new ur(h,this.current.fogDensity),o>.01){this.auroraHue=r*.1%1;const d=new pt().setHSL(.75+this.auroraHue*.15,.8,.5),f=new pt().setHSL(.55+this.auroraHue*.2,.7,.4);s.point1.color.lerp(d,o*.3),s.point2.color.lerp(f,o*.3)}else s.point1.color.set(6514417),s.point2.color.set(11032055)}lerp(t,e,n){return t+(e-t)*n}}const Ag=50,as=[];function Rg(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 Cg(i,t){if(as.length<=Ag)return;const e=as.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 Pg(i,t,e){var d,f;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 p=i.data;if(!p.id)break;const g={id:p.id,label:(p.content??"").slice(0,60),type:p.node_type??"fact",retention:p.retention??.9,tags:p.tags??[],createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),isCenter:!1},v=Rg(g,e,c),m=s.addNode(g,v);a.addNode(p.id,m),as.push(p.id),Cg(t,e);const u=new pt(Fl[g.type]||"#00ffd1");n.createRainbowBurst(v,u),n.createShockwave(v,u,o);const T=u.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 p=i.data;if(!p.source_id||!p.target_id)break;const g=c.get(p.source_id),v=c.get(p.target_id),m={source:p.source_id,target:p.target_id,weight:p.weight??.5,type:p.connection_type??"semantic"};r.addEdge(m,c),g&&v&&n.createConnectionFlash(g,v,new pt(54527)),p.source_id&&h.has(p.source_id)&&n.addPulse(p.source_id,1,new pt(54527),.02),p.target_id&&h.has(p.target_id)&&n.addPulse(p.target_id,1,new pt(54527),.02),l({type:"edgeAdded",edge:m});break}case"MemoryDeleted":{const p=i.data;if(!p.id)break;const g=c.get(p.id);if(g){const m=new pt(16729943);n.createImplosion(g,m)}r.removeEdgesForNode(p.id),s.removeNode(p.id),a.removeNode(p.id);const v=as.indexOf(p.id);v!==-1&&as.splice(v,1),l({type:"edgesRemoved",nodeId:p.id}),l({type:"nodeRemoved",nodeId:p.id});break}case"MemoryPromoted":{const p=i.data,g=p==null?void 0:p.id;if(!g)break;const v=p.new_retention??.95;if(h.has(g)){s.growNode(g,v),n.addPulse(g,1.2,new pt(65416),.01);const m=c.get(g);m&&(n.createShockwave(m,new pt(65416),o),n.createSpawnBurst(m,new pt(65416))),l({type:"nodeUpdated",nodeId:g,retention:v})}break}case"MemoryDemoted":{const p=i.data,g=p==null?void 0:p.id;if(!g)break;const v=p.new_retention??.3;h.has(g)&&(s.growNode(g,v),n.addPulse(g,.8,new pt(16729943),.03),l({type:"nodeUpdated",nodeId:g,retention:v}));break}case"MemoryUpdated":{const p=i.data,g=p==null?void 0:p.id;if(!g||!h.has(g))break;n.addPulse(g,.6,new pt(8490232),.02),p.retention!==void 0&&(s.growNode(g,p.retention),l({type:"nodeUpdated",nodeId:g,retention:p.retention}));break}case"SearchPerformed":{h.forEach((p,g)=>{n.addPulse(g,.6+Math.random()*.4,new pt(8490232),.02)});break}case"DreamStarted":{h.forEach((p,g)=>{n.addPulse(g,1,new pt(11032055),.005)});break}case"DreamProgress":{const p=(d=i.data)==null?void 0:d.memory_id;p&&h.has(p)&&n.addPulse(p,1.5,new pt(12616956),.01);break}case"DreamCompleted":{n.createSpawnBurst(new P(0,0,0),new pt(11032055)),n.createShockwave(new P(0,0,0),new pt(11032055),o);break}case"RetentionDecayed":{const p=(f=i.data)==null?void 0:f.id;p&&h.has(p)&&n.addPulse(p,.8,new pt(16729943),.03);break}case"ConsolidationCompleted":{h.forEach((p,g)=>{n.addPulse(g,.4+Math.random()*.3,new pt(16758784),.015)});break}case"ActivationSpread":{const p=i.data;if(p.source_id&&p.target_ids){const g=c.get(p.source_id);if(g)for(const v of p.target_ids){const m=c.get(v);m&&n.createConnectionFlash(g,m,new pt(1370310))}}break}}}const Dg=` + }`})}}ts.BlurDirectionX=new xt(1,0);ts.BlurDirectionY=new xt(0,1);function Rg(){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 ce(t,3)),n.setAttribute("color",new ce(e,3));const s=new li({size:1.6,sizeAttenuation:!0,vertexColors:!0,transparent:!0,opacity:.6,depthWrite:!1,blending:De});return new Yi(n,s)}function Cg(i){const t=new au;t.background=new st(328975),t.fog=new Tr(657946,.0035);const e=new $e(60,i.clientWidth/i.clientHeight,.1,2e3);e.position.set(0,30,80);const n=new ag({antialias:!0,alpha:!0,powerPreference:"high-performance"});n.setSize(i.clientWidth,i.clientHeight),n.setPixelRatio(Math.min(window.devicePixelRatio,2)),n.toneMapping=Zl,n.toneMappingExposure=1.25,i.appendChild(n.domElement);const s=new lg(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 Tg(n);r.addPass(new wg(t,e));const a=new ts(new xt(i.clientWidth,i.clientHeight),.55,.6,.2);r.addPass(a);const o=new vu(2763354,.7);t.add(o);const l=new rl(6514417,1.8,240);l.position.set(50,50,50),t.add(l);const c=new rl(11032055,1.2,240);c.position.set(-50,-30,-50),t.add(c);const h=Rg();t.add(h);const d=new Su;d.params.Points={threshold:2};const f=new xt;return{scene:t,camera:e,renderer:n,controls:s,composer:r,bloomPass:a,raycaster:d,mouse:f,lights:{ambient:o,point1:l,point2:c},starfield:h}}function Pg(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 Dg(i){i.scene.traverse(t=>{var e;(t instanceof Ee||t instanceof hu)&&((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 Lg{constructor(t){Bt(this,"positions");Bt(this,"velocities");Bt(this,"running",!0);Bt(this,"step",0);Bt(this,"repulsionStrength",500);Bt(this,"attractionStrength",.01);Bt(this,"dampening",.9);Bt(this,"baseMaxSteps",300);Bt(this,"maxSteps",300);Bt(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 ro={active:"#10b981",dormant:"#f59e0b",silent:"#8b5cf6",unavailable:"#6b7280"},Ig={active:"Easily retrievable (retention ≥ 70%)",dormant:"Retrievable with effort (40–70%)",silent:"Difficult, needs cues (10–40%)",unavailable:"Needs reinforcement (< 10%)"};function Bl(i,t){return t==="state"?ro[Ug(i.retention)]:Yl[i.type]||"#8B95A5"}let ps=null;function ao(){if(ps)return ps;const i=128,t=document.createElement("canvas");t.width=i,t.height=i;const e=t.getContext("2d");if(!e)return ps=new Ae,ps;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 vc(t);return s.needsUpdate=!0,ps=s,s}function zl(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 Ng(i){return i*i*((1.70158+1)*i-1.70158)}class Fg{constructor(){Bt(this,"group");Bt(this,"meshMap",new Map);Bt(this,"glowMap",new Map);Bt(this,"positions",new Map);Bt(this,"labelSprites",new Map);Bt(this,"hoveredNode",null);Bt(this,"selectedNode",null);Bt(this,"colorMode","type");Bt(this,"materializingNodes",[]);Bt(this,"dissolvingNodes",[]);Bt(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,a={type:n.userData.type??"fact",retention:s},o=Bl(a,t),l=new st(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 wr(s,16,16),l=new fu({color:new st(r),emissive:new st(r),emissiveIntensity:a?0:.3+t.retention*.5,roughness:.3,metalness:.1,transparent:!0,opacity:a?.2:.3+t.retention*.7}),c=new Ee(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 Xi({map:ao(),color:new st(r),transparent:!0,opacity:n>0?a?.1:.3+t.retention*.35:0,blending:De,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 f=t.label||t.type,u=this.createTextSprite(f,"#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 p=new Ae;return new Bi(new Xi({map:p,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,f=(n.height-h)/2,u=h/2;s.fillStyle="rgba(10, 16, 28, 0.82)",s.beginPath(),s.moveTo(d+u,f),s.lineTo(d+c-u,f),s.quadraticCurveTo(d+c,f,d+c,f+u),s.lineTo(d+c,f+h-u),s.quadraticCurveTo(d+c,f+h,d+c-u,f+h),s.lineTo(d+u,f+h),s.quadraticCurveTo(d,f+h,d,f+h-u),s.lineTo(d,f+u),s.quadraticCurveTo(d,f,d+u,f),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 vc(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 Ee&&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=zl(h);if(c.mesh.scale.setScalar(Math.max(.001,d)),c.frame>=5){const f=Math.min((c.frame-5)/5,1),u=c.glow.material;u.opacity=f*.4;const g=c.targetScale*6*d;c.glow.scale.set(g,g,1)}if(c.frame>=40){const f=Math.min((c.frame-40)/20,1);c.label.material.opacity=f*.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-Ng(h),f=Math.max(.001,c.originalScale*d);c.mesh.scale.setScalar(f);const u=f*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)*zl(h),f=this.meshMap.get(c.id);f&&f.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 f=this.positions.get(c),u=f?n.position.distanceTo(f):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 p=this.glowMap.get(c);if(p){const T=p.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),f=l.material,u=c===this.hoveredNode||c===this.selectedNode?1:d<40?.9:d<80?.9*(1-(d-40)/40):0;f.opacity+=(u-f.opacity)*.1})}getMeshes(){return Array.from(this.meshMap.values())}dispose(){this.group.traverse(t=>{var e,n,s,r,a;t instanceof Ee?((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 Og(i){return 1-Math.pow(1-i,3)}class Bg{constructor(){Bt(this,"group");Bt(this,"growingEdges",[]);Bt(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 Sr({color:9133302,transparent:!0,opacity:Math.min(.25+n.weight*.5,.8),blending:De,depthWrite:!1}),c=new eo(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 Sr({color:9133302,transparent:!0,opacity:0,blending:De,depthWrite:!1}),l=new eo(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=Og(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 zg{constructor(t){Bt(this,"starField");Bt(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 ce(n,3)),e.setAttribute("size",new ce(s,1));const r=new li({color:6514417,size:.5,transparent:!0,opacity:.4,sizeAttenuation:!0,blending:De});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 ce(n,3)),e.setAttribute("color",new ce(s,3));const r=new li({size:.3,vertexColors:!0,transparent:!0,opacity:.4,blending:De,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 f=0;f=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 st(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 st(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),f=.85+Math.sin(r.age*.35)*.15,u=.5+d*4.5*f,g=.2+d*1.8*f;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,f=r.startPos,u=r.lastTargetPos,g=u.x-f.x,_=u.y-f.y,m=u.z-f.z,p=Math.sqrt(g*g+_*_+m*m),T=(f.x+u.x)*.5,E=(f.y+u.y)*.5+30+p*.15,y=(f.z+u.z)*.5,D=1-d,A=D*D,C=2*D*d,I=d*d,S=A*f.x+C*T+I*u.x,M=A*f.y+C*E+I*u.y,w=A*f.z+C*y+I*u.z;r.sprite.position.set(S,M,w),r.core.position.set(S,M,w);const Y=1-d*.35;r.sprite.scale.setScalar(5*Y),r.core.scale.setScalar(2*Y),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 Tn={bloomStrength:.8,rotateSpeed:.3,fogColor:328976,fogDensity:.008,nebulaIntensity:0,chromaticIntensity:.002,vignetteRadius:.9,breatheAmplitude:1},Bn={bloomStrength:1.8,rotateSpeed:.08,fogColor:656672,fogDensity:.006,nebulaIntensity:1,chromaticIntensity:.005,vignetteRadius:.7,breatheAmplitude:2};class Hg{constructor(){Bt(this,"active",!1);Bt(this,"transition",0);Bt(this,"transitionSpeed",.008);Bt(this,"current");Bt(this,"auroraHue",0);this.current={...Tn}}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(Tn.bloomStrength,Bn.bloomStrength,o),this.current.rotateSpeed=this.lerp(Tn.rotateSpeed,Bn.rotateSpeed,o),this.current.fogDensity=this.lerp(Tn.fogDensity,Bn.fogDensity,o),this.current.nebulaIntensity=this.lerp(Tn.nebulaIntensity,Bn.nebulaIntensity,o),this.current.chromaticIntensity=this.lerp(Tn.chromaticIntensity,Bn.chromaticIntensity,o),this.current.vignetteRadius=this.lerp(Tn.vignetteRadius,Bn.vignetteRadius,o),this.current.breatheAmplitude=this.lerp(Tn.breatheAmplitude,Bn.breatheAmplitude,o),e.strength=this.current.bloomStrength,n.autoRotateSpeed=this.current.rotateSpeed;const l=new st(Tn.fogColor),c=new st(Bn.fogColor),h=l.clone().lerp(c,o);if(t.fog=new Tr(h,this.current.fogDensity),o>.01){this.auroraHue=r*.1%1;const d=new st().setHSL(.75+this.auroraHue*.15,.8,.5),f=new st().setHSL(.55+this.auroraHue*.2,.7,.4);s.point1.color.lerp(d,o*.3),s.point2.color.lerp(f,o*.3)}else s.point1.color.set(6514417),s.point2.color.set(11032055)}lerp(t,e,n){return t+(e-t)*n}}const Vg=50,_s=[];function Gg(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 Wg(i,t){if(_s.length<=Vg)return;const e=_s.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 Xg(i,t,e){var d,f;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},_=Gg(g,e,c),m=s.addNode(g,_,{isBirthRitual:!0});a.addNode(u.id,m),_s.push(u.id),Wg(t,e);const p=new st(Yl[g.type]||"#00ffd1"),T=p.clone();T.offsetHSL(.15,0,0),n.createBirthOrb(o,p,()=>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,p),n.createShockwave(E,p,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 st(54527)),u.source_id&&h.has(u.source_id)&&n.addPulse(u.source_id,1,new st(54527),.02),u.target_id&&h.has(u.target_id)&&n.addPulse(u.target_id,1,new st(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 st(16729943);n.createImplosion(g,m)}r.removeEdgesForNode(u.id),s.removeNode(u.id),a.removeNode(u.id);const _=_s.indexOf(u.id);_!==-1&&_s.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 st(65416),.01);const m=c.get(g);m&&(n.createShockwave(m,new st(65416),o),n.createSpawnBurst(m,new st(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 st(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 st(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 st(8490232),.02)});break}case"DreamStarted":{h.forEach((u,g)=>{n.addPulse(g,1,new st(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 st(12616956),.01);break}case"DreamCompleted":{n.createSpawnBurst(new P(0,0,0),new st(11032055)),n.createShockwave(new P(0,0,0),new st(11032055),o);break}case"RetentionDecayed":{const u=(f=i.data)==null?void 0:f.id;u&&h.has(u)&&n.addPulse(u,.8,new st(16729943),.03);break}case"ConsolidationCompleted":{h.forEach((u,g)=>{n.addPulse(g,.4+Math.random()*.3,new st(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 st(1370310))}}break}case"MemorySuppressed":{const u=i.data;if(!u.id)break;const g=c.get(u.id);if(g){n.createImplosion(g,new st(11032055));const _=Math.max(1,u.suppression_count??1),m=Math.min(.4+_*.15,1);n.addPulse(u.id,m,new st(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 st(65416)),n.addPulse(u.id,1,new st(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 p=0;p');function kg(i,t){lr(t,!0);let e=Qs(t,"events",19,()=>[]),n=Qs(t,"isDreaming",3,!1),s,r,a,o,l,c,h,d,f,p,g,v=0,m=[];Al(()=>{r=gg(s),p=Ug(r.scene).material,g=Bg(r.composer),c=new bg(r.scene),o=new Sg,l=new Eg,h=new Tg(r.scene),f=new wg;const U=o.createNodes(t.nodes);l.createEdges(t.edges,U),d=new xg(U),m=[...t.nodes],r.scene.add(l.group),r.scene.add(o.group),u(),window.addEventListener("resize",b),s.addEventListener("pointermove",y),s.addEventListener("click",L)}),Rl(()=>{cancelAnimationFrame(a),window.removeEventListener("resize",b),s==null||s.removeEventListener("pointermove",y),s==null||s.removeEventListener("click",L),h==null||h.dispose(),c==null||c.dispose(),o==null||o.dispose(),l==null||l.dispose(),r&&vg(r)});function u(){a=requestAnimationFrame(u);const A=performance.now()*.001;d.tick(t.edges),o.updatePositions(),l.updatePositions(o.positions),l.animateEdges(o.positions),c.animate(A),o.animate(A,m,r.camera),f.setActive(n()),f.update(r.scene,r.bloomPass,r.controls,r.lights,A),Ig(p,A,f.current.nebulaIntensity,s.clientWidth,s.clientHeight),zg(g,A,f.current.nebulaIntensity),T(),h.update(o.meshMap,r.camera,o.positions),r.controls.update(),r.composer.render()}function T(){if(!e()||e().length<=v)return;const A=e().slice(v);v=e().length;const U={effects:h,nodeManager:o,edgeManager:l,forceSim:d,camera:r.camera,onMutation:S=>{var M;S.type==="nodeAdded"?m=[...m,S.node]:S.type==="nodeRemoved"&&(m=m.filter(D=>D.id!==S.nodeId)),(M=t.onGraphMutation)==null||M.call(t,S)}};for(const S of A)Pg(S,U,m)}function b(){!s||!r||_g(r,s)}function y(A){const U=s.getBoundingClientRect();r.mouse.x=(A.clientX-U.left)/U.width*2-1,r.mouse.y=-((A.clientY-U.top)/U.height)*2+1,r.raycaster.setFromCamera(r.mouse,r.camera);const S=r.raycaster.intersectObjects(o.getMeshes());S.length>0?(o.hoveredNode=S[0].object.userData.nodeId,s.style.cursor="pointer"):(o.hoveredNode=null,s.style.cursor="grab")}function L(){var A;if(o.hoveredNode){o.selectedNode=o.hoveredNode,(A=t.onSelect)==null||A.call(t,o.hoveredNode);const U=o.positions.get(o.hoveredNode);U&&r.controls.target.lerp(U.clone(),.5)}}var R=Hg();Lc(R,A=>s=A,()=>s),Be(i,R),cr()}var Vg=qe('
'),Gg=qe('
');function Wg(i,t){lr(t,!0);let e=Qs(t,"width",3,240),n=Qs(t,"height",3,80);function s(m){return t.stability<=0?0:Math.exp(-m/t.stability)}let r=ti(()=>{const m=[],u=Math.max(t.stability*3,30),T=4,b=e()-T*2,y=n()-T*2;for(let L=0;L<=50;L++){const R=L/50*u,A=s(R),U=T+L/50*b,S=T+(1-A)*y;m.push(`${L===0?"M":"L"}${U.toFixed(1)},${S.toFixed(1)}`)}return m.join(" ")}),a=ti(()=>[{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=Gg(),c=Dt(l),h=Dt(c),d=Bt(h),f=Bt(d),p=Bt(f),g=Bt(p);Cl(),Rt(c);var v=Bt(c,2);$r(v,21,()=>q(a),Jr,(m,u)=>{var T=Vg(),b=Dt(T),y=Dt(b);Rt(b);var L=Bt(b,2),R=Dt(L);Rt(L),Rt(T),ln((A,U)=>{_e(y,`${q(u).label??""}:`),Ul(L,`color: ${A??""}`),_e(R,`${U??""}%`)},[()=>o(q(u).value),()=>(q(u).value*100).toFixed(0)]),Be(m,T)}),Rt(v),Rt(l),ln(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(f,"d",q(r)),Ve(p,"d",`${q(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)]),Be(i,l),cr()}function wl(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,f=hs.has(l.source)&&s.has(l.target));return{visibleNodes:a,visibleEdges:o,nodeOpacities:r}}function Xg(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 Yg=qe(`
"u")return pr;const i=localStorage.getItem(Pc);if(i===null)return pr;const t=Number(i);return Number.isFinite(t)?Math.min(lo,Math.max(oo,t)):pr}const ni=n_();function n_(){let i=me(!1),t=me(ha(new Date)),e=me(!1),n=me(1),s=me(!1),r=me(ha(e_()));return{get temporalEnabled(){return H(i)},set temporalEnabled(a){Gt(i,a,!0)},get temporalDate(){return H(t)},set temporalDate(a){Gt(t,a,!0)},get temporalPlaying(){return H(e)},set temporalPlaying(a){Gt(e,a,!0)},get temporalSpeed(){return H(n)},set temporalSpeed(a){Gt(n,a,!0)},get dreamMode(){return H(s)},set dreamMode(a){Gt(s,a,!0)},get brightness(){return H(r)},set brightness(a){const o=Math.min(lo,Math.max(oo,a));if(Gt(r,o,!0),typeof localStorage<"u")try{localStorage.setItem(Pc,String(o))}catch{}},brightnessMin:oo,brightnessMax:lo,brightnessDefault:pr}}var i_=Re('
');function s_(i,t){Ms(t,!0);let e=gs(t,"events",19,()=>[]),n=gs(t,"isDreaming",3,!1),s=gs(t,"colorMode",3,"type");zc(()=>{l==null||l.setColorMode(s())});let r,a,o,l,c,h,d,f,u,g,_,m=null,p=[];Hl(()=>{a=Cg(r),g=jg(a.scene).material,_=Qg(a.composer),h=new zg(a.scene),l=new Fg,l.colorMode=s(),c=new Bg,d=new kg(a.scene),u=new Hg;const M=l.createNodes(t.nodes);c.createEdges(t.edges,M),f=new Lg(M),p=[...t.nodes],a.scene.add(c.group),a.scene.add(l.group),E(),window.addEventListener("resize",D),r.addEventListener("pointermove",A),r.addEventListener("click",C)}),Vl(()=>{cancelAnimationFrame(o),window.removeEventListener("resize",D),r==null||r.removeEventListener("pointermove",A),r==null||r.removeEventListener("click",C),d==null||d.dispose(),h==null||h.dispose(),l==null||l.dispose(),c==null||c.dispose(),a&&Dg(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 w=S*.001;f.tick(t.edges),l.updatePositions(),c.updatePositions(l.positions),c.animateEdges(l.positions),h.animate(w),l.animate(w,p,a.camera,ni.brightness),u.setActive(n()),u.update(a.scene,a.bloomPass,a.controls,a.lights,w),Zg(g,w,u.current.nebulaIntensity,r.clientWidth,r.clientHeight),t_(_,w,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 w of e()){if(w===m)break;S.push(w)}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:f,camera:a.camera,onMutation:w=>{var Y;w.type==="nodeAdded"?p=[...p,w.node]:w.type==="nodeRemoved"&&(p=p.filter(V=>V.id!==w.nodeId)),(Y=t.onGraphMutation)==null||Y.call(t,w)}};for(let w=S.length-1;w>=0;w--)Xg(S[w],M,p)}function D(){!r||!a||Pg(a,r)}function A(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 w=a.raycaster.intersectObjects(l.getMeshes());w.length>0?(l.hoveredNode=w[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=i_();Wc(I,S=>r=S,()=>r),ye(i,I),Ss()}var r_=Re('
'),a_=Re('
');function o_(i,t){Ms(t,!0);let e=gs(t,"width",3,240),n=gs(t,"height",3,80);function s(m){return t.stability<=0?0:Math.exp(-m/t.stability)}let r=oi(()=>{const m=[],p=Math.max(t.stability*3,30),T=4,E=e()-T*2,y=n()-T*2;for(let D=0;D<=50;D++){const A=D/50*p,C=s(A),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=oi(()=>[{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=a_(),c=yt(l),h=yt(c),d=Tt(h),f=Tt(d),u=Tt(f),g=Tt(u);kc(),St(c);var _=Tt(c,2);mr(_,21,()=>H(a),ua,(m,p)=>{var T=r_(),E=yt(T),y=yt(E);St(E);var D=Tt(E,2),A=yt(D);St(D),St(T),nn((C,I)=>{fe(y,`${H(p).label??""}:`),co(D,`color: ${C??""}`),fe(A,`${I??""}%`)},[()=>o(H(p).value),()=>(H(p).value*100).toFixed(0)]),ye(m,T)}),St(_),St(l),nn(m=>{Me(c,"width",e()),Me(c,"height",n()),Me(c,"viewBox",`0 0 ${e()??""} ${n()??""}`),Me(h,"y1",4+(n()-8)*.5),Me(h,"x2",e()-4),Me(h,"y2",4+(n()-8)*.5),Me(d,"y1",4+(n()-8)*.8),Me(d,"x2",e()-4),Me(d,"y2",4+(n()-8)*.8),Me(f,"d",H(r)),Me(u,"d",`${H(r)??""} L${e()-4},${n()-4} L4,${n()-4} Z`),Me(g,"cy",4+(1-t.retention)*(n()-8)),Me(g,"fill",m)},[()=>o(t.retention)]),ye(i,l),Ss()}function kl(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,f=hs.has(l.source)&&s.has(l.target));return{visibleNodes:a,visibleEdges:o,nodeOpacities:r}}function l_(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 c_=Re(`
`),qg=qe('');function jg(i,t){lr(t,!0);let e=Ie(!1),n=Ie(!1),s=Ie(1),r=Ie(100),a,o=0,l=ti(()=>Xg(t.nodes)),c=ti(()=>{const b=q(l).oldest.getTime(),L=q(l).newest.getTime()-b||1;return new Date(b+q(r)/100*L)});function h(b){return b.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function d(){se(e,!q(e)),t.onToggle(q(e)),q(e)&&(se(r,100),t.onDateChange(q(c)))}function f(){se(n,!q(n)),q(n)?(se(r,0),o=performance.now(),p()):cancelAnimationFrame(a)}function p(){a=requestAnimationFrame(b=>{const y=(b-o)/1e3;o=b;const L=q(l).oldest.getTime(),A=(q(l).newest.getTime()-L)/(1440*60*1e3)||1,U=q(s)/A*100;if(se(r,Math.min(100,q(r)+U*y),!0),t.onDateChange(q(c)),q(r)>=100){se(n,!1);return}p()})}function g(){t.onDateChange(q(c))}Rl(()=>{cancelAnimationFrame(a)});var v=Rc(),m=Pl(v);{var u=b=>{var y=Yg(),L=Dt(y),R=Dt(L),A=Dt(R),U=Dt(A),S=Dt(U,!0);Rt(U);var M=Bt(U,2),D=Dt(M);D.value=D.__value=1;var W=Bt(D);W.value=W.__value=7;var z=Bt(W);z.value=z.__value=30,Rt(M),Rt(A);var V=Bt(A,2),$=Dt(V,!0);Rt(V);var G=Bt(V,2);Rt(R);var J=Bt(R,2);Ll(J);var k=Bt(J,2),it=Dt(k),ut=Dt(it,!0);Rt(it);var yt=Bt(it,2),Lt=Dt(yt,!0);Rt(yt),Rt(k),Rt(L),Rt(y),ln((jt,Y,et)=>{_e(S,q(n)?"⏸":"▶"),_e($,jt),_e(ut,Y),_e(Lt,et)},[()=>h(q(c)),()=>h(q(l).oldest),()=>h(q(l).newest)]),$e("click",U,f),Nl(M,()=>q(s),jt=>se(s,jt)),$e("click",G,d),$e("input",J,g),Il(J,()=>q(r),jt=>se(r,jt)),Be(b,y)},T=b=>{var y=qg();$e("click",y,d),Be(b,y)};wi(m,b=>{q(e)?b(u):b(T,!1)})}Be(i,v),cr()}Dl(["click","input"]);var Zg=qe('

Loading memory graph...

'),Kg=qe('

Your Mind Awaits

'),$g=qe(' · · ',1),Jg=qe(' '),Qg=qe('
'),t_=qe("
"),e_=qe(`

Memory Detail

Retention Forecast
◬ Explore Connections
`),n_=qe(`
`);function y_(i,t){lr(t,!0);const e=()=>Dc(Uc,"$eventFeed",n),[n,s]=Pc();let r=Ie(null),a=Ie(null),o=Ie(!0),l=Ie(""),c=Ie(!1),h=Ie(""),d=Ie(150),f=Ie(!1),p=Ie(Ac(new Date)),g=Ie(0),v=Ie(0),m=ti(()=>q(r)?q(f)?wl(q(r).nodes,q(r).edges,q(p)).visibleNodes:q(r).nodes:[]),u=ti(()=>q(r)?q(f)?wl(q(r).nodes,q(r).edges,q(p)).visibleEdges:q(r).edges:[]);function T(rt){if(q(r))switch(rt.type){case"nodeAdded":q(r).nodes=[...q(r).nodes,rt.node],q(r).nodeCount=q(r).nodes.length,se(g,q(r).nodeCount,!0);break;case"nodeRemoved":q(r).nodes=q(r).nodes.filter(Ct=>Ct.id!==rt.nodeId),q(r).nodeCount=q(r).nodes.length,se(g,q(r).nodeCount,!0);break;case"edgeAdded":q(r).edges=[...q(r).edges,rt.edge],q(r).edgeCount=q(r).edges.length,se(v,q(r).edgeCount,!0);break;case"edgesRemoved":q(r).edges=q(r).edges.filter(Ct=>Ct.source!==rt.nodeId&&Ct.target!==rt.nodeId),q(r).edgeCount=q(r).edges.length,se(v,q(r).edgeCount,!0);break;case"nodeUpdated":{const Ct=q(r).nodes.find(w=>w.id===rt.nodeId);Ct&&(Ct.retention=rt.retention);break}}}Al(()=>b());async function b(rt,Ct){se(o,!0),se(l,"");try{se(r,await Yi.graph({max_nodes:q(d),depth:3,query:rt||void 0,center_id:Ct||void 0}),!0),q(r)&&(se(g,q(r).nodeCount,!0),se(v,q(r).edgeCount,!0))}catch{se(l,"No memories yet. Start using Vestige to populate your graph.")}finally{se(o,!1)}}async function y(){se(c,!0);try{await Yi.dream(),await b()}catch{}finally{se(c,!1)}}async function L(rt){try{se(a,await Yi.memories.get(rt),!0)}catch{se(a,null)}}function R(){q(h).trim()&&b(q(h))}var A=n_(),U=Dt(A);{var S=rt=>{var Ct=Zg();Be(rt,Ct)},M=rt=>{var Ct=Kg(),w=Dt(Ct),ve=Bt(Dt(w),4),Ft=Dt(ve,!0);Rt(ve),Rt(w),Rt(Ct),ln(()=>_e(Ft,q(l))),Be(rt,Ct)},D=rt=>{kg(rt,{get nodes(){return q(m)},get edges(){return q(u)},get centerId(){return q(r).center_id},get events(){return e()},get isDreaming(){return q(c)},onSelect:L,onGraphMutation:T})};wi(U,rt=>{q(o)?rt(S):q(l)?rt(M,1):q(r)&&rt(D,2)})}var W=Bt(U,2),z=Dt(W),V=Dt(z);Ll(V);var $=Bt(V,2);Rt(z);var G=Bt(z,2),J=Dt(G),k=Dt(J);k.value=k.__value=50;var it=Bt(k);it.value=it.__value=100;var ut=Bt(it);ut.value=ut.__value=150;var yt=Bt(ut);yt.value=yt.__value=200,Rt(J);var Lt=Bt(J,2),jt=Dt(Lt,!0);Rt(Lt);var Y=Bt(Lt,2);Rt(G),Rt(W);var et=Bt(W,2),xt=Dt(et);{var at=rt=>{var Ct=$g(),w=Pl(Ct),ve=Dt(w);Rt(w);var Ft=Bt(w,4),kt=Dt(Ft);Rt(Ft);var Mt=Bt(Ft,4),ie=Dt(Mt);Rt(Mt),ln(()=>{_e(ve,`${q(g)??""} nodes`),_e(kt,`${q(v)??""} edges`),_e(ie,`depth ${q(r).depth??""}`)}),Be(rt,Ct)};wi(xt,rt=>{q(r)&&rt(at)})}Rt(et);var wt=Bt(et,2);{var Ut=rt=>{jg(rt,{get nodes(){return q(r).nodes},onDateChange:Ct=>{se(p,Ct,!0)},onToggle:Ct=>{se(f,Ct,!0)}})};wi(wt,rt=>{q(r)&&rt(Ut)})}var Gt=Bt(wt,2);{var ce=rt=>{var Ct=e_(),w=Dt(Ct),ve=Bt(Dt(w),2);Rt(w);var Ft=Bt(w,2),kt=Dt(Ft),Mt=Dt(kt),ie=Dt(Mt,!0);Rt(Mt);var Et=Bt(Mt,2);$r(Et,17,()=>q(a).tags,Jr,(Xt,C)=>{var nt=Jg(),H=Dt(nt,!0);Rt(nt),ln(()=>_e(H,q(C))),Be(Xt,nt)}),Rt(kt);var E=Bt(kt,2),_=Dt(E,!0);Rt(E);var F=Bt(E,2);$r(F,21,()=>[{label:"Retention",value:q(a).retentionStrength},{label:"Storage",value:q(a).storageStrength},{label:"Retrieval",value:q(a).retrievalStrength}],Jr,(Xt,C)=>{var nt=Qg(),H=Dt(nt),Z=Dt(H),ht=Dt(Z,!0);Rt(Z);var lt=Bt(Z,2),Ot=Dt(lt);Rt(lt),Rt(H);var he=Bt(H,2),Ee=Dt(he);Rt(he),Rt(nt),ln(Qt=>{_e(ht,q(C).label),_e(Ot,`${Qt??""}%`),Ul(Ee,`width: ${q(C).value*100}%; background: ${q(C).value>.7?"#10b981":q(C).value>.4?"#f59e0b":"#ef4444"}`)},[()=>(q(C).value*100).toFixed(1)]),Be(Xt,nt)}),Rt(F);var j=Bt(F,2),K=Bt(Dt(j),2);{let Xt=ti(()=>q(a).storageStrength*30);Wg(K,{get retention(){return q(a).retentionStrength},get stability(){return q(Xt)}})}Rt(j);var X=Bt(j,2),St=Dt(X),ot=Dt(St);Rt(St);var dt=Bt(St,2),Zt=Dt(dt);Rt(dt);var tt=Bt(dt,2);{var mt=Xt=>{var C=t_(),nt=Dt(C);Rt(C),ln(H=>_e(nt,`Accessed: ${H??""}`),[()=>new Date(q(a).lastAccessedAt).toLocaleString()]),Be(Xt,C)};wi(tt,Xt=>{q(a).lastAccessedAt&&Xt(mt)})}var bt=Bt(tt,2),Pt=Dt(bt);Rt(bt),Rt(X);var ft=Bt(X,2),Yt=Dt(ft),zt=Bt(Yt,2);Rt(ft),Cl(2),Rt(Ft),Rt(Ct),ln((Xt,C)=>{_e(ie,q(a).nodeType),_e(_,q(a).content),_e(ot,`Created: ${Xt??""}`),_e(Zt,`Updated: ${C??""}`),_e(Pt,`Reviews: ${q(a).reviewCount??0??""}`)},[()=>new Date(q(a).createdAt).toLocaleString(),()=>new Date(q(a).updatedAt).toLocaleString()]),$e("click",ve,()=>se(a,null)),$e("click",Yt,()=>{q(a)&&Yi.memories.promote(q(a).id)}),$e("click",zt,()=>{q(a)&&Yi.memories.demote(q(a).id)}),Be(rt,Ct)};wi(Gt,rt=>{q(a)&&rt(ce)})}Rt(A),ln(()=>{Lt.disabled=q(c),Cc(Lt,1,`px-4 py-2 rounded-xl bg-dream/20 border border-dream/40 text-dream-glow text-sm + [&::-webkit-slider-thumb]:shadow-[0_0_8px_rgba(129,140,248,0.4)]"/>
`),h_=Re('');function u_(i,t){Ms(t,!0);let e=me(!1),n=me(!1),s=me(1),r=me(100),a,o=0,l=oi(()=>l_(t.nodes)),c=oi(()=>{const E=H(l).oldest.getTime(),D=H(l).newest.getTime()-E||1;return new Date(E+H(r)/100*D)});function h(E){return E.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}function d(){Gt(e,!H(e)),t.onToggle(H(e)),H(e)&&(Gt(r,100),t.onDateChange(H(c)))}function f(){Gt(n,!H(n)),H(n)?(Gt(r,0),o=performance.now(),u()):cancelAnimationFrame(a)}function u(){H(n)&&(a=requestAnimationFrame(E=>{const y=(E-o)/1e3;o=E;const D=H(l).oldest.getTime(),C=(H(l).newest.getTime()-D)/(1440*60*1e3)||1,I=H(s)/C*100;if(Gt(r,Math.min(100,H(r)+I*y),!0),t.onDateChange(H(c)),H(r)>=100){Gt(n,!1);return}u()}))}function g(){t.onDateChange(H(c))}Vl(()=>{Gt(n,!1),cancelAnimationFrame(a)});var _=Hc(),m=Gl(_);{var p=E=>{var y=c_(),D=yt(y),A=yt(D),C=yt(A),I=yt(C),S=yt(I,!0);St(I);var M=Tt(I,2),w=yt(M);w.value=w.__value=1;var Y=Tt(w);Y.value=Y.__value=7;var V=Tt(Y);V.value=V.__value=30,St(M),St(C);var j=Tt(C,2),$=yt(j,!0);St(j);var q=Tt(j,2);St(A);var J=Tt(A,2);da(J);var X=Tt(J,2),it=yt(X),ft=yt(it,!0);St(it);var Mt=Tt(it,2),Nt=yt(Mt,!0);St(Mt),St(X),St(D),St(y),nn((Wt,Z,nt)=>{fe(S,H(n)?"⏸":"▶"),fe($,Wt),fe(ft,Z),fe(Nt,nt)},[()=>h(H(c)),()=>h(H(l).oldest),()=>h(H(l).newest)]),He("click",I,f),Xl(M,()=>H(s),Wt=>Gt(s,Wt)),He("click",q,d),He("input",J,g),fa(J,()=>H(r),Wt=>Gt(r,Wt)),ye(E,y)},T=E=>{var y=h_();He("click",y,d),ye(E,y)};ti(m,E=>{H(e)?E(p):E(T,!1)})}ye(i,_),Ss()}Wl(["click","input"]);var d_=Re('
'),f_=Re('
FSRS accessibility
');function p_(i,t){Ms(t,!1);const e=["active","dormant","silent","unavailable"];Xc();var n=f_(),s=Tt(yt(n),2);mr(s,1,()=>e,r=>r,(r,a)=>{var o=d_(),l=yt(o),c=Tt(l,2),h=yt(c,!0);St(c);var d=Tt(c,2),f=yt(d,!0);St(d),St(o),nn(u=>{co(l,`background: ${ro[H(a)]??""}; box-shadow: 0 0 6px ${ro[H(a)]??""}55;`),fe(h,H(a)),fe(f,u)},[()=>{var u;return((u=Ig[H(a)].match(/\(([^)]+)\)/))==null?void 0:u[1])??""}]),ye(r,o)}),St(n),ye(i,n),Ss()}var m_=Re('

Loading memory graph...

'),g_=Re(`

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
`),__=Re('

Your Mind Awaits

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

'),v_=Re('

Your Mind Awaits

'),x_=Re(' · · ',1),M_=Re('
'),S_=Re(' '),y_=Re('
'),E_=Re("
"),b_=Re(`

Memory Detail

Retention Forecast
◬ Explore Connections
`),T_=Re(`
`);function q_(i,t){Ms(t,!0);const e=()=>Gc(Yc,"$eventFeed",n),[n,s]=Vc();let r=me(null),a=me(null),o=me(!0),l=me(""),c=me(!1),h=me(""),d=me(150),f=me(!1),u=me(ha(new Date)),g=me("type"),_=me(0),m=me(0),p=oi(()=>H(r)?H(f)?kl(H(r).nodes,H(r).edges,H(u)).visibleNodes:H(r).nodes:[]),T=oi(()=>H(r)?H(f)?kl(H(r).nodes,H(r).edges,H(u)).visibleEdges:H(r).edges:[]);function E(F){if(H(r))switch(F.type){case"nodeAdded":H(r).nodes=[...H(r).nodes,F.node],H(r).nodeCount=H(r).nodes.length,Gt(_,H(r).nodeCount,!0);break;case"nodeRemoved":H(r).nodes=H(r).nodes.filter(k=>k.id!==F.nodeId),H(r).nodeCount=H(r).nodes.length,Gt(_,H(r).nodeCount,!0);break;case"edgeAdded":H(r).edges=[...H(r).edges,F.edge],H(r).edgeCount=H(r).edges.length,Gt(m,H(r).edgeCount,!0);break;case"edgesRemoved":H(r).edges=H(r).edges.filter(k=>k.source!==F.nodeId&&k.target!==F.nodeId),H(r).edgeCount=H(r).edges.length,Gt(m,H(r).edgeCount,!0);break;case"nodeUpdated":{const k=H(r).nodes.find(G=>G.id===F.nodeId);k&&(k.retention=F.retention);break}}}Hl(()=>y());async function y(F,k){var G;Gt(o,!0),Gt(l,"");try{const ot=!F&&!k;if(Gt(r,await gi.graph({max_nodes:H(d),depth:3,query:F||void 0,center_id:k||void 0,sort:ot?"recent":void 0}),!0),ot&&H(r)&&H(r).nodeCount<=1&&H(r).edgeCount===0){const Q=await gi.graph({max_nodes:H(d),depth:3,sort:"connected"});Q&&Q.nodeCount>H(r).nodeCount&&Gt(r,Q,!0)}H(r)&&(Gt(_,H(r).nodeCount,!0),Gt(m,H(r).edgeCount,!0))}catch(ot){const Q=ot instanceof Error?ot.message:String(ot),lt=Q.replace(/\/[\w./-]+\.(sqlite|rs|db|toml|lock)\b/g,"[path]").slice(0,200),Ft=ot instanceof TypeError||/failed to fetch|NetworkError|load failed/i.test(Q)||/^API 500:?\s*(Internal Server Error)?\s*$/i.test(Q.trim()),tt=(((G=H(r))==null?void 0:G.nodeCount)??0)===0&&/not found|404|empty|no memor/i.test(Q);Ft?Gt(l,"OFFLINE"):tt?Gt(l,"EMPTY"):Gt(l,`Failed to load graph: ${lt}`)}finally{Gt(o,!1)}}async function D(){Gt(c,!0);try{await gi.dream(),await y()}catch{}finally{Gt(c,!1)}}async function A(F){try{Gt(a,await gi.memories.get(F),!0)}catch{Gt(a,null)}}function C(){H(h).trim()&&y(H(h))}var I=T_(),S=yt(I);{var M=F=>{var k=m_();ye(F,k)},w=F=>{var k=g_(),G=yt(k),ot=Tt(yt(G),8),Q=yt(ot),lt=Tt(Q,2);St(ot),St(G),St(k),nn(()=>Me(lt,"href",`${bo??""}/settings`)),He("click",Q,()=>y()),ye(F,k)},Y=F=>{var k=__();ye(F,k)},V=F=>{var k=v_(),G=yt(k),ot=Tt(yt(G),4),Q=yt(ot,!0);St(ot),St(G),St(k),nn(()=>fe(Q,H(l))),ye(F,k)},j=F=>{s_(F,{get nodes(){return H(p)},get edges(){return H(T)},get centerId(){return H(r).center_id},get events(){return e()},get isDreaming(){return H(c)},get colorMode(){return H(g)},onSelect:A,onGraphMutation:E})};ti(S,F=>{H(o)?F(M):H(l)==="OFFLINE"?F(w,1):H(l)==="EMPTY"?F(Y,2):H(l)?F(V,3):H(r)&&F(j,4)})}var $=Tt(S,2),q=yt($),J=yt(q);da(J);var X=Tt(J,2);St(q);var it=Tt(q,2),ft=yt(it),Mt=yt(ft),Nt=Tt(Mt,2);St(ft);var Wt=Tt(ft,2),Z=yt(Wt);Z.value=Z.__value=50;var nt=Tt(Z);nt.value=nt.__value=100;var _t=Tt(nt);_t.value=_t.__value=150;var at=Tt(_t);at.value=at.__value=200,St(Wt);var wt=Tt(Wt,2),Pt=Tt(yt(wt),2);da(Pt);var kt=Tt(Pt,2),le=yt(kt);St(kt),St(wt);var Ht=Tt(wt,2),de=yt(Ht,!0);St(Ht);var R=Tt(Ht,2);St(it),St($);var Ue=Tt($,2),qt=yt(Ue);{var jt=F=>{var k=x_(),G=Gl(k),ot=yt(G);St(G);var Q=Tt(G,4),lt=yt(Q);St(Q);var Ft=Tt(Q,4),tt=yt(Ft);St(Ft),nn(()=>{fe(ot,`${H(_)??""} nodes`),fe(lt,`${H(m)??""} edges`),fe(tt,`depth ${H(r).depth??""}`)}),ye(F,k)};ti(qt,F=>{H(r)&&F(jt)})}St(Ue);var bt=Tt(Ue,2);{var oe=F=>{var k=M_(),G=yt(k);p_(G,{}),St(k),ye(F,k)};ti(bt,F=>{H(g)==="state"&&F(oe)})}var Et=Tt(bt,2);{var b=F=>{u_(F,{get nodes(){return H(r).nodes},onDateChange:k=>{Gt(u,k,!0)},onToggle:k=>{Gt(f,k,!0)}})};ti(Et,F=>{H(r)&&F(b)})}var v=Tt(Et,2);{var O=F=>{var k=b_(),G=yt(k),ot=Tt(yt(G),2);St(G);var Q=Tt(G,2),lt=yt(Q),Ft=yt(lt),tt=yt(Ft,!0);St(Ft);var mt=Tt(Ft,2);mr(mt,17,()=>H(a).tags,ua,(Ge,Ie)=>{var Ce=S_(),on=yt(Ce,!0);St(Ce),nn(()=>fe(on,H(Ie))),ye(Ge,Ce)}),St(lt);var At=Tt(lt,2),Dt=yt(At,!0);St(At);var pt=Tt(At,2);mr(pt,21,()=>[{label:"Retention",value:H(a).retentionStrength},{label:"Storage",value:H(a).storageStrength},{label:"Retrieval",value:H(a).retrievalStrength}],ua,(Ge,Ie)=>{var Ce=y_(),on=yt(Ce),pi=yt(on),ws=yt(pi,!0);St(pi);var Xn=Tt(pi,2),As=yt(Xn);St(Xn),St(on);var Yn=Tt(on,2),Rs=yt(Yn);St(Yn),St(Ce),nn(Cs=>{fe(ws,H(Ie).label),fe(As,`${Cs??""}%`),co(Rs,`width: ${H(Ie).value*100}%; background: ${H(Ie).value>.7?"#10b981":H(Ie).value>.4?"#f59e0b":"#ef4444"}`)},[()=>(H(Ie).value*100).toFixed(1)]),ye(Ge,Ce)}),St(pt);var Xt=Tt(pt,2),Ot=Tt(yt(Xt),2);{let Ge=oi(()=>H(a).storageStrength*30);o_(Ot,{get retention(){return H(a).retentionStrength},get stability(){return H(Ge)}})}St(Xt);var Qt=Tt(Xt,2),L=yt(Qt),ct=yt(L);St(L);var W=Tt(L,2),K=yt(W);St(W);var ht=Tt(W,2);{var ut=Ge=>{var Ie=E_(),Ce=yt(Ie);St(Ie),nn(on=>fe(Ce,`Accessed: ${on??""}`),[()=>new Date(H(a).lastAccessedAt).toLocaleString()]),ye(Ge,Ie)};ti(ht,Ge=>{H(a).lastAccessedAt&&Ge(ut)})}var Ut=Tt(ht,2),he=yt(Ut);St(Ut),St(Qt);var _e=Tt(Qt,2),Kt=yt(_e),qe=Tt(Kt,2);St(_e);var an=Tt(_e,2);St(Q),St(k),nn((Ge,Ie)=>{fe(tt,H(a).nodeType),fe(Dt,H(a).content),fe(ct,`Created: ${Ge??""}`),fe(K,`Updated: ${Ie??""}`),fe(he,`Reviews: ${H(a).reviewCount??0??""}`),Me(an,"href",`${bo??""}/explore`)},[()=>new Date(H(a).createdAt).toLocaleString(),()=>new Date(H(a).updatedAt).toLocaleString()]),He("click",ot,()=>Gt(a,null)),He("click",Kt,()=>{H(a)&&gi.memories.promote(H(a).id)}),He("click",qe,()=>{H(a)&&gi.memories.demote(H(a).id)}),ye(F,k)};ti(v,F=>{H(a)&&F(O)})}St(I),nn((F,k)=>{Me(Mt,"aria-checked",H(g)==="type"),Cr(Mt,1,`px-3 py-1.5 rounded-lg transition ${H(g)==="type"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),Me(Nt,"aria-checked",H(g)==="state"),Cr(Nt,1,`px-3 py-1.5 rounded-lg transition ${H(g)==="state"?"bg-synapse/25 text-synapse-glow":"text-dim hover:text-text"}`),Me(wt,"title",`Adjust graph brightness (${F??""}x). Combines with auto distance compensation.`),Me(Pt,"min",ni.brightnessMin),Me(Pt,"max",ni.brightnessMax),fe(le,`${k??""}x`),Ht.disabled=H(c),Cr(Ht,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 - ${q(c)?"glow-dream animate-pulse-glow":""}`),_e(jt,q(c)?"◈ Dreaming...":"◈ Dream")}),$e("keydown",V,rt=>rt.key==="Enter"&&R()),Il(V,()=>q(h),rt=>se(h,rt)),$e("click",$,R),$e("change",J,()=>b()),Nl(J,()=>q(d),rt=>se(d,rt)),$e("click",Lt,y),$e("click",Y,()=>b()),Be(i,A),cr(),s()}Dl(["keydown","click","change"]);export{y_ as component}; + ${H(c)?"glow-dream animate-pulse-glow":""}`),fe(de,H(c)?"◈ Dreaming...":"◈ Dream")},[()=>ni.brightness.toFixed(1),()=>ni.brightness.toFixed(1)]),He("keydown",J,F=>F.key==="Enter"&&C()),fa(J,()=>H(h),F=>Gt(h,F)),He("click",X,C),He("click",Mt,()=>Gt(g,"type")),He("click",Nt,()=>Gt(g,"state")),He("change",Wt,()=>y()),Xl(Wt,()=>H(d),F=>Gt(d,F)),fa(Pt,()=>ni.brightness,F=>ni.brightness=F),He("click",Ht,D),He("click",R,()=>y()),ye(i,I),Ss(),s()}Wl(["click","keydown","change"]);export{q_ as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js.br b/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js.br new file mode 100644 index 0000000..860c091 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js.gz b/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js.gz new file mode 100644 index 0000000..ec29bd6 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/10.CPGa_1iF.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js.br b/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js.br deleted file mode 100644 index dceb4a1..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js.gz b/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js.gz deleted file mode 100644 index ba54cb3..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js.br b/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js.br deleted file mode 100644 index 59f4ea4..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js.gz b/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js.gz deleted file mode 100644 index 67aa5a4..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js b/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js new file mode 100644 index 0000000..09afad5 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js @@ -0,0 +1,7 @@ +import"../chunks/Bzak7iHL.js";import{o as Pt}from"../chunks/CNjeV5xa.js";import{m as Kt,ab as qt,aO as Ht,b as Wt,p as Rt,h as C,e as i,t as S,g as t,d as o,r as n,a as Tt,u as y,s as q,f as U,c as Ct,a2 as Xt,i as Zt,n as Gt}from"../chunks/CvjSAYrz.js";import{s as _,d as Ut,a as kt}from"../chunks/FzvEaXMa.js";import{i as R}from"../chunks/ciN1mm2W.js";import{B as Vt}from"../chunks/DE4u6cUg.js";import{e as V,i as rt}from"../chunks/DTnG8poT.js";import{a as x,c as Nt,b as yt,f as h}from"../chunks/BsvCUYx-.js";import{s as Yt}from"../chunks/Bhad70Ss.js";import{b as Jt}from"../chunks/CVpUe0w3.js";import{g as Qt}from"../chunks/S0ILvWpb.js";import{b as te}from"../chunks/DJWRm1Ki.js";import{a as Ft}from"../chunks/DNjM5a-l.js";import{N as ee}from"../chunks/DzfRjky4.js";import{s as p}from"../chunks/CNfQDikv.js";import{p as ae}from"../chunks/B_YDQCB6.js";const re=Symbol("NaN");function se(a,F,m){Kt&&qt();var v=new Vt(a),T=!Ht();Wt(()=>{var w=F();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 F=$t(a);let m;switch(a){case"lg":m=44;break;case"sm":m=4;break;default:m=28}return Math.max(0,F/2-m)}var ie=yt(''),le=yt(''),de=yt(''),ce=yt(' ',1),ve=yt('');function Et(a,F){Rt(F,!0);let m=ae(F,"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:F.novelty,arousal:F.arousal,reward:F.reward,attention:F.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);C($,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('
'),Fe=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,F){Rt(F,!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))){C(T,!0),C(w,null);try{C(v,await Ft.importance(e),!0),Xt(Y)}catch(s){C(w,s instanceof Error?s.message:String(s),!0),C(v,null)}finally{C(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(Ct([])),ot=q(!0),_t=Ct({});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(){C(ot,!0);try{const s=(await Ft.memories.list({limit:"20"})).memories.slice().sort((u,f)=>P(f)-P(u)).slice(0,20);C($,s,!0),t($).forEach(async u=>{try{const f=await Ft.importance(u.content);_t[u.id]=f.channels}catch{}})}catch{C($,[],!0)}finally{C(ot,!1)}}Pt(it);function ht(e){Qt(`${te}/memories`)}var W=Fe(),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=>C(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.j3H5l-xO.js.br b/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js.br new file mode 100644 index 0000000..a632edd Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js.gz b/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js.gz new file mode 100644 index 0000000..b4543c6 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/11.j3H5l-xO.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js b/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js new file mode 100644 index 0000000..482ce1e --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{o as gt}from"../chunks/CNjeV5xa.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 O,g as r,r as a,n as ht}from"../chunks/CvjSAYrz.js";import{d as wt,s as d,a as Rt}from"../chunks/FzvEaXMa.js";import{i as R}from"../chunks/ciN1mm2W.js";import{e as L,i as U}from"../chunks/DTnG8poT.js";import{a as l,f as v}from"../chunks/BsvCUYx-.js";import{s as W}from"../chunks/DPl3NjBv.js";import{a as Z}from"../chunks/DNjM5a-l.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.DZiW_IZ_.js.br b/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js.br new file mode 100644 index 0000000..0d77603 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js.gz b/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js.gz new file mode 100644 index 0000000..806f119 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/12.DZiW_IZ_.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js b/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js new file mode 100644 index 0000000..1795cd8 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js @@ -0,0 +1,6 @@ +import"../chunks/Bzak7iHL.js";import{o as Ye}from"../chunks/CNjeV5xa.js";import{p as Ge,s as R,c as Ne,h as M,e as s,g as e,r as t,a as Je,f as Ke,d as n,t as k,u as ae}from"../chunks/CvjSAYrz.js";import{d as Ue,s as w,a as h}from"../chunks/FzvEaXMa.js";import{i as q}from"../chunks/ciN1mm2W.js";import{e as ue,i as je}from"../chunks/DTnG8poT.js";import{a as c,f as g,b as re}from"../chunks/BsvCUYx-.js";import{s as X,r as qe}from"../chunks/CNfQDikv.js";import{s as Le}from"../chunks/DPl3NjBv.js";import{s as Z}from"../chunks/Bhad70Ss.js";import{b as Qe}from"../chunks/CVpUe0w3.js";import{b as it}from"../chunks/DMu1Byux.js";import{a as H}from"../chunks/DNjM5a-l.js";import{N as lt}from"../chunks/DzfRjky4.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.DReyqY5Q.js.br b/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js.br new file mode 100644 index 0000000..ccbcb5d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js.gz b/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js.gz new file mode 100644 index 0000000..e5df1ea Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/13.DReyqY5Q.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js b/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js new file mode 100644 index 0000000..e66c260 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js @@ -0,0 +1,3 @@ +import"../chunks/Bzak7iHL.js";import{o as Ut}from"../chunks/CNjeV5xa.js";import{p as Mt,e as r,d as a,r as e,t as S,g as t,u as H,a as Et,s as rt,h as $,f as Rt,c as Vt}from"../chunks/CvjSAYrz.js";import{d as Ht,s as o,a as at,e as Ft}from"../chunks/FzvEaXMa.js";import{i as Y}from"../chunks/ciN1mm2W.js";import{e as V,i as Yt}from"../chunks/DTnG8poT.js";import{a as m,c as Jt,f as h}from"../chunks/BsvCUYx-.js";import{s as yt}from"../chunks/CNfQDikv.js";import{s as bt}from"../chunks/DPl3NjBv.js";import{s as wt}from"../chunks/Bhad70Ss.js";function Xt(y,x,A=3){const g={};for(const v of y){g[v]={};for(const u of y)g[v][u]={count:0,topNames:[]}}for(const v of x){const u=v.origin_project;if(g[u])for(const L of v.transferred_to)g[u][L]&&(g[u][L].count+=1,g[u][L].topNames.push(v.name))}const d=Math.max(0,A);for(const v of y)for(const u of y)g[v][u].topNames=g[v][u].topNames.slice(0,d);return g}function te(y,x){let A=0;for(const g of y){const d=x[g];if(d)for(const v of y){const u=d[v];u&&u.count>A&&(A=u.count)}}return A}function ee(y,x){var g;const A=[];for(const d of y)for(const v of y){const u=(g=x[d])==null?void 0:g[v];u&&u.count>0&&A.push({from:d,to:v,count:u.count,topNames:u.topNames})}return A.sort((d,v)=>v.count-d.count)}function re(y){return y?y.length>12?y.slice(0,11)+"…":y:""}var ae=h('
      '),ne=h(''),se=h(' '),oe=h('
      '),ie=h('
      Top patterns
      '),ce=h('
      No transfers recorded
      '),le=h('
      '),de=h('
      No cross-project transfers recorded yet.
      '),ve=h('
      '),ue=h(''),fe=h('
      ');function pe(y,x){Mt(x,!0);const A=H(()=>Xt(x.projects,x.patterns)),g=H(()=>te(x.projects,t(A))||1);let d=rt(null);function v(s){if(s===0)return"background: rgba(255,255,255,0.02); border-color: rgba(99,102,241,0.05);";const n=s/t(g),f=.1+n*.7;if(n<.5)return`background: rgba(99, 102, 241, ${f.toFixed(3)}); border-color: rgba(129, 140, 248, ${(f*.6).toFixed(3)}); box-shadow: 0 0 ${(n*14).toFixed(1)}px rgba(129, 140, 248, ${(n*.45).toFixed(3)});`;{const p=(n-.5)*2,l=Math.round(99+69*p),k=Math.round(102+-17*p),b=Math.round(241+6*p);return`background: rgba(${l}, ${k}, ${b}, ${f.toFixed(3)}); border-color: rgba(192, 132, 252, ${(f*.7).toFixed(3)}); box-shadow: 0 0 ${(6+n*18).toFixed(1)}px rgba(192, 132, 252, ${(n*.55).toFixed(3)});`}}function u(s){if(s===0)return"text-muted";const n=s/t(g);return n>=.5?"text-bright font-semibold":n>=.2?"text-text":"text-dim"}function L(s,n,f){const l=s.currentTarget.getBoundingClientRect();$(d,{from:n,to:f,x:l.left+l.width/2,y:l.top},!0)}function j(){$(d,null)}const B=re;function nt(s,n){return x.selectedCell!==null&&x.selectedCell.from===s&&x.selectedCell.to===n}const I=H(()=>ee(x.projects,t(A)));var Q=fe(),J=r(Q),X=r(J),st=a(r(X),2),ot=a(r(st),4),jt=r(ot,!0);e(ot),e(st),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,s=>s,(s,n)=>{var f=ae(),p=r(f),l=r(p),k=r(l,!0);e(l),e(p),e(f),S(b=>{yt(f,"title",n),o(k,b)},[()=>B(n)]),m(s,f)}),e(tt),e(q);var xt=a(q);V(xt,20,()=>x.projects,s=>s,(s,n)=>{var f=se(),p=r(f),l=r(p,!0);e(p);var k=a(p);V(k,16,()=>x.projects,b=>b,(b,N)=>{const P=H(()=>t(A)[n][N]),W=H(()=>n===N);var F=ne(),z=r(F),M=r(z),U=r(M,!0);e(M),e(z),e(F),S((E,Z,T)=>{wt(z,`${E??""} ${Z??""} ${t(W)&&t(P).count>0?"border-style: dashed;":""}`),yt(z,"aria-label",`${t(P).count??""} patterns from ${n??""} to ${N??""}`),bt(M,1,`text-[11px] ${T??""}`),o(U,t(P).count||"")},[()=>v(t(P).count),()=>nt(n,N)?"outline: 2px solid var(--color-dream-glow); outline-offset: 1px;":"",()=>u(t(P).count)]),at("click",z,()=>x.onCellClick(n,N)),Ft("mouseenter",z,E=>L(E,n,N)),Ft("mouseleave",z,j),m(b,F)}),e(f),S(b=>{yt(p,"title",n),o(l,b)},[()=>B(n)]),m(s,f)}),e(xt),e(gt),e(it);var Tt=a(it,2);{var Ct=s=>{const n=H(()=>t(A)[t(d).from][t(d).to]);var f=le(),p=r(f),l=r(p),k=r(l,!0);e(l);var b=a(l,4),N=r(b,!0);e(b),e(p);var P=a(p,2),W=r(P),F=a(W),z=r(F);e(F),e(P);var M=a(P,2);{var U=Z=>{var T=ie(),G=a(r(T),2);V(G,17,()=>t(n).topNames,Yt,(K,dt)=>{var vt=oe(),mt=r(vt);e(vt),S(()=>o(mt,`· ${t(dt)??""}`)),m(K,vt)}),e(T),m(Z,T)},E=Z=>{var T=ce();m(Z,T)};Y(M,Z=>{t(n).topNames.length>0?Z(U):Z(E,!1)})}e(f),S((Z,T)=>{wt(f,`left: ${t(d).x??""}px; top: ${t(d).y-12}px; transform: translate(-50%, -100%);`),o(k,Z),o(N,T),o(W,`${t(n).count??""} `),o(z,`${t(n).count===1?"pattern":"patterns"} transferred`)},[()=>B(t(d).from),()=>B(t(d).to)]),m(s,f)};Y(Tt,s=>{t(d)&&s(Ct)})}e(J);var _t=a(J,2),lt=r(_t),i=r(lt);e(lt);var c=a(lt,2);{var _=s=>{var n=de();m(s,n)},C=s=>{var n=Jt(),f=Rt(n);V(f,17,()=>t(I),p=>p.from+"->"+p.to,(p,l)=>{var k=ue(),b=r(k),N=r(b),P=r(N),W=r(P,!0);e(P);var F=a(P,4),z=r(F,!0);e(F),e(N);var M=a(N,2);{var U=T=>{var G=ve(),K=r(G,!0);e(G),S(dt=>o(K,dt),[()=>t(l).topNames.join(" · ")]),m(T,G)};Y(M,T=>{t(l).topNames.length>0&&T(U)})}e(b);var E=a(b,2),Z=r(E,!0);e(E),e(k),S((T,G,K)=>{bt(k,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] ${T??""}`),o(W,G),o(z,K),o(Z,t(l).count)},[()=>nt(t(l).from,t(l).to)?"ring-1 ring-dream-glow":"",()=>B(t(l).from),()=>B(t(l).to)]),at("click",k,()=>x.onCellClick(t(l).from,t(l).to)),m(p,k)}),m(s,n)};Y(c,s=>{t(I).length===0?s(_):s(C,!1)})}e(_t),e(Q),S(()=>{o(jt,t(g)),o(i,`${t(I).length??""} transfer pair${t(I).length===1?"":"s"} · tap to filter`)}),m(y,Q),Et()}Ht(["click"]);var ge=h(''),xe=h(`
      Couldn't load pattern transfers
      `),_e=h('
      '),me=h('
      Filtered to
      '),he=h('
      No matching patterns
      '),ye=h('
    1. '),be=h('
        '),we=h('
        ',1),je=h('

        Cross-Project Intelligence

        Patterns learned here, applied there.

        ');function Fe(y,x){Mt(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 d=rt("All"),v=rt(Vt({projects:[],patterns:[]})),u=rt(!0),L=rt(null),j=rt(null);async function B(){return await new Promise(_=>setTimeout(_,420)),{projects:["vestige","nullgaze","injeranet","nemotron","orbit-wars","nightvision","aimo3"],patterns:[{name:"Result with thiserror context",category:"ErrorHandling",origin_project:"vestige",transferred_to:["nullgaze","injeranet","nemotron","nightvision"],transfer_count:4,last_used:"2026-04-18T14:22:00Z",confidence:.94},{name:"Axum error middleware with tower-http",category:"ErrorHandling",origin_project:"nullgaze",transferred_to:["vestige","nightvision"],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","injeranet","nightvision"],transfer_count:3,last_used:"2026-04-15T22:01:00Z",confidence:.82},{name:"Python try/except with contextual re-raise",category:"ErrorHandling",origin_project:"aimo3",transferred_to:["nemotron"],transfer_count:1,last_used:"2026-04-10T11:30:00Z",confidence:.7},{name:"Arc> reader/writer split",category:"AsyncConcurrency",origin_project:"vestige",transferred_to:["nullgaze","injeranet"],transfer_count:2,last_used:"2026-04-14T16:42:00Z",confidence:.91},{name:"tokio::select! for cancellation propagation",category:"AsyncConcurrency",origin_project:"injeranet",transferred_to:["vestige","nightvision"],transfer_count:2,last_used:"2026-04-19T08:05:00Z",confidence:.86},{name:"Bounded mpsc channel with backpressure",category:"AsyncConcurrency",origin_project:"injeranet",transferred_to:["vestige","nullgaze"],transfer_count:2,last_used:"2026-04-12T13:18:00Z",confidence:.83},{name:"asyncio.gather with return_exceptions",category:"AsyncConcurrency",origin_project:"nemotron",transferred_to:["aimo3"],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:["nullgaze","injeranet"],transfer_count:2,last_used:"2026-04-11T10:22:00Z",confidence:.89},{name:"Snapshot testing with insta",category:"Testing",origin_project:"nullgaze",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:["nullgaze","injeranet"],transfer_count:2,last_used:"2026-04-19T18:30:00Z",confidence:.87},{name:"One-variable-at-a-time Kaggle submission",category:"Testing",origin_project:"aimo3",transferred_to:["nemotron","orbit-wars"],transfer_count:2,last_used:"2026-04-20T07:15:00Z",confidence:.95},{name:"Kaggle pre-flight Input-panel screenshot",category:"Testing",origin_project:"aimo3",transferred_to:["nemotron","orbit-wars"],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:["nullgaze","nightvision"],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:["nullgaze","nightvision","injeranet"],transfer_count:3,last_used:"2026-04-20T09:00:00Z",confidence:.9},{name:"Tauri 2 + Rust/Axum sidecar",category:"Architecture",origin_project:"injeranet",transferred_to:["nightvision"],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:["injeranet"],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:["nullgaze"],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:["nullgaze","injeranet","nightvision"],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:"aimo3",transferred_to:["nemotron"],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:["nullgaze"],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:["nullgaze","injeranet","nightvision"],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:["nullgaze"],transfer_count:1,last_used:"2026-04-10T13:40:00Z",confidence:.89},{name:"664-pattern secret scanner",category:"Security",origin_project:"nullgaze",transferred_to:["vestige","nightvision","injeranet"],transfer_count:3,last_used:"2026-04-20T05:30:00Z",confidence:.97},{name:"CSP header with nonce-based script allow",category:"Security",origin_project:"nullgaze",transferred_to:["nightvision"],transfer_count:1,last_used:"2026-04-05T16:08:00Z",confidence:.8}]}}async function nt(){$(u,!0),$(L,null);try{$(v,await B(),!0)}catch(i){$(L,i instanceof Error?i.message:"Failed to load pattern transfers",!0),$(v,{projects:[],patterns:[]},!0)}finally{$(u,!1)}}Ut(()=>nt());const I=H(()=>t(d)==="All"?t(v).patterns:t(v).patterns.filter(i=>i.category===t(d))),Q=H(()=>[...t(j)?t(I).filter(c=>c.origin_project===t(j).from&&c.transferred_to.includes(t(j).to)):t(I)].sort((c,_)=>_.transfer_count-c.transfer_count)),J=H(()=>t(I).reduce((i,c)=>i+c.transferred_to.length,0)),X=H(()=>t(v).projects.length),st=H(()=>t(I).length);function ot(i){$(d,i,!0),$(j,null)}function jt(i,c){t(j)&&t(j).from===i&&t(j).to===c?$(j,null):$(j,{from:i,to:c},!0)}function it(){$(j,null)}function gt(i){const c=new Date(i).getTime(),_=Date.now(),C=Math.floor((_-c)/864e5);return C<=0?"today":C===1?"1d ago":C<30?`${C}d ago`:`${Math.floor(C/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 _=ge(),C=r(_),s=a(C);e(_),S(()=>{bt(_,1,`flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition ${t(d)===c?"bg-synapse/25 text-synapse-glow":"text-dim hover:bg-white/[0.04] hover:text-text"}`),wt(C,`background: ${g[c]??""}`),o(s,` ${c??""}`)}),at("click",_,()=>ot(c)),m(i,_)}),e(tt);var Tt=a(tt,2);{var Ct=i=>{var c=xe(),_=a(r(c),2),C=r(_,!0);e(_);var s=a(_,2);e(c),S(()=>o(C,t(L))),at("click",s,nt),m(i,c)},_t=i=>{var c=_e();m(i,c)},lt=i=>{var c=we(),_=Rt(c),C=r(_),s=r(C);pe(s,{get projects(){return t(v).projects},get patterns(){return t(I)},get selectedCell(){return t(j)},onCellClick:jt});var n=a(s,2);{var f=O=>{var R=me(),D=r(R),w=a(r(D),2),ut=r(w,!0);e(w);var ft=a(w,4),pt=r(ft,!0);e(ft),e(D);var et=a(D,2);e(R),S(()=>{o(ut,t(j).from),o(pt,t(j).to)}),at("click",et,it),m(O,R)};Y(n,O=>{t(j)&&O(f)})}e(C);var p=a(C,2),l=r(p),k=a(r(l),2),b=r(k);e(k),e(l);var N=a(l,2);{var P=O=>{var R=he(),D=a(r(R),2),w=r(D,!0);e(D),e(R),S(()=>o(w,t(j)?"No patterns transferred from this origin to this destination.":"No patterns in this category.")),m(O,R)},W=O=>{var R=be();V(R,21,()=>t(Q),D=>D.name,(D,w)=>{var ut=ye(),ft=r(ut),pt=r(ft),et=r(pt),It=r(et,!0);e(et);var kt=a(et,2),ht=r(kt),Gt=r(ht,!0);e(ht);var Pt=a(ht,2),Ot=r(Pt,!0);e(Pt),e(kt);var zt=a(kt,2),Zt=r(zt),Dt=r(Zt,!0);e(Zt);var St=a(Zt,4),Kt=r(St);e(St),e(zt),e(pt);var Nt=a(pt,2),At=r(Nt),Bt=r(At,!0);e(At);var $t=a(At,2),Qt=r($t);e($t),e(Nt),e(ft),e(ut),S((Wt,qt)=>{yt(et,"title",t(w).name),o(It,t(w).name),wt(ht,`border-color: ${g[t(w).category]??""}66; color: ${g[t(w).category]??""}; background: ${g[t(w).category]??""}1a;`),o(Gt,t(w).category),o(Ot,Wt),o(Dt,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)]),m(D,ut)}),e(R),m(O,R)};Y(N,O=>{t(Q).length===0?O(P):O(W,!1)})}e(p),e(_);var F=a(_,2),z=r(F),M=r(z),U=r(M,!0);e(M);var E=a(M),Z=a(E),T=r(Z,!0);e(Z);var G=a(Z),K=a(G),dt=r(K,!0);e(K);var vt=a(K);e(z);var mt=a(z,2),Lt=r(mt,!0);e(mt),e(F),S(()=>{o(b,`${t(Q).length??""} + ${t(Q).length===1?"pattern":"patterns"}`),o(U,t(st)),o(E,` pattern${t(st)===1?"":"s"} across `),o(T,t(X)),o(G,` project${t(X)===1?"":"s"}, `),o(dt,t(J)),o(vt,` total transfer${t(J)===1?"":"s"}`),o(Lt,t(d)==="All"?"All categories":t(d))}),m(i,c)};Y(Tt,i=>{t(L)?i(Ct):t(u)?i(_t,1):i(lt,!1)})}e(q),S(()=>bt(ct,1,`rounded-lg px-3 py-1.5 text-xs font-medium transition ${t(d)==="All"?"bg-synapse/25 text-synapse-glow":"text-dim hover:bg-white/[0.04] hover:text-text"}`)),at("click",ct,()=>ot("All")),m(y,q),Et()}Ht(["click"]);export{Fe as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js.br b/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js.br new file mode 100644 index 0000000..c9f52d5 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js.gz b/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js.gz new file mode 100644 index 0000000..b4ddab6 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/14.BpCacSGt.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js b/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js new file mode 100644 index 0000000..2a20f0b --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js @@ -0,0 +1,5 @@ +import"../chunks/Bzak7iHL.js";import{o as Nt}from"../chunks/CNjeV5xa.js";import{p as We,e as s,g as e,d as a,r as t,n as Fe,t as w,a as Ke,u as Z,s as ce,c as Tt,y as Ft,h as D,bc as jt,f as rt}from"../chunks/CvjSAYrz.js";import{s as v,d as Lt,a as Ge}from"../chunks/FzvEaXMa.js";import{i as O}from"../chunks/ciN1mm2W.js";import{e as re,i as ye}from"../chunks/DTnG8poT.js";import{a as f,f as _,b as ct}from"../chunks/BsvCUYx-.js";import{h as Dt}from"../chunks/DObx9JW_.js";import{s as K,r as Ot}from"../chunks/CNfQDikv.js";import{s as b}from"../chunks/Bhad70Ss.js";import{b as It}from"../chunks/CVpUe0w3.js";import{b as it}from"../chunks/D3XWCg9-.js";import{a as Mt}from"../chunks/DNjM5a-l.js";import{s as ut}from"../chunks/DPl3NjBv.js";import{p as ie}from"../chunks/B_YDQCB6.js";import{N as zt}from"../chunks/DzfRjky4.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(M,z){return G()[M]??e(Y)[M]??z}var P=Gt();let ue;re(P,23,()=>I,M=>M.key,(M,z,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(z).icon),v(pe,`0${e(H)+1}`),v(me,e(z).label),v(r,n)},[()=>Q(e(z).key,e(z).base)]),f(M,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?zt[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 M=a(P,1,!0);t(Q);var z=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(z,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(M,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 Mt.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 AIMO3 submission score 36/50?","How does FSRS-6 trust scoring work?"];var M=fs();Dt("q2v96u",r=>{Ft(()=>{jt.title="Reasoning Theater · Vestige"})});var z=a(s(M),2),H=s(z),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(z);var te=a(z,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 Me=a(Ie,2),ze=s(Me),Ze=s(ze),Je=a(s(Ze),2),_t=s(Je);t(Je),t(Ze),Fe(2),t(ze);var $e=a(ze,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(Me);var tt=a(Me,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(M),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,M),Ke()}Lt(["keydown","click"]);export{Ls as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js.br b/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js.br new file mode 100644 index 0000000..9378b0f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js.gz b/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js.gz new file mode 100644 index 0000000..581fc71 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/15.DFbOY736.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js b/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js new file mode 100644 index 0000000..d1f1531 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js @@ -0,0 +1,9 @@ +import"../chunks/Bzak7iHL.js";import{o as Ve}from"../chunks/CNjeV5xa.js";import{p as Ye,e as a,d 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/CvjSAYrz.js";import{d as Ue,s as u,a as Ae}from"../chunks/FzvEaXMa.js";import{i as L}from"../chunks/ciN1mm2W.js";import{e as Q,i as pe}from"../chunks/DTnG8poT.js";import{c as at,a as v,f as m,b as Oe}from"../chunks/BsvCUYx-.js";import{s as Fe}from"../chunks/DPl3NjBv.js";import{a as Ee}from"../chunks/DNjM5a-l.js";import{s as M}from"../chunks/CNfQDikv.js";import{s as Re}from"../chunks/Bhad70Ss.js";import{p as st}from"../chunks/B_YDQCB6.js";import{N as rt}from"../chunks/DzfRjky4.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.DMIuRZWa.js.br b/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js.br new file mode 100644 index 0000000..71ed886 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js.gz b/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js.gz new file mode 100644 index 0000000..9bfdbe0 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/16.DMIuRZWa.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js b/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js new file mode 100644 index 0000000..df55a62 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js @@ -0,0 +1,2 @@ +import"../chunks/Bzak7iHL.js";import{o as Qe}from"../chunks/CNjeV5xa.js";import{p as Xe,t as w,a as Ze,d as i,e as t,g as s,s as k,h as m,a2 as et,r as e,n as _,f as tt,u as P}from"../chunks/CvjSAYrz.js";import{d as st,a as E,s as p}from"../chunks/FzvEaXMa.js";import{i as u}from"../chunks/ciN1mm2W.js";import{e as se,i as ae}from"../chunks/DTnG8poT.js";import{a as v,f as l,t as he}from"../chunks/BsvCUYx-.js";import{s as we}from"../chunks/DPl3NjBv.js";import{s as ke}from"../chunks/Bhad70Ss.js";import{s as at,a as ie}from"../chunks/D81f-o_I.js";import{a as F}from"../chunks/DNjM5a-l.js";import{w as it,m as dt,a as rt,i as ot}from"../chunks/CtkE7HV2.js";import{f as nt}from"../chunks/Casl2yrL.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.PvQmHhRC.js.br b/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js.br new file mode 100644 index 0000000..ccfee22 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js.gz b/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js.gz new file mode 100644 index 0000000..207b098 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/17.PvQmHhRC.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js similarity index 76% rename from apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js rename to apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js index 2ac1492..a80255d 100644 --- a/apps/dashboard/build/_app/immutable/nodes/10.CsJcFbdU.js +++ b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js @@ -1 +1 @@ -import"../chunks/Bzak7iHL.js";import{o as Ft}from"../chunks/CkyfbJUz.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,G as P,t as B,u as G}from"../chunks/C9Z4nxhR.js";import{d as Rt,s as i,a as At}from"../chunks/DP9qWekZ.js";import{i as X}from"../chunks/C2oj68pw.js";import{e as O,i as U}from"../chunks/kH-DTQyy.js";import{a as p,f as u}from"../chunks/DPfxVJHQ.js";import{s as A}from"../chunks/BkopTN9z.js";import{a as w}from"../chunks/BcuCGYSa.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 q(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 z=Pt(),lt=s(e(z),2);{var ct=d=>{var r=Mt();O(r,20,()=>Array(8),U,(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);O(T,21,()=>t(l).distribution,U,(g,c,v)=>{const C=G(()=>Math.max(...t(l).distribution.map(V=>V.count),1)),R=G(()=>t(c).count/t(C)*100),_=G(()=>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);O(j,21,()=>Object.entries(t(l).byType),U,(g,c)=>{var v=G(()=>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);O(R,21,()=>t(l).endangered.slice(0,20),U,(_,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??""}%`)},[()=>q(t(m).status),()=>q(t(m).status),()=>q(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(z),p(ot,z),Ct()}Rt(["click"]);export{Lt as component}; +import"../chunks/Bzak7iHL.js";import{o as Ft}from"../chunks/CNjeV5xa.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,n as P,t as B,u as O}from"../chunks/CvjSAYrz.js";import{d as Rt,s as i,a as At}from"../chunks/FzvEaXMa.js";import{i as X}from"../chunks/ciN1mm2W.js";import{e as U,i as q}from"../chunks/DTnG8poT.js";import{a as p,f as u}from"../chunks/BsvCUYx-.js";import{s as A}from"../chunks/Bhad70Ss.js";import{a as w}from"../chunks/DNjM5a-l.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.Df4fIuu-.js.br b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js.br new file mode 100644 index 0000000..8c0615f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js.gz b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js.gz new file mode 100644 index 0000000..43f6179 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/18.Df4fIuu-.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js similarity index 85% rename from apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js rename to apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js index 5fb29ba..dddca2b 100644 --- a/apps/dashboard/build/_app/immutable/nodes/11.C5VMEnLV.js +++ b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js @@ -1 +1 @@ -import"../chunks/Bzak7iHL.js";import{o as pe}from"../chunks/CkyfbJUz.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/C9Z4nxhR.js";import{d as ue,a as K,s as m}from"../chunks/DP9qWekZ.js";import{i as M}from"../chunks/C2oj68pw.js";import{e as h,i as P}from"../chunks/kH-DTQyy.js";import{a as l,f as v}from"../chunks/DPfxVJHQ.js";import{s as Q}from"../chunks/BkopTN9z.js";import{b as xe}from"../chunks/-jeO_JOJ.js";import{a as fe}from"../chunks/BcuCGYSa.js";import{N as U}from"../chunks/CZ45jJaw.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/CNjeV5xa.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/CvjSAYrz.js";import{d as ue,a as K,s as m}from"../chunks/FzvEaXMa.js";import{i as M}from"../chunks/ciN1mm2W.js";import{e as h,i as P}from"../chunks/DTnG8poT.js";import{a as l,f as v}from"../chunks/BsvCUYx-.js";import{s as Q}from"../chunks/Bhad70Ss.js";import{b as xe}from"../chunks/DMu1Byux.js";import{a as fe}from"../chunks/DNjM5a-l.js";import{N as U}from"../chunks/DzfRjky4.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.CMsn8k5A.js.br b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js.br new file mode 100644 index 0000000..0a55f3d Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js.gz b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js.gz new file mode 100644 index 0000000..5d017b5 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/19.CMsn8k5A.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js b/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js deleted file mode 100644 index 5c688d6..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{f as m}from"../chunks/C9Z4nxhR.js";import{c as n,a as p}from"../chunks/DPfxVJHQ.js";import{s as i}from"../chunks/D00YwZ1M.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.Bv9w28KX.js.br b/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js.br deleted file mode 100644 index f91da89..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js.gz b/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js.gz deleted file mode 100644 index 912d0d0..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/2.Bv9w28KX.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js b/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js new file mode 100644 index 0000000..e1fb4c2 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{f as m}from"../chunks/CvjSAYrz.js";import{c as n,a as p}from"../chunks/BsvCUYx-.js";import{s as i}from"../chunks/ckF4CxmX.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.CD5F7bS_.js.br b/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js.br new file mode 100644 index 0000000..0461b7b Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js.gz b/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js.gz new file mode 100644 index 0000000..4200ecf Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/2.CD5F7bS_.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js b/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js new file mode 100644 index 0000000..77ab624 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js @@ -0,0 +1 @@ +import"../chunks/Bzak7iHL.js";import{i as p}from"../chunks/Bz1l2A_1.js";import{o as r}from"../chunks/CNjeV5xa.js";import{p as t,a}from"../chunks/CvjSAYrz.js";import{g as m}from"../chunks/S0ILvWpb.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.D16O8s7t.js.br b/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js.br new file mode 100644 index 0000000..e8f7f30 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js.br @@ -0,0 +1,3 @@ +$ ep`i$fXfa\[mdi{wyJQD+]JJEB]yw(. +-qŭ<4 u1'0YGY^ӎhXgDS +$WY۟D}"K&J=h:*9Dѽ@)zv 7 j9B%  QGb2b \ No newline at end of file diff --git a/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js.gz b/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js.gz new file mode 100644 index 0000000..d7c5f09 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/3.D16O8s7t.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js b/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js deleted file mode 100644 index dfff3ad..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{i as p}from"../chunks/_Va07L2l.js";import{o as r}from"../chunks/CkyfbJUz.js";import{p as t,a}from"../chunks/C9Z4nxhR.js";import{g as m}from"../chunks/DunNqS1N.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.D_o4dH3z.js.br b/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js.br deleted file mode 100644 index 7b4aa5b..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js.gz b/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js.gz deleted file mode 100644 index 212c83f..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/3.D_o4dH3z.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js b/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js new file mode 100644 index 0000000..7b83027 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js @@ -0,0 +1,8 @@ +import"../chunks/Bzak7iHL.js";import{o as we,a as Ne}from"../chunks/CNjeV5xa.js";import{p as Me,s as A,c as le,aB as ve,d as y,e 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/CvjSAYrz.js";import{s as K,d as Ie,a as xe}from"../chunks/FzvEaXMa.js";import{i as ue}from"../chunks/ciN1mm2W.js";import{a as E,c as Fe,b as oe,f as X}from"../chunks/BsvCUYx-.js";import{s as o,r as ge}from"../chunks/CNfQDikv.js";import{b as Ge,a as Pe}from"../chunks/CVpUe0w3.js";import{a as he}from"../chunks/DNjM5a-l.js";import{e as ke}from"../chunks/CtkE7HV2.js";import{e as fe,i as ye}from"../chunks/DTnG8poT.js";import{p as W}from"../chunks/B_YDQCB6.js";import{N as Ce}from"../chunks/DzfRjky4.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.BSlP3-UA.js.br b/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js.br new file mode 100644 index 0000000..31eeb91 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js.gz b/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js.gz new file mode 100644 index 0000000..0851ccd Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/4.BSlP3-UA.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js.br b/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js.br deleted file mode 100644 index 5673275..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js.gz b/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js.gz deleted file mode 100644 index 12ccefb..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js b/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js new file mode 100644 index 0000000..bfd2f94 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js @@ -0,0 +1,3 @@ +import"../chunks/Bzak7iHL.js";import{p as Qe,e as a,d as o,f as We,t as O,g as e,h as E,u as A,r,n as ke,a as Je,s as re}from"../chunks/CvjSAYrz.js";import{d as Ze,a as W,e as ue,s as v}from"../chunks/FzvEaXMa.js";import{a as w,f as M,b as Le}from"../chunks/BsvCUYx-.js";import{i as B}from"../chunks/ciN1mm2W.js";import{e as te,i as Ne}from"../chunks/DTnG8poT.js";import{s as He}from"../chunks/DPl3NjBv.js";import{s as q}from"../chunks/Bhad70Ss.js";import{b as ut}from"../chunks/DMu1Byux.js";import{s as i}from"../chunks/CNfQDikv.js";import{p as Pe}from"../chunks/B_YDQCB6.js";const et=.7,tt=.5,ft="#ef4444",xt="#f59e0b",bt="#fde047";function Ce(m){return m>et?ft:m>tt?xt:bt}function rt(m){return m>et?"strong":m>tt?"moderate":"mild"}const Ue="#8b5cf6",gt={fact:"#3b82f6",concept:"#8b5cf6",event:"#f59e0b",person:"#10b981",place:"#06b6d4",note:"#6b7280",pattern:"#ec4899",decision:"#ef4444"};function Ke(m){return m?gt[m]??Ue:Ue}const qe=5,kt=9;function ht(m){if(!Number.isFinite(m))return qe;const _=m<0?0:m>1?1:m;return qe+_*kt}const wt=.12;function Xe(m,_){return _==null||_===m?1:wt}function he(m,_=60){return m==null||typeof m!="string"||_<=0?"":m.length<=_?m:m.slice(0,_-1)+"…"}function jt(m){if(!m||m.length===0)return 0;const _=new Set;for(const x of m)x.memory_a_id&&_.add(x.memory_a_id),x.memory_b_id&&_.add(x.memory_b_id);return _.size}function St(m){if(!m||m.length===0)return 0;let _=0;for(const x of m)_+=Math.abs((x.trust_a??0)-(x.trust_b??0));return _/m.length}var Mt=Le('',1),It=Le(' '),Rt=Le('',1),Tt=M('
        '),Ot=M('
        '),At=M('
        '),Et=M('
        topic:
        '),Gt=M('
        SEVERITYstrong (>0.7)moderate (0.5-0.7)mild (0.3-0.5)
        ');function Ft(m,_){Qe(_,!0);let x=Pe(_,"focusedPairIndex",3,null),G=Pe(_,"width",3,800),F=Pe(_,"height",3,600);const R=A(()=>{const n=G()/2,t=F()/2,k=Math.min(G(),F())*.38;return{cx:n,cy:t,R:k}}),H=A(()=>{const n=[],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,N=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"},S={x:e(R).cx+Math.cos(d)*N,y:e(R).cy+Math.sin(d)*N,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"};n.push(u,S);const $=(u.x+S.x)/2,I=(u.y+S.y)/2,T=.55-l.similarity*.25,P=$+(e(R).cx-$)*T,Y=I+(e(R).cy-I)*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 ${P.toFixed(1)} ${Y.toFixed(1)} ${S.x.toFixed(1)} ${S.y.toFixed(1)}`,color:Ce(l.similarity),thickness:je,severity:rt(l.similarity),topic:l.topic,similarity:l.similarity,dateDiff:l.date_diff_days,aPoint:u,bPoint:S,midX:P,midY:Y})}),{nodes:n,arcs:t}});let b=re(null),C=re(null),ie=re(0),le=re(0);function me(n){const t=n.currentTarget.getBoundingClientRect();E(ie,n.clientX-t.left),E(le,n.clientY-t.top)}function ae(n){_.onSelectPair&&_.onSelectPair(x()===n?null:n)}function ne(){var n;(n=_.onSelectPair)==null||n.call(_,null)}var U=Gt(),D=a(U),se=o(a(D)),X=o(se),_e=o(X),oe=o(_e);te(oe,17,()=>e(H).arcs,n=>n.pairIndex,(n,t)=>{const k=A(()=>Xe(e(t).pairIndex,x())),l=A(()=>x()===e(t).pairIndex);var y=Mt(),j=We(y),p=o(j),g=o(p);O(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)),q(g,`animation-duration: ${4+e(t).pairIndex%5}s`)},[()=>Math.max(1,e(t).thickness*.6)]),W("click",p,d=>{d.stopPropagation(),ae(e(t).pairIndex)}),ue("mouseenter",p,()=>E(C,e(t),!0)),ue("mouseleave",p,()=>E(C,null)),W("keydown",p,d=>{d.key==="Enter"&&ae(e(t).pairIndex)}),w(n,y)});var fe=o(oe);te(fe,19,()=>e(H).nodes,(n,t)=>n.memoryId+"-"+n.side+"-"+t,(n,t)=>{const k=A(()=>Xe(e(t).pairIndex,x())),l=A(()=>x()===e(t).pairIndex),y=A(()=>ht(e(t).trust)),j=A(()=>Ke(e(t).type));var p=Rt(),g=We(p),d=o(g),L=o(d);{var N=u=>{var S=It(),$=a(S,!0);r(S),O(I=>{i(S,"x",e(t).x),i(S,"y",e(t).y-e(y)-8),v($,I)},[()=>he(e(t).preview,40)]),w(u,S)};B(L,u=>{e(l)&&u(N)})}O(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,()=>E(b,e(t),!0)),ue("mouseleave",d,()=>E(b,null)),W("click",d,u=>{u.stopPropagation(),ae(e(t).pairIndex)}),W("keydown",d,u=>{u.key==="Enter"&&ae(e(t).pairIndex)}),w(n,p)}),ke(),r(D);var we=o(D,2);{var de=n=>{var t=At(),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 N=o(d,2);{var u=I=>{var T=Tt(),P=a(T);r(T),O(()=>v(P,`created ${e(b).created??""}`)),w(I,T)};B(N,I=>{e(b).created&&I(u)})}var S=o(N,2);{var $=I=>{var T=Ot(),P=a(T,!0);r(T),O(Y=>v(P,Y),[()=>e(b).tags.slice(0,4).join(" · ")]),w(I,T)};B(S,I=>{e(b).tags&&e(b).tags.length>0&&I($)})}r(t),O((I,T,P,Y)=>{q(t,`left: ${I??""}px; top: ${T??""}px;`),q(l,`background: ${P??""}`),v(j,e(b).type??"memory"),v(g,`trust ${Y??""}%`),v(L,e(b).preview)},[()=>Math.max(0,Math.min(e(ie)+12,G()-240)),()=>Math.max(0,Math.min(e(le)-8,F()-120)),()=>Ke(e(b).type),()=>(e(b).trust*100).toFixed(0)]),w(n,t)},xe=n=>{var t=Et(),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),N=a(L);r(L),r(t),O((u,S,$)=>{q(t,`left: ${u??""}px; top: ${S??""}px;`),q(l,`background: ${e(C).color??""}`),v(j,`${e(C).severity??""} conflict`),v(d,e(C).topic),v(N,`similarity ${$??""}% · ${e(C).dateDiff??""}d apart`)},[()=>Math.max(0,Math.min(e(ie)+12,G()-240)),()=>Math.max(0,Math.min(e(le)-8,F()-120)),()=>(e(C).similarity*100).toFixed(0)]),w(n,t)};B(we,n=>{e(b)?n(de):e(C)&&n(xe,1)})}r(U),O(()=>{q(U,`aspect-ratio: ${G()??""} / ${F()??""};`),i(D,"width",G()),i(D,"height",F()),i(D,"viewBox",`0 0 ${G()??""} ${F()??""}`),i(se,"width",G()),i(se,"height",F()),i(X,"cx",e(R).cx),i(X,"cy",e(R).cy),i(X,"r",e(R).R),i(_e,"cx",e(R).cx),i(_e,"cy",e(R).cy)}),W("mousemove",D,me),ue("mouseleave",D,()=>{E(b,null),E(C,null)}),W("click",D,ne),w(m,U),Je()}Ze(["mousemove","click","keydown"]);var Dt=M(""),Nt=M(""),Pt=M(''),Ct=M(''),Lt=M('
        No contradictions match this filter.
        '),Bt=M('
        No pairs visible.
        '),$t=M(' '),Yt=M('
        '),zt=M(' '),Vt=M('
        '),Wt=M('
        Full memory A
        Full memory B
        '),Ht=M(''),Ut=M('

        Contradiction Constellation

        Where your memory disagrees with itself

        average trust delta
        visible in current filter
        strong conflicts
        ');function or(m,_){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 diversity helps at T>=0.6 per GPT-OSS paper",memory_b_preview:"Prompt diversity monotonically HURTS at T>=0.6 (arxiv 2603.27844)",memory_a_type:"concept",memory_b_type:"fact",memory_a_created:"2026-03-30",memory_b_created:"2026-04-03",memory_a_tags:["aimo3","prompting"],memory_b_tags:["aimo3","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 GPT-OSS-120B sampling",memory_b_preview:"min_p scheduling fails at competition temperatures",memory_a_type:"pattern",memory_b_type:"fact",memory_a_created:"2026-04-01",memory_b_created:"2026-04-05",memory_a_tags:["aimo3","sampling"],memory_b_tags:["aimo3","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:"Sam prefers Rust for all backend services",memory_b_preview:"Sam chose Axum + Rust for Nullgaze 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","sam"],memory_b_tags:["nullgaze","backend"],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 GPT-OSS with 16384 token budget for AIMO",memory_b_preview:"AIMO3 baseline at 32768 tokens scored 44/50",memory_a_type:"pattern",memory_b_type:"event",memory_a_created:"2026-04-04",memory_b_created:"2026-04-10",memory_a_tags:["aimo3","tokens"],memory_b_tags:["aimo3","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:"Kaggle API silently ignores invalid modelDataSources slugs",memory_b_preview:"Kaggle API throws an error when model 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:["kaggle","bug-fix","api"],memory_b_tags:["kaggle","api"],trust_a:.89,trust_b:.28,similarity:.91,date_diff_days:46,topic:"Kaggle 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:"Orbit Wars leaderboard scores weight by top-10 consistency",memory_b_preview:"Orbit Wars 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:["orbit-wars","scoring"],memory_b_tags:["orbit-wars","scoring"],trust_a:.64,trust_b:.52,similarity:.82,date_diff_days:8,topic:"Orbit Wars scoring"},{memory_a_id:"a14",memory_b_id:"b14",memory_a_preview:"Sam commits to morning posts 8am ET",memory_b_preview:"Morning cadence moved to 9am ET after energy 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 G=re("all"),F=re("");const R=A(()=>Array.from(new Set(x.map(s=>s.topic))).sort()),H=A(()=>{switch(e(G)){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,K=h.memory_b_created?new Date(h.memory_b_created).getTime():0;return s-Math.max(f,K)<=c})}case"high-trust":return x.filter(s=>Math.min(s.trust_a,s.trust_b)>.6);case"topic":return e(F)?x.filter(s=>s.topic===e(F)):x;case"all":default:return x}});let b=re(null);function C(s){E(b,s,!0)}const ie=A(()=>jt(x)),le=A(()=>St(x)),me=A(()=>{const s=new Map(x.map((c,h)=>[c.memory_a_id+"|"+c.memory_b_id,h]));return e(H).map(c=>({orig:s.get(c.memory_a_id+"|"+c.memory_b_id)??0,c}))});function ae(s){E(b,e(b)===s?null:s,!0)}var ne=Ut(),U=o(a(ne),2),D=a(U),se=a(D);se.textContent="47";var X=o(se,2),_e=a(X);r(X),r(D);var oe=o(D,2),fe=a(oe),we=a(fe,!0);r(fe),ke(2),r(oe);var de=o(oe,2),xe=a(de),n=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(U);var y=o(U,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=Dt(),f=a(h,!0);r(h),O(()=>{He(h,1,`px-3 py-1.5 rounded-lg text-xs border transition + ${e(G)===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)}),W("click",h,()=>{E(G,c.id,!0),E(b,null)}),w(s,h)});var p=o(j,2);{var g=s=>{var c=Pt(),h=a(c);h.value=h.__value="";var f=o(h);te(f,17,()=>e(R),Ne,(K,z)=>{var V=Nt(),be=a(V,!0);r(V);var Q={};O(()=>{v(be,e(z)),Q!==(Q=e(z))&&(V.value=(V.__value=e(z))??"")}),w(K,V)}),r(c),ut(c,()=>e(F),K=>E(F,K)),w(s,c)};B(p,s=>{e(G)==="topic"&&s(g)})}var d=o(p,2);{var L=s=>{var c=Ct();W("click",c,()=>E(b,null)),w(s,c)};B(d,s=>{e(b)!==null&&s(L)})}r(y);var N=o(y,2),u=a(N),S=a(u);{var $=s=>{var c=Lt();w(s,c)},I=s=>{Ft(s,{get contradictions(){return e(H)},get focusedPairIndex(){return e(b)},onSelectPair:C,width:800,height:600})};B(S,s=>{e(H).length===0?s($):s(I,!1)})}r(u);var T=o(u,2),P=a(T),Y=o(a(P),2),je=a(Y,!0);r(Y),r(P);var Be=o(P,2);{var at=s=>{var c=Bt();w(s,c)};B(Be,s=>{e(me).length===0&&s(at)})}var st=o(Be,2);te(st,19,()=>e(me),s=>s.c.memory_a_id+"|"+s.c.memory_b_id,(s,c,h)=>{const f=A(()=>e(c).c),K=A(()=>e(b)===e(h));var z=Ht(),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 Se=o(V,2),lt=a(Se,!0);r(Se);var Me=o(Se,2),Ie=a(Me),Re=o(a(Ie),2),mt=a(Re,!0);r(Re);var Ye=o(Re,2),nt=a(Ye);r(Ye),r(Ie);var ze=o(Ie,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(Me);var ct=o(Me,2);{var vt=ce=>{var ve=Wt(),pe=o(a(ve),2),Oe=a(pe,!0);r(pe);var ge=o(pe,2);{var Ae=J=>{var Z=Yt();te(Z,21,()=>e(f).memory_a_tags,Ne,(Ge,Fe)=>{var ee=$t(),De=a(ee,!0);r(ee),O(()=>v(De,e(Fe))),w(Ge,ee)}),r(Z),w(J,Z)};B(ge,J=>{e(f).memory_a_tags&&e(f).memory_a_tags.length>0&&J(Ae)})}var ye=o(ge,4),Ee=a(ye,!0);r(ye);var pt=o(ye,2);{var yt=J=>{var Z=Vt();te(Z,21,()=>e(f).memory_b_tags,Ne,(Ge,Fe)=>{var ee=zt(),De=a(ee,!0);r(ee),O(()=>v(De,e(Fe))),w(Ge,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),O(()=>{v(Oe,e(f).memory_a_preview),v(Ee,e(f).memory_b_preview)}),w(ce,ve)};B(ct,ce=>{e(K)&&ce(vt)})}r(z),O((ce,ve,pe,Oe,ge,Ae,ye,Ee)=>{He(z,1,`w-full text-left p-3 rounded-xl border transition + ${e(K)?"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]"}`),q(be,`background: ${ce??""}`),q(Q,`color: ${ve??""}`),v(ot,pe),v(it,`${Oe??""}% sim · ${e(f).date_diff_days??""}d`),v(lt,e(f).topic),v(mt,ge),v(nt,`${Ae??""}%`),v(_t,ye),v(dt,`${Ee??""}%`)},[()=>Ce(e(f).similarity),()=>Ce(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)]),W("click",z,()=>ae(e(h))),w(s,z)}),r(T),r(N),r(ne),O((s,c,h)=>{v(_e,`contradictions across ${s??""} memories`),v(we,c),v(n,e(H).length),v(l,h),v(je,e(me).length)},[()=>e(ie).toLocaleString(),()=>e(le).toFixed(2),()=>e(H).filter(s=>s.similarity>.7).length]),w(m,ne),Je()}Ze(["click"]);export{or as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js.br b/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js.br new file mode 100644 index 0000000..808578f Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js.gz b/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js.gz new file mode 100644 index 0000000..3af34eb Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/5.B300rRjT.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js.br b/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js.br deleted file mode 100644 index 6266711..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js.gz b/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js.gz deleted file mode 100644 index e00d286..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js b/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js new file mode 100644 index 0000000..e9d381e --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js @@ -0,0 +1,14 @@ +import"../chunks/Bzak7iHL.js";import{p as qe,e as a,r as t,d 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,y as Ge,bc as Ae}from"../chunks/CvjSAYrz.js";import{s as x,d as Ye,a as pe}from"../chunks/FzvEaXMa.js";import{c as Fe,a as m,f as _,b as Ve,t as Ie}from"../chunks/BsvCUYx-.js";import{i as j}from"../chunks/ciN1mm2W.js";import{e as ge}from"../chunks/DTnG8poT.js";import{h as Be}from"../chunks/DObx9JW_.js";import{s as A,r as We,a as Xe}from"../chunks/CNfQDikv.js";import{s as _e}from"../chunks/DPl3NjBv.js";import{a as ze}from"../chunks/DNjM5a-l.js";import{s as ae}from"../chunks/Bhad70Ss.js";import{p as Ue}from"../chunks/B_YDQCB6.js";import{b as Je}from"../chunks/DJWRm1Ki.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(W,`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),B=a(Z),o=i(a(B),2),y=a(o,!0);t(o),t(B);var O=i(B,2),$=a(O);t(O),t(Z);var W=i(Z,2),P=i(a(W),2),le=a(P,!0);t(P),t(W);var oe=i(W,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 Bt(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();Be("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)},B=o=>{var y=Dt(),O=he(y),$=a(O),W=a($),P=a(W);t(W);var le=i(W,2),oe=a(le),ee=i(oe,2);t(le),t($);var S=i($,2),D=a(S);We(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(B,!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{Bt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js.br b/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js.br new file mode 100644 index 0000000..ca2fab5 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js.gz b/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js.gz new file mode 100644 index 0000000..5061aab Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/6.DBS_R5Hl.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js.br b/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js.br deleted file mode 100644 index 8efc5d1..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js.gz b/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js.gz deleted file mode 100644 index 768e97b..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/6.DXXEUSu1.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js b/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js deleted file mode 100644 index 82325de..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js +++ /dev/null @@ -1 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as ut}from"../chunks/CkyfbJUz.js";import{p as gt,s as T,c as Q,t as u,a as ft,d,e as s,h as $,g as t,r as e,G as ht}from"../chunks/C9Z4nxhR.js";import{d as bt,s as l,a as yt}from"../chunks/DP9qWekZ.js";import{i as R}from"../chunks/C2oj68pw.js";import{e as U,i as D}from"../chunks/kH-DTQyy.js";import{a as o,f as n}from"../chunks/DPfxVJHQ.js";import{s as q}from"../chunks/Co2v30Gm.js";import{a as Z}from"../chunks/BcuCGYSa.js";var wt=n(""),Rt=n('
        '),Nt=n('
        '),St=n('

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

        '),kt=n(' '),Ot=n(' '),$t=n('

        '),zt=n('
        '),Ct=n('

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

        '),It=n(" "),Pt=n(' '),Tt=n('

        '),Ut=n('
        '),Dt=n('

        Intentions & Predictions

        Prospective Memory

        "Remember to do X when Y happens"

        Predicted Needs

        What you might need next
        ');function Wt(tt,et){gt(et,!0);let z=T(Q([])),j=T(Q([])),A=T(!0),N=T("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={critical:"text-decay",high:"text-amber-400",normal:"text-dim",low:"text-muted"},rt={time:"⏰",context:"◎",event:"⚡"};ut(async()=>{await B()});async function B(){$(A,!0);try{const[r,i]=await Promise.all([Z.intentions(t(N)),Z.predict()]);$(z,r.intentions||[],!0),$(j,i.predictions||[],!0)}catch{}finally{$(A,!1)}}async function it(r){$(N,r,!0),await B()}function F(r){if(!r)return"";try{return new Date(r).toLocaleDateString("en-US",{month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"})}catch{return r}}var G=Dt(),L=s(G),H=d(s(L),2),dt=s(H);e(H),e(L);var M=d(L,2),Y=d(s(M),2);U(Y,20,()=>["active","fulfilled","snoozed","cancelled","all"],D,(r,i)=>{var v=wt(),a=s(v,!0);e(v),u(x=>{q(v,1,`px-3 py-1.5 rounded-xl text-xs transition ${t(N)===i?"bg-synapse/20 text-synapse-glow border border-synapse/40":"glass-subtle text-dim hover:bg-white/[0.03]"}`),l(a,x)},[()=>i.charAt(0).toUpperCase()+i.slice(1)]),yt("click",v,()=>it(i)),o(r,v)}),e(Y);var lt=d(Y,2);{var vt=r=>{var i=Nt();U(i,20,()=>Array(4),D,(v,a)=>{var x=Rt();o(v,x)}),e(i),o(r,i)},ot=r=>{var i=St(),v=d(s(i),2),a=s(v);e(v),ht(2),e(i),u(()=>l(a,`No ${t(N)==="all"?"":t(N)+" "}intentions.`)),o(r,i)},nt=r=>{var i=zt();U(i,21,()=>t(z),D,(v,a)=>{var x=$t(),f=s(x),h=s(f),C=s(h,!0);e(h);var g=d(h,2),S=s(g),I=s(S,!0);e(S);var b=d(S,2),y=s(b),P=s(y,!0);e(y);var w=d(y,2),E=s(w);e(w);var k=d(w,2),c=s(k);e(k);var p=d(k,2);{var O=m=>{var _=kt(),W=s(_);e(_),u(X=>l(W,`deadline: ${X??""}`),[()=>F(t(a).deadline)]),o(m,_)};R(p,m=>{t(a).deadline&&m(O)})}var V=d(p,2);{var mt=m=>{var _=Ot(),W=s(_);e(_),u(X=>l(W,`snoozed until ${X??""}`),[()=>F(t(a).snoozed_until)]),o(m,_)};R(V,m=>{t(a).snoozed_until&&m(mt)})}e(b),e(g);var K=d(g,2),_t=s(K,!0);e(K),e(f),e(x),u((m,_)=>{l(C,rt[t(a).trigger_type]||"◇"),l(I,t(a).content),q(y,1,`px-2 py-0.5 text-[10px] rounded-lg border ${(at[t(a).status]||"text-dim bg-white/[0.03] border-subtle/20")??""}`),l(P,t(a).status),q(w,1,`text-[10px] ${(st[t(a).priority]||"text-muted")??""}`),l(E,`${t(a).priority??""} priority`),l(c,`${t(a).trigger_type??""}: ${m??""}`),l(_t,_)},[()=>t(a).trigger_value.length>40?t(a).trigger_value.slice(0,37)+"...":t(a).trigger_value,()=>F(t(a).created_at)]),o(v,x)}),e(i),o(r,i)};R(lt,r=>{t(A)?r(vt):t(z).length===0?r(ot,1):r(nt,!1)})}e(M);var J=d(M,2),pt=d(s(J),2);{var xt=r=>{var i=Ct();o(r,i)},ct=r=>{var i=Ut();U(i,21,()=>t(j),D,(v,a,x)=>{var f=Tt(),h=s(f);h.textContent=x+1;var C=d(h,2),g=s(C),S=s(g,!0);e(g);var I=d(g,2),b=s(I),y=s(b,!0);e(b);var P=d(b,2);{var w=c=>{var p=It(),O=s(p);e(p),u(V=>l(O,`${V??""}% retention`),[()=>(Number(t(a).retention)*100).toFixed(0)]),o(c,p)};R(P,c=>{t(a).retention&&c(w)})}var E=d(P,2);{var k=c=>{var p=Pt(),O=s(p);e(p),u(()=>l(O,`${t(a).predictedNeed??""} need`)),o(c,p)};R(E,c=>{t(a).predictedNeed&&c(k)})}e(I),e(C),e(f),u(()=>{l(S,t(a).content),l(y,t(a).nodeType)}),o(v,f)}),e(i),o(r,i)};R(pt,r=>{t(j).length===0?r(xt):r(ct,!1)})}e(J),e(G),u(()=>l(dt,`${t(z).length??""} intentions`)),o(tt,G),ft()}bt(["click"]);export{Wt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js.br b/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js.br deleted file mode 100644 index 5467de2..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js.gz b/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js.gz deleted file mode 100644 index 5057033..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/7.BI22Pt_j.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js b/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js new file mode 100644 index 0000000..861d5b8 --- /dev/null +++ b/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js @@ -0,0 +1,5 @@ +import"../chunks/Bzak7iHL.js";import{o as Ce,a as Le}from"../chunks/CNjeV5xa.js";import{p as _e,f as we,g as e,a as Te,u as P,e as r,r as s,d as o,t as S,h as w,s as j,c as xe,n as he}from"../chunks/CvjSAYrz.js";import{d as Ae,s as g,a as z}from"../chunks/FzvEaXMa.js";import{i as G}from"../chunks/ciN1mm2W.js";import{e as oe,i as ke}from"../chunks/DTnG8poT.js";import{c as Re,a as v,f as m}from"../chunks/BsvCUYx-.js";import{s as ne,r as Fe}from"../chunks/CNfQDikv.js";import{b as Ne}from"../chunks/CVpUe0w3.js";import{s as pe}from"../chunks/DPl3NjBv.js";import{s as re}from"../chunks/Bhad70Ss.js";import{N as Ie}from"../chunks/DzfRjky4.js";function Me(n){return n>=.92?"near-identical":n>=.8?"strong":"weak"}function ge(n){const i=Me(n);return i==="near-identical"?"var(--color-decay)":i==="strong"?"var(--color-warning)":"#fde047"}function Ze(n){const i=Me(n);return i==="near-identical"?"Near-identical":i==="strong"?"Strong match":"Weak match"}function Ee(n){return n>.7?"#10b981":n>.4?"#f59e0b":"#ef4444"}function Oe(n){if(!n||n.length===0)return null;let i=n[0],d=Number.isFinite(i.retention)?i.retention:-1/0;for(let u=1;ud&&(i=b,d=_)}return i}function Pe(n,i){return n.filter(d=>d.similarity>=i)}function be(n){return n.map(i=>i.id).slice().sort().join("|")}function Be(n,i=80){if(!n)return"";const d=n.trim().replace(/\s+/g," ");return d.length<=i?d:d.slice(0,i)+"…"}function ye(n){if(!n||typeof n!="string")return"";const i=new Date(n);return Number.isNaN(i.getTime())?"":i.toLocaleDateString(void 0,{year:"numeric",month:"short",day:"numeric"})}function He(n,i=4){return Array.isArray(n)?n.slice(0,i):[]}var Ke=m('WINNER'),Ue=m(' '),We=m('
        '),je=m('

        '),ze=m('
        ');function Ge(n,i){_e(i,!0);let d=j(!1);const u=P(()=>Oe(i.memories)),b=P(()=>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 k=Re(),V=we(k);{var le=x=>{var X=ze(),B=r(X),Y=r(B),h=r(Y),C=r(h),ee=r(C);s(C);var H=o(C,2),de=r(H,!0);s(H);var K=o(H,2),q=r(K);s(K),s(h);var L=o(h,2),U=r(L);s(L),s(Y);var W=o(Y,2),ce=r(W);s(W),s(B);var R=o(B,2);oe(R,21,()=>i.memories,T=>T.id,(T,c)=>{var M=je(),N=r(M),I=o(N,2),t=r(I),a=r(t),l=r(a,!0);s(a);var p=o(a,2);{var Z=y=>{var A=Ke();v(y,A)};G(p,y=>{e(c).id===e(u).id&&y(Z)})}var D=o(p,2);oe(D,17,()=>He(e(c).tags,4),ke,(y,A)=>{var O=Ue(),me=r(O,!0);s(O),S(()=>g(me,e(A))),v(y,O)}),s(t);var f=o(t,2),E=r(f,!0);s(f);var ae=o(f,2);{var Q=y=>{var A=We(),O=r(A,!0);s(A),S(me=>g(O,me),[()=>ye(e(c).createdAt)]),v(y,A)},ve=P(()=>ye(e(c).createdAt));G(ae,y=>{e(ve)&&y(Q)})}s(I);var se=o(I,2),$=r(se),De=r($);s($);var fe=o($,2),Se=r(fe);s(fe),s(se),s(M),S((y,A,O)=>{pe(M,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: ${(Ie[e(c).nodeType]||"#8B95A5")??""}`),ne(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(E,y),re(De,`width: ${e(c).retention*100}%; background: ${A??""}`),g(Se,`${O??""}%`)},[()=>e(d)?e(c).content:Be(e(c).content),()=>Ee(e(c).retention),()=>(e(c).retention*100).toFixed(0)]),v(T,M)}),s(R);var te=o(R,2),J=r(te),F=o(J,2),ue=r(F,!0);s(F);var ie=o(F,2);s(te),s(X),S((T,c,M,N,I,t,a,l)=>{re(C,`color: ${T??""}`),g(ee,`${c??""}%`),g(de,M),g(q,`· ${i.memories.length??""} memories`),ne(L,"aria-valuenow",N),re(U,`width: ${I??""}%; background: ${t??""}; box-shadow: 0 0 12px ${a??""}66`),pe(W,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"}`),ne(J,"title",`Merge all into highest-retention memory (${l??""}%)`),ne(F,"aria-expanded",e(d)),g(ue,e(d)?"Collapse":"Review")},[()=>ge(i.similarity),()=>(i.similarity*100).toFixed(1),()=>Ze(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(...T){var c;(c=i.onDismiss)==null||c.apply(this,T)}),v(x,X)};G(V,x=>{i.memories.length>0&&e(u)&&x(le)})}v(n,k),Te()}Ae(["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(n,i){_e(i,!0);let d=j(.8),u=j(xe([])),b=j(xe(new Set)),_=j(!0),k=j(null),V;async function le(t){return await new Promise(l=>setTimeout(l,450)),{clusters:Pe([{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","aimo3","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","aimo3"],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 AIMO3 submissions. Alternatives considered: sglang (slower cold start), TensorRT-LLM (deployment friction).",nodeType:"decision",tags:["vllm","aimo3","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 AIMO3 inference.",nodeType:"decision",tags:["vllm","aimo3"],retention:.72,createdAt:"2026-04-06T10:30:00Z"}]},{similarity:.83,suggestedAction:"review",memories:[{id:"m-006",content:"Sam prefers to ship one change per Kaggle submission — stacking changes destroyed signal at AIMO3 (30/50 regression from 12 stacked variables).",nodeType:"pattern",tags:["kaggle","methodology","aimo3"],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 12 changes at AIMO3 cost a submission. 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(k,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(k,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 B(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}),B(t)}const h=P(()=>e(u).map(t=>({c:t,key:be(t.memories)})).filter(({key:t})=>!e(b).has(t))),C=P(()=>e(h).reduce((t,{c:a})=>t+a.memories.length,0)),ee=50,H=P(()=>e(h).length>ee),de=P(()=>e(H)?e(h).slice(0,ee):e(h));Ce(()=>x()),Le(()=>clearTimeout(V));var K=at(),q=o(r(K),2),L=r(q),U=o(r(L),2);Fe(U);var W=o(U,2),ce=r(W);s(W),s(L);var R=o(L,2),te=r(R);{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);s(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(k)?t(F,1):t(ue,!1)})}s(R);var ie=o(R,2);s(q);var T=o(q,2);{var c=t=>{var a=qe(),l=o(r(a),2),p=r(l,!0);s(l);var Z=o(l,2);s(a),S(()=>g(p,e(k))),z("click",Z,x),v(t,a)},M=t=>{var a=Qe();oe(a,20,()=>Array(3),ke,(l,p)=>{var Z=Je();v(l,Z)}),s(a),v(t,a)},N=t=>{var a=$e();v(t,a)},I=t=>{var a=it(),l=r(a);{var p=D=>{var f=et(),E=r(f);s(f),S(()=>g(E,`Showing first 50 of ${e(h).length??""} clusters. Raise the + threshold to narrow results.`)),v(D,f)};G(l,D=>{e(H)&&D(p)})}var Z=o(l,2);oe(Z,17,()=>e(de),({c:D,key:f})=>f,(D,f)=>{let E=()=>e(f).c,ae=()=>e(f).key;var Q=tt(),ve=r(Q);Ge(ve,{get similarity(){return E().similarity},get memories(){return E().memories},get suggestedAction(){return E().suggestedAction},onDismiss:()=>B(ae()),onMerge:(se,$)=>Y(ae(),se,$)}),s(Q),v(D,Q)}),s(a),v(t,a)};G(T,t=>{e(k)?t(c):e(_)?t(M,1):e(h).length===0?t(N,2):t(I,!1)})}s(K),S(t=>{g(ce,`${t??""}%`),ie.disabled=e(_)},[()=>(e(d)*100).toFixed(0)]),z("input",U,X),Ne(U,()=>e(d),t=>w(d,t)),z("click",ie,x),v(n,K),Te()}Ae(["input","click"]);export{ft as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js.br b/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js.br new file mode 100644 index 0000000..f020465 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js.gz b/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js.gz new file mode 100644 index 0000000..e5f616e Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/7.br0Vbs-w.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js similarity index 94% rename from apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js rename to apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js index b6f1009..8f98651 100644 --- a/apps/dashboard/build/_app/immutable/nodes/4.CeoFmj14.js +++ b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.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/C9Z4nxhR.js";import{d as Be,a as q,s as o}from"../chunks/DP9qWekZ.js";import{a as c,f as m,c as De}from"../chunks/DPfxVJHQ.js";import{i as k}from"../chunks/C2oj68pw.js";import{e as ie,i as ne}from"../chunks/kH-DTQyy.js";import{r as ye}from"../chunks/ZesQ8l8p.js";import{s as oe}from"../chunks/Co2v30Gm.js";import{s as Ke}from"../chunks/BkopTN9z.js";import{b as de}from"../chunks/P9ZHwQBL.js";import{a as X}from"../chunks/BcuCGYSa.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.CDAVQcae.js.br b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js.br new file mode 100644 index 0000000..9bdbe88 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js.gz b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js.gz new file mode 100644 index 0000000..b56b486 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/8.CDAVQcae.js.gz differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js b/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js deleted file mode 100644 index 6bbf26e..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js +++ /dev/null @@ -1,4 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as qe}from"../chunks/CkyfbJUz.js";import{p as Be,s as b,c as Qe,t as E,g as e,a as Ye,d as t,e as s,h as u,r as a}from"../chunks/C9Z4nxhR.js";import{d as ze,a as p,s as v}from"../chunks/DP9qWekZ.js";import{i as ce}from"../chunks/C2oj68pw.js";import{e as Z,i as ue}from"../chunks/kH-DTQyy.js";import{a as _,f as g}from"../chunks/DPfxVJHQ.js";import{r as xe}from"../chunks/ZesQ8l8p.js";import{s as Ge}from"../chunks/Co2v30Gm.js";import{s as _e}from"../chunks/BkopTN9z.js";import{b as fe}from"../chunks/P9ZHwQBL.js";import{b as He}from"../chunks/-jeO_JOJ.js";import{a as f}from"../chunks/BcuCGYSa.js";import{N as Ie}from"../chunks/CZ45jJaw.js";var Je=g('
        '),Ke=g('
        '),Ue=g(' '),Ve=g('

        Promote Demote Delete
        '),We=g(''),Xe=g('
        '),Ze=g(`

        Memories

        Min retention:
        `);function xt(ge,me){Be(me,!0);let k=b(Qe([])),P=b(""),S=b(""),be="",h=b(0),F=b(!0),T=b(null),ee;qe(()=>m());async function m(){u(F,!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 f.memories.list(i);u(k,c.memories,!0)}catch{u(k,[],!0)}finally{u(F,!1)}}function he(){clearTimeout(ee),ee=setTimeout(m,300)}function ye(i){return i>.7?"#10b981":i>.4?"#f59e0b":"#ef4444"}var M=Ze(),A=s(M),te=t(s(A),2),we=s(te);a(te),a(A);var C=t(A,2),$=s(C);xe($);var y=t($,2),N=s(y);N.value=N.__value="";var O=t(N);O.value=O.__value="fact";var R=t(O);R.value=R.__value="concept";var j=t(R);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 ae=t(Q);ae.value=ae.__value="decision",a(y);var se=t(y,2),D=t(s(se),2);xe(D);var ie=t(D,2),ke=s(ie);a(ie),a(se),a(C);var Pe=t(C,2);{var Se=i=>{var c=Ke();Z(c,20,()=>Array(8),ue,(w,o)=>{var x=Je();_(w,x)}),a(c),_(i,c)},Te=i=>{var c=Xe();Z(c,21,()=>e(k),w=>w.id,(w,o)=>{var x=We(),Y=s(x),z=s(Y),G=s(z),oe=s(G),H=t(oe,2),$e=s(H,!0);a(H);var De=t(H,2);Z(De,17,()=>e(o).tags.slice(0,3),ue,(n,l)=>{var d=Ue(),J=s(d,!0);a(d),E(()=>v(J,e(l))),_(n,d)}),a(G);var re=t(G,2),Ee=s(re,!0);a(re),a(z);var ne=t(z,2),I=s(ne),Fe=s(I);a(I);var le=t(I,2),Me=s(le);a(le),a(ne),a(Y);var Ae=t(Y,2);{var Ce=n=>{var l=Ve(),d=s(l),J=s(d,!0);a(d);var K=t(d,2),U=s(K),Ne=s(U);a(U);var V=t(U,2),Oe=s(V);a(V);var de=t(V,2),Re=s(de);a(de),a(K);var pe=t(K,2),W=s(pe),X=t(W,2),ve=t(X,2);a(pe),a(l),E((r,je,Le)=>{v(J,e(o).content),v(Ne,`Storage: ${r??""}%`),v(Oe,`Retrieval: ${je??""}%`),v(Re,`Created: ${Le??""}`)},[()=>(e(o).storageStrength*100).toFixed(1),()=>(e(o).retrievalStrength*100).toFixed(1),()=>new Date(e(o).createdAt).toLocaleDateString()]),p("click",W,r=>{r.stopPropagation(),f.memories.promote(e(o).id)}),p("keydown",W,r=>{r.key==="Enter"&&(r.stopPropagation(),f.memories.promote(e(o).id))}),p("click",X,r=>{r.stopPropagation(),f.memories.demote(e(o).id)}),p("keydown",X,r=>{r.key==="Enter"&&(r.stopPropagation(),f.memories.demote(e(o).id))}),p("click",ve,async r=>{r.stopPropagation(),await f.memories.delete(e(o).id),m()}),p("keydown",ve,async r=>{r.key==="Enter"&&(r.stopPropagation(),await f.memories.delete(e(o).id),m())}),_(n,l)};ce(Ae,n=>{var l;((l=e(T))==null?void 0:l.id)===e(o).id&&n(Ce)})}a(x),E((n,l)=>{var d;Ge(x,1,`text-left p-4 glass-subtle rounded-xl hover:bg-white/[0.04] - transition-all duration-200 group - ${((d=e(T))==null?void 0:d.id)===e(o).id?"!border-synapse/40 glow-synapse":""}`),_e(oe,`background: ${(Ie[e(o).nodeType]||"#8B95A5")??""}`),v($e,e(o).nodeType),v(Ee,e(o).content),_e(Fe,`width: ${e(o).retentionStrength*100}%; background: ${n??""}`),v(Me,`${l??""}%`)},[()=>ye(e(o).retentionStrength),()=>(e(o).retentionStrength*100).toFixed(0)]),p("click",x,()=>{var n;return u(T,((n=e(T))==null?void 0:n.id)===e(o).id?null:e(o),!0)}),_(w,x)}),a(c),_(i,c)};ce(Pe,i=>{e(F)?i(Se):i(Te,!1)})}a(M),E(i=>{v(we,`${e(k).length??""} results`),v(ke,`${i??""}%`)},[()=>(e(h)*100).toFixed(0)]),p("input",$,he),fe($,()=>e(P),i=>u(P,i)),p("change",y,m),He(y,()=>e(S),i=>u(S,i)),p("change",D,m),fe(D,()=>e(h),i=>u(h,i)),_(ge,M),Ye()}ze(["input","change","click","keydown"]);export{xt as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js.br b/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js.br deleted file mode 100644 index 28cfe5e..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js.gz b/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js.gz deleted file mode 100644 index 8b7d58d..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/8.Cq7jwWnG.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js b/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js deleted file mode 100644 index 6b5d886..0000000 --- a/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js +++ /dev/null @@ -1,2 +0,0 @@ -import"../chunks/Bzak7iHL.js";import{o as ze}from"../chunks/CkyfbJUz.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,G as b,f as Be,u as j}from"../chunks/C9Z4nxhR.js";import{d as He,a as X,s as p}from"../chunks/DP9qWekZ.js";import{i as u}from"../chunks/C2oj68pw.js";import{e as Z,i as ee}from"../chunks/kH-DTQyy.js";import{a as v,f as l,t as ue}from"../chunks/DPfxVJHQ.js";import{s as ge}from"../chunks/Co2v30Gm.js";import{s as fe}from"../chunks/BkopTN9z.js";import{s as Le,a as te}from"../chunks/DWr9YED7.js";import{a as T}from"../chunks/BcuCGYSa.js";import{m as Ue,a as Ye,i as qe}from"../chunks/BmeMLq0p.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 E=S(!1),A=S(!1),y=S(null),g=S(null),we=S(null),$=S(null),ae=S(!0),Se=S(null);ze(()=>{N()});async function N(){m(ae,!0);try{const[a,o,c]=await Promise.all([T.stats().catch(()=>null),T.health().catch(()=>null),T.retentionDistribution().catch(()=>null)]);m(we,a,!0),m(Se,o,!0),m($,c,!0)}finally{m(ae,!1)}}async function ke(){m(E,!0),m(y,null);try{m(y,await T.consolidate(),!0),await N()}catch{}finally{m(E,!1)}}async function Ce(){m(A,!0),m(g,null);try{m(g,await T.dream(),!0),await N()}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),O=r(t(U),2),Ge=t(O);{var Me=a=>{var o=Qe();b(),v(a,o)},Fe=a=>{var o=ue("Consolidate");v(a,o)};u(Ge,a=>{s(E)?a(Me):a(Fe,!1)})}e(O),e(U);var je=r(U,2);{var Te=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(Te)})}e(L);var ve=r(L,2),Y=t(ve),D=r(t(Y),2),Ee=t(D);{var Ne=a=>{var o=st();b(),v(a,o)},Oe=a=>{var o=ue("Dream");v(a,o)};u(Ee,a=>{s(A)?a(Ne):a(Oe,!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"),O.disabled=s(E),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,N),X("click",O,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.19crbYuZ.js.br b/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js.br deleted file mode 100644 index 76953f8..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js.br and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js.gz b/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js.gz deleted file mode 100644 index 1aab59f..0000000 Binary files a/apps/dashboard/build/_app/immutable/nodes/9.19crbYuZ.js.gz and /dev/null differ diff --git a/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js similarity index 81% rename from apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js rename to apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js index b7cd273..d989ca5 100644 --- a/apps/dashboard/build/_app/immutable/nodes/5.CgbdGsQS.js +++ b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js @@ -1,8 +1,8 @@ -import"../chunks/Bzak7iHL.js";import{i as oe}from"../chunks/_Va07L2l.js";import{p as ee,ah as ie,g as e,h as M,e as o,d,r as s,f as ne,t as $,a as te,s as K,u as X,C as Z}from"../chunks/C9Z4nxhR.js";import{s as x,d as de,a as ce}from"../chunks/DP9qWekZ.js";import{a as l,f as m}from"../chunks/DPfxVJHQ.js";import{i as w}from"../chunks/C2oj68pw.js";import{e as re,i as ae}from"../chunks/kH-DTQyy.js";import{s as C}from"../chunks/BkopTN9z.js";import{s as le,a as me}from"../chunks/DWr9YED7.js";import{w as ve,e as ue}from"../chunks/BmeMLq0p.js";import{E as O}from"../chunks/CZ45jJaw.js";import{s as pe}from"../chunks/ZesQ8l8p.js";import{s as fe}from"../chunks/Co2v30Gm.js";import{p as Q}from"../chunks/Do8TgQ-j.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)&&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=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 +import"../chunks/Bzak7iHL.js";import{i as oe}from"../chunks/Bz1l2A_1.js";import{p as ee,aB as ie,g as e,h as M,e as o,d,r as s,f as ne,t as $,a as te,s as K,u as X,V as Z}from"../chunks/CvjSAYrz.js";import{s as x,d as de,a as ce}from"../chunks/FzvEaXMa.js";import{a as l,f as m}from"../chunks/BsvCUYx-.js";import{i as w}from"../chunks/ciN1mm2W.js";import{e as re,i as ae}from"../chunks/DTnG8poT.js";import{s as C}from"../chunks/Bhad70Ss.js";import{s as le,a as me}from"../chunks/D81f-o_I.js";import{w as ve,e as ue}from"../chunks/CtkE7HV2.js";import{E as O}from"../chunks/DzfRjky4.js";import{s as pe}from"../chunks/CNfQDikv.js";import{s as fe}from"../chunks/DPl3NjBv.js";import{p as Q}from"../chunks/B_YDQCB6.js";var xe=m(' '),_e=m('
        '),ge=m('
        ',1),he=m('
        '),ye=m('
        '),$e=m('
        Cognitive Search Pipeline
        ');function be(V,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);ie(()=>{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),I=d(o(b),2);{var L=t=>{var a=xe(),i=o(a);s(a),$(()=>x(i,`${S()??""} results in ${j()??""}ms`)),l(t,a)};w(I,t=>{e(u)&&t(L)})}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=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??""}%; 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(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(),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 ${(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(A,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}; + 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(V,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(V,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)}}oe();var u=ke(),P=o(u),D=d(o(P),2),b=o(D),I=o(b);s(b);var L=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 ${(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(A,n=>{S().length===0?n(N):n(z,!1)})}s(u),$(()=>x(I,`${S().length??""} events`)),ce("click",L,()=>ve.clearEvents()),l(V,u),te(),q()}de(["click"]);export{ze as component}; diff --git a/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js.br b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js.br new file mode 100644 index 0000000..244d903 Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js.br differ diff --git a/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js.gz b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js.gz new file mode 100644 index 0000000..574217c Binary files /dev/null and b/apps/dashboard/build/_app/immutable/nodes/9.DVbfK-u1.js.gz differ diff --git a/apps/dashboard/build/_app/version.json b/apps/dashboard/build/_app/version.json index 10d0f4c..0d5a126 100644 --- a/apps/dashboard/build/_app/version.json +++ b/apps/dashboard/build/_app/version.json @@ -1 +1 @@ -{"version":"1772569216424"} \ No newline at end of file +{"version":"1776927992944"} \ 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 40a86c3..0208eb0 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 74640ff..ae05356 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/index.html b/apps/dashboard/build/index.html index 38a3b72..25f08dd 100644 --- a/apps/dashboard/build/index.html +++ b/apps/dashboard/build/index.html @@ -8,23 +8,24 @@ - - - - - - - - - - - + + + + + + + + + + + + - - - - - + + + + + Vestige @@ -32,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/ForgettingIndicator.svelte b/apps/dashboard/src/lib/components/ForgettingIndicator.svelte new file mode 100644 index 0000000..1efd094 --- /dev/null +++ b/apps/dashboard/src/lib/components/ForgettingIndicator.svelte @@ -0,0 +1,25 @@ + + + +{#if $suppressedCount > 0} +
        +
        + + +
        + + Actively forgetting {$suppressedCount} {$suppressedCount === 1 ? 'memory' : 'memories'} + +
        +{/if} diff --git a/apps/dashboard/src/lib/components/Graph3D.svelte b/apps/dashboard/src/lib/components/Graph3D.svelte index f0f0954..7a55436 100644 --- a/apps/dashboard/src/lib/components/Graph3D.svelte +++ b/apps/dashboard/src/lib/components/Graph3D.svelte @@ -3,7 +3,7 @@ import type { GraphNode, GraphEdge, VestigeEvent } from '$types'; import { createScene, resizeScene, disposeScene, type SceneContext } from '$lib/graph/scene'; import { ForceSimulation } from '$lib/graph/force-sim'; - import { NodeManager } from '$lib/graph/nodes'; + import { NodeManager, type ColorMode } from '$lib/graph/nodes'; import { EdgeManager } from '$lib/graph/edges'; import { ParticleSystem } from '$lib/graph/particles'; import { EffectManager } from '$lib/graph/effects'; @@ -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,11 +20,31 @@ 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. + colorMode?: ColorMode; onSelect?: (nodeId: string) => void; onGraphMutation?: (mutation: GraphMutation) => void; } - let { nodes, edges, centerId, events = [], isDreaming = false, onSelect, onGraphMutation }: Props = $props(); + let { + nodes, + edges, + centerId, + events = [], + isDreaming = false, + colorMode = 'type', + onSelect, + onGraphMutation, + }: Props = $props(); + + // Re-tint every live node whenever the color mode flips. The NodeManager's + // setColorMode is idempotent and mutates materials in place, so this + // effect runs once per toggle and doesn't rebuild the scene. + $effect(() => { + nodeManager?.setColorMode(colorMode); + }); let container: HTMLDivElement; let ctx: SceneContext; @@ -39,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[] = []; @@ -58,6 +84,10 @@ // Modules particles = new ParticleSystem(ctx.scene); nodeManager = new NodeManager(); + // Apply the initial colour mode before node creation so the first paint + // already reflects the user's prop choice. Prevents a visible flash from + // type-colour to state-colour on mount when the page defaults to state. + nodeManager.colorMode = colorMode; edgeManager = new EdgeManager(); effects = new EffectManager(ctx.scene); dreamMode = new DreamMode(); @@ -92,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); @@ -108,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); @@ -133,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, @@ -156,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..f941911 --- /dev/null +++ b/apps/dashboard/src/lib/components/InsightToast.svelte @@ -0,0 +1,253 @@ + + + +
        + {#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/MemoryStateLegend.svelte b/apps/dashboard/src/lib/components/MemoryStateLegend.svelte new file mode 100644 index 0000000..2a609a9 --- /dev/null +++ b/apps/dashboard/src/lib/components/MemoryStateLegend.svelte @@ -0,0 +1,52 @@ + + + +
        +
        + FSRS accessibility +
        + {#each STATES as state (state)} +
        + + {state} + + {MEMORY_STATE_DESCRIPTIONS[state].match(/\(([^)]+)\)/)?.[1] ?? ''} + +
        + {/each} +
        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/TimeSlider.svelte b/apps/dashboard/src/lib/components/TimeSlider.svelte index 6d96318..acdc19a 100644 --- a/apps/dashboard/src/lib/components/TimeSlider.svelte +++ b/apps/dashboard/src/lib/components/TimeSlider.svelte @@ -51,6 +51,7 @@ } function playLoop() { + if (!playing) return; animFrameId = requestAnimationFrame((now) => { const deltaSeconds = (now - lastTime) / 1000; lastTime = now; @@ -78,6 +79,7 @@ } onDestroy(() => { + playing = false; cancelAnimationFrame(animFrameId); }); 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..c7b9ccf --- /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', 'nullgaze', 'injeranet'] as const; + +const PATTERNS: TransferPatternLike[] = [ + { + name: 'Result', + category: 'ErrorHandling', + origin_project: 'vestige', + transferred_to: ['nullgaze', 'injeranet'], + transfer_count: 2, + }, + { + name: 'Axum middleware', + category: 'ErrorHandling', + origin_project: 'nullgaze', + transferred_to: ['vestige'], + transfer_count: 1, + }, + { + name: 'proptest', + category: 'Testing', + origin_project: 'vestige', + transferred_to: ['nullgaze'], + 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 → nullgaze: Result + proptest = 2 + expect(m.vestige.nullgaze.count).toBe(2); + // vestige → injeranet: Result only = 1 + expect(m.vestige.injeranet.count).toBe(1); + // nullgaze → vestige: Axum middleware = 1 + expect(m.nullgaze.vestige.count).toBe(1); + // injeranet → anywhere: zero (no origin in injeranet in fixtures) + expect(m.injeranet.vestige.count).toBe(0); + expect(m.injeranet.nullgaze.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.nullgaze.count).not.toBe(m.nullgaze.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: ['nullgaze'], + transfer_count: 1, + })); + const m = buildTransferMatrix(['vestige', 'nullgaze'], manyPatterns); + expect(m.vestige.nullgaze.count).toBe(5); + expect(m.vestige.nullgaze.topNames).toHaveLength(3); + expect(m.vestige.nullgaze.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', 'nullgaze'], + transfer_count: 2, + }; + const m = buildTransferMatrix(PROJECTS, [strayDest]); + // The known destination counts; the ghost doesn't. + expect(m.vestige.nullgaze.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: ['nullgaze'], + transfer_count: 1, + }, + { + name: 'b', + category: 'Testing', + origin_project: 'vestige', + transferred_to: ['nullgaze'], + transfer_count: 1, + }, + ]; + const m = buildTransferMatrix(['vestige', 'nullgaze'], pats, 1); + expect(m.vestige.nullgaze.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→nullgaze 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→nullgaze = 2 + // vestige→injeranet = 1 + // vestige→vestige = 1 + // nullgaze→vestige = 1 + expect(rows).toHaveLength(4); + expect(rows[0]).toMatchObject({ from: 'vestige', to: 'nullgaze', 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 new file mode 100644 index 0000000..6eb1bbe --- /dev/null +++ b/apps/dashboard/src/lib/graph/__tests__/color-mode.test.ts @@ -0,0 +1,664 @@ +/** + * v2.0.8 Memory-state colour mode — ruthless coverage. + * + * Every line added in v2.0.8 is exercised here: pure helpers, palette + * integrity, NodeManager mode switching, in-place retinting, edge cases, + * suppression interaction, new-node inheritance, idempotence, and + * round-trip fidelity. If this file is green, the feature is wired. + */ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +vi.mock('three', async () => { + const mock = await import('./three-mock'); + return { ...mock }; +}); + +import { + NodeManager, + getMemoryState, + getNodeColor, + MEMORY_STATE_COLORS, + MEMORY_STATE_DESCRIPTIONS, + type MemoryState, + type ColorMode, +} from '../nodes'; +import { NODE_TYPE_COLORS } from '$types'; +import { Color, Vector3, MeshStandardMaterial, SpriteMaterial } from './three-mock'; +import { makeNode, resetNodeCounter } from './helpers'; + +// Global spy cleanup — prototype-level spies must not leak between tests. +afterEach(() => { + vi.restoreAllMocks(); +}); + +// ---------------------------------------------------------------------------- +// getMemoryState — boundary analysis across all 4 FSRS buckets +// ---------------------------------------------------------------------------- + +describe('getMemoryState — bucket classification', () => { + it.each<[number, MemoryState]>([ + [1.0, 'active'], + [0.95, 'active'], + [0.7, 'active'], // inclusive lower bound of active + [0.6999999, 'dormant'], // just below active threshold + [0.5, 'dormant'], + [0.4, 'dormant'], // inclusive lower bound of dormant + [0.3999999, 'silent'], // just below dormant threshold + [0.25, 'silent'], + [0.1, 'silent'], // inclusive lower bound of silent + [0.0999999, 'unavailable'], // just below silent threshold + [0.05, 'unavailable'], + [0.0, 'unavailable'], + ])('classifies retention %f as %s', (retention, expected) => { + expect(getMemoryState(retention)).toBe(expected); + }); + + it('handles retention > 1 as active (over-strength, shouldn\'t happen but clamp-free)', () => { + expect(getMemoryState(1.5)).toBe('active'); + expect(getMemoryState(999)).toBe('active'); + }); + + it('handles negative retention as unavailable (defensive)', () => { + expect(getMemoryState(-0.5)).toBe('unavailable'); + expect(getMemoryState(-1000)).toBe('unavailable'); + }); + + it('classifies NaN as unavailable (no predicate is true)', () => { + expect(getMemoryState(NaN)).toBe('unavailable'); + }); + + it('classifies +Infinity as active', () => { + expect(getMemoryState(Infinity)).toBe('active'); + }); + + it('classifies -Infinity as unavailable', () => { + expect(getMemoryState(-Infinity)).toBe('unavailable'); + }); + + it('is deterministic and pure — same input gives same output across 10k calls', () => { + const samples = Array.from({ length: 10000 }, () => Math.random()); + const first = samples.map(getMemoryState); + const second = samples.map(getMemoryState); + expect(first).toEqual(second); + }); +}); + +// ---------------------------------------------------------------------------- +// MEMORY_STATE_COLORS — palette integrity +// ---------------------------------------------------------------------------- + +describe('MEMORY_STATE_COLORS — palette integrity', () => { + const states: MemoryState[] = ['active', 'dormant', 'silent', 'unavailable']; + + it('defines a colour for every bucket', () => { + for (const s of states) { + expect(MEMORY_STATE_COLORS[s]).toBeDefined(); + } + }); + + it.each(states)('%s colour is a valid 6-digit hex string', (state) => { + const hex = MEMORY_STATE_COLORS[state]; + expect(hex).toMatch(/^#[0-9a-fA-F]{6}$/); + }); + + it('all four bucket colours are distinct', () => { + const palette = states.map((s) => MEMORY_STATE_COLORS[s].toLowerCase()); + const unique = new Set(palette); + expect(unique.size).toBe(4); + }); + + it('does not reuse any NODE_TYPE_COLORS value (type mode and state mode stay visually separate)', () => { + const typeColours = new Set( + Object.values(NODE_TYPE_COLORS).map((c) => c.toLowerCase()) + ); + for (const s of states) { + expect(typeColours.has(MEMORY_STATE_COLORS[s].toLowerCase())).toBe(false); + } + }); + + it('palette is a frozen record shape — all values are strings', () => { + for (const s of states) { + expect(typeof MEMORY_STATE_COLORS[s]).toBe('string'); + } + }); +}); + +// ---------------------------------------------------------------------------- +// MEMORY_STATE_DESCRIPTIONS — legend text integrity +// ---------------------------------------------------------------------------- + +describe('MEMORY_STATE_DESCRIPTIONS — legend copy', () => { + const states: MemoryState[] = ['active', 'dormant', 'silent', 'unavailable']; + + it('defines a description for every bucket', () => { + for (const s of states) { + expect(MEMORY_STATE_DESCRIPTIONS[s]).toBeDefined(); + expect(MEMORY_STATE_DESCRIPTIONS[s].length).toBeGreaterThan(5); + } + }); + + it.each(states)('%s description contains a threshold parenthetical', (state) => { + expect(MEMORY_STATE_DESCRIPTIONS[state]).toMatch(/\([^)]+\)/); + }); + + it('active description references the ≥ 70% threshold from getMemoryState', () => { + expect(MEMORY_STATE_DESCRIPTIONS.active).toMatch(/70/); + }); + + it('dormant description references the 40–70% band', () => { + expect(MEMORY_STATE_DESCRIPTIONS.dormant).toMatch(/40/); + expect(MEMORY_STATE_DESCRIPTIONS.dormant).toMatch(/70/); + }); + + it('silent description references the 10–40% band', () => { + expect(MEMORY_STATE_DESCRIPTIONS.silent).toMatch(/10/); + expect(MEMORY_STATE_DESCRIPTIONS.silent).toMatch(/40/); + }); + + it('unavailable description references the < 10% threshold', () => { + expect(MEMORY_STATE_DESCRIPTIONS.unavailable).toMatch(/10/); + }); + + it('descriptions are all distinct (no copy-paste bug)', () => { + const lines = states.map((s) => MEMORY_STATE_DESCRIPTIONS[s]); + expect(new Set(lines).size).toBe(4); + }); +}); + +// ---------------------------------------------------------------------------- +// getNodeColor — dispatch correctness across modes +// ---------------------------------------------------------------------------- + +describe('getNodeColor — type mode', () => { + it.each(Object.keys(NODE_TYPE_COLORS))('returns NODE_TYPE_COLORS[%s] in type mode', (t) => { + const node = makeNode({ type: t, retention: 0.5 }); + expect(getNodeColor(node, 'type')).toBe(NODE_TYPE_COLORS[t]); + }); + + it('falls back to steel grey for an unknown type in type mode', () => { + const node = makeNode({ type: 'totally-fake-type' as any, retention: 0.8 }); + expect(getNodeColor(node, 'type')).toBe('#8B95A5'); + }); + + it('type-mode output ignores retention entirely', () => { + const a = makeNode({ type: 'fact', retention: 0.01 }); + const b = makeNode({ type: 'fact', retention: 0.99 }); + expect(getNodeColor(a, 'type')).toBe(getNodeColor(b, 'type')); + }); +}); + +describe('getNodeColor — state mode', () => { + it.each<[number, MemoryState]>([ + [0.9, 'active'], + [0.5, 'dormant'], + [0.2, 'silent'], + [0.0, 'unavailable'], + ])('retention %f yields %s colour', (retention, state) => { + const node = makeNode({ retention }); + expect(getNodeColor(node, 'state')).toBe(MEMORY_STATE_COLORS[state]); + }); + + it('state-mode output ignores node.type entirely', () => { + const a = makeNode({ type: 'fact', retention: 0.8 }); + const b = makeNode({ type: 'decision', retention: 0.8 }); + expect(getNodeColor(a, 'state')).toBe(getNodeColor(b, 'state')); + }); + + it('state-mode tolerates unknown type (does not throw, no fallback branch used)', () => { + const node = makeNode({ type: 'bogus' as any, retention: 0.75 }); + expect(getNodeColor(node, 'state')).toBe(MEMORY_STATE_COLORS.active); + }); +}); + +// ---------------------------------------------------------------------------- +// NodeManager — default state + colorMode field +// ---------------------------------------------------------------------------- + +describe('NodeManager — colorMode field', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('defaults colorMode to "type"', () => { + expect(manager.colorMode).toBe('type'); + }); + + it('colorMode is writable before createNodes (so Graph3D can pre-set)', () => { + manager.colorMode = 'state'; + expect(manager.colorMode).toBe('state'); + }); + + it('setColorMode("state") updates the field', () => { + manager.setColorMode('state'); + expect(manager.colorMode).toBe('state'); + }); + + 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'. + manager.setColorMode('type'); + expect(manager.colorMode).toBe('type'); + }); + + it('setColorMode is idempotent — second call in same mode short-circuits', () => { + const nodes = [makeNode({ id: 'n1', type: 'fact', retention: 0.8 })]; + manager.createNodes(nodes); + + manager.setColorMode('state'); + const meshBefore = manager.meshMap.get('n1')!; + const colorCopy = vi.spyOn(meshBefore.material.color, 'copy'); + + manager.setColorMode('state'); // second call in same mode + expect(colorCopy).not.toHaveBeenCalled(); + }); + + it('does not throw on empty meshMap', () => { + expect(() => manager.setColorMode('state')).not.toThrow(); + expect(() => manager.setColorMode('type')).not.toThrow(); + }); +}); + +// ---------------------------------------------------------------------------- +// NodeManager — setColorMode retints meshes + glows in place +// ---------------------------------------------------------------------------- + +describe('NodeManager.setColorMode — retint semantics', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('calls mesh.material.color.copy for every node', () => { + const nodes = [ + makeNode({ id: 'a', type: 'fact', retention: 0.9 }), + makeNode({ id: 'b', type: 'concept', retention: 0.5 }), + makeNode({ id: 'c', type: 'event', retention: 0.2 }), + ]; + manager.createNodes(nodes); + + const spies = nodes.map((n) => { + const mat = manager.meshMap.get(n.id)!.material as MeshStandardMaterial; + return vi.spyOn(mat.color, 'copy'); + }); + + manager.setColorMode('state'); + + for (const spy of spies) { + expect(spy).toHaveBeenCalledTimes(1); + } + }); + + it('calls mesh.material.emissive.copy for every node (emissive follows colour)', () => { + const nodes = [makeNode({ id: 'a' }), makeNode({ id: 'b' })]; + manager.createNodes(nodes); + + const spies = nodes.map((n) => { + const mat = manager.meshMap.get(n.id)!.material as MeshStandardMaterial; + return vi.spyOn(mat.emissive, 'copy'); + }); + + manager.setColorMode('state'); + + for (const spy of spies) { + expect(spy).toHaveBeenCalledTimes(1); + } + }); + + it('calls glow sprite material.color.copy for every node', () => { + const nodes = [makeNode({ id: 'g1' }), makeNode({ id: 'g2' })]; + manager.createNodes(nodes); + + const spies = nodes.map((n) => { + const mat = manager.glowMap.get(n.id)!.material as SpriteMaterial; + return vi.spyOn(mat.color, 'copy'); + }); + + manager.setColorMode('state'); + + for (const spy of spies) { + expect(spy).toHaveBeenCalledTimes(1); + } + }); + + it('passes matching Color instance to mesh.emissive (same target as mesh.color)', () => { + const nodes = [makeNode({ id: 'a', retention: 0.9 })]; + manager.createNodes(nodes); + + const mat = manager.meshMap.get('a')!.material as MeshStandardMaterial; + const colorSpy = vi.spyOn(mat.color, 'copy'); + const emissiveSpy = vi.spyOn(mat.emissive, 'copy'); + + manager.setColorMode('state'); + + // Both copies should receive Color instances with identical rgb (constructed + // from the same hex). The mock's Color(string) always returns rgb=1,1,1, so + // we assert both receive Color instances of equal rgb. + const colorArg = colorSpy.mock.calls[0][0] as Color; + const emissiveArg = emissiveSpy.mock.calls[0][0] as Color; + expect(emissiveArg.r).toBe(colorArg.r); + expect(emissiveArg.g).toBe(colorArg.g); + expect(emissiveArg.b).toBe(colorArg.b); + }); + + it('preserves mesh reference (does not replace the mesh, only mutates material)', () => { + const nodes = [makeNode({ id: 'a' })]; + manager.createNodes(nodes); + + const meshBefore = manager.meshMap.get('a'); + const materialBefore = meshBefore!.material; + + manager.setColorMode('state'); + manager.setColorMode('type'); + + expect(manager.meshMap.get('a')).toBe(meshBefore); + expect(manager.meshMap.get('a')!.material).toBe(materialBefore); + }); + + it('preserves glow sprite reference (in-place mutation, not replacement)', () => { + const nodes = [makeNode({ id: 'a' })]; + manager.createNodes(nodes); + + const glowBefore = manager.glowMap.get('a'); + const glowMatBefore = glowBefore!.material; + + manager.setColorMode('state'); + + expect(manager.glowMap.get('a')).toBe(glowBefore); + expect(manager.glowMap.get('a')!.material).toBe(glowMatBefore); + }); + + it('preserves userData.retention across mode switches', () => { + const nodes = [makeNode({ id: 'a', retention: 0.42 })]; + manager.createNodes(nodes); + + manager.setColorMode('state'); + expect(manager.meshMap.get('a')!.userData.retention).toBe(0.42); + + manager.setColorMode('type'); + expect(manager.meshMap.get('a')!.userData.retention).toBe(0.42); + }); + + it('preserves userData.type across mode switches', () => { + const nodes = [makeNode({ id: 'a', type: 'decision', retention: 0.8 })]; + manager.createNodes(nodes); + + manager.setColorMode('state'); + expect(manager.meshMap.get('a')!.userData.type).toBe('decision'); + + manager.setColorMode('type'); + expect(manager.meshMap.get('a')!.userData.type).toBe('decision'); + }); + + it('preserves userData.nodeId across mode switches', () => { + const nodes = [makeNode({ id: 'unique-id-123' })]; + manager.createNodes(nodes); + + manager.setColorMode('state'); + expect(manager.meshMap.get('unique-id-123')!.userData.nodeId).toBe('unique-id-123'); + }); +}); + +// ---------------------------------------------------------------------------- +// Round-trip + initial-mode fidelity +// ---------------------------------------------------------------------------- + +describe('NodeManager — mode round-trips', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('createNodes honours a pre-set colorMode = "state" (no flash on mount)', () => { + manager.colorMode = 'state'; + const nodes = [makeNode({ id: 'a', type: 'fact', retention: 0.9 })]; + manager.createNodes(nodes); + + // Because string Colors collapse to rgb=1,1,1 in the mock, we verify the + // mode is preserved and that a subsequent retint to 'state' is a no-op. + expect(manager.colorMode).toBe('state'); + const mat = manager.meshMap.get('a')!.material as MeshStandardMaterial; + const spy = vi.spyOn(mat.color, 'copy'); + manager.setColorMode('state'); + expect(spy).not.toHaveBeenCalled(); // idempotent — already state + }); + + it('type -> state -> type round-trip leaves mode at "type" and preserves mesh identity', () => { + const nodes = [makeNode({ id: 'a', retention: 0.5 })]; + manager.createNodes(nodes); + const meshId = manager.meshMap.get('a'); + + manager.setColorMode('state'); + manager.setColorMode('type'); + + expect(manager.colorMode).toBe('type'); + expect(manager.meshMap.get('a')).toBe(meshId); + }); + + it('rapid mode-toggle (5x type<->state) completes without throwing or losing nodes', () => { + const nodes = [ + makeNode({ id: 'x', type: 'fact', retention: 0.9 }), + makeNode({ id: 'y', type: 'concept', retention: 0.3 }), + makeNode({ id: 'z', type: 'decision', retention: 0.05 }), + ]; + manager.createNodes(nodes); + + for (let i = 0; i < 5; i++) { + manager.setColorMode('state'); + manager.setColorMode('type'); + } + + expect(manager.meshMap.size).toBe(3); + expect(manager.glowMap.size).toBe(3); + expect(manager.labelSprites.size).toBe(3); + }); +}); + +// ---------------------------------------------------------------------------- +// Live-added nodes inherit the active mode +// ---------------------------------------------------------------------------- + +describe('NodeManager — live addNode in state mode', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('addNode uses the current colorMode (state) for the new mesh', () => { + const seed = [makeNode({ id: 'seed', retention: 0.5 })]; + manager.createNodes(seed); + manager.setColorMode('state'); + + const live = makeNode({ id: 'live', type: 'fact', retention: 0.9 }); + manager.addNode(live); + + // The new mesh exists and was created while colorMode is 'state'. A + // same-mode setColorMode('state') must still be a no-op (no re-copy). + expect(manager.meshMap.has('live')).toBe(true); + const mat = manager.meshMap.get('live')!.material as MeshStandardMaterial; + const spy = vi.spyOn(mat.color, 'copy'); + manager.setColorMode('state'); + expect(spy).not.toHaveBeenCalled(); + }); + + it('after setColorMode(state), subsequent addNode then switch to type retints the new node', () => { + manager.setColorMode('state'); + const live = makeNode({ id: 'live', retention: 0.8 }); + manager.addNode(live); + + const mat = manager.meshMap.get('live')!.material as MeshStandardMaterial; + const spy = vi.spyOn(mat.color, 'copy'); + + manager.setColorMode('type'); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); + +// ---------------------------------------------------------------------------- +// Suppressed-node interaction (v2.0.5 active forgetting) +// ---------------------------------------------------------------------------- + +describe('NodeManager — colour mode + suppression compose', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('setColorMode does not touch material.opacity (suppression visual channel untouched)', () => { + const nodes = [makeNode({ id: 'sup', retention: 0.8, suppression_count: 1 } as any)]; + manager.createNodes(nodes); + const mat = manager.meshMap.get('sup')!.material as MeshStandardMaterial; + const opacityBefore = mat.opacity; + + manager.setColorMode('state'); + expect(mat.opacity).toBe(opacityBefore); + + manager.setColorMode('type'); + expect(mat.opacity).toBe(opacityBefore); + }); + + it('setColorMode does not touch emissiveIntensity (suppression visual channel untouched)', () => { + const nodes = [makeNode({ id: 'sup', retention: 0.8, suppression_count: 2 } as any)]; + manager.createNodes(nodes); + const mat = manager.meshMap.get('sup')!.material as MeshStandardMaterial; + const intensityBefore = mat.emissiveIntensity; + + manager.setColorMode('state'); + expect(mat.emissiveIntensity).toBe(intensityBefore); + + manager.setColorMode('type'); + expect(mat.emissiveIntensity).toBe(intensityBefore); + }); + + it('suppressed node still receives the new colour (so the SIF dim + hue both update)', () => { + const nodes = [makeNode({ id: 'sup', retention: 0.8, suppression_count: 1 } as any)]; + manager.createNodes(nodes); + const mat = manager.meshMap.get('sup')!.material as MeshStandardMaterial; + const spy = vi.spyOn(mat.color, 'copy'); + + manager.setColorMode('state'); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); + +// ---------------------------------------------------------------------------- +// Defensive: missing glow (race between createNodes and removeNode) +// ---------------------------------------------------------------------------- + +describe('NodeManager.setColorMode — defensive paths', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('handles a mesh without a corresponding glow (manually deleted) without throwing', () => { + const nodes = [makeNode({ id: 'orphan' })]; + manager.createNodes(nodes); + manager.glowMap.delete('orphan'); + + expect(() => manager.setColorMode('state')).not.toThrow(); + }); + + it('uses retention fallback 0 when userData.retention is missing', () => { + const nodes = [makeNode({ id: 'no-ud' })]; + manager.createNodes(nodes); + const mesh = manager.meshMap.get('no-ud')!; + delete mesh.userData.retention; + + // 0 retention -> unavailable bucket colour. We assert no throw and that + // the retint completes for this mesh. + const mat = mesh.material as MeshStandardMaterial; + const spy = vi.spyOn(mat.color, 'copy'); + manager.setColorMode('state'); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('uses type fallback "fact" when userData.type is missing', () => { + const nodes = [makeNode({ id: 'no-type', retention: 0.5 })]; + manager.createNodes(nodes); // starts in 'type' mode + const mesh = manager.meshMap.get('no-type')!; + delete mesh.userData.type; + + // Switch to state first (not idempotent), then back to type so the + // fallback branch actually executes and we can observe the retint. + manager.setColorMode('state'); + + const mat = mesh.material as MeshStandardMaterial; + const spy = vi.spyOn(mat.color, 'copy'); + manager.setColorMode('type'); + expect(spy).toHaveBeenCalledTimes(1); + }); +}); + +// ---------------------------------------------------------------------------- +// Cross-validation: the colour a mesh SHOULD get matches what the pure +// function produces. We verify this by capturing the hex passed into `new +// Color(...)` via a spy on the Color constructor. +// ---------------------------------------------------------------------------- + +describe('setColorMode — hex values match getNodeColor', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('state-mode retint invokes mesh.color.copy and glow.color.copy per node', () => { + const nodes = [ + makeNode({ id: 'high', retention: 0.9 }), + makeNode({ id: 'mid', retention: 0.5 }), + makeNode({ id: 'low', retention: 0.2 }), + makeNode({ id: 'gone', retention: 0.05 }), + ]; + manager.createNodes(nodes); + + // Instance-level spies on each mesh and glow so prototype state isn't + // polluted across tests. Expected: one copy per mesh + one per glow. + const meshSpies = nodes.map((n) => { + const mat = manager.meshMap.get(n.id)!.material as MeshStandardMaterial; + return vi.spyOn(mat.color, 'copy'); + }); + const glowSpies = nodes.map((n) => { + const mat = manager.glowMap.get(n.id)!.material as SpriteMaterial; + return vi.spyOn(mat.color, 'copy'); + }); + + manager.setColorMode('state'); + + for (const s of meshSpies) expect(s).toHaveBeenCalledTimes(1); + for (const s of glowSpies) expect(s).toHaveBeenCalledTimes(1); + }); + + it('type-mode retint results are deterministic for fixed nodes', () => { + const nodes = [ + makeNode({ id: 'a', type: 'fact', retention: 0.3 }), + makeNode({ id: 'b', type: 'event', retention: 0.8 }), + ]; + manager.createNodes(nodes); + manager.setColorMode('state'); + + const matA = manager.meshMap.get('a')!.material as MeshStandardMaterial; + const matB = manager.meshMap.get('b')!.material as MeshStandardMaterial; + const spyA = vi.spyOn(matA.color, 'copy'); + const spyB = vi.spyOn(matB.color, 'copy'); + + manager.setColorMode('type'); + + // Two distinct types -> two copy() calls, one per mesh. + expect(spyA).toHaveBeenCalledTimes(1); + expect(spyB).toHaveBeenCalledTimes(1); + }); +}); diff --git a/apps/dashboard/src/lib/graph/__tests__/edges.test.ts b/apps/dashboard/src/lib/graph/__tests__/edges.test.ts index eca71ec..8aa9ec4 100644 --- a/apps/dashboard/src/lib/graph/__tests__/edges.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/edges.test.ts @@ -48,12 +48,14 @@ describe('EdgeManager', () => { expect(line.userData.target).toBe('b'); }); - it('caps opacity at 0.6', () => { + it('caps opacity at 0.8 (raised from 0.6 in v2.0.6 issue #31 fix)', () => { const edges = [makeEdge('a', 'b', { weight: 10.0 })]; manager.createEdges(edges, positions); const line = manager.group.children[0] as any; - expect(line.material.opacity).toBeLessThanOrEqual(0.6); + expect(line.material.opacity).toBeLessThanOrEqual(0.8); + // Baseline floor too — with weight 10 we should be at the cap, not below old 0.6 + expect(line.material.opacity).toBeGreaterThanOrEqual(0.6); }); }); @@ -122,7 +124,8 @@ describe('EdgeManager', () => { } const line = manager.group.children[0] as any; - expect(line.material.opacity).toBe(0.5); + // v2.0.6 issue #31 fix raised final edge opacity 0.5 → 0.65 + expect(line.material.opacity).toBe(0.65); }); it('uses easeOutCubic for smooth deceleration', () => { @@ -266,7 +269,8 @@ describe('EdgeManager', () => { // Both should be fully grown expect(manager.group.children.length).toBe(2); manager.group.children.forEach((child) => { - expect((child as any).material.opacity).toBe(0.5); + // v2.0.6 issue #31 fix raised final edge opacity 0.5 → 0.65 + expect((child as any).material.opacity).toBe(0.65); }); }); diff --git a/apps/dashboard/src/lib/graph/__tests__/effects.test.ts b/apps/dashboard/src/lib/graph/__tests__/effects.test.ts index dcadbe7..e495046 100644 --- a/apps/dashboard/src/lib/graph/__tests__/effects.test.ts +++ b/apps/dashboard/src/lib/graph/__tests__/effects.test.ts @@ -245,11 +245,11 @@ describe('EffectManager', () => { expect(n1Pulses.length).toBeLessThanOrEqual(1); }); - it('applies scale bump to contacted nodes', () => { + it('adds pulse to contacted nodes instead of direct scale mutation', () => { const nodePositions = new Map([ ['bump', new Vector3(3, 0, 0)], ]); - const mesh = createMockMesh('bump', new Vector3(3, 0, 0)); + createMockMesh('bump', new Vector3(3, 0, 0)); effects.createRippleWave(new Vector3(0, 0, 0) as any); @@ -258,8 +258,9 @@ describe('EffectManager', () => { effects.update(nodeMeshMap, camera, nodePositions); } - // Scale should have been bumped (1.3x) - expect(mesh.scale.x).toBeGreaterThan(1.0); + // Ripple wave should add a pulse effect (not a direct scale mutation) + const bumpPulses = effects.pulseEffects.filter(p => p.nodeId === 'bump'); + expect(bumpPulses.length).toBeGreaterThan(0); }); it('completes and cleans up after 90 frames', () => { @@ -496,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__/setup.ts b/apps/dashboard/src/lib/graph/__tests__/setup.ts index 64b79b0..ac9febd 100644 --- a/apps/dashboard/src/lib/graph/__tests__/setup.ts +++ b/apps/dashboard/src/lib/graph/__tests__/setup.ts @@ -3,15 +3,40 @@ */ import { vi } from 'vitest'; +// Minimal canvas gradient mock — collects colour stops so tests can inspect +// them if they want to, but is mostly a no-op for runtime. +function createMockGradient() { + return { + colorStops: [] as Array<{ offset: number; color: string }>, + addColorStop(offset: number, color: string) { + this.colorStops.push({ offset, color }); + }, + }; +} + // Minimal canvas 2D context mock const mockContext2D = { clearRect: vi.fn(), + fillRect: vi.fn(), fillText: vi.fn(), + strokeText: vi.fn(), measureText: vi.fn(() => ({ width: 100 })), + createRadialGradient: vi.fn(() => createMockGradient()), + createLinearGradient: vi.fn(() => createMockGradient()), + beginPath: vi.fn(), + closePath: vi.fn(), + moveTo: vi.fn(), + lineTo: vi.fn(), + quadraticCurveTo: vi.fn(), + arc: vi.fn(), + fill: vi.fn(), + stroke: vi.fn(), font: '', textAlign: '', textBaseline: '', - fillStyle: '', + fillStyle: '' as string | object, + strokeStyle: '' as string | object, + lineWidth: 1, shadowColor: '', shadowBlur: 0, shadowOffsetX: 0, diff --git a/apps/dashboard/src/lib/graph/__tests__/three-mock.ts b/apps/dashboard/src/lib/graph/__tests__/three-mock.ts index 3ff349c..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 { @@ -282,10 +342,16 @@ export class MeshBasicMaterial extends BaseMaterial { } export class LineBasicMaterial extends BaseMaterial { + depthWrite = true; constructor(params?: Record) { super(); if (params) { if (typeof params.opacity === 'number') this.opacity = params.opacity; + if (typeof params.transparent === 'boolean') this.transparent = params.transparent; + if (params.color instanceof Color) this.color = params.color; + else if (typeof params.color === 'number') this.color = new Color(params.color); + if (typeof params.blending === 'number') this.blending = params.blending; + if (typeof params.depthWrite === 'boolean') this.depthWrite = params.depthWrite; } } } @@ -303,10 +369,19 @@ export class PointsMaterial extends BaseMaterial { } export class SpriteMaterial extends BaseMaterial { + depthWrite = true; constructor(params?: Record) { super(); if (params) { if (typeof params.opacity === 'number') this.opacity = params.opacity; + if (typeof params.transparent === 'boolean') this.transparent = params.transparent; + if (params.color instanceof Color) this.color = params.color; + else if (typeof params.color === 'number') this.color = new Color(params.color); + if (typeof params.blending === 'number') this.blending = params.blending; + if (typeof params.depthWrite === 'boolean') this.depthWrite = params.depthWrite; + if (params.map && typeof params.map === 'object') { + this.map = params.map as { dispose: () => void }; + } } } } @@ -314,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; @@ -413,6 +490,9 @@ export function installThreeMock() { Vector3, Vector2, Color, + Quaternion, + QuadraticBezierCurve3, + Texture, BufferAttribute, BufferGeometry, SphereGeometry, diff --git a/apps/dashboard/src/lib/graph/__tests__/ui-fixes.test.ts b/apps/dashboard/src/lib/graph/__tests__/ui-fixes.test.ts new file mode 100644 index 0000000..05023d7 --- /dev/null +++ b/apps/dashboard/src/lib/graph/__tests__/ui-fixes.test.ts @@ -0,0 +1,236 @@ +/** + * Regression tests for vestige issue #31 (v2.0.6 Phase 1 dashboard UI fix). + * + * Before v2.0.6 the graph view rendered "glowing cubes" instead of round halos, + * with navy edges swallowed by heavy fog. Root cause: the node glow SpriteMaterial + * had no `map` set, so THREE.Sprite rendered as a solid-coloured plane whose + * square edges were then amplified by UnrealBloomPass into hard bright squares. + * + * These tests lock in every property that was broken so any regression surfaces + * as a red test instead of shipping another ugly screenshot into the issue tracker. + * + * The scene.ts assertions are intentionally source-level (fs regex) because the + * real scene.ts pulls in three/addons (OrbitControls, EffectComposer, UnrealBloomPass, + * WebGLRenderer) which are painful to mock in isolation. Reading the .ts file and + * regex-checking the magic numbers catches accidental revert/tweaks without needing + * a full WebGL harness. + */ +import { describe, it, expect, vi, beforeEach, beforeAll } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +vi.mock('three', async () => { + const mock = await import('./three-mock'); + return { ...mock }; +}); + +import { NodeManager } from '../nodes'; +import { EdgeManager } from '../edges'; +import { Vector3, AdditiveBlending } from './three-mock'; +import { makeNode, makeEdge, resetNodeCounter } from './helpers'; + +// --------------------------------------------------------------------------- +// 1. Node glow sprite — THE fix for the "glowing cubes" artifact +// --------------------------------------------------------------------------- +describe('issue #31 — node glow sprites render as round halos, not squares', () => { + let manager: NodeManager; + + beforeEach(() => { + resetNodeCounter(); + manager = new NodeManager(); + }); + + it('glow SpriteMaterial has a map set (the root cause of the square artifact)', () => { + manager.createNodes([makeNode({ id: 'a', retention: 0.8 })]); + const glow = manager.glowMap.get('a')!; + const mat = glow.material as any; + + // Without a map, THREE.Sprite renders as a solid coloured plane — + // additive blend + bloom then turns it into a glowing square. + // The fix generates a shared radial-gradient CanvasTexture and assigns + // it here, so bloom has a soft circular shape to diffuse. + expect(mat.map).not.toBeNull(); + expect(mat.map).toBeDefined(); + }); + + it('glow sprites on multiple nodes SHARE the same texture instance (singleton cache)', () => { + // The shared texture is a module-level cache — if a future refactor + // accidentally creates one per-node we'll leak memory on large graphs. + manager.createNodes([ + makeNode({ id: 'a' }), + makeNode({ id: 'b' }), + makeNode({ id: 'c' }), + ]); + const a = (manager.glowMap.get('a')!.material as any).map; + const b = (manager.glowMap.get('b')!.material as any).map; + const c = (manager.glowMap.get('c')!.material as any).map; + + expect(a).toBe(b); + expect(b).toBe(c); + }); + + it('glow sprite has depthWrite:false to prevent z-fighting with the sphere behind it', () => { + manager.createNodes([makeNode({ id: 'a' })]); + const mat = manager.glowMap.get('a')!.material as any; + expect(mat.depthWrite).toBe(false); + }); + + it('glow sprite uses additive blending (required for bloom to read as light)', () => { + manager.createNodes([makeNode({ id: 'a' })]); + const mat = manager.glowMap.get('a')!.material as any; + expect(mat.blending).toBe(AdditiveBlending); + }); + + it('glow sprite scale uses the new 6× multiplier (was 4× — gradient needed more footprint)', () => { + // size = 0.5 + retention*2 → 0.5 + 1.0*2 = 2.5 + // glow scale with new formula: 2.5 * 6 * 1.0 = 15 + manager.createNodes([makeNode({ id: 'full', retention: 1.0 })]); + const glow = manager.glowMap.get('full')!; + expect(glow.scale.x).toBeCloseTo(15, 5); + expect(glow.scale.y).toBeCloseTo(15, 5); + }); + + it('glow sprite base opacity is 0.3 + retention*0.35 (was 0.15 + retention*0.2)', () => { + manager.createNodes([makeNode({ id: 'full', retention: 1.0 })]); + const mat = manager.glowMap.get('full')!.material as any; + // 0.3 + 1.0 * 0.35 = 0.65 + expect(mat.opacity).toBeCloseTo(0.65, 5); + }); + + it('suppressed node glow opacity drops to 0.1 (v2.0.5 active forgetting)', () => { + manager.createNodes([makeNode({ id: 's', retention: 0.8, suppression_count: 2 })]); + const mat = manager.glowMap.get('s')!.material as any; + expect(mat.opacity).toBeCloseTo(0.1, 5); + }); +}); + +// --------------------------------------------------------------------------- +// 2. Edge materials — dark navy → brand violet, higher opacity +// --------------------------------------------------------------------------- +describe('issue #31 — edges are brand violet and actually visible', () => { + let manager: EdgeManager; + let positions: Map>; + + beforeEach(() => { + manager = new EdgeManager(); + positions = new Map([ + ['a', new Vector3(0, 0, 0)], + ['b', new Vector3(10, 0, 0)], + ]); + }); + + it('edge color is brand violet 0x8b5cf6, not the old dark navy 0x4a4a7a', () => { + manager.createEdges([makeEdge('a', 'b', { weight: 0.5 })], positions); + const line = manager.group.children[0] as any; + const c = line.material.color; + + // 0x8b5cf6 → r=139/255, g=92/255, b=246/255 + expect(c.r).toBeCloseTo(0x8b / 255, 3); + expect(c.g).toBeCloseTo(0x5c / 255, 3); + expect(c.b).toBeCloseTo(0xf6 / 255, 3); + + // And definitely NOT the old navy 0x4a4a7a (74/255, 74/255, 122/255) + expect(c.r).not.toBeCloseTo(0x4a / 255, 3); + }); + + it('edges have depthWrite:false so they additively blend through fog cleanly', () => { + manager.createEdges([makeEdge('a', 'b')], positions); + const line = manager.group.children[0] as any; + expect(line.material.depthWrite).toBe(false); + }); + + it('edge opacity base is 0.25 + weight*0.5 (was 0.1 + weight*0.5)', () => { + manager.createEdges([makeEdge('a', 'b', { weight: 0.5 })], positions); + const line = manager.group.children[0] as any; + // 0.25 + 0.5 * 0.5 = 0.5 + expect(line.material.opacity).toBeCloseTo(0.5, 5); + }); + + it('edge opacity with low weight still reads (new floor catches regressions)', () => { + manager.createEdges([makeEdge('a', 'b', { weight: 0.0 })], positions); + const line = manager.group.children[0] as any; + // Floor is 0.25 — used to be 0.1 which was invisible through fog + expect(line.material.opacity).toBeGreaterThanOrEqual(0.25); + }); + + it('edge opacity cap is 0.8 (was 0.6)', () => { + manager.createEdges([makeEdge('a', 'b', { weight: 100.0 })], positions); + const line = manager.group.children[0] as any; + expect(line.material.opacity).toBeCloseTo(0.8, 5); + }); +}); + +// --------------------------------------------------------------------------- +// 3. Scene config — source-level regex assertions (scene.ts needs three/addons) +// --------------------------------------------------------------------------- +describe('issue #31 — scene.ts bloom/fog/starfield config is locked in', () => { + const __dirname = dirname(fileURLToPath(import.meta.url)); + let src: string; + + beforeAll(() => { + src = readFileSync(resolve(__dirname, '../scene.ts'), 'utf-8'); + }); + + it('fog density is reduced from 0.008 → 0.0035', () => { + // Positive match: the new density appears inside a FogExp2 call + expect(src).toMatch(/FogExp2\(\s*0x[0-9a-f]+,\s*0\.0035/i); + // Negative match: the old aggressive density is gone + expect(src).not.toMatch(/FogExp2\(\s*0x[0-9a-f]+,\s*0\.008\b/i); + }); + + it('bloom strength is 0.55 (was 0.8 — was blown out)', () => { + // Match on the constructor signature: (size, strength, radius, threshold) + expect(src).toMatch( + /new UnrealBloomPass\([\s\S]*?,\s*0\.55,\s*0\.6,\s*0\.2\s*\)/ + ); + // Old values must be gone + expect(src).not.toMatch(/new UnrealBloomPass\([\s\S]*?,\s*0\.8,\s*0\.4,\s*0\.85\s*\)/); + }); + + it('scene.background is explicitly set (not left as default black void)', () => { + expect(src).toMatch(/scene\.background\s*=/); + }); + + it('a starfield is created and added to the scene', () => { + // createStarfield helper exists and is called at least once + expect(src).toMatch(/function\s+createStarfield\s*\(/); + expect(src).toMatch(/createStarfield\s*\(\s*\)/); + expect(src).toMatch(/scene\.add\(\s*starfield\s*\)/); + }); + + it('starfield is exposed on SceneContext (so dispose/update can touch it later)', () => { + expect(src).toMatch(/starfield:\s*THREE\.Points/); + }); + + it('ACESFilmicToneMapping still active (did not accidentally revert tone map)', () => { + expect(src).toMatch(/ACESFilmicToneMapping/); + }); +}); + +// --------------------------------------------------------------------------- +// 4. Source-level checks on nodes.ts — the shared glow texture helper +// --------------------------------------------------------------------------- +describe('issue #31 — nodes.ts glow texture helper exists and is a singleton', () => { + const __dirname = dirname(fileURLToPath(import.meta.url)); + let src: string; + + beforeAll(() => { + src = readFileSync(resolve(__dirname, '../nodes.ts'), 'utf-8'); + }); + + it('shared glow texture cache exists at module level', () => { + expect(src).toMatch(/let\s+sharedGlowTexture/); + expect(src).toMatch(/function\s+getGlowTexture\s*\(/); + }); + + it('radial gradient has a transparent outer stop (not hard edge)', () => { + // The key insight — colour stops must go to rgba(255,255,255,0) at the edge + expect(src).toMatch(/createRadialGradient/); + expect(src).toMatch(/rgba\(255,\s*255,\s*255,\s*0(?:\.0)?\)/); + }); + + it('SpriteMaterial is constructed with a map parameter', () => { + expect(src).toMatch(/new THREE\.SpriteMaterial\(\{[\s\S]*?map:\s*getGlowTexture\(\)/); + }); +}); diff --git a/apps/dashboard/src/lib/graph/edges.ts b/apps/dashboard/src/lib/graph/edges.ts index 9e8e37a..8022443 100644 --- a/apps/dashboard/src/lib/graph/edges.ts +++ b/apps/dashboard/src/lib/graph/edges.ts @@ -36,11 +36,15 @@ export class EdgeManager { const points = [sourcePos, targetPos]; const geometry = new THREE.BufferGeometry().setFromPoints(points); + // Brand violet (#8b5cf6) instead of the old dark navy 0x4a4a7a + // which was invisible against the fog. Higher opacity base so + // edges actually read as a graph. const material = new THREE.LineBasicMaterial({ - color: 0x4a4a7a, + color: 0x8b5cf6, transparent: true, - opacity: Math.min(0.1 + edge.weight * 0.5, 0.6), + opacity: Math.min(0.25 + edge.weight * 0.5, 0.8), blending: THREE.AdditiveBlending, + depthWrite: false, }); const line = new THREE.Line(geometry, material); @@ -58,10 +62,11 @@ export class EdgeManager { const points = [sourcePos.clone(), sourcePos.clone()]; const geometry = new THREE.BufferGeometry().setFromPoints(points); const material = new THREE.LineBasicMaterial({ - color: 0x4a4a7a, + color: 0x8b5cf6, transparent: true, opacity: 0, blending: THREE.AdditiveBlending, + depthWrite: false, }); const line = new THREE.Line(geometry, material); @@ -111,11 +116,11 @@ export class EdgeManager { attrs.needsUpdate = true; const mat = g.line.material as THREE.LineBasicMaterial; - mat.opacity = progress * 0.5; + mat.opacity = progress * 0.65; if (g.frame >= g.totalFrames) { - // Final opacity from weight - mat.opacity = 0.5; + // Final opacity matches new createEdges baseline + mat.opacity = 0.65; this.growingEdges.splice(i, 1); } } @@ -126,7 +131,7 @@ export class EdgeManager { d.frame++; const progress = d.frame / d.totalFrames; const mat = d.line.material as THREE.LineBasicMaterial; - mat.opacity = Math.max(0, 0.5 * (1 - progress)); + mat.opacity = Math.max(0, 0.65 * (1 - progress)); if (d.frame >= d.totalFrames) { this.group.remove(d.line); diff --git a/apps/dashboard/src/lib/graph/effects.ts b/apps/dashboard/src/lib/graph/effects.ts index 0e37a8e..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, @@ -335,11 +447,8 @@ export class EffectManager { rw.pulsedNodes.add(id); // Mini-pulse on contact this.addPulse(id, 0.8, new THREE.Color(0x00ffd1), 0.03); - // Mini scale bump on the mesh - const mesh = nodeMeshMap.get(id); - if (mesh) { - mesh.scale.multiplyScalar(1.3); - } + // Pulse handles the visual bump — no direct scale mutation + // (multiplyScalar was cumulative and fought with animation system) } }); } @@ -434,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() { @@ -467,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 = []; @@ -474,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 f1d2c54..31fd3cc 100644 --- a/apps/dashboard/src/lib/graph/events.ts +++ b/apps/dashboard/src/lib/graph/events.ts @@ -115,7 +115,7 @@ export function mapEventToEffects( id: data.id, label: (data.content ?? '').slice(0, 60), type: data.node_type ?? 'fact', - retention: data.retention ?? 0.9, + retention: Math.max(0, Math.min(1, data.retention ?? 0.9)), tags: data.tags ?? [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), @@ -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; @@ -327,5 +361,118 @@ export function mapEventToEffects( } break; } + + // v2.0.5 Active Forgetting — Anderson 2025 SIF + Davis Rac1 + // These events fire when the `suppress` MCP tool is called and when + // the background Rac1 cascade worker sweeps recently-suppressed seeds. + // Before these handlers landed, suppression fired silently in the graph. + + case 'MemorySuppressed': { + const data = event.data as { + id?: string; + suppression_count?: number; + }; + if (!data.id) break; + const pos = nodePositions.get(data.id); + if (pos) { + // Violet implosion: top-down inhibitory control collapsing into the node + effects.createImplosion(pos, new THREE.Color(0xa855f7)); + // Plus a slow violet pulse so the suppressed node is visually marked + // even after the implosion frames finish. Strength scales with + // compounding suppression_count (more hits = stronger pulse). + const count = Math.max(1, data.suppression_count ?? 1); + const strength = Math.min(0.4 + count * 0.15, 1.0); + effects.addPulse(data.id, strength, new THREE.Color(0xa855f7), 0.04); + } + break; + } + + case 'MemoryUnsuppressed': { + const data = event.data as { id?: string; remaining_count?: number }; + if (!data.id) break; + const pos = nodePositions.get(data.id); + if (pos && nodeMeshMap.has(data.id)) { + // Reversal within the 24h labile window — bring the memory back. + // Rainbow spawn burst celebrates the reversal, then a green pulse + // to mark the node as "active again" (paralleling MemoryPromoted). + effects.createRainbowBurst(pos, new THREE.Color(0x00ff88)); + effects.addPulse(data.id, 1.0, new THREE.Color(0x00ff88), 0.02); + } + break; + } + + case 'Rac1CascadeSwept': { + // Rac1 cascade runs as a background sweep. The event carries counts, + // not specific node IDs, so we visualize it as a subtle violet wave + // rippling through random sampled neighbors to indicate "decay is + // spreading through co-activated memories." Future v2.1 events may + // carry the actual affected IDs; this handler can be tightened then. + const data = event.data as { + seeds?: number; + neighbors_affected?: number; + }; + const affected = data.neighbors_affected ?? 0; + if (affected === 0) break; + const allIds = Array.from(nodeMeshMap.keys()); + const sampleSize = Math.min(affected, allIds.length, 12); + for (let i = 0; i < sampleSize; i++) { + const idx = Math.floor(Math.random() * allIds.length); + const targetId = allIds[idx]; + effects.addPulse(targetId, 0.5, new THREE.Color(0xa855f7), 0.035); + } + break; + } + + // v2.0.6: wire three previously-silent core events. Before this, the + // live feed showed Connected / ConsolidationStarted / ImportanceScored + // firing but the 3D graph stayed motionless — users perceived the + // dashboard as unresponsive during real cognitive work. + + case 'Connected': { + // WebSocket handshake just completed. A gentle ripple from the + // first node signals "link is live" without dominating the scene. + const firstId = nodeMeshMap.keys().next().value; + if (!firstId) break; + const pos = nodePositions.get(firstId); + if (pos) { + effects.createRippleWave(pos); + } + break; + } + + case 'ConsolidationStarted': { + // FSRS-6 consolidation cycle starting. Amber pulses across a + // random sample signal "retention scores are recomputing across + // the graph." Intentionally subtle — consolidation runs for + // seconds, so the visual shouldn't demand attention the whole + // time. Colour matches the ConsolidationStarted feed entry. + const allIds = Array.from(nodeMeshMap.keys()); + const sampleSize = Math.min(allIds.length, 20); + for (let i = 0; i < sampleSize; i++) { + const idx = Math.floor(Math.random() * allIds.length); + const targetId = allIds[idx]; + effects.addPulse(targetId, 0.45, new THREE.Color(0xffb800), 0.025); + } + break; + } + + case 'ImportanceScored': { + // A memory just had its 4-channel importance score recomputed + // (novelty + arousal + reward + attention). Magenta pulse on the + // scored node with strength proportional to composite score so + // users can visually rank importance across the graph. + const data = event.data as { + id?: string; + composite_score?: number; + }; + if (!data.id) break; + const pos = nodePositions.get(data.id); + if (pos && nodeMeshMap.has(data.id)) { + const score = Math.max(0, Math.min(1, data.composite_score ?? 0.5)); + const strength = 0.3 + score * 0.7; + effects.addPulse(data.id, strength, new THREE.Color(0xff3cac), 0.03); + } + break; + } } } diff --git a/apps/dashboard/src/lib/graph/nodes.ts b/apps/dashboard/src/lib/graph/nodes.ts index e4b879a..b075851 100644 --- a/apps/dashboard/src/lib/graph/nodes.ts +++ b/apps/dashboard/src/lib/graph/nodes.ts @@ -2,6 +2,96 @@ import * as THREE from 'three'; import type { GraphNode } from '$types'; import { NODE_TYPE_COLORS } from '$types'; +// ============================================================================ +// v2.0.8: Memory state coloring (FSRS accessibility bucket) +// ============================================================================ +// +// Every knowledge_node has an FSRS accessibility score computed from +// (retention × 0.5 + retrieval × 0.3 + storage × 0.2). That score gates which +// memories surface in search and drives the Active / Dormant / Silent / +// Unavailable lifecycle documented by Bjork & Bjork 1992 dual-strength model. +// +// The backend computes all three channels, but `GraphNode` only carries +// `retention` — which is already the dominant weight (0.5 of 1.0). Using +// retention alone as a proxy is a known approximation; the buckets line up +// with the same thresholds `execute_system_status` uses server-side, so the +// visual labelling matches what `/api/stats` reports in its +// `stateDistribution` block. + +export type MemoryState = 'active' | 'dormant' | 'silent' | 'unavailable'; + +/// Map an FSRS retention score to its accessibility bucket. +/// +/// Thresholds match `execute_system_status` at the backend so the 3D graph's +/// colours line up with the numbers reported by `/api/stats`. +export function getMemoryState(retention: number): MemoryState { + if (retention >= 0.7) return 'active'; + if (retention >= 0.4) return 'dormant'; + if (retention >= 0.1) return 'silent'; + return 'unavailable'; +} + +/// FSRS state palette. Distinct from NODE_TYPE_COLORS so the two modes can +/// coexist in the UI without overloading a single colour channel. +export const MEMORY_STATE_COLORS: Record = { + active: '#10b981', // emerald — easily retrievable + dormant: '#f59e0b', // amber — retrievable with effort + silent: '#8b5cf6', // violet — difficult, needs cues + unavailable: '#6b7280', // slate — needs reinforcement +}; + +export const MEMORY_STATE_DESCRIPTIONS: Record = { + active: 'Easily retrievable (retention ≥ 70%)', + dormant: 'Retrievable with effort (40–70%)', + silent: 'Difficult, needs cues (10–40%)', + unavailable: 'Needs reinforcement (< 10%)', +}; + +/// Color mode controls whether node spheres are tinted by node type +/// (fact / concept / event / …) or by FSRS memory state. +/// Type mode is the long-standing default; state mode is the v2.0.8 addition. +export type ColorMode = 'type' | 'state'; + +/// 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. +export function getNodeColor(node: GraphNode, mode: ColorMode): string { + if (mode === 'state') { + return MEMORY_STATE_COLORS[getMemoryState(node.retention)]; + } + return NODE_TYPE_COLORS[node.type] || '#8B95A5'; +} + +// 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; +export function getGlowTexture(): THREE.Texture { + if (sharedGlowTexture) return sharedGlowTexture; + const size = 128; + const canvas = document.createElement('canvas'); + canvas.width = size; + canvas.height = size; + const ctx = canvas.getContext('2d'); + if (!ctx) { + // Fallback: empty 1x1 texture; halos will be invisible but nothing crashes. + sharedGlowTexture = new THREE.Texture(); + return sharedGlowTexture; + } + const gradient = ctx.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2); + gradient.addColorStop(0.0, 'rgba(255, 255, 255, 1.0)'); + gradient.addColorStop(0.25, 'rgba(255, 255, 255, 0.7)'); + gradient.addColorStop(0.55, 'rgba(255, 255, 255, 0.2)'); + gradient.addColorStop(1.0, 'rgba(255, 255, 255, 0.0)'); + ctx.fillStyle = gradient; + ctx.fillRect(0, 0, size, size); + const tex = new THREE.CanvasTexture(canvas); + tex.needsUpdate = true; + sharedGlowTexture = tex; + return tex; +} + function easeOutElastic(t: number): number { if (t === 0 || t === 1) return t; const p = 0.3; @@ -49,6 +139,9 @@ 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`. + colorMode: ColorMode = 'type'; private materializingNodes: MaterializingNode[] = []; private dissolvingNodes: DissolvingNode[] = []; @@ -58,6 +151,38 @@ export class NodeManager { this.group = new THREE.Group(); } + /// Switch the active colour mode and re-tint every live node in place. + /// Safe to call mid-animation — the mesh + glow materials are mutable. + /// Suppressed nodes keep their 20% opacity / zero-emissive treatment + /// since that is a separate visual channel (v2.0.5 SIF). + setColorMode(mode: ColorMode) { + if (this.colorMode === mode) return; + this.colorMode = mode; + 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 stubNode = { + id, + label: '', + type, + retention, + tags: [], + createdAt: '', + updatedAt: '', + isCenter: false, + } as GraphNode; + const hex = getNodeColor(stubNode, mode); + const newColor = new THREE.Color(hex); + const mat = mesh.material as THREE.MeshStandardMaterial; + mat.color.copy(newColor); + mat.emissive.copy(newColor); + const glow = this.glowMap.get(id); + if (glow) { + (glow.material as THREE.SpriteMaterial).color.copy(newColor); + } + } + } + createNodes(nodes: GraphNode[]): Map { const phi = (1 + Math.sqrt(5)) / 2; const count = nodes.length; @@ -88,18 +213,24 @@ export class NodeManager { private createNodeMeshes(node: GraphNode, pos: THREE.Vector3, initialScale: number) { const size = 0.5 + node.retention * 2; - const color = NODE_TYPE_COLORS[node.type] || '#8B95A5'; + // v2.0.8: respect the active colour mode. Newly-added nodes during the + // same session follow the mode toggled at the UI layer. + const color = getNodeColor(node, this.colorMode); + + // v2.0.5 Active Forgetting: suppressed memories dim to 20% opacity + // and lose their emissive glow, mimicking inhibitory-control silencing. + const isSuppressed = (node.suppression_count ?? 0) > 0; // Node mesh const geometry = new THREE.SphereGeometry(size, 16, 16); const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(color), emissive: new THREE.Color(color), - emissiveIntensity: 0.3 + node.retention * 0.5, + emissiveIntensity: isSuppressed ? 0.0 : 0.3 + node.retention * 0.5, roughness: 0.3, metalness: 0.1, transparent: true, - opacity: 0.3 + node.retention * 0.7, + opacity: isSuppressed ? 0.2 : 0.3 + node.retention * 0.7, }); const mesh = new THREE.Mesh(geometry, material); @@ -109,15 +240,20 @@ export class NodeManager { this.meshMap.set(node.id, mesh); this.group.add(mesh); - // Glow sprite + // Glow sprite — radial-gradient texture kills the square-halo artefact + // from issue #31. depthWrite:false prevents z-fighting with the sphere. const spriteMat = new THREE.SpriteMaterial({ + map: getGlowTexture(), color: new THREE.Color(color), transparent: true, - opacity: initialScale > 0 ? 0.15 + node.retention * 0.2 : 0, + opacity: initialScale > 0 ? (isSuppressed ? 0.1 : 0.3 + node.retention * 0.35) : 0, blending: THREE.AdditiveBlending, + depthWrite: false, }); const sprite = new THREE.Sprite(spriteMat); - sprite.scale.set(size * 4 * initialScale, size * 4 * initialScale, 1); + // Slightly larger halo — the gradient falls off quickly so we need + // more screen real estate for a visible soft bloom footprint. + sprite.scale.set(size * 6 * initialScale, size * 6 * initialScale, 1); sprite.position.copy(pos); sprite.userData = { isGlow: true, nodeId: node.id }; this.glowMap.set(node.id, sprite); @@ -125,7 +261,7 @@ export class NodeManager { // Text label sprite const labelText = node.label || node.type; - const labelSprite = this.createTextSprite(labelText, '#e2e8f0'); + const labelSprite = this.createTextSprite(labelText, '#94a3b8'); labelSprite.position.copy(pos); labelSprite.position.y += size * 2 + 1.5; labelSprite.userData = { isLabel: true, nodeId: node.id, offset: size * 2 + 1.5 }; @@ -135,7 +271,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( @@ -153,17 +293,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) { @@ -203,24 +388,78 @@ export class NodeManager { }); } + /// Render a label as a dark rounded "pill" with dim slate text. + /// + /// 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: + /// + /// 1. A ~85%-opaque dark pill under the text so the background is + /// well below the bloom threshold, stopping the halo before it + /// spreads past the label bounds. + /// 2. Mid-luminance slate text (#94a3b8 by default) — still legible + /// but dim enough that bloom only adds a soft glow, not a blast. + /// 3. Smaller font (22px) and tighter sprite scale (9×1.2) so labels + /// don't visually compete with the node spheres they annotate. private createTextSprite(text: string, color: string): THREE.Sprite { const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d')!; + const ctx = canvas.getContext('2d'); + if (!ctx) { + const tex = new THREE.Texture(); + return new THREE.Sprite(new THREE.SpriteMaterial({ map: tex, transparent: true, opacity: 0 })); + } canvas.width = 512; canvas.height = 64; const label = text.length > 40 ? text.slice(0, 37) + '...' : text; ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, sans-serif'; + + // Measure the label so the backing pill hugs the text instead of + // spanning the full canvas width (which would leave a giant empty + // dark bar on short labels like "fact" or "note"). + ctx.font = '600 22px -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif'; + const metrics = ctx.measureText(label); + const textWidth = metrics.width; + const padX = 14; + const padY = 9; + const pillW = Math.min(textWidth + padX * 2, canvas.width - 4); + const pillH = 40; + const pillX = (canvas.width - pillW) / 2; + const pillY = (canvas.height - pillH) / 2; + const radius = pillH / 2; + + // Dark glass pill — low enough luminance that UnrealBloomPass at + // threshold 0.2 does not amplify its pixels. + ctx.fillStyle = 'rgba(10, 16, 28, 0.82)'; + ctx.beginPath(); + ctx.moveTo(pillX + radius, pillY); + ctx.lineTo(pillX + pillW - radius, pillY); + ctx.quadraticCurveTo(pillX + pillW, pillY, pillX + pillW, pillY + radius); + ctx.lineTo(pillX + pillW, pillY + pillH - radius); + ctx.quadraticCurveTo( + pillX + pillW, + pillY + pillH, + pillX + pillW - radius, + pillY + pillH + ); + ctx.lineTo(pillX + radius, pillY + pillH); + ctx.quadraticCurveTo(pillX, pillY + pillH, pillX, pillY + pillH - radius); + ctx.lineTo(pillX, pillY + radius); + ctx.quadraticCurveTo(pillX, pillY, pillX + radius, pillY); + ctx.closePath(); + ctx.fill(); + + // Hairline stroke for definition at small camera distances. + ctx.strokeStyle = 'rgba(148, 163, 184, 0.18)'; + ctx.lineWidth = 1; + ctx.stroke(); + ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.shadowColor = 'rgba(0, 0, 0, 0.8)'; - ctx.shadowBlur = 6; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 2; ctx.fillStyle = color; - ctx.fillText(label, canvas.width / 2, canvas.height / 2); + ctx.fillText(label, canvas.width / 2, canvas.height / 2 + 1); const texture = new THREE.CanvasTexture(canvas); texture.needsUpdate = true; @@ -234,7 +473,7 @@ export class NodeManager { }); const sprite = new THREE.Sprite(mat); - sprite.scale.set(12, 1.5, 1); + sprite.scale.set(9, 1.2, 1); return sprite; } @@ -256,7 +495,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]; @@ -271,8 +515,8 @@ export class NodeManager { if (mn.frame >= 5) { const glowT = Math.min((mn.frame - 5) / 5, 1); const glowMat = mn.glow.material as THREE.SpriteMaterial; - glowMat.opacity = glowT * 0.25; - const glowSize = mn.targetScale * 4 * scale; + glowMat.opacity = glowT * 0.4; + const glowSize = mn.targetScale * 6 * scale; mn.glow.scale.set(glowSize, glowSize, 1); } @@ -296,7 +540,7 @@ export class NodeManager { const scale = Math.max(0.001, dn.originalScale * shrink); dn.mesh.scale.setScalar(scale); - const glowScale = scale * 4; + const glowScale = scale * 6; dn.glow.scale.set(glowScale, glowScale, 1); // Fade opacity @@ -338,7 +582,7 @@ export class NodeManager { const glow = this.glowMap.get(gn.id); if (glow) { - const glowSize = scale * 4; + const glowSize = scale * 6; glow.scale.set(glowSize, glowSize, 1); } @@ -362,16 +606,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 bbd39f0..00713d7 100644 --- a/apps/dashboard/src/lib/graph/scene.ts +++ b/apps/dashboard/src/lib/graph/scene.ts @@ -18,11 +18,53 @@ export interface SceneContext { point1: THREE.PointLight; point2: THREE.PointLight; }; + starfield: THREE.Points; +} + +function createStarfield(): THREE.Points { + // 2000 dim points distributed on a spherical shell at radius 600-1000. + // Purely decorative depth cue — never intersects the graph area and + // sits below the bloom threshold so it doesn't bloom. + const starCount = 2000; + const positions = new Float32Array(starCount * 3); + const colors = new Float32Array(starCount * 3); + for (let i = 0; i < starCount; i++) { + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + const r = 600 + Math.random() * 400; + positions[i * 3] = r * Math.sin(phi) * Math.cos(theta); + positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta); + positions[i * 3 + 2] = r * Math.cos(phi); + // Subtle colour variation — mostly cool white, some violet tint. + const tint = Math.random(); + colors[i * 3] = 0.55 + tint * 0.25; + colors[i * 3 + 1] = 0.55 + tint * 0.15; + colors[i * 3 + 2] = 0.75 + tint * 0.25; + } + const geo = new THREE.BufferGeometry(); + geo.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geo.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + const mat = new THREE.PointsMaterial({ + size: 1.6, + sizeAttenuation: true, + vertexColors: true, + transparent: true, + opacity: 0.6, + depthWrite: false, + blending: THREE.AdditiveBlending, + }); + return new THREE.Points(geo, mat); } export function createScene(container: HTMLDivElement): SceneContext { const scene = new THREE.Scene(); - scene.fog = new THREE.FogExp2(0x050510, 0.008); + // Darker-than-black background with a subtle colour cast. Combined with + // the starfield and reduced fog, the void has depth instead of reading + // as a broken shader canvas. + scene.background = new THREE.Color(0x05050f); + // Fog density reduced 0.008 → 0.0035 — the old value was killing every + // edge and node past 50 units. Lighter colour blends into the background. + scene.fog = new THREE.FogExp2(0x0a0a1a, 0.0035); const camera = new THREE.PerspectiveCamera( 60, @@ -40,7 +82,7 @@ export function createScene(container: HTMLDivElement): SceneContext { renderer.setSize(container.clientWidth, container.clientHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); renderer.toneMapping = THREE.ACESFilmicToneMapping; - renderer.toneMappingExposure = 1.2; + renderer.toneMappingExposure = 1.25; container.appendChild(renderer.domElement); const controls = new OrbitControls(camera, renderer.domElement); @@ -48,32 +90,46 @@ 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; const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera)); + // Bloom retuned for radial-gradient glow sprites (issue #31 fix): + // strength 0.8 → 0.55 — gentler, avoids the old blown-out look + // radius 0.4 → 0.6 — softer falloff, diffuses cleanly through glow + // threshold 0.85 → 0.2 — let mid-tones bloom instead of highlights only const bloomPass = new UnrealBloomPass( new THREE.Vector2(container.clientWidth, container.clientHeight), - 0.8, - 0.4, - 0.85 + 0.55, + 0.6, + 0.2 ); composer.addPass(bloomPass); - const ambient = new THREE.AmbientLight(0x1a1a3a, 0.5); + const ambient = new THREE.AmbientLight(0x2a2a5a, 0.7); scene.add(ambient); - const point1 = new THREE.PointLight(0x6366f1, 1.5, 200); + const point1 = new THREE.PointLight(0x6366f1, 1.8, 240); point1.position.set(50, 50, 50); scene.add(point1); - const point2 = new THREE.PointLight(0xa855f7, 1, 200); + const point2 = new THREE.PointLight(0xa855f7, 1.2, 240); point2.position.set(-50, -30, -50); scene.add(point2); + const starfield = createStarfield(); + scene.add(starfield); + const raycaster = new THREE.Raycaster(); raycaster.params.Points = { threshold: 2 }; const mouse = new THREE.Vector2(); @@ -88,6 +144,7 @@ export function createScene(container: HTMLDivElement): SceneContext { raycaster, mouse, lights: { ambient, point1, point2 }, + starfield, }; } 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..f5e527b --- /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 c8da20b..a831cd3 100644 --- a/apps/dashboard/src/routes/(app)/graph/+page.svelte +++ b/apps/dashboard/src/routes/(app)/graph/+page.svelte @@ -1,12 +1,16 @@ + +
        +
        +
        +

        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 + {#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)/intentions/+page.svelte b/apps/dashboard/src/routes/(app)/intentions/+page.svelte index 16236ac..ca078fb 100644 --- a/apps/dashboard/src/routes/(app)/intentions/+page.svelte +++ b/apps/dashboard/src/routes/(app)/intentions/+page.svelte @@ -15,19 +15,59 @@ snoozed: 'text-dream-glow bg-dream/10 border-dream/30', }; - const PRIORITY_COLORS: Record = { - critical: 'text-decay', - high: 'text-amber-400', - normal: 'text-dim', - low: 'text-muted', + const PRIORITY_LABELS: Record = { + 4: 'critical', + 3: 'high', + 2: 'normal', + 1: 'low', + }; + + const PRIORITY_COLORS: Record = { + 4: 'text-decay', + 3: 'text-amber-400', + 2: 'text-dim', + 1: 'text-muted', }; const TRIGGER_ICONS: Record = { time: '⏰', context: '◎', event: '⚡', + manual: '◇', }; + function summarizeTrigger(intention: IntentionItem): string { + // The API returns trigger_data as a JSON-encoded string. Parse it, pick the + // most human-readable field, then truncate for display. + let result: string; + try { + const data = JSON.parse(intention.trigger_data || '{}') as Record; + if (typeof data.condition === 'string' && data.condition) { + result = data.condition; + } else if (typeof data.topic === 'string' && data.topic) { + result = data.topic; + } else if (typeof data.at === 'string' && data.at) { + try { + result = new Date(data.at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + } catch { + result = data.at; + } + } else if (typeof data.in_minutes === 'number') { + result = `in ${data.in_minutes} min`; + } else if (typeof data.inMinutes === 'number') { + result = `in ${data.inMinutes} min`; + } else if (typeof data.codebase === 'string' && data.codebase) { + const fp = typeof data.filePattern === 'string' && data.filePattern ? `/${data.filePattern}` : ''; + result = `${data.codebase}${fp}`; + } else { + result = intention.trigger_type; + } + } catch { + result = intention.trigger_type; + } + return result.length > 40 ? result.slice(0, 37) + '...' : result; + } + onMount(async () => { await loadData(); }); @@ -116,13 +156,11 @@ - {intention.priority} priority + {PRIORITY_LABELS[intention.priority] || 'normal'} priority - {intention.trigger_type}: {intention.trigger_value.length > 40 - ? intention.trigger_value.slice(0, 37) + '...' - : intention.trigger_value} + {intention.trigger_type}: {summarizeTrigger(intention)} {#if intention.deadline} diff --git a/apps/dashboard/src/routes/(app)/memories/+page.svelte b/apps/dashboard/src/routes/(app)/memories/+page.svelte index 66ba391..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); } }} @@ -130,6 +166,22 @@ { e.stopPropagation(); api.memories.demote(memory.id); }} onkeydown={(e) => { if (e.key === 'Enter') { e.stopPropagation(); api.memories.demote(memory.id); } }} class="px-3 py-1.5 bg-decay/20 text-decay text-xs rounded-lg hover:bg-decay/30 cursor-pointer select-none">Demote + + { + e.stopPropagation(); + await api.memories.suppress(memory.id, 'dashboard trigger'); + }} + onkeydown={async (e) => { + if (e.key === 'Enter') { + e.stopPropagation(); + await api.memories.suppress(memory.id, 'dashboard trigger'); + } + }} + title="Top-down inhibition (Anderson 2025). Compounds. Reversible for 24h." + class="px-3 py-1.5 bg-purple-500/20 text-purple-400 text-xs rounded-lg hover:bg-purple-500/30 cursor-pointer select-none">Suppress { e.stopPropagation(); await api.memories.delete(memory.id); loadMemories(); }} onkeydown={async (e) => { if (e.key === 'Enter') { e.stopPropagation(); await api.memories.delete(memory.id); loadMemories(); } }} class="px-3 py-1.5 bg-decay/10 text-decay/60 text-xs rounded-lg hover:bg-decay/20 ml-auto cursor-pointer select-none">Delete 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..05fcbb0 --- /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..64f5ccf --- /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 @@ @@ -98,7 +124,7 @@
        +
        {@render children()}
        @@ -160,7 +200,7 @@ {#each mobileNav as item} {@const active = isActive(item.href, $page.url.pathname)}
        @@ -180,6 +220,9 @@
        + + + {#if showCommandPalette} diff --git a/crates/vestige-core/Cargo.toml b/crates/vestige-core/Cargo.toml index ef29cd2..6e213a3 100644 --- a/crates/vestige-core/Cargo.toml +++ b/crates/vestige-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vestige-core" -version = "2.0.3" +version = "2.0.9" edition = "2024" rust-version = "1.91" authors = ["Vestige Team"] @@ -11,22 +11,42 @@ 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) -embeddings = ["dep:fastembed"] +# 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"] +# 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"] @@ -77,12 +97,16 @@ notify = "8" # ============================================================================ # 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 = { version = "5.11", optional = true } +fastembed = { version = "5.11", default-features = false, features = ["hf-hub-native-tls", "image-models"], optional = true } # ============================================================================ # OPTIONAL: Vector Search (USearch - HNSW, 20x faster than FAISS) # ============================================================================ -usearch = { version = "2", optional = true } +# Pinned to 2.23.0 — 2.24.0 introduced a Windows MSVC compile break because +# its memory_mapping_allocator_gt template references the POSIX MAP_FAILED +# macro from , which doesn't exist on MSVC. Tracked upstream in +# unum-cloud/usearch#746. Unpin when the upstream fix lands. +usearch = { version = "=2.23.0", optional = true } # LRU cache for query embeddings lru = "0.16" diff --git a/crates/vestige-core/benches/search_bench.rs b/crates/vestige-core/benches/search_bench.rs index b27f286..c10ef9d 100644 --- a/crates/vestige-core/benches/search_bench.rs +++ b/crates/vestige-core/benches/search_bench.rs @@ -3,10 +3,10 @@ //! Benchmarks for core search operations using Criterion. //! Run with: cargo bench -p vestige-core -use criterion::{criterion_group, criterion_main, Criterion, black_box}; -use vestige_core::search::hyde::{classify_intent, expand_query, centroid_embedding}; -use vestige_core::search::{reciprocal_rank_fusion, linear_combination, sanitize_fts5_query}; +use criterion::{Criterion, black_box, criterion_group, criterion_main}; use vestige_core::embeddings::cosine_similarity; +use vestige_core::search::hyde::{centroid_embedding, classify_intent, expand_query}; +use vestige_core::search::{linear_combination, reciprocal_rank_fusion, sanitize_fts5_query}; fn bench_classify_intent(c: &mut Criterion) { let queries = [ @@ -29,7 +29,9 @@ fn bench_classify_intent(c: &mut Criterion) { fn bench_expand_query(c: &mut Criterion) { c.bench_function("expand_query", |b| { b.iter(|| { - black_box(expand_query("What is spaced repetition and how does FSRS work?")); + black_box(expand_query( + "What is spaced repetition and how does FSRS work?", + )); }) }); } @@ -37,11 +39,7 @@ fn bench_expand_query(c: &mut Criterion) { fn bench_centroid_embedding(c: &mut Criterion) { // Simulate 4 embeddings of 256 dimensions let embeddings: Vec> = (0..4) - .map(|i| { - (0..256) - .map(|j| ((i * 256 + j) as f32).sin()) - .collect() - }) + .map(|i| (0..256).map(|j| ((i * 256 + j) as f32).sin()).collect()) .collect(); c.bench_function("centroid_256d_4vecs", |b| { @@ -61,7 +59,11 @@ fn bench_rrf_fusion(c: &mut Criterion) { c.bench_function("rrf_50x50", |b| { b.iter(|| { - black_box(reciprocal_rank_fusion(&keyword_results, &semantic_results, 60.0)); + black_box(reciprocal_rank_fusion( + &keyword_results, + &semantic_results, + 60.0, + )); }) }); } @@ -76,7 +78,12 @@ fn bench_linear_combination(c: &mut Criterion) { c.bench_function("linear_combo_50x50", |b| { b.iter(|| { - black_box(linear_combination(&keyword_results, &semantic_results, 0.3, 0.7)); + black_box(linear_combination( + &keyword_results, + &semantic_results, + 0.3, + 0.7, + )); }) }); } @@ -84,7 +91,9 @@ fn bench_linear_combination(c: &mut Criterion) { fn bench_sanitize_fts5(c: &mut Criterion) { c.bench_function("sanitize_fts5_query", |b| { b.iter(|| { - black_box(sanitize_fts5_query("hello world \"exact phrase\" OR special-chars!@#")); + black_box(sanitize_fts5_query( + "hello world \"exact phrase\" OR special-chars!@#", + )); }) }); } diff --git a/crates/vestige-core/src/advanced/adaptive_embedding.rs b/crates/vestige-core/src/advanced/adaptive_embedding.rs index d4b39d3..001c191 100644 --- a/crates/vestige-core/src/advanced/adaptive_embedding.rs +++ b/crates/vestige-core/src/advanced/adaptive_embedding.rs @@ -766,6 +766,6 @@ fn main() -> Result<(), std::io::Error> { embedder.embed_auto("Another text sample."); let stats = embedder.stats(); - assert!(stats.len() > 0); + assert!(!stats.is_empty()); } } diff --git a/crates/vestige-core/src/advanced/chains.rs b/crates/vestige-core/src/advanced/chains.rs index 9d6e59a..3b92bd3 100644 --- a/crates/vestige-core/src/advanced/chains.rs +++ b/crates/vestige-core/src/advanced/chains.rs @@ -300,7 +300,11 @@ impl MemoryChainBuilder { } // Sort by score (descending) - all_paths.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); + all_paths.sort_by(|a, b| { + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + }); // Return top paths all_paths.into_iter().take(10).collect() @@ -329,7 +333,7 @@ impl MemoryChainBuilder { // Sort by frequency let mut bridge_list: Vec<_> = bridges.into_iter().collect(); - bridge_list.sort_by(|a, b| b.1.cmp(&a.1)); + bridge_list.sort_by_key(|b| std::cmp::Reverse(b.1)); bridge_list.into_iter().map(|(id, _)| id).collect() } diff --git a/crates/vestige-core/src/advanced/compression.rs b/crates/vestige-core/src/advanced/compression.rs index 0a53664..2ea2309 100644 --- a/crates/vestige-core/src/advanced/compression.rs +++ b/crates/vestige-core/src/advanced/compression.rs @@ -298,7 +298,7 @@ impl MemoryCompressor { // Sort by age (oldest first) let mut sorted: Vec<_> = memories.iter().collect(); - sorted.sort_by(|a, b| a.created_at.cmp(&b.created_at)); + sorted.sort_by_key(|a| a.created_at); for memory in sorted { if assigned.contains(&memory.id) { @@ -445,7 +445,11 @@ impl MemoryCompressor { } // Sort by importance and deduplicate - facts.sort_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal)); + facts.sort_by(|a, b| { + b.importance + .partial_cmp(&a.importance) + .unwrap_or(std::cmp::Ordering::Equal) + }); self.deduplicate_facts(facts) } diff --git a/crates/vestige-core/src/advanced/cross_project.rs b/crates/vestige-core/src/advanced/cross_project.rs index 126463c..ffda53a 100644 --- a/crates/vestige-core/src/advanced/cross_project.rs +++ b/crates/vestige-core/src/advanced/cross_project.rs @@ -417,7 +417,7 @@ impl CrossProjectLearner { } } - suggestions.sort_by(|a, b| b.priority.cmp(&a.priority)); + suggestions.sort_by_key(|b| std::cmp::Reverse(b.priority)); suggestions } @@ -432,10 +432,11 @@ impl CrossProjectLearner { // Check each trigger for trigger in &pattern.pattern.triggers { if let Some((matches, reason)) = self.check_trigger(trigger, context) - && matches { - match_scores.push(trigger.confidence); - match_reasons.push(reason); - } + && matches + { + match_scores.push(trigger.confidence); + match_reasons.push(reason); + } } if match_scores.is_empty() { @@ -547,10 +548,11 @@ impl CrossProjectLearner { let success_rate = success_count as f64 / total_count as f64; if let Ok(mut patterns) = self.patterns.write() - && let Some(pattern) = patterns.get_mut(pattern_id) { - pattern.success_rate = success_rate; - pattern.application_count = total_count as u32; - } + && let Some(pattern) = patterns.get_mut(pattern_id) + { + pattern.success_rate = success_rate; + pattern.application_count = total_count as u32; + } } fn extract_patterns_from_category( @@ -595,38 +597,39 @@ impl CrossProjectLearner { let pattern_id = format!("auto-{}-{}", category_to_string(&category), keyword); if let Ok(mut patterns) = self.patterns.write() - && !patterns.contains_key(&pattern_id) { - patterns.insert( - pattern_id.clone(), - UniversalPattern { - id: pattern_id, - pattern: CodePattern { - name: format!("{} pattern", keyword), - category: category.clone(), - description: format!( - "Pattern involving '{}' observed in {} projects", - keyword, - projects.len() - ), - example: None, - triggers: vec![PatternTrigger { - trigger_type: TriggerType::Topic, - value: keyword.clone(), - confidence: 0.5, - }], - benefits: vec![], - considerations: vec![], - }, - projects_seen_in: projects.iter().map(|s| s.to_string()).collect(), - success_rate: 0.5, // Default until validated - applicability: format!("When working with {}", keyword), - confidence: 0.5, - first_seen: Utc::now(), - last_seen: Utc::now(), - application_count: 0, + && !patterns.contains_key(&pattern_id) + { + patterns.insert( + pattern_id.clone(), + UniversalPattern { + id: pattern_id, + pattern: CodePattern { + name: format!("{} pattern", keyword), + category: category.clone(), + description: format!( + "Pattern involving '{}' observed in {} projects", + keyword, + projects.len() + ), + example: None, + triggers: vec![PatternTrigger { + trigger_type: TriggerType::Topic, + value: keyword.clone(), + confidence: 0.5, + }], + benefits: vec![], + considerations: vec![], }, - ); - } + projects_seen_in: projects.iter().map(|s| s.to_string()).collect(), + success_rate: 0.5, // Default until validated + applicability: format!("When working with {}", keyword), + confidence: 0.5, + first_seen: Utc::now(), + last_seen: Utc::now(), + application_count: 0, + }, + ); + } } } } diff --git a/crates/vestige-core/src/advanced/dreams.rs b/crates/vestige-core/src/advanced/dreams.rs index 88a86ce..d01d8e2 100644 --- a/crates/vestige-core/src/advanced/dreams.rs +++ b/crates/vestige-core/src/advanced/dreams.rs @@ -455,9 +455,10 @@ impl ConsolidationScheduler { // Strengthen connections between sequentially replayed memories for window in replay.sequence.windows(2) { if let [id_a, id_b] = window - && graph.strengthen_connection(id_a, id_b, 0.1) { - strengthened += 1; - } + && graph.strengthen_connection(id_a, id_b, 0.1) + { + strengthened += 1; + } } // Also strengthen based on discovered patterns @@ -704,11 +705,12 @@ impl ConnectionGraph { for (a, b) in [(from_id, to_id), (to_id, from_id)] { if let Some(connections) = self.connections.get_mut(a) - && let Some(conn) = connections.iter_mut().find(|c| c.target_id == b) { - conn.strength = (conn.strength + boost).min(2.0); - conn.last_strengthened = now; - strengthened = true; - } + && let Some(conn) = connections.iter_mut().find(|c| c.target_id == b) + { + conn.strength = (conn.strength + boost).min(2.0); + conn.last_strengthened = now; + strengthened = true; + } } strengthened @@ -1076,6 +1078,9 @@ pub struct DiscoveredConnection { pub connection_type: DiscoveredConnectionType, /// Reasoning for this connection pub reasoning: String, + /// When this connection was discovered (used for recency scoring during eviction) + #[serde(default = "Utc::now")] + pub discovered_at: DateTime, } /// Types of connections discovered during dreaming @@ -1277,6 +1282,7 @@ impl MemoryDreamer { similarity, connection_type, reasoning, + discovered_at: Utc::now(), }); } } @@ -1477,9 +1483,10 @@ 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 { - insights.push(insight); - } + && insight.novelty_score >= self.config.min_novelty + { + insights.push(insight); + } if insights.len() >= self.config.max_insights { break; @@ -1699,10 +1706,38 @@ impl MemoryDreamer { fn store_connections(&self, connections: &[DiscoveredConnection]) { if let Ok(mut stored) = self.connections.write() { stored.extend(connections.iter().cloned()); - // Keep last 1000 connections + // Keep the 1000 highest-scoring connections using a composite score + // that balances quality (similarity) and recency (age-based decay). + // + // score = similarity * 0.6 + recency * 0.4 + // + // Recency uses exponential decay with a 7-day half-life: + // recency = 0.5 ^ (age_days / 7.0) + // + // This means: + // - A brand-new connection with similarity 0.5 scores 0.70 + // - A week-old connection with similarity 0.9 scores 0.74 + // - A month-old connection with similarity 0.9 scores 0.58 + // 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 { - stored.drain(0..(len - 1000)); + let now = Utc::now(); + stored.sort_unstable_by(|a, b| { + let score = |c: &DiscoveredConnection| -> f64 { + let age_days = now + .signed_duration_since(c.discovered_at) + .num_seconds() + .max(0) as f64 + / 86_400.0; + let recency = (0.5_f64).powf(age_days / 7.0); + c.similarity * 0.6 + recency * 0.4 + }; + score(b) + .partial_cmp(&score(a)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + stored.truncate(1000); } } } diff --git a/crates/vestige-core/src/advanced/importance.rs b/crates/vestige-core/src/advanced/importance.rs index 3579626..6c06376 100644 --- a/crates/vestige-core/src/advanced/importance.rs +++ b/crates/vestige-core/src/advanced/importance.rs @@ -232,9 +232,10 @@ impl ImportanceTracker { // Store context with event if let Ok(mut events) = self.recent_events.write() && let Some(event) = events.last_mut() - && event.memory_id == memory_id { - event.context = Some(context.to_string()); - } + && event.memory_id == memory_id + { + event.context = Some(context.to_string()); + } } /// Apply importance decay to all memories @@ -339,7 +340,11 @@ impl ImportanceTracker { /// Get memories sorted by importance pub fn get_top_by_importance(&self, limit: usize) -> Vec { let mut scores = self.get_all_scores(); - scores.sort_by(|a, b| b.final_score.partial_cmp(&a.final_score).unwrap_or(std::cmp::Ordering::Equal)); + scores.sort_by(|a, b| { + b.final_score + .partial_cmp(&a.final_score) + .unwrap_or(std::cmp::Ordering::Equal) + }); scores.truncate(limit); scores } @@ -355,7 +360,9 @@ impl ImportanceTracker { scores.sort_by(|a, b| { let a_neglect = a.base_importance - a.usage_importance; let b_neglect = b.base_importance - b.usage_importance; - b_neglect.partial_cmp(&a_neglect).unwrap_or(std::cmp::Ordering::Equal) + b_neglect + .partial_cmp(&a_neglect) + .unwrap_or(std::cmp::Ordering::Equal) }); scores.truncate(limit); @@ -446,7 +453,10 @@ mod tests { assert_eq!(score.retrieval_count, 3); assert_eq!(score.helpful_count, 3); // 0.1 * 1.15^3 = ~0.152, so should be > initial 0.1 - assert!(score.usage_importance > 0.1, "Should be boosted from baseline"); + assert!( + score.usage_importance > 0.1, + "Should be boosted from baseline" + ); } #[test] diff --git a/crates/vestige-core/src/advanced/intent.rs b/crates/vestige-core/src/advanced/intent.rs index 61cfacc..a59cb2e 100644 --- a/crates/vestige-core/src/advanced/intent.rs +++ b/crates/vestige-core/src/advanced/intent.rs @@ -562,9 +562,10 @@ impl IntentDetector { } ActionType::FileOpened | ActionType::FileEdited => { if let Some(file) = &action.file - && let Some(name) = file.file_name() { - suspected_area = name.to_string_lossy().to_string(); - } + && let Some(name) = file.file_name() + { + suspected_area = name.to_string_lossy().to_string(); + } } _ => {} } diff --git a/crates/vestige-core/src/advanced/mod.rs b/crates/vestige-core/src/advanced/mod.rs index 8b41f6e..fdbdfe4 100644 --- a/crates/vestige-core/src/advanced/mod.rs +++ b/crates/vestige-core/src/advanced/mod.rs @@ -29,7 +29,10 @@ pub mod speculative; // Re-exports for convenient access pub use adaptive_embedding::{AdaptiveEmbedder, ContentType, EmbeddingStrategy, Language}; -pub use chains::{ChainStep, ConnectionType, MemoryChainBuilder, MemoryPath, ReasoningChain}; +pub use chains::{ + ChainStep, Connection, ConnectionType, MemoryChainBuilder, MemoryNode, MemoryPath, + ReasoningChain, +}; pub use compression::{CompressedMemory, CompressionConfig, CompressionStats, MemoryCompressor}; pub use cross_project::{ ApplicableKnowledge, CrossProjectLearner, ProjectContext, UniversalPattern, @@ -58,14 +61,14 @@ pub use dreams::{ }; pub use importance::{ImportanceDecayConfig, ImportanceScore, ImportanceTracker, UsageEvent}; pub use intent::{ActionType, DetectedIntent, IntentDetector, MaintenanceType, UserAction}; -pub use reconsolidation::{ - AccessContext, AccessTrigger, AppliedModification, ChangeSummary, LabileState, MemorySnapshot, - Modification, ReconsolidatedMemory, ReconsolidationManager, ReconsolidationStats, - RelationshipType, RetrievalRecord, -}; pub use prediction_error::{ CandidateMemory, CreateReason, EvaluationIntent, GateDecision, GateStats, MergeStrategy, PredictionErrorConfig, PredictionErrorGate, SimilarityResult, SupersedeReason, UpdateType, cosine_similarity, }; +pub use reconsolidation::{ + AccessContext, AccessTrigger, AppliedModification, ChangeSummary, LabileState, MemorySnapshot, + Modification, ReconsolidatedMemory, ReconsolidationManager, ReconsolidationStats, + RelationshipType, RetrievalRecord, +}; pub use speculative::{PredictedMemory, PredictionContext, SpeculativeRetriever, UsagePattern}; diff --git a/crates/vestige-core/src/advanced/prediction_error.rs b/crates/vestige-core/src/advanced/prediction_error.rs index d458fd1..3693d77 100644 --- a/crates/vestige-core/src/advanced/prediction_error.rs +++ b/crates/vestige-core/src/advanced/prediction_error.rs @@ -123,9 +123,15 @@ impl GateDecision { /// Get the prediction error score pub fn prediction_error(&self) -> f32 { match self { - Self::Create { prediction_error, .. } => *prediction_error, - Self::Update { prediction_error, .. } => *prediction_error, - Self::Supersede { prediction_error, .. } => *prediction_error, + Self::Create { + prediction_error, .. + } => *prediction_error, + Self::Update { + prediction_error, .. + } => *prediction_error, + Self::Supersede { + prediction_error, .. + } => *prediction_error, Self::Merge { avg_similarity, .. } => 1.0 - avg_similarity, } } @@ -368,7 +374,11 @@ impl PredictionErrorGate { .collect(); // Sort by similarity (highest first) - similarities.sort_by(|a, b| b.similarity.partial_cmp(&a.similarity).unwrap_or(std::cmp::Ordering::Equal)); + similarities.sort_by(|a, b| { + b.similarity + .partial_cmp(&a.similarity) + .unwrap_or(std::cmp::Ordering::Equal) + }); // Take top candidates let top_candidates: Vec<_> = similarities @@ -394,8 +404,9 @@ impl PredictionErrorGate { if let Some(c) = candidate { // If similar and the existing memory was demoted, supersede it if best.similarity >= self.config.similarity_threshold - && c.was_demoted - && self.config.auto_supersede_demoted { + && c.was_demoted + && self.config.auto_supersede_demoted + { self.stats.supersedes += 1; return GateDecision::Supersede { old_memory_id: c.id.clone(), @@ -406,8 +417,8 @@ impl PredictionErrorGate { } // Check for correction (similar but contradictory) - if best.similarity >= self.config.correction_threshold - && best.appears_contradictory { + if best.similarity >= self.config.correction_threshold && best.appears_contradictory + { self.stats.supersedes += 1; return GateDecision::Supersede { old_memory_id: c.id.clone(), @@ -418,7 +429,8 @@ impl PredictionErrorGate { } // Regular update for similar content - if best.similarity >= self.config.similarity_threshold && self.config.prefer_updates { + if best.similarity >= self.config.similarity_threshold && self.config.prefer_updates + { self.stats.updates += 1; return GateDecision::Update { target_id: best.memory_id.clone(), @@ -442,7 +454,10 @@ impl PredictionErrorGate { self.stats.merges += 1; return GateDecision::Merge { - memory_ids: merge_candidates.iter().map(|s| s.memory_id.clone()).collect(), + memory_ids: merge_candidates + .iter() + .map(|s| s.memory_id.clone()) + .collect(), avg_similarity, strategy: MergeStrategy::Combine, }; @@ -501,7 +516,10 @@ impl PredictionErrorGate { self.evaluate(new_content, new_embedding, candidates) } } - EvaluationIntent::Supersede { old_memory_id, reason } => { + EvaluationIntent::Supersede { + old_memory_id, + reason, + } => { if let Some(c) = candidates.iter().find(|c| c.id == old_memory_id) { let similarity = cosine_similarity(new_embedding, &c.embedding); self.stats.supersedes += 1; @@ -515,9 +533,7 @@ impl PredictionErrorGate { self.evaluate(new_content, new_embedding, candidates) } } - EvaluationIntent::Auto => { - self.evaluate(new_content, new_embedding, candidates) - } + EvaluationIntent::Auto => self.evaluate(new_content, new_embedding, candidates), } } @@ -596,7 +612,10 @@ pub enum EvaluationIntent { /// Force update of specific memory ForceUpdate { target_id: String }, /// Force supersede of specific memory - Supersede { old_memory_id: String, reason: SupersedeReason }, + Supersede { + old_memory_id: String, + reason: SupersedeReason, + }, } // ============================================================================ @@ -680,18 +699,22 @@ mod tests { // Create embeddings with controlled similarity based on seed // Seeds close to each other = similar vectors // Seeds far apart = different vectors - (0..384).map(|i| { - let base = (i as f32 / 384.0) * std::f32::consts::PI * 2.0; - (base * seed).sin() - }).collect() + (0..384) + .map(|i| { + let base = (i as f32 / 384.0) * std::f32::consts::PI * 2.0; + (base * seed).sin() + }) + .collect() } fn make_orthogonal_embedding() -> Vec { // Create an embedding that's orthogonal to seed=1.0 - (0..384).map(|i| { - let base = (i as f32 / 384.0) * std::f32::consts::PI * 2.0; - (base + std::f32::consts::PI / 2.0).sin() // 90 degree phase shift - }).collect() + (0..384) + .map(|i| { + let base = (i as f32 / 384.0) * std::f32::consts::PI * 2.0; + (base + std::f32::consts::PI / 2.0).sin() // 90 degree phase shift + }) + .collect() } fn make_candidate(id: &str, seed: f32) -> CandidateMemory { @@ -728,7 +751,13 @@ mod tests { let decision = gate.evaluate("New content", &embedding, &[]); - assert!(matches!(decision, GateDecision::Create { reason: CreateReason::FirstMemory, .. })); + assert!(matches!( + decision, + GateDecision::Create { + reason: CreateReason::FirstMemory, + .. + } + )); } #[test] @@ -762,7 +791,10 @@ mod tests { // Should supersede the demoted memory if similarity is above threshold // If not superseding, it should at least update - assert!(matches!(decision, GateDecision::Supersede { .. } | GateDecision::Update { .. })); + assert!(matches!( + decision, + GateDecision::Supersede { .. } | GateDecision::Update { .. } + )); } #[test] @@ -812,7 +844,13 @@ mod tests { EvaluationIntent::ForceCreate, ); - assert!(matches!(decision, GateDecision::Create { reason: CreateReason::ExplicitCreate, .. })); + assert!(matches!( + decision, + GateDecision::Create { + reason: CreateReason::ExplicitCreate, + .. + } + )); } #[test] @@ -825,7 +863,9 @@ mod tests { "Updated content", &embedding, &[candidate], - EvaluationIntent::ForceUpdate { target_id: "mem-1".to_string() }, + EvaluationIntent::ForceUpdate { + target_id: "mem-1".to_string(), + }, ); assert!(matches!(decision, GateDecision::Update { .. })); diff --git a/crates/vestige-core/src/advanced/reconsolidation.rs b/crates/vestige-core/src/advanced/reconsolidation.rs index 7b201e2..933442c 100644 --- a/crates/vestige-core/src/advanced/reconsolidation.rs +++ b/crates/vestige-core/src/advanced/reconsolidation.rs @@ -517,13 +517,14 @@ impl ReconsolidationManager { } if let Some(state) = self.labile_memories.get_mut(memory_id) - && state.is_within_window(self.labile_window) { - let success = state.add_modification(modification); - if success { - self.stats.total_modifications += 1; - } - return success; + && state.is_within_window(self.labile_window) + { + let success = state.add_modification(modification); + if success { + self.stats.total_modifications += 1; } + return success; + } false } @@ -677,7 +678,7 @@ impl ReconsolidationManager { .read() .map(|history| { let mut recent: Vec<_> = history.iter().cloned().collect(); - recent.sort_by(|a, b| b.retrieved_at.cmp(&a.retrieved_at)); + recent.sort_by_key(|b| std::cmp::Reverse(b.retrieved_at)); recent.into_iter().take(limit).collect() }) .unwrap_or_default() @@ -690,13 +691,14 @@ impl ReconsolidationManager { if let Ok(history) = self.retrieval_history.read() { for record in history.iter() { if record.memory_id == memory_id - && let Some(context) = &record.context { - for co_id in &context.co_retrieved { - if co_id != memory_id { - *co_retrieved.entry(co_id.clone()).or_insert(0) += 1; - } + && let Some(context) = &record.context + { + for co_id in &context.co_retrieved { + if co_id != memory_id { + *co_retrieved.entry(co_id.clone()).or_insert(0) += 1; } } + } } } @@ -921,7 +923,7 @@ mod tests { #[test] fn test_relationship_types() { - let relationships = vec![ + let relationships = [ RelationshipType::Supports, RelationshipType::Contradicts, RelationshipType::Elaborates, diff --git a/crates/vestige-core/src/advanced/speculative.rs b/crates/vestige-core/src/advanced/speculative.rs index 7b9442e..eebe947 100644 --- a/crates/vestige-core/src/advanced/speculative.rs +++ b/crates/vestige-core/src/advanced/speculative.rs @@ -193,7 +193,11 @@ impl SpeculativeRetriever { // Deduplicate and sort by confidence predictions = self.deduplicate_predictions(predictions); - predictions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)); + predictions.sort_by(|a, b| { + b.confidence + .partial_cmp(&a.confidence) + .unwrap_or(std::cmp::Ordering::Equal) + }); predictions.truncate(MAX_PREDICTIONS); // Filter by minimum confidence @@ -266,11 +270,12 @@ impl SpeculativeRetriever { // Update file-memory associations if let Some(file) = file_context - && let Ok(mut map) = self.file_memory_map.write() { - map.entry(file.to_string()) - .or_insert_with(Vec::new) - .push(memory_id.to_string()); - } + && let Ok(mut map) = self.file_memory_map.write() + { + map.entry(file.to_string()) + .or_insert_with(Vec::new) + .push(memory_id.to_string()); + } } /// Get cached predictions diff --git a/crates/vestige-core/src/codebase/context.rs b/crates/vestige-core/src/codebase/context.rs index a8ff103..170d556 100644 --- a/crates/vestige-core/src/codebase/context.rs +++ b/crates/vestige-core/src/codebase/context.rs @@ -587,9 +587,10 @@ impl ContextCapture { // Java Spring if let Ok(content) = fs::read_to_string(self.project_root.join("pom.xml")) - && content.contains("spring") { - frameworks.push(Framework::Spring); - } + && content.contains("spring") + { + frameworks.push(Framework::Spring); + } // Ruby Rails if self.file_exists("config/routes.rb") { @@ -613,36 +614,40 @@ impl ContextCapture { fn detect_project_name(&self) -> Result> { // Try Cargo.toml if let Ok(content) = fs::read_to_string(self.project_root.join("Cargo.toml")) - && let Some(name) = self.extract_toml_value(&content, "name") { - return Ok(Some(name)); - } + && let Some(name) = self.extract_toml_value(&content, "name") + { + return Ok(Some(name)); + } // Try package.json if let Ok(content) = fs::read_to_string(self.project_root.join("package.json")) - && let Some(name) = self.extract_json_value(&content, "name") { - return Ok(Some(name)); - } + && let Some(name) = self.extract_json_value(&content, "name") + { + return Ok(Some(name)); + } // Try pyproject.toml if let Ok(content) = fs::read_to_string(self.project_root.join("pyproject.toml")) - && let Some(name) = self.extract_toml_value(&content, "name") { - return Ok(Some(name)); - } + && let Some(name) = self.extract_toml_value(&content, "name") + { + return Ok(Some(name)); + } // Try go.mod if let Ok(content) = fs::read_to_string(self.project_root.join("go.mod")) && let Some(line) = content.lines().next() - && line.starts_with("module ") { - let name = line - .trim_start_matches("module ") - .split('/') - .next_back() - .unwrap_or("") - .to_string(); - if !name.is_empty() { - return Ok(Some(name)); - } - } + && line.starts_with("module ") + { + let name = line + .trim_start_matches("module ") + .split('/') + .next_back() + .unwrap_or("") + .to_string(); + if !name.is_empty() { + return Ok(Some(name)); + } + } // Fall back to directory name Ok(self @@ -729,17 +734,18 @@ impl ContextCapture { for test_dir in test_dirs { let test_path = self.project_root.join(test_dir); if test_path.exists() - && let Ok(entries) = fs::read_dir(&test_path) { - for entry in entries.filter_map(|e| e.ok()) { - let entry_path = entry.path(); - if let Some(entry_stem) = entry_path.file_stem() { - let entry_stem = entry_stem.to_string_lossy(); - if entry_stem.contains(&stem) { - related.push(entry_path); - } + && let Ok(entries) = fs::read_dir(&test_path) + { + for entry in entries.filter_map(|e| e.ok()) { + let entry_path = entry.path(); + if let Some(entry_stem) = entry_path.file_stem() { + let entry_stem = entry_stem.to_string_lossy(); + if entry_stem.contains(&stem) { + related.push(entry_path); } } } + } } // For Rust, look for mod.rs in same directory @@ -794,38 +800,40 @@ impl ContextCapture { // For Rust, use the parent directory name relative to src/ if path.extension().map(|e| e == "rs").unwrap_or(false) && let Ok(relative) = path.strip_prefix(&self.project_root) - && let Ok(src_relative) = relative.strip_prefix("src") { - // Get the module path - let components: Vec<_> = src_relative - .parent()? - .components() - .map(|c| c.as_os_str().to_string_lossy().to_string()) - .collect(); + && let Ok(src_relative) = relative.strip_prefix("src") + { + // Get the module path + let components: Vec<_> = src_relative + .parent()? + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect(); - if !components.is_empty() { - return Some(components.join("::")); - } - } + if !components.is_empty() { + return Some(components.join("::")); + } + } // For TypeScript/JavaScript, use the parent directory if path .extension() .map(|e| e == "ts" || e == "tsx" || e == "js" || e == "jsx") .unwrap_or(false) - && let Ok(relative) = path.strip_prefix(&self.project_root) { - // Skip src/ or lib/ prefix - let relative = relative - .strip_prefix("src") - .or_else(|_| relative.strip_prefix("lib")) - .unwrap_or(relative); + && let Ok(relative) = path.strip_prefix(&self.project_root) + { + // Skip src/ or lib/ prefix + let relative = relative + .strip_prefix("src") + .or_else(|_| relative.strip_prefix("lib")) + .unwrap_or(relative); - if let Some(parent) = relative.parent() { - let module = parent.to_string_lossy().replace('/', "."); - if !module.is_empty() { - return Some(module); - } + if let Some(parent) = relative.parent() { + let module = parent.to_string_lossy().replace('/', "."); + if !module.is_empty() { + return Some(module); } } + } None } @@ -865,10 +873,11 @@ impl ContextCapture { let trimmed = line.trim(); if (trimmed.starts_with(&format!("{} ", key)) || trimmed.starts_with(&format!("{}=", key))) - && let Some(value) = trimmed.split('=').nth(1) { - let value = value.trim().trim_matches('"').trim_matches('\''); - return Some(value.to_string()); - } + && let Some(value) = trimmed.split('=').nth(1) + { + let value = value.trim().trim_matches('"').trim_matches('\''); + return Some(value.to_string()); + } } None } diff --git a/crates/vestige-core/src/codebase/git.rs b/crates/vestige-core/src/codebase/git.rs index 2b7e21d..648f7b1 100644 --- a/crates/vestige-core/src/codebase/git.rs +++ b/crates/vestige-core/src/codebase/git.rs @@ -275,9 +275,10 @@ impl GitAnalyzer { files.push(path.to_path_buf()); } if let Some(path) = delta.old_file().path() - && !files.contains(&path.to_path_buf()) { - files.push(path.to_path_buf()); - } + && !files.contains(&path.to_path_buf()) + { + files.push(path.to_path_buf()); + } } } @@ -408,7 +409,11 @@ impl GitAnalyzer { } // Sort by strength - relationships.sort_by(|a, b| b.strength.partial_cmp(&a.strength).unwrap_or(std::cmp::Ordering::Equal)); + relationships.sort_by(|a, b| { + b.strength + .partial_cmp(&a.strength) + .unwrap_or(std::cmp::Ordering::Equal) + }); Ok(relationships) } @@ -492,9 +497,10 @@ impl GitAnalyzer { .unwrap_or_else(Utc::now); if let Some(since_time) = since - && commit_time < since_time { - continue; - } + && commit_time < since_time + { + continue; + } let message = commit.message().map(|m| m.to_string()).unwrap_or_default(); @@ -541,7 +547,12 @@ impl GitAnalyzer { let symptom = if let Some(colon_byte_pos) = first_line.find(':') { // Convert byte position to char position for safe slicing let colon_char_pos = first_line[..colon_byte_pos].chars().count(); - first_line.chars().skip(colon_char_pos + 1).collect::().trim().to_string() + first_line + .chars() + .skip(colon_char_pos + 1) + .collect::() + .trim() + .to_string() } else { first_line.to_string() }; @@ -662,11 +673,11 @@ impl GitAnalyzer { // Top contributors let mut top_contributors: Vec<_> = author_counts.into_iter().collect(); - top_contributors.sort_by(|a, b| b.1.cmp(&a.1)); + top_contributors.sort_by_key(|b| std::cmp::Reverse(b.1)); // Hot files (most frequently changed) let mut hot_files: Vec<_> = file_counts.into_iter().collect(); - hot_files.sort_by(|a, b| b.1.cmp(&a.1)); + hot_files.sort_by_key(|b| std::cmp::Reverse(b.1)); Ok(HistoryAnalysis { bug_fixes, diff --git a/crates/vestige-core/src/codebase/patterns.rs b/crates/vestige-core/src/codebase/patterns.rs index ae9e972..e6835b0 100644 --- a/crates/vestige-core/src/codebase/patterns.rs +++ b/crates/vestige-core/src/codebase/patterns.rs @@ -210,18 +210,23 @@ impl PatternDetector { for pattern in relevant_patterns { if let Some(confidence) = self.calculate_match_confidence(code, &code_lower, pattern) - && confidence >= 0.3 { - matches.push(PatternMatch { - pattern: pattern.clone(), - confidence, - location: None, // Would need line-level analysis - suggestions: self.generate_suggestions(pattern, code), - }); - } + && confidence >= 0.3 + { + matches.push(PatternMatch { + pattern: pattern.clone(), + confidence, + location: None, // Would need line-level analysis + suggestions: self.generate_suggestions(pattern, code), + }); + } } // Sort by confidence - matches.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)); + matches.sort_by(|a, b| { + b.confidence + .partial_cmp(&a.confidence) + .unwrap_or(std::cmp::Ordering::Equal) + }); Ok(matches) } @@ -325,7 +330,11 @@ impl PatternDetector { } // Sort by relevance - suggestions.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap_or(std::cmp::Ordering::Equal)); + suggestions.sort_by(|a, b| { + b.relevance + .partial_cmp(&a.relevance) + .unwrap_or(std::cmp::Ordering::Equal) + }); Ok(suggestions) } diff --git a/crates/vestige-core/src/codebase/relationships.rs b/crates/vestige-core/src/codebase/relationships.rs index 8430756..ce7a1d2 100644 --- a/crates/vestige-core/src/codebase/relationships.rs +++ b/crates/vestige-core/src/codebase/relationships.rs @@ -579,7 +579,7 @@ impl RelationshipTracker { } let mut sorted: Vec<_> = file_degrees.into_iter().collect(); - sorted.sort_by(|a, b| b.1.cmp(&a.1)); + sorted.sort_by_key(|b| std::cmp::Reverse(b.1)); sorted.truncate(limit); sorted @@ -630,9 +630,7 @@ mod tests { let related = tracker.get_related_files(Path::new("src/main.rs")).unwrap(); assert!(!related.is_empty()); - assert!(related - .iter() - .any(|r| r.path == PathBuf::from("src/lib.rs"))); + assert!(related.iter().any(|r| r.path == Path::new("src/lib.rs"))); } #[test] diff --git a/crates/vestige-core/src/codebase/types.rs b/crates/vestige-core/src/codebase/types.rs index 802ad29..4abbff1 100644 --- a/crates/vestige-core/src/codebase/types.rs +++ b/crates/vestige-core/src/codebase/types.rs @@ -221,7 +221,6 @@ pub enum DecisionStatus { Deprecated, } - // ============================================================================ // BUG FIX // ============================================================================ @@ -273,7 +272,6 @@ pub enum BugSeverity { Trivial, } - // ============================================================================ // CODE PATTERN // ============================================================================ diff --git a/crates/vestige-core/src/codebase/watcher.rs b/crates/vestige-core/src/codebase/watcher.rs index 187bb72..c69ad81 100644 --- a/crates/vestige-core/src/codebase/watcher.rs +++ b/crates/vestige-core/src/codebase/watcher.rs @@ -10,13 +10,13 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; use chrono::{DateTime, Utc}; use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; -use tokio::sync::{broadcast, mpsc, RwLock}; +use tokio::sync::{RwLock, broadcast, mpsc}; use super::patterns::PatternDetector; use super::relationships::RelationshipTracker; @@ -576,11 +576,12 @@ impl ManualEventHandler { // Detect patterns if self.config.detect_patterns - && let Ok(content) = std::fs::read_to_string(path) { - let language = CodebaseWatcher::detect_language(path); - let detector = self.detector.read().await; - let _ = detector.detect_patterns(&content, &language); - } + && let Ok(content) = std::fs::read_to_string(path) + { + let language = CodebaseWatcher::detect_language(path); + let detector = self.detector.read().await; + let _ = detector.detect_patterns(&content, &language); + } Ok(()) } diff --git a/crates/vestige-core/src/consolidation/mod.rs b/crates/vestige-core/src/consolidation/mod.rs index dbae9b2..8a7aa94 100644 --- a/crates/vestige-core/src/consolidation/mod.rs +++ b/crates/vestige-core/src/consolidation/mod.rs @@ -7,12 +7,11 @@ //! - Prune very weak memories (optional) //! - 4-Phase biologically-accurate dream cycle (v2.0) -mod sleep; pub mod phases; +mod sleep; -pub use sleep::SleepConsolidation; pub use phases::{ - DreamEngine, DreamPhase, FourPhaseDreamResult, PhaseResult, - TriagedMemory, TriageCategory, CreativeConnection, CreativeConnectionType, - DreamInsight, + CreativeConnection, CreativeConnectionType, DreamEngine, DreamInsight, DreamPhase, + FourPhaseDreamResult, PhaseResult, TriageCategory, TriagedMemory, }; +pub use sleep::SleepConsolidation; diff --git a/crates/vestige-core/src/consolidation/phases.rs b/crates/vestige-core/src/consolidation/phases.rs index 71f3724..abf6019 100644 --- a/crates/vestige-core/src/consolidation/phases.rs +++ b/crates/vestige-core/src/consolidation/phases.rs @@ -18,7 +18,7 @@ use std::time::Instant; use chrono::{DateTime, Utc}; use crate::memory::KnowledgeNode; -use crate::neuroscience::emotional_memory::{EmotionalMemory, EmotionCategory}; +use crate::neuroscience::emotional_memory::{EmotionCategory, EmotionalMemory}; use crate::neuroscience::importance_signals::ImportanceSignals; use crate::neuroscience::synaptic_tagging::SynapticTaggingSystem; @@ -197,13 +197,11 @@ impl DreamEngine { phases.push(phase2); // ==================== PHASE 3: REM (Creative) ==================== - let (connections, emotional_processed, phase3) = - self.phase_rem(&triaged, emotional_memory); + let (connections, emotional_processed, phase3) = self.phase_rem(&triaged, emotional_memory); phases.push(phase3); // ==================== PHASE 4: Integration ==================== - let (insights, phase4) = - self.phase_integration(&connections, &triaged); + let (insights, phase4) = self.phase_integration(&connections, &triaged); phases.push(phase4); FourPhaseDreamResult { @@ -262,26 +260,31 @@ impl DreamEngine { } // Sort by importance (highest first) - triaged.sort_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal)); + triaged.sort_by(|a, b| { + b.importance + .partial_cmp(&a.importance) + .unwrap_or(std::cmp::Ordering::Equal) + }); // Build replay queue: 70% high-value, 30% random noise floor let high_value_count = (triaged.len() as f64 * self.high_value_ratio).ceil() as usize; let random_count = triaged.len().saturating_sub(high_value_count); - let mut replay_queue: Vec = triaged.iter() + let mut replay_queue: Vec = triaged + .iter() .take(high_value_count) .map(|m| m.id.clone()) .collect(); // Add random noise floor from the remaining memories if random_count > 0 { - let remaining: Vec<&TriagedMemory> = triaged.iter() - .skip(high_value_count) - .collect(); + let remaining: Vec<&TriagedMemory> = triaged.iter().skip(high_value_count).collect(); // Simple deterministic shuffle using content hash let mut noise: Vec<&TriagedMemory> = remaining; noise.sort_by_key(|m| { - let hash: u64 = m.id.bytes().fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64)); + let hash: u64 = + m.id.bytes() + .fold(0u64, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u64)); hash }); for m in noise.iter().take(random_count) { @@ -307,7 +310,9 @@ impl DreamEngine { actions.push(format!( "Replay queue: {} high-value + {} noise = {} total", high_value_count.min(triaged.len()), - replay_queue.len().saturating_sub(high_value_count.min(triaged.len())), + replay_queue + .len() + .saturating_sub(high_value_count.min(triaged.len())), replay_queue.len() )); @@ -333,16 +338,25 @@ impl DreamEngine { emotion: &EmotionCategory, ) -> TriageCategory { // High emotional content - if matches!(emotion, EmotionCategory::Frustration | EmotionCategory::Urgency | EmotionCategory::Joy | EmotionCategory::Surprise) - && node.sentiment_magnitude > 0.4 { - return TriageCategory::Emotional; - } + if matches!( + emotion, + EmotionCategory::Frustration + | EmotionCategory::Urgency + | EmotionCategory::Joy + | EmotionCategory::Surprise + ) && node.sentiment_magnitude > 0.4 + { + return TriageCategory::Emotional; + } // Future-relevant (intentions, TODOs) let content_lower = node.content.to_lowercase(); - if content_lower.contains("todo") || content_lower.contains("remind") - || content_lower.contains("intention") || content_lower.contains("next time") - || content_lower.contains("plan to") { + if content_lower.contains("todo") + || content_lower.contains("remind") + || content_lower.contains("intention") + || content_lower.contains("next time") + || content_lower.contains("plan to") + { return TriageCategory::FutureRelevant; } @@ -380,9 +394,6 @@ impl DreamEngine { let mut strengthened_ids = Vec::new(); let replay_set: HashSet<&String> = replay_queue.iter().collect(); - let _triaged_map: HashMap<&str, &TriagedMemory> = triaged.iter() - .map(|m| (m.id.as_str(), m)) - .collect(); // Process replay queue in oscillation waves let wave_count = replay_queue.len().div_ceil(self.wave_batch_size); @@ -406,7 +417,8 @@ impl DreamEngine { actions.push(format!( "Processed {} waves of {} memories", - wave_count, replay_queue.len() + wave_count, + replay_queue.len() )); actions.push(format!( "Strengthened {} memories via synaptic tagging", @@ -462,7 +474,11 @@ impl DreamEngine { // Group memories by primary tag for cross-domain pairing let mut tag_groups: HashMap> = HashMap::new(); for tm in triaged { - let primary_tag = tm.tags.first().cloned().unwrap_or_else(|| "untagged".to_string()); + let primary_tag = tm + .tags + .first() + .cloned() + .unwrap_or_else(|| "untagged".to_string()); tag_groups.entry(primary_tag).or_default().push(tm); } @@ -490,7 +506,11 @@ impl DreamEngine { if similarity > self.min_insight_confidence { let conn_type = self.classify_connection(mem_a, mem_b, similarity); let insight = self.generate_connection_insight( - mem_a, mem_b, &tag_keys[i], &tag_keys[j], conn_type, + mem_a, + mem_b, + &tag_keys[i], + &tag_keys[j], + conn_type, ); connections.push(CreativeConnection { @@ -534,7 +554,10 @@ impl DreamEngine { // Pattern extraction: find repeated patterns across memories let pattern_count = self.extract_patterns(triaged, &mut connections); if pattern_count > 0 { - actions.push(format!("Pattern extraction: {} shared patterns found", pattern_count)); + actions.push(format!( + "Pattern extraction: {} shared patterns found", + pattern_count + )); } let phase = PhaseResult { @@ -548,11 +571,13 @@ impl DreamEngine { } fn content_similarity(&self, a: &str, b: &str) -> f64 { - let words_a: HashSet<&str> = a.split_whitespace() + let words_a: HashSet<&str> = a + .split_whitespace() .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric())) .filter(|w| w.len() > 3) .collect(); - let words_b: HashSet<&str> = b.split_whitespace() + let words_b: HashSet<&str> = b + .split_whitespace() .map(|w| w.trim_matches(|c: char| !c.is_alphanumeric())) .filter(|w| w.len() > 3) .collect(); @@ -601,8 +626,16 @@ impl DreamEngine { tag_b: &str, conn_type: CreativeConnectionType, ) -> String { - let a_summary = if a.content.len() > 60 { &a.content[..60] } else { &a.content }; - let b_summary = if b.content.len() > 60 { &b.content[..60] } else { &b.content }; + let a_summary = if a.content.len() > 60 { + &a.content[..60] + } else { + &a.content + }; + let b_summary = if b.content.len() > 60 { + &b.content[..60] + } else { + &b.content + }; match conn_type { CreativeConnectionType::CrossDomain => { @@ -641,7 +674,9 @@ impl DreamEngine { let mut bigram_index: HashMap<(String, String), Vec> = HashMap::new(); for (idx, tm) in triaged.iter().enumerate() { - let words: Vec = tm.content.split_whitespace() + let words: Vec = tm + .content + .split_whitespace() .map(|w| w.to_lowercase()) .filter(|w| w.len() > 3) .collect(); @@ -659,18 +694,21 @@ impl DreamEngine { pattern_count += 1; // Create a connection between the first and last memory sharing this pattern if let (Some(&first), Some(&last)) = (indices.first(), indices.last()) - && first != last { - connections.push(CreativeConnection { - memory_a_id: triaged[first].id.clone(), - memory_b_id: triaged[last].id.clone(), - insight: format!( - "Shared pattern '{} {}' found across {} memories", - bigram.0, bigram.1, indices.len() - ), - confidence: (indices.len() as f64 / triaged.len() as f64).min(1.0), - connection_type: CreativeConnectionType::CrossDomain, - }); - } + && first != last + { + connections.push(CreativeConnection { + memory_a_id: triaged[first].id.clone(), + memory_b_id: triaged[last].id.clone(), + insight: format!( + "Shared pattern '{} {}' found across {} memories", + bigram.0, + bigram.1, + indices.len() + ), + confidence: (indices.len() as f64 / triaged.len() as f64).min(1.0), + connection_type: CreativeConnectionType::CrossDomain, + }); + } } } @@ -695,7 +733,8 @@ impl DreamEngine { let mut actions = Vec::new(); // Validate connections: keep only those above threshold - let valid_connections: Vec<&CreativeConnection> = connections.iter() + let valid_connections: Vec<&CreativeConnection> = connections + .iter() .filter(|c| c.confidence >= self.validation_threshold) .collect(); @@ -726,10 +765,12 @@ impl DreamEngine { let mut seen_pairs: HashSet<(String, String)> = HashSet::new(); insights.retain(|i| { if i.source_memory_ids.len() >= 2 { - let pair = ( - i.source_memory_ids[0].clone().min(i.source_memory_ids[1].clone()), - i.source_memory_ids[0].clone().max(i.source_memory_ids[1].clone()), - ); + let (a, b) = (&i.source_memory_ids[0], &i.source_memory_ids[1]); + let pair = if a <= b { + (a.clone(), b.clone()) + } else { + (b.clone(), a.clone()) + }; seen_pairs.insert(pair) } else { true @@ -740,7 +781,9 @@ impl DreamEngine { insights.sort_by(|a, b| { let score_a = a.confidence * a.novelty; let score_b = b.confidence * b.novelty; - score_b.partial_cmp(&score_a).unwrap_or(std::cmp::Ordering::Equal) + score_b + .partial_cmp(&score_a) + .unwrap_or(std::cmp::Ordering::Equal) }); // Cap at 20 insights @@ -754,7 +797,10 @@ impl DreamEngine { } else { triaged.iter().map(|m| m.retention_strength).sum::() / triaged.len() as f64 }; - actions.push(format!("Average retention across dreamed memories: {:.2}", avg_retention)); + actions.push(format!( + "Average retention across dreamed memories: {:.2}", + avg_retention + )); let phase = PhaseResult { phase: DreamPhase::Integration, @@ -841,6 +887,8 @@ mod tests { temporal_level: None, has_embedding: None, embedding_model: None, + suppression_count: 0, + suppressed_at: None, } } @@ -864,13 +912,15 @@ mod tests { let importance = ImportanceSignals::new(); let mut synaptic = SynapticTaggingSystem::new(); - let memories: Vec = (0..10).map(|i| { - make_test_node( - &format!("mem-{}", i), - &format!("Test memory content for dream cycle number {}", i), - &["test"], - ) - }).collect(); + let memories: Vec = (0..10) + .map(|i| { + make_test_node( + &format!("mem-{}", i), + &format!("Test memory content for dream cycle number {}", i), + &["test"], + ) + }) + .collect(); let result = engine.run(&memories, &mut emotional, &importance, &mut synaptic); @@ -891,7 +941,11 @@ mod tests { let memories = vec![ make_emotional_node("emo-1", "Critical production crash error panic!", 0.9), - make_test_node("future-1", "TODO: remind me to add caching next time", &["planning"]), + make_test_node( + "future-1", + "TODO: remind me to add caching next time", + &["planning"], + ), make_test_node("standard-1", "The function returns a string", &["docs"]), ]; @@ -916,13 +970,15 @@ mod tests { let mut emotional = EmotionalMemory::new(); let importance = ImportanceSignals::new(); - let memories: Vec = (0..20).map(|i| { - make_test_node( - &format!("mem-{}", i), - &format!("Memory with varying importance content {}", i), - &["test"], - ) - }).collect(); + let memories: Vec = (0..20) + .map(|i| { + make_test_node( + &format!("mem-{}", i), + &format!("Memory with varying importance content {}", i), + &["test"], + ) + }) + .collect(); let (_triaged, queue, _phase) = engine.phase_nrem1(&memories, &mut emotional, &importance); @@ -935,8 +991,8 @@ mod tests { let engine = DreamEngine::new(); let mut synaptic = SynapticTaggingSystem::new(); - let triaged: Vec = (0..10).map(|i| { - TriagedMemory { + let triaged: Vec = (0..10) + .map(|i| TriagedMemory { id: format!("mem-{}", i), content: format!("Test memory {}", i), importance: 0.5, @@ -946,8 +1002,8 @@ mod tests { retention_strength: 0.7, emotional_valence: 0.0, is_flashbulb: false, - } - }).collect(); + }) + .collect(); let replay_queue: Vec = triaged.iter().map(|m| m.id.clone()).collect(); @@ -1032,7 +1088,10 @@ mod tests { assert_eq!(phase.phase, DreamPhase::Rem); // Should find connection via shared "error handling" and "pattern" words - assert!(!connections.is_empty(), "Should find cross-domain error handling pattern"); + assert!( + !connections.is_empty(), + "Should find cross-domain error handling pattern" + ); } #[test] @@ -1040,23 +1099,25 @@ mod tests { let engine = DreamEngine::new(); let mut emotional = EmotionalMemory::new(); - let triaged = vec![ - TriagedMemory { - id: "angry-1".to_string(), - content: "Critical production error crashed the entire system".to_string(), - importance: 0.8, - category: TriageCategory::Emotional, - tags: vec!["incident".to_string()], - created_at: Utc::now(), - retention_strength: 0.9, - emotional_valence: -0.8, - is_flashbulb: false, - }, - ]; + let triaged = vec![TriagedMemory { + id: "angry-1".to_string(), + content: "Critical production error crashed the entire system".to_string(), + importance: 0.8, + category: TriageCategory::Emotional, + tags: vec!["incident".to_string()], + created_at: Utc::now(), + retention_strength: 0.9, + emotional_valence: -0.8, + is_flashbulb: false, + }]; - let (_connections, emotional_processed, _phase) = engine.phase_rem(&triaged, &mut emotional); + let (_connections, emotional_processed, _phase) = + engine.phase_rem(&triaged, &mut emotional); - assert_eq!(emotional_processed, 1, "Negative emotional memory should be processed"); + assert_eq!( + emotional_processed, 1, + "Negative emotional memory should be processed" + ); } #[test] @@ -1121,7 +1182,11 @@ mod tests { "error handling with Result type pattern", "error handling with try-catch pattern", ); - assert!(sim > 0.2, "Similar content should have >0.2 Jaccard: {}", sim); + assert!( + sim > 0.2, + "Similar content should have >0.2 Jaccard: {}", + sim + ); let dissim = engine.content_similarity( "Rust memory management with ownership", @@ -1152,16 +1217,19 @@ mod tests { let importance = ImportanceSignals::new(); let mut synaptic = SynapticTaggingSystem::new(); - let memories: Vec = (0..5).map(|i| { - make_test_node(&format!("m{}", i), &format!("Content {}", i), &["test"]) - }).collect(); + let memories: Vec = (0..5) + .map(|i| make_test_node(&format!("m{}", i), &format!("Content {}", i), &["test"])) + .collect(); let result = engine.run(&memories, &mut emotional, &importance, &mut synaptic); for phase in &result.phases { // Duration should be non-negative (might be 0ms for fast operations) assert!(phase.duration_ms < 10000); - assert!(!phase.actions.is_empty(), "Each phase should report actions"); + assert!( + !phase.actions.is_empty(), + "Each phase should report actions" + ); } } @@ -1171,7 +1239,11 @@ mod tests { let mut emotional = EmotionalMemory::new(); let importance = ImportanceSignals::new(); - let mut node = make_test_node("flash-1", "CRITICAL: Production server crash! Emergency rollback needed immediately!", &["incident"]); + let mut node = make_test_node( + "flash-1", + "CRITICAL: Production server crash! Emergency rollback needed immediately!", + &["incident"], + ); node.sentiment_magnitude = 0.9; let (triaged, _queue, phase) = engine.phase_nrem1(&[node], &mut emotional, &importance); diff --git a/crates/vestige-core/src/embeddings/local.rs b/crates/vestige-core/src/embeddings/local.rs index a727a45..3dfc363 100644 --- a/crates/vestige-core/src/embeddings/local.rs +++ b/crates/vestige-core/src/embeddings/local.rs @@ -33,18 +33,26 @@ pub const BATCH_SIZE: usize = 32; /// Result type for model initialization static EMBEDDING_MODEL_RESULT: OnceLock, String>> = OnceLock::new(); -/// Get the default cache directory for fastembed models -/// Uses FASTEMBED_CACHE_PATH env var, or falls back to platform cache directory -fn get_cache_dir() -> std::path::PathBuf { +/// Get the default cache directory for fastembed models. +/// +/// Resolution order: +/// 1. `FASTEMBED_CACHE_PATH` env var (explicit override) +/// 2. Platform cache dir via `directories::ProjectDirs` +/// - Linux: `$XDG_CACHE_HOME/vestige/fastembed` (typically `~/.cache/vestige/fastembed`) +/// - macOS: `~/Library/Caches/vestige/fastembed` +/// - Windows: `%LOCALAPPDATA%\vestige\cache\fastembed` +/// 3. `~/.cache/vestige/fastembed` (home-dir fallback) +/// 4. `.fastembed_cache` relative to CWD (absolute last resort, should never trigger) +pub(crate) fn get_cache_dir() -> std::path::PathBuf { if let Ok(path) = std::env::var("FASTEMBED_CACHE_PATH") { return std::path::PathBuf::from(path); } - // Use platform-appropriate cache directory via directories crate - // macOS: ~/Library/Caches/com.vestige.core/fastembed - // Linux: ~/.cache/vestige/fastembed - // Windows: %LOCALAPPDATA%\vestige\cache\fastembed - if let Some(proj_dirs) = directories::ProjectDirs::from("com", "vestige", "core") { + // qualifier="" produces a clean app-name-only path on Linux/Windows; + // on macOS the qualifier is used for the bundle ID so we keep it empty + // to get ~/Library/Caches/vestige/fastembed rather than + // ~/Library/Caches/com.vestige.vestige/fastembed. + if let Some(proj_dirs) = directories::ProjectDirs::from("", "vestige", "vestige") { return proj_dirs.cache_dir().join("fastembed"); } @@ -53,7 +61,7 @@ fn get_cache_dir() -> std::path::PathBuf { return base_dirs.home_dir().join(".cache/vestige/fastembed"); } - // Last resort fallback (shouldn't happen) + // Last resort fallback (shouldn't happen in practice) std::path::PathBuf::from(".fastembed_cache") } @@ -210,9 +218,7 @@ impl Default for EmbeddingService { impl EmbeddingService { /// Create a new embedding service pub fn new() -> Self { - Self { - _unused: (), - } + Self { _unused: () } } /// Check if the model is ready @@ -240,9 +246,13 @@ impl EmbeddingService { /// Get the model name pub fn model_name(&self) -> &'static str { #[cfg(feature = "nomic-v2")] - { "nomic-ai/nomic-embed-text-v2-moe" } + { + "nomic-ai/nomic-embed-text-v2-moe" + } #[cfg(not(feature = "nomic-v2"))] - { "nomic-ai/nomic-embed-text-v1.5" } + { + "nomic-ai/nomic-embed-text-v1.5" + } } /// Get the embedding dimensions diff --git a/crates/vestige-core/src/embeddings/mod.rs b/crates/vestige-core/src/embeddings/mod.rs index fcbb9d1..5d89c10 100644 --- a/crates/vestige-core/src/embeddings/mod.rs +++ b/crates/vestige-core/src/embeddings/mod.rs @@ -13,9 +13,10 @@ mod code; mod hybrid; mod local; +pub(crate) use local::get_cache_dir; pub use local::{ - cosine_similarity, dot_product, euclidean_distance, matryoshka_truncate, Embedding, - EmbeddingError, EmbeddingService, BATCH_SIZE, EMBEDDING_DIMENSIONS, MAX_TEXT_LENGTH, + BATCH_SIZE, EMBEDDING_DIMENSIONS, Embedding, EmbeddingError, EmbeddingService, MAX_TEXT_LENGTH, + cosine_similarity, dot_product, euclidean_distance, matryoshka_truncate, }; pub use code::CodeEmbedding; diff --git a/crates/vestige-core/src/fsrs/algorithm.rs b/crates/vestige-core/src/fsrs/algorithm.rs index 5973e08..2e85832 100644 --- a/crates/vestige-core/src/fsrs/algorithm.rs +++ b/crates/vestige-core/src/fsrs/algorithm.rs @@ -348,7 +348,8 @@ mod tests { #[test] fn test_fsrs6_constants() { assert_eq!(FSRS6_WEIGHTS.len(), 21); - assert!(FSRS6_WEIGHTS[20] > 0.0 && FSRS6_WEIGHTS[20] < 1.0); + let w20 = FSRS6_WEIGHTS[20]; + assert!(w20 > 0.0 && w20 < 1.0); } #[test] diff --git a/crates/vestige-core/src/fsrs/mod.rs b/crates/vestige-core/src/fsrs/mod.rs index 36a0ddc..f882c66 100644 --- a/crates/vestige-core/src/fsrs/mod.rs +++ b/crates/vestige-core/src/fsrs/mod.rs @@ -19,6 +19,14 @@ mod optimizer; mod scheduler; pub use algorithm::{ + DEFAULT_DECAY, + DEFAULT_RETENTION, + // Constants + FSRS6_WEIGHTS, + MAX_DIFFICULTY, + MAX_STABILITY, + MIN_DIFFICULTY, + MIN_STABILITY, apply_sentiment_boost, fuzz_interval, initial_difficulty, @@ -38,14 +46,6 @@ pub use algorithm::{ retrievability_with_decay, same_day_stability, same_day_stability_with_weights, - DEFAULT_DECAY, - DEFAULT_RETENTION, - // Constants - FSRS6_WEIGHTS, - MAX_DIFFICULTY, - MAX_STABILITY, - MIN_DIFFICULTY, - MIN_STABILITY, }; pub use scheduler::{ diff --git a/crates/vestige-core/src/fsrs/optimizer.rs b/crates/vestige-core/src/fsrs/optimizer.rs index 90d5be0..59fcacf 100644 --- a/crates/vestige-core/src/fsrs/optimizer.rs +++ b/crates/vestige-core/src/fsrs/optimizer.rs @@ -3,7 +3,7 @@ //! Personalizes FSRS parameters based on user review history. //! Uses gradient-free optimization to minimize prediction error. -use super::algorithm::{retrievability_with_decay, FSRS6_WEIGHTS}; +use super::algorithm::{FSRS6_WEIGHTS, retrievability_with_decay}; use chrono::{DateTime, Utc}; // ============================================================================ diff --git a/crates/vestige-core/src/fsrs/scheduler.rs b/crates/vestige-core/src/fsrs/scheduler.rs index 1c7edc7..e5ab0d6 100644 --- a/crates/vestige-core/src/fsrs/scheduler.rs +++ b/crates/vestige-core/src/fsrs/scheduler.rs @@ -7,11 +7,10 @@ use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use super::algorithm::{ - apply_sentiment_boost, fuzz_interval, initial_difficulty_with_weights, - initial_stability_with_weights, next_difficulty_with_weights, + DEFAULT_RETENTION, FSRS6_WEIGHTS, MAX_STABILITY, apply_sentiment_boost, fuzz_interval, + initial_difficulty_with_weights, initial_stability_with_weights, next_difficulty_with_weights, next_forget_stability_with_weights, next_interval_with_decay, next_recall_stability_with_weights, retrievability_with_decay, same_day_stability_with_weights, - DEFAULT_RETENTION, FSRS6_WEIGHTS, MAX_STABILITY, }; // ============================================================================ @@ -243,13 +242,11 @@ impl FSRSScheduler { // Apply sentiment boost if self.enable_sentiment_boost && let Some(sentiment) = sentiment_boost - && sentiment > 0.0 { - new_state.stability = apply_sentiment_boost( - new_state.stability, - sentiment, - self.max_sentiment_boost, - ); - } + && sentiment > 0.0 + { + new_state.stability = + apply_sentiment_boost(new_state.stability, sentiment, self.max_sentiment_boost); + } let mut interval = next_interval_with_decay(new_state.stability, self.params.desired_retention, w20) @@ -436,9 +433,11 @@ mod tests { #[test] fn test_custom_parameters() { - let mut params = FSRSParameters::default(); - params.desired_retention = 0.85; - params.enable_fuzz = false; + let params = FSRSParameters { + desired_retention: 0.85, + enable_fuzz: false, + ..FSRSParameters::default() + }; let scheduler = FSRSScheduler::new(params); let card = scheduler.new_card(); diff --git a/crates/vestige-core/src/fts.rs b/crates/vestige-core/src/fts.rs index c3d0752..eae8ed8 100644 --- a/crates/vestige-core/src/fts.rs +++ b/crates/vestige-core/src/fts.rs @@ -7,6 +7,53 @@ /// 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: @@ -84,10 +131,7 @@ mod tests { #[test] fn test_sanitize_fts5_query_special_chars() { assert_eq!(sanitize_fts5_query("hello* world"), "\"hello world\""); - assert_eq!( - sanitize_fts5_query("content:secret"), - "\"content secret\"" - ); + assert_eq!(sanitize_fts5_query("content:secret"), "\"content secret\""); assert_eq!(sanitize_fts5_query("^boost"), "\"boost\""); } diff --git a/crates/vestige-core/src/lib.rs b/crates/vestige-core/src/lib.rs index d0bcd0b..306b2d2 100644 --- a/crates/vestige-core/src/lib.rs +++ b/crates/vestige-core/src/lib.rs @@ -114,20 +114,27 @@ pub mod neuroscience; // Memory types pub use memory::{ - ConsolidationResult, EmbeddingResult, IngestInput, KnowledgeNode, MatchType, MemoryStats, - NodeType, RecallInput, SearchMode, SearchResult, SimilarityResult, TemporalRange, + ConsolidationResult, // GOD TIER 2026: New types - EdgeType, KnowledgeEdge, MemoryScope, MemorySystem, + EdgeType, + EmbeddingResult, + IngestInput, + KnowledgeEdge, + KnowledgeNode, + MatchType, + MemoryScope, + MemoryStats, + MemorySystem, + NodeType, + RecallInput, + SearchMode, + SearchResult, + SimilarityResult, + TemporalRange, }; // FSRS-6 algorithm pub use fsrs::{ - initial_difficulty, - initial_stability, - next_interval, - // Core functions for advanced usage - retrievability, - retrievability_with_decay, FSRSParameters, FSRSScheduler, FSRSState, @@ -135,6 +142,12 @@ pub use fsrs::{ PreviewResults, Rating, ReviewResult, + initial_difficulty, + initial_stability, + next_interval, + // Core functions for advanced usage + retrievability, + retrievability_with_decay, }; // Storage layer @@ -146,9 +159,8 @@ pub use storage::{ // Consolidation (sleep-inspired memory processing) pub use consolidation::SleepConsolidation; pub use consolidation::{ - DreamEngine, DreamPhase, FourPhaseDreamResult, PhaseResult, - TriagedMemory, TriageCategory, CreativeConnection, CreativeConnectionType, - DreamInsight, + CreativeConnection, CreativeConnectionType, DreamEngine, DreamInsight, DreamPhase, + FourPhaseDreamResult, PhaseResult, TriageCategory, TriagedMemory, }; // Advanced features (bleeding edge 2026) @@ -162,6 +174,8 @@ pub use advanced::{ AdaptiveEmbedder, ApplicableKnowledge, AppliedModification, + // Prediction Error Gating (solves bad vs good similar memory problem) + CandidateMemory, ChainStep, ChangeSummary, CompressedMemory, @@ -175,16 +189,20 @@ pub use advanced::{ // Sleep consolidation (automatic background consolidation) ConsolidationScheduler, ContentType, + CreateReason, // Cross-project learning CrossProjectLearner, DetectedIntent, + DiscoveredConnection, + DiscoveredConnectionType, DreamConfig, // DreamMemory - input type for dreaming DreamMemory, - DiscoveredConnection, - DiscoveredConnectionType, DreamResult, EmbeddingStrategy, + EvaluationIntent, + GateDecision, + GateStats, ImportanceDecayConfig, ImportanceScore, // Importance tracking @@ -204,11 +222,14 @@ pub use advanced::{ MemoryPath, MemoryReplay, MemorySnapshot, + MergeStrategy, Modification, Pattern, PatternType, PredictedMemory, PredictionContext, + PredictionErrorConfig, + PredictionErrorGate, ProjectContext, ReasoningChain, ReconsolidatedMemory, @@ -217,25 +238,16 @@ pub use advanced::{ ReconsolidationStats, RelationshipType, RetrievalRecord, + SimilarityResult as PredictionSimilarityResult, // Speculative retrieval SpeculativeRetriever, + SupersedeReason, SynthesizedInsight, UniversalPattern, + UpdateType, UsageEvent, UsagePattern, UserAction, - // Prediction Error Gating (solves bad vs good similar memory problem) - CandidateMemory, - CreateReason, - EvaluationIntent, - GateDecision, - GateStats, - MergeStrategy, - PredictionErrorConfig, - PredictionErrorGate, - SimilarityResult as PredictionSimilarityResult, - SupersedeReason, - UpdateType, }; // Codebase memory (Vestige's killer differentiator) @@ -315,14 +327,20 @@ pub use neuroscience::{ ContextReinstatement, ContextWeights, DecayFunction, + // Emotional Memory (Brown & Kulik 1977, Bower 1981, LaBar & Cabeza 2006) + EmotionCategory, EmotionalContext, + EmotionalEvaluation, EmotionalMarker, + EmotionalMemory, + EmotionalMemoryStats, EncodingContext, FullMemory, // Hippocampal Indexing (Teyler & Rudy, 2007) HippocampalIndex, HippocampalIndexConfig, HippocampalIndexError, + INDEX_EMBEDDING_DIM, ImportanceCluster, ImportanceConsolidationConfig, ImportanceEncodingConfig, @@ -374,40 +392,34 @@ pub use neuroscience::{ TemporalMarker, TimeOfDay, TopicalContext, - INDEX_EMBEDDING_DIM, - // Emotional Memory (Brown & Kulik 1977, Bower 1981, LaBar & Cabeza 2006) - EmotionCategory, - EmotionalEvaluation, - EmotionalMemory, - EmotionalMemoryStats, }; // Embeddings (when feature enabled) #[cfg(feature = "embeddings")] pub use embeddings::{ - cosine_similarity, euclidean_distance, Embedding, EmbeddingError, EmbeddingService, - EMBEDDING_DIMENSIONS, + EMBEDDING_DIMENSIONS, Embedding, EmbeddingError, EmbeddingService, cosine_similarity, + euclidean_distance, }; // Search (when feature enabled) #[cfg(feature = "vector-search")] pub use search::{ - linear_combination, - reciprocal_rank_fusion, HybridSearchConfig, // Hybrid search HybridSearcher, // Keyword search KeywordSearcher, - VectorIndex, - VectorIndexConfig, - VectorIndexStats, - VectorSearchError, + RerankedResult, // GOD TIER 2026: Reranking Reranker, RerankerConfig, RerankerError, - RerankedResult, + VectorIndex, + VectorIndexConfig, + VectorIndexStats, + VectorSearchError, + linear_combination, + reciprocal_rank_fusion, }; // ============================================================================ @@ -450,6 +462,8 @@ pub mod prelude { // Sleep consolidation ConsolidationScheduler, CrossProjectLearner, + EvaluationIntent, + GateDecision, ImportanceTracker, IntentDetector, LabileState, @@ -459,14 +473,12 @@ pub mod prelude { MemoryReplay, Modification, PredictedMemory, + // Prediction Error Gating + PredictionErrorGate, ReconsolidatedMemory, // Reconsolidation ReconsolidationManager, SpeculativeRetriever, - // Prediction Error Gating - PredictionErrorGate, - GateDecision, - EvaluationIntent, }; // Codebase memory diff --git a/crates/vestige-core/src/memory/mod.rs b/crates/vestige-core/src/memory/mod.rs index 0b5350e..e7c61d4 100644 --- a/crates/vestige-core/src/memory/mod.rs +++ b/crates/vestige-core/src/memory/mod.rs @@ -299,7 +299,6 @@ pub struct ConsolidationResult { pub w20_optimized: Option, } - // ============================================================================ // SEARCH RESULTS // ============================================================================ @@ -360,4 +359,3 @@ pub struct EmbeddingResult { /// Error messages for failures pub errors: Vec, } - diff --git a/crates/vestige-core/src/memory/node.rs b/crates/vestige-core/src/memory/node.rs index 90e672d..9785387 100644 --- a/crates/vestige-core/src/memory/node.rs +++ b/crates/vestige-core/src/memory/node.rs @@ -179,6 +179,15 @@ pub struct KnowledgeNode { /// Which model generated the embedding #[serde(skip_serializing_if = "Option::is_none")] pub embedding_model: Option, + + // ========== Active Forgetting (v2.0.5, Anderson 2025 + Davis Rac1) ========== + /// Top-down suppression count — compounds with each `suppress` call + /// (Suppression-Induced Forgetting, Anderson 2025). + #[serde(default)] + pub suppression_count: i32, + /// Timestamp of the most recent suppression (for 24h labile window). + #[serde(skip_serializing_if = "Option::is_none")] + pub suppressed_at: Option>, } impl Default for KnowledgeNode { @@ -213,6 +222,8 @@ impl Default for KnowledgeNode { temporal_level: None, has_embedding: None, embedding_model: None, + suppression_count: 0, + suppressed_at: None, } } } diff --git a/crates/vestige-core/src/neuroscience/active_forgetting.rs b/crates/vestige-core/src/neuroscience/active_forgetting.rs new file mode 100644 index 0000000..0a90769 --- /dev/null +++ b/crates/vestige-core/src/neuroscience/active_forgetting.rs @@ -0,0 +1,226 @@ +//! Active Forgetting — Top-Down Inhibitory Control of Memory (v2.0.5) +//! +//! Implements user-initiated memory suppression, distinct from passive FSRS +//! decay and from bottom-up retrieval-induced forgetting (Anderson 1994, +//! `memory_states.rs`). This module models the right-lateral-prefrontal-cortex +//! gated inhibitory pathway, where top-down cognitive control compounds with +//! each stopping attempt (Suppression-Induced Forgetting) and spreads via a +//! Rac1-GTPase-like cascade to co-activated synaptic neighbors. +//! +//! ## References +//! +//! - Anderson, M. C., Hanslmayr, S., & Quaegebeur, L. (2025). Brain mechanisms +//! underlying the inhibitory control of thought. *Nature Reviews Neuroscience*. +//! DOI: 10.1038/s41583-025-00929-y. Establishes rDLPFC as the domain-general +//! inhibitory controller; SIF scales with stopping attempts; incentive-resistant. +//! - Cervantes-Sandoval, I., Chakraborty, M., MacMullen, C., & Davis, R. L. +//! (2020). Rac1 Impairs Forgetting-Induced Cellular Plasticity in Mushroom +//! Body Output Neurons. *Front Cell Neurosci*. PMC7477079. Establishes Rac1 +//! GTPase as the active synaptic destabilization mechanism. +//! +//! ## Contrast with existing modules +//! +//! - `memory_states.rs` (Anderson 1994, RIF): BOTTOM-UP, passive consequence +//! of retrieval competition. When memory A wins a query, its competitors +//! automatically lose retrievability. +//! - `active_forgetting.rs` (Anderson 2025, SIF + Davis Rac1): TOP-DOWN, +//! user-initiated via the `suppress` MCP tool. Compounds with each call. +//! Spreads to neighbors. Reversible within a 24h labile window. + +use chrono::{DateTime, Duration, Utc}; +use serde::{Deserialize, Serialize}; + +/// Default SIF penalty coefficient per suppression increment. +pub const DEFAULT_SIF_K: f64 = 0.15; + +/// Maximum cumulative penalty from compounding suppression. +/// Matches Anderson's empirical SIF saturation. +pub const DEFAULT_MAX_PENALTY: f64 = 0.8; + +/// Cascade attenuation factor for Rac1 spreading to co-activated neighbors. +pub const DEFAULT_CASCADE_DECAY: f64 = 0.3; + +/// Labile window in hours during which a suppression may be reversed. +/// Parallels Nader's 5-minute reconsolidation window on a 24-hour axis. +pub const DEFAULT_LABILE_HOURS: i64 = 24; + +/// Maximum per-neighbor retrieval-strength decrement during cascade. +pub const DEFAULT_CASCADE_RETRIEVAL_DECREMENT_CAP: f64 = 0.15; + +/// Top-down inhibitory control over memory retrieval. +/// +/// Stateless — all persistent state lives on the `knowledge_nodes` table +/// (columns `suppression_count`, `suppressed_at`). This struct exposes pure +/// helper functions consumed by `Storage::suppress_memory`, +/// `Storage::reverse_suppression`, `Storage::apply_rac1_cascade`, and the +/// `search_unified` score adjustment stage. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ActiveForgettingSystem { + /// Penalty coefficient per suppression increment (SIF). + pub k: f64, + /// Maximum cumulative penalty cap. + pub max_penalty: f64, + /// Cascade attenuation factor for Rac1 spreading. + pub cascade_decay: f64, + /// Reversal window in hours. + pub labile_hours: i64, +} + +impl Default for ActiveForgettingSystem { + fn default() -> Self { + Self { + k: DEFAULT_SIF_K, + max_penalty: DEFAULT_MAX_PENALTY, + cascade_decay: DEFAULT_CASCADE_DECAY, + labile_hours: DEFAULT_LABILE_HOURS, + } + } +} + +impl ActiveForgettingSystem { + /// Create a new system with default parameters. + pub fn new() -> Self { + Self::default() + } + + /// Compute the retrieval-score penalty for a memory with the given + /// suppression count. Penalty grows linearly then saturates at + /// `max_penalty` (Anderson's empirical SIF ceiling). + /// + /// Applied in `search_unified` as `score *= (1.0 - penalty)`. + pub fn retrieval_penalty(&self, suppression_count: i32) -> f64 { + if suppression_count <= 0 { + return 0.0; + } + (self.k * suppression_count as f64).min(self.max_penalty) + } + + /// Return `true` if a suppression is within the labile window and + /// therefore reversible. Matches reconsolidation semantics on a 24h axis. + pub fn is_reversible(&self, suppressed_at: DateTime) -> bool { + Utc::now() - suppressed_at < Duration::hours(self.labile_hours) + } + + /// Stability multiplier to apply to a neighbor of a suppressed memory + /// during the Rac1 cascade. Stronger co-activation edges propagate more + /// decay. A 1.0 edge yields `(1 - cascade_decay)` = 0.7 by default + /// (30% stability loss per cascade hop), clamped never below 0.1. + pub fn cascade_stability_factor(&self, edge_strength: f64) -> f64 { + (1.0 - self.cascade_decay * edge_strength.clamp(0.0, 1.0)).max(0.1) + } + + /// Retrieval-strength decrement for a cascade neighbor, proportional to + /// co-activation edge strength and capped at + /// `DEFAULT_CASCADE_RETRIEVAL_DECREMENT_CAP`. + pub fn cascade_retrieval_decrement(&self, edge_strength: f64) -> f64 { + (0.05 * edge_strength.clamp(0.0, 1.0)).min(DEFAULT_CASCADE_RETRIEVAL_DECREMENT_CAP) + } + + /// Time remaining in the labile window, or `None` if expired. + pub fn remaining_labile_time(&self, suppressed_at: DateTime) -> Option { + let window = Duration::hours(self.labile_hours); + let elapsed = Utc::now() - suppressed_at; + if elapsed >= window { + None + } else { + Some(window - elapsed) + } + } + + /// Deadline timestamp after which reversal will fail. + pub fn reversible_until(&self, suppressed_at: DateTime) -> DateTime { + suppressed_at + Duration::hours(self.labile_hours) + } +} + +/// Aggregate statistics about active-forgetting state across all memories. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct SuppressionStats { + /// Total memories with suppression_count > 0. + pub total_suppressed: usize, + /// Memories suppressed within the last `labile_hours` (still reversible). + pub recently_reversible: usize, + /// Mean suppression_count across all suppressed memories. + pub avg_suppression_count: f64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sif_penalty_compounds() { + let sys = ActiveForgettingSystem::new(); + assert_eq!(sys.retrieval_penalty(0), 0.0); + assert!((sys.retrieval_penalty(1) - 0.15).abs() < 1e-9); + assert!((sys.retrieval_penalty(2) - 0.30).abs() < 1e-9); + assert!((sys.retrieval_penalty(5) - 0.75).abs() < 1e-9); + // Saturates at max_penalty + assert!((sys.retrieval_penalty(6) - 0.80).abs() < 1e-9); + assert!((sys.retrieval_penalty(100) - 0.80).abs() < 1e-9); + } + + #[test] + fn test_labile_window_reversible() { + let sys = ActiveForgettingSystem::new(); + let recent = Utc::now() - Duration::hours(23); + assert!(sys.is_reversible(recent)); + let expired = Utc::now() - Duration::hours(25); + assert!(!sys.is_reversible(expired)); + assert!(sys.is_reversible(Utc::now())); + } + + #[test] + fn test_cascade_attenuation() { + let sys = ActiveForgettingSystem::new(); + let strong = sys.cascade_stability_factor(0.9); + let weak = sys.cascade_stability_factor(0.1); + assert!(strong < weak, "strong edges should propagate more decay"); + // Zero edge → no decay + assert!((sys.cascade_stability_factor(0.0) - 1.0).abs() < 1e-9); + // Factor never zeroes out + assert!(sys.cascade_stability_factor(1.0) >= 0.1); + } + + #[test] + fn test_default_params_reasonable() { + let sys = ActiveForgettingSystem::new(); + assert!(sys.k > 0.0 && sys.k <= 0.25, "k should be in (0, 0.25]"); + assert!( + sys.max_penalty >= 0.5 && sys.max_penalty <= 0.95, + "max_penalty should be in [0.5, 0.95]" + ); + assert!(sys.labile_hours >= 12 && sys.labile_hours <= 72); + assert!(sys.cascade_decay > 0.0 && sys.cascade_decay < 1.0); + } + + #[test] + fn test_reversible_until_deadline() { + let sys = ActiveForgettingSystem::new(); + let now = Utc::now(); + let deadline = sys.reversible_until(now); + let expected = now + Duration::hours(24); + assert!((deadline - expected).num_milliseconds().abs() < 100); + } + + #[test] + fn test_remaining_labile_time_expired_returns_none() { + let sys = ActiveForgettingSystem::new(); + let past = Utc::now() - Duration::hours(30); + assert!(sys.remaining_labile_time(past).is_none()); + let recent = Utc::now() - Duration::hours(10); + let remaining = sys.remaining_labile_time(recent); + assert!(remaining.is_some()); + // Should have ~14 hours left (24h window - 10h elapsed) + let hours_left = remaining.unwrap().num_hours(); + assert!((13..=14).contains(&hours_left)); + } + + #[test] + fn test_cascade_retrieval_decrement_capped() { + let sys = ActiveForgettingSystem::new(); + assert!((sys.cascade_retrieval_decrement(0.0) - 0.0).abs() < 1e-9); + assert!(sys.cascade_retrieval_decrement(0.5) <= DEFAULT_CASCADE_RETRIEVAL_DECREMENT_CAP); + assert!(sys.cascade_retrieval_decrement(1.0) <= DEFAULT_CASCADE_RETRIEVAL_DECREMENT_CAP); + } +} diff --git a/crates/vestige-core/src/neuroscience/context_memory.rs b/crates/vestige-core/src/neuroscience/context_memory.rs index e220f9a..d89594c 100644 --- a/crates/vestige-core/src/neuroscience/context_memory.rs +++ b/crates/vestige-core/src/neuroscience/context_memory.rs @@ -911,33 +911,38 @@ impl ContextMatcher { // Same session is a very strong match if let (Some(e_id), Some(r_id)) = (&encoding.session_id, &retrieval.session_id) - && e_id == r_id { - return 1.0; - } + && e_id == r_id + { + return 1.0; + } // Project match (0.4 weight) if let (Some(e_proj), Some(r_proj)) = (&encoding.project, &retrieval.project) - && e_proj == r_proj { - score += 0.4; - } + && e_proj == r_proj + { + score += 0.4; + } // Activity type match (0.3 weight) if let (Some(e_act), Some(r_act)) = (&encoding.activity_type, &retrieval.activity_type) - && e_act == r_act { - score += 0.3; - } + && e_act == r_act + { + score += 0.3; + } // Git branch match (0.2 weight) if let (Some(e_br), Some(r_br)) = (&encoding.git_branch, &retrieval.git_branch) - && e_br == r_br { - score += 0.2; - } + && e_br == r_br + { + score += 0.2; + } // Active file match (0.1 weight) if let (Some(e_file), Some(r_file)) = (&encoding.active_file, &retrieval.active_file) - && e_file == r_file { - score += 0.1; - } + && e_file == r_file + { + score += 0.1; + } score } @@ -985,7 +990,11 @@ impl ContextMatcher { .collect(); // Sort by combined score (descending) - scored.sort_by(|a, b| b.combined_score.partial_cmp(&a.combined_score).unwrap_or(std::cmp::Ordering::Equal)); + scored.sort_by(|a, b| { + b.combined_score + .partial_cmp(&a.combined_score) + .unwrap_or(std::cmp::Ordering::Equal) + }); scored } @@ -1103,9 +1112,11 @@ mod tests { topical.add_topic("security"); topical.extract_keywords_from("implementing OAuth2 authentication flow"); - assert!(topical - .active_topics - .contains(&"authentication".to_string())); + assert!( + topical + .active_topics + .contains(&"authentication".to_string()) + ); assert!(topical.keywords.contains(&"oauth2".to_string())); let terms = topical.all_terms(); @@ -1118,10 +1129,11 @@ mod tests { ctx.add_topic("api-design"); ctx.set_project("vestige"); - assert!(ctx - .topical - .active_topics - .contains(&"api-design".to_string())); + assert!( + ctx.topical + .active_topics + .contains(&"api-design".to_string()) + ); assert_eq!(ctx.session.project, Some("vestige".to_string())); } @@ -1137,7 +1149,11 @@ mod tests { let ctx2 = ctx1.clone(); let similarity = matcher.match_contexts(&ctx1, &ctx2); - assert!(similarity > 0.8, "Same context should have high similarity, got {}", similarity); + assert!( + similarity > 0.8, + "Same context should have high similarity, got {}", + similarity + ); } #[test] diff --git a/crates/vestige-core/src/neuroscience/emotional_memory.rs b/crates/vestige-core/src/neuroscience/emotional_memory.rs index 9fcb7de..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 { @@ -216,15 +201,25 @@ impl EmotionalMemory { // Check negation context (simple window-based) let negation_words: Vec<&str> = vec![ - "not", "no", "never", "don't", "doesn't", "didn't", "won't", - "can't", "couldn't", "shouldn't", "without", "hardly", + "not", + "no", + "never", + "don't", + "doesn't", + "didn't", + "won't", + "can't", + "couldn't", + "shouldn't", + "without", + "hardly", ]; for (i, word) in words.iter().enumerate() { if let Some(&(valence, arousal)) = self.lexicon.get(word.as_str()) { // Check for negation in 3-word window before - let negated = (i.saturating_sub(3)..i) - .any(|j| negation_words.contains(&words[j].as_str())); + let negated = + (i.saturating_sub(3)..i).any(|j| negation_words.contains(&words[j].as_str())); let effective_valence = if negated { -valence * 0.7 } else { valence }; @@ -269,9 +264,14 @@ impl EmotionalMemory { }; // Flashbulb detection: high novelty proxy (urgency/surprise markers) + high arousal - let novelty_proxy = urgency_boost + if category == EmotionCategory::Surprise { 0.4 } else { 0.0 }; - let is_flashbulb = novelty_proxy >= FLASHBULB_NOVELTY_THRESHOLD - && arousal >= FLASHBULB_AROUSAL_THRESHOLD; + let novelty_proxy = urgency_boost + + if category == EmotionCategory::Surprise { + 0.4 + } else { + 0.0 + }; + let is_flashbulb = + novelty_proxy >= FLASHBULB_NOVELTY_THRESHOLD && arousal >= FLASHBULB_AROUSAL_THRESHOLD; if is_flashbulb { self.flashbulbs_detected += 1; @@ -447,66 +447,114 @@ impl EmotionalMemory { // Positive / Low arousal for (word, v, a) in [ - ("good", 0.6, 0.3), ("nice", 0.5, 0.2), ("clean", 0.4, 0.2), - ("simple", 0.3, 0.1), ("smooth", 0.4, 0.2), ("stable", 0.4, 0.1), - ("helpful", 0.5, 0.3), ("elegant", 0.6, 0.3), ("solid", 0.4, 0.2), + ("good", 0.6, 0.3), + ("nice", 0.5, 0.2), + ("clean", 0.4, 0.2), + ("simple", 0.3, 0.1), + ("smooth", 0.4, 0.2), + ("stable", 0.4, 0.1), + ("helpful", 0.5, 0.3), + ("elegant", 0.6, 0.3), + ("solid", 0.4, 0.2), ] { lex.insert(word.to_string(), (v, a)); } // Positive / High arousal for (word, v, a) in [ - ("amazing", 0.9, 0.8), ("excellent", 0.8, 0.6), ("perfect", 0.9, 0.7), - ("awesome", 0.8, 0.7), ("great", 0.7, 0.5), ("fantastic", 0.9, 0.8), - ("brilliant", 0.8, 0.7), ("incredible", 0.9, 0.8), ("love", 0.8, 0.7), - ("success", 0.7, 0.6), ("solved", 0.7, 0.6), ("fixed", 0.6, 0.5), - ("working", 0.5, 0.4), ("breakthrough", 0.9, 0.9), ("discovered", 0.7, 0.7), + ("amazing", 0.9, 0.8), + ("excellent", 0.8, 0.6), + ("perfect", 0.9, 0.7), + ("awesome", 0.8, 0.7), + ("great", 0.7, 0.5), + ("fantastic", 0.9, 0.8), + ("brilliant", 0.8, 0.7), + ("incredible", 0.9, 0.8), + ("love", 0.8, 0.7), + ("success", 0.7, 0.6), + ("solved", 0.7, 0.6), + ("fixed", 0.6, 0.5), + ("working", 0.5, 0.4), + ("breakthrough", 0.9, 0.9), + ("discovered", 0.7, 0.7), ] { lex.insert(word.to_string(), (v, a)); } // Negative / Low arousal for (word, v, a) in [ - ("bad", -0.5, 0.3), ("wrong", -0.4, 0.3), ("slow", -0.3, 0.2), - ("confusing", -0.4, 0.3), ("unclear", -0.3, 0.2), ("messy", -0.4, 0.3), - ("annoying", -0.5, 0.4), ("boring", -0.3, 0.1), ("ugly", -0.5, 0.3), - ("deprecated", -0.3, 0.2), ("stale", -0.3, 0.1), + ("bad", -0.5, 0.3), + ("wrong", -0.4, 0.3), + ("slow", -0.3, 0.2), + ("confusing", -0.4, 0.3), + ("unclear", -0.3, 0.2), + ("messy", -0.4, 0.3), + ("annoying", -0.5, 0.4), + ("boring", -0.3, 0.1), + ("ugly", -0.5, 0.3), + ("deprecated", -0.3, 0.2), + ("stale", -0.3, 0.1), ] { lex.insert(word.to_string(), (v, a)); } // Negative / High arousal (bugs, errors, failures) for (word, v, a) in [ - ("error", -0.6, 0.7), ("bug", -0.6, 0.6), ("crash", -0.8, 0.9), - ("fail", -0.7, 0.7), ("failed", -0.7, 0.7), ("failure", -0.7, 0.7), - ("broken", -0.7, 0.7), ("panic", -0.9, 0.9), ("fatal", -0.9, 0.9), - ("critical", -0.5, 0.9), ("severe", -0.6, 0.8), ("urgent", -0.3, 0.9), - ("emergency", -0.5, 0.9), ("vulnerability", -0.7, 0.8), - ("exploit", -0.7, 0.8), ("leaked", -0.8, 0.9), ("compromised", -0.8, 0.9), - ("timeout", -0.5, 0.6), ("deadlock", -0.7, 0.8), ("overflow", -0.6, 0.7), - ("corruption", -0.8, 0.8), ("regression", -0.6, 0.7), - ("blocker", -0.6, 0.8), ("outage", -0.8, 0.9), ("incident", -0.5, 0.7), + ("error", -0.6, 0.7), + ("bug", -0.6, 0.6), + ("crash", -0.8, 0.9), + ("fail", -0.7, 0.7), + ("failed", -0.7, 0.7), + ("failure", -0.7, 0.7), + ("broken", -0.7, 0.7), + ("panic", -0.9, 0.9), + ("fatal", -0.9, 0.9), + ("critical", -0.5, 0.9), + ("severe", -0.6, 0.8), + ("urgent", -0.3, 0.9), + ("emergency", -0.5, 0.9), + ("vulnerability", -0.7, 0.8), + ("exploit", -0.7, 0.8), + ("leaked", -0.8, 0.9), + ("compromised", -0.8, 0.9), + ("timeout", -0.5, 0.6), + ("deadlock", -0.7, 0.8), + ("overflow", -0.6, 0.7), + ("corruption", -0.8, 0.8), + ("regression", -0.6, 0.7), + ("blocker", -0.6, 0.8), + ("outage", -0.8, 0.9), + ("incident", -0.5, 0.7), ] { lex.insert(word.to_string(), (v, a)); } // Surprise / Discovery for (word, v, a) in [ - ("unexpected", 0.0, 0.7), ("surprising", 0.1, 0.7), - ("strange", -0.1, 0.6), ("weird", -0.2, 0.5), - ("interesting", 0.4, 0.6), ("curious", 0.3, 0.5), - ("insight", 0.6, 0.7), ("realized", 0.4, 0.6), - ("found", 0.3, 0.5), ("noticed", 0.2, 0.4), + ("unexpected", 0.0, 0.7), + ("surprising", 0.1, 0.7), + ("strange", -0.1, 0.6), + ("weird", -0.2, 0.5), + ("interesting", 0.4, 0.6), + ("curious", 0.3, 0.5), + ("insight", 0.6, 0.7), + ("realized", 0.4, 0.6), + ("found", 0.3, 0.5), + ("noticed", 0.2, 0.4), ] { lex.insert(word.to_string(), (v, a)); } // Technical intensity markers for (word, v, a) in [ - ("production", -0.1, 0.7), ("deploy", 0.1, 0.6), - ("migration", -0.1, 0.5), ("refactor", 0.1, 0.4), - ("security", -0.1, 0.6), ("performance", 0.1, 0.4), - ("important", 0.2, 0.6), ("remember", 0.1, 0.5), + ("production", -0.1, 0.7), + ("deploy", 0.1, 0.6), + ("migration", -0.1, 0.5), + ("refactor", 0.1, 0.4), + ("security", -0.1, 0.6), + ("performance", 0.1, 0.4), + ("important", 0.2, 0.6), + ("remember", 0.1, 0.5), ] { lex.insert(word.to_string(), (v, a)); } @@ -572,16 +620,33 @@ mod tests { fn test_positive_content() { let mut em = EmotionalMemory::new(); let eval = em.evaluate_content("Amazing breakthrough! The fix is working perfectly"); - assert!(eval.valence > 0.3, "Expected positive valence, got {}", eval.valence); - assert!(eval.arousal > 0.4, "Expected high arousal, got {}", eval.arousal); + assert!( + eval.valence > 0.3, + "Expected positive valence, got {}", + eval.valence + ); + assert!( + eval.arousal > 0.4, + "Expected high arousal, got {}", + eval.arousal + ); } #[test] fn test_negative_content() { let mut em = EmotionalMemory::new(); - let eval = em.evaluate_content("Critical bug: production server crash with data corruption"); - assert!(eval.valence < -0.3, "Expected negative valence, got {}", eval.valence); - assert!(eval.arousal > 0.5, "Expected high arousal, got {}", eval.arousal); + let eval = + em.evaluate_content("Critical bug: production server crash with data corruption"); + assert!( + eval.valence < -0.3, + "Expected negative valence, got {}", + eval.valence + ); + assert!( + eval.arousal > 0.5, + "Expected high arousal, got {}", + eval.arousal + ); } #[test] @@ -592,7 +657,10 @@ mod tests { 0.8, // High novelty 0.9, // High arousal ); - assert!(eval.is_flashbulb, "Should detect flashbulb with high novelty + arousal"); + assert!( + eval.is_flashbulb, + "Should detect flashbulb with high novelty + arousal" + ); } #[test] @@ -611,7 +679,10 @@ mod tests { let mut em = EmotionalMemory::new(); let positive = em.evaluate_content("This is amazing"); let negated = em.evaluate_content("This is not amazing"); - assert!(negated.valence < positive.valence, "Negation should reduce valence"); + assert!( + negated.valence < positive.valence, + "Negation should reduce valence" + ); } #[test] @@ -632,15 +703,24 @@ mod tests { em.evaluate_content("Great amazing perfect success"); } let (mood_v, _) = em.current_mood(); - assert!(mood_v > 0.3, "Mood should be positive after positive content"); + assert!( + mood_v > 0.3, + "Mood should be positive after positive content" + ); // Positive memory should get boost let boost = em.mood_congruence_boost(0.7); - assert!(boost > 0.0, "Positive memory should get mood-congruent boost"); + assert!( + boost > 0.0, + "Positive memory should get mood-congruent boost" + ); // Negative memory should get less/no boost let neg_boost = em.mood_congruence_boost(-0.7); - assert!(neg_boost < boost, "Negative memory should get less boost in positive mood"); + assert!( + neg_boost < boost, + "Negative memory should get less boost in positive mood" + ); } #[test] @@ -674,7 +754,10 @@ mod tests { } let (v1, a1) = em.current_mood(); assert!(v1 < 0.0, "Mood should be negative after negative content"); - assert!(a1 > 0.3, "Arousal should be elevated after negative content"); + assert!( + a1 > 0.3, + "Arousal should be elevated after negative content" + ); } #[test] diff --git a/crates/vestige-core/src/neuroscience/hippocampal_index.rs b/crates/vestige-core/src/neuroscience/hippocampal_index.rs index a8fa0f9..5e1a367 100644 --- a/crates/vestige-core/src/neuroscience/hippocampal_index.rs +++ b/crates/vestige-core/src/neuroscience/hippocampal_index.rs @@ -1076,9 +1076,10 @@ impl ContentStore { // Check cache first let cache_key = self.cache_key(pointer); if let Ok(cache) = self.cache.read() - && let Some(data) = cache.get(&cache_key) { - return Ok(data.clone()); - } + && let Some(data) = cache.get(&cache_key) + { + return Ok(data.clone()); + } // Retrieve from storage let data = match &pointer.storage_location { @@ -1131,22 +1132,23 @@ impl ContentStore { } if let Ok(mut cache) = self.cache.write() - && let Ok(mut size) = self.current_cache_size.write() { - // Evict if necessary - while *size + data_size > self.max_cache_size && !cache.is_empty() { - // Simple eviction: remove first entry - if let Some(key_to_remove) = cache.keys().next().cloned() { - if let Some(removed) = cache.remove(&key_to_remove) { - *size = size.saturating_sub(removed.len()); - } - } else { - break; + && let Ok(mut size) = self.current_cache_size.write() + { + // Evict if necessary + while *size + data_size > self.max_cache_size && !cache.is_empty() { + // Simple eviction: remove first entry + if let Some(key_to_remove) = cache.keys().next().cloned() { + if let Some(removed) = cache.remove(&key_to_remove) { + *size = size.saturating_sub(removed.len()); } + } else { + break; } - - cache.insert(key.to_string(), data.to_vec()); - *size += data_size; } + + cache.insert(key.to_string(), data.to_vec()); + *size += data_size; + } } /// Retrieve from SQLite (placeholder - to be integrated with Storage) @@ -1393,15 +1395,16 @@ impl HippocampalIndex { // Calculate semantic score if let Some(ref query_embedding) = query.semantic_embedding - && !index.semantic_summary.is_empty() { - let query_compressed = self.compress_embedding(query_embedding); - match_result.semantic_score = - self.cosine_similarity(&query_compressed, &index.semantic_summary); + && !index.semantic_summary.is_empty() + { + let query_compressed = self.compress_embedding(query_embedding); + match_result.semantic_score = + self.cosine_similarity(&query_compressed, &index.semantic_summary); - if match_result.semantic_score < query.min_similarity { - continue; - } + if match_result.semantic_score < query.min_similarity { + continue; } + } // Calculate text score if let Some(ref text_query) = query.text_query { @@ -1442,21 +1445,24 @@ impl HippocampalIndex { fn passes_filters(&self, index: &MemoryIndex, query: &IndexQuery) -> bool { // Time range filter if let Some((start, end)) = query.time_range - && (index.temporal_marker.created_at < start || index.temporal_marker.created_at > end) { - return false; - } + && (index.temporal_marker.created_at < start || index.temporal_marker.created_at > end) + { + return false; + } // Importance flags filter if let Some(ref required) = query.required_flags - && !index.matches_importance(required.to_bits()) { - return false; - } + && !index.matches_importance(required.to_bits()) + { + return false; + } // Node type filter if let Some(ref types) = query.node_types - && !types.contains(&index.node_type) { - return false; - } + && !types.contains(&index.node_type) + { + return false; + } true } @@ -1574,9 +1580,10 @@ impl HippocampalIndex { for m in matches { // Record access if let Ok(mut indices) = self.indices.write() - && let Some(index) = indices.get_mut(&m.index.memory_id) { - index.record_access(); - } + && let Some(index) = indices.get_mut(&m.index.memory_id) + { + index.record_access(); + } match self.retrieve_content(&m.index) { Ok(memory) => memories.push(memory), @@ -1881,37 +1888,39 @@ impl HippocampalIndex { ) -> Result { // Check if already indexed if let Ok(indices) = self.indices.read() - && indices.contains_key(node_id) { - return Err(HippocampalIndexError::MigrationError( - "Node already indexed".to_string(), - )); - } + && indices.contains_key(node_id) + { + return Err(HippocampalIndexError::MigrationError( + "Node already indexed".to_string(), + )); + } // Create the index let barcode = self.index_memory(node_id, content, node_type, created_at, embedding)?; // Update importance flags based on existing data if let Ok(mut indices) = self.indices.write() - && let Some(index) = indices.get_mut(node_id) { - // Set high retention flag if applicable - if retention_strength > 0.7 { - index.importance_flags.set_high_retention(true); - } - - // Set emotional flag if applicable - if sentiment_magnitude > 0.5 { - index.importance_flags.set_emotional(true); - } - - // Add SQLite content pointer - index.content_pointers.clear(); - index.add_content_pointer(ContentPointer::sqlite( - "knowledge_nodes", - barcode.id as i64, - ContentType::Text, - )); + && let Some(index) = indices.get_mut(node_id) + { + // Set high retention flag if applicable + if retention_strength > 0.7 { + index.importance_flags.set_high_retention(true); } + // Set emotional flag if applicable + if sentiment_magnitude > 0.5 { + index.importance_flags.set_emotional(true); + } + + // Add SQLite content pointer + index.content_pointers.clear(); + index.add_content_pointer(ContentPointer::sqlite( + "knowledge_nodes", + barcode.id as i64, + ContentType::Text, + )); + } + Ok(barcode) } diff --git a/crates/vestige-core/src/neuroscience/importance_signals.rs b/crates/vestige-core/src/neuroscience/importance_signals.rs index cf26e63..26da186 100644 --- a/crates/vestige-core/src/neuroscience/importance_signals.rs +++ b/crates/vestige-core/src/neuroscience/importance_signals.rs @@ -359,17 +359,18 @@ impl PredictionModel { let ngrams = self.extract_ngrams(content); if let Ok(mut patterns) = self.patterns.write() - && let Ok(mut total) = self.total_count.write() { - for ngram in ngrams { - *patterns.entry(ngram).or_insert(0) += 1; - *total += 1; - } - - // Prune if too large - if patterns.len() > MAX_PREDICTION_PATTERNS { - self.apply_decay(&mut patterns); - } + && let Ok(mut total) = self.total_count.write() + { + for ngram in ngrams { + *patterns.entry(ngram).or_insert(0) += 1; + *total += 1; } + + // Prune if too large + if patterns.len() > MAX_PREDICTION_PATTERNS { + self.apply_decay(&mut patterns); + } + } } fn compute_prediction_error(&self, content: &str) -> f64 { @@ -463,7 +464,7 @@ impl PredictionModel { .filter_map(|ng| patterns.get(&ng).map(|&count| (ng, count))) .collect(); - familiar.sort_by(|a, b| b.1.cmp(&a.1)); + familiar.sort_by_key(|b| std::cmp::Reverse(b.1)); familiar.into_iter().take(5).map(|(ng, _)| ng).collect() } @@ -488,7 +489,7 @@ impl PredictionModel { fn apply_decay(&self, patterns: &mut HashMap) { // Remove lowest frequency patterns let mut entries: Vec<_> = patterns.iter().map(|(k, v)| (k.clone(), *v)).collect(); - entries.sort_by(|a, b| a.1.cmp(&b.1)); + entries.sort_by_key(|a| a.1); // Remove bottom 20% let remove_count = patterns.len() / 5; @@ -1186,7 +1187,11 @@ impl RewardSignal { // Limit pattern count if patterns.len() > 1000 { - patterns.sort_by(|a, b| b.strength.partial_cmp(&a.strength).unwrap_or(std::cmp::Ordering::Equal)); + patterns.sort_by(|a, b| { + b.strength + .partial_cmp(&a.strength) + .unwrap_or(std::cmp::Ordering::Equal) + }); patterns.truncate(500); } } @@ -1226,7 +1231,9 @@ impl RewardSignal { entries.sort_by(|a, b| { // Sort by score, then by recency - b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal).then_with(|| b.2.cmp(&a.2)) + b.1.partial_cmp(&a.1) + .unwrap_or(std::cmp::Ordering::Equal) + .then_with(|| b.2.cmp(&a.2)) }); // Keep top entries diff --git a/crates/vestige-core/src/neuroscience/memory_states.rs b/crates/vestige-core/src/neuroscience/memory_states.rs index 8f1bf33..c5f898a 100644 --- a/crates/vestige-core/src/neuroscience/memory_states.rs +++ b/crates/vestige-core/src/neuroscience/memory_states.rs @@ -1267,23 +1267,22 @@ impl MemoryStateInfo { } MemoryState::Unavailable => { if let Some(until) = lifecycle.suppression_until - && until > now { - recommendations.push(format!( - "This memory is temporarily suppressed. \ + && until > now + { + recommendations.push(format!( + "This memory is temporarily suppressed. \ It will become accessible again after {}.", - until.format("%Y-%m-%d %H:%M UTC") - )); - } - } - MemoryState::Dormant => { - if duration_since_access.num_days() > 20 { - recommendations.push( - "Consider accessing this memory soon to prevent it from \ - becoming harder to retrieve." - .to_string(), - ); + until.format("%Y-%m-%d %H:%M UTC") + )); } } + MemoryState::Dormant if duration_since_access.num_days() > 20 => { + recommendations.push( + "Consider accessing this memory soon to prevent it from \ + becoming harder to retrieve." + .to_string(), + ); + } _ => {} } diff --git a/crates/vestige-core/src/neuroscience/mod.rs b/crates/vestige-core/src/neuroscience/mod.rs index 65a933c..21aab14 100644 --- a/crates/vestige-core/src/neuroscience/mod.rs +++ b/crates/vestige-core/src/neuroscience/mod.rs @@ -57,6 +57,7 @@ //! - Collins, A. M., & Loftus, E. F. (1975). A spreading-activation theory of semantic //! processing. Psychological Review. +pub mod active_forgetting; pub mod context_memory; pub mod emotional_memory; pub mod hippocampal_index; @@ -67,6 +68,12 @@ pub mod prospective_memory; pub mod spreading_activation; pub mod synaptic_tagging; +// Active forgetting — top-down inhibitory control (Anderson 2025 + Davis Rac1) +pub use active_forgetting::{ + ActiveForgettingSystem, DEFAULT_CASCADE_DECAY, DEFAULT_LABILE_HOURS, DEFAULT_MAX_PENALTY, + DEFAULT_SIF_K, SuppressionStats, +}; + // Re-exports for convenient access pub use synaptic_tagging::{ // Results @@ -94,15 +101,23 @@ pub use context_memory::{ // Memory states (accessibility continuum) pub use memory_states::{ + // Constants + ACCESSIBILITY_ACTIVE, + ACCESSIBILITY_DORMANT, + ACCESSIBILITY_SILENT, + ACCESSIBILITY_UNAVAILABLE, // Accessibility scoring AccessibilityCalculator, BatchUpdateResult, + COMPETITION_SIMILARITY_THRESHOLD, CompetitionCandidate, CompetitionConfig, CompetitionEvent, // Competition system (Retrieval-Induced Forgetting) CompetitionManager, CompetitionResult, + DEFAULT_ACTIVE_DECAY_HOURS, + DEFAULT_DORMANT_DECAY_DAYS, LifecycleSummary, MemoryLifecycle, // Core types @@ -116,14 +131,6 @@ pub use memory_states::{ StateTransitionReason, // State management StateUpdateService, - // Constants - ACCESSIBILITY_ACTIVE, - ACCESSIBILITY_DORMANT, - ACCESSIBILITY_SILENT, - ACCESSIBILITY_UNAVAILABLE, - COMPETITION_SIMILARITY_THRESHOLD, - DEFAULT_ACTIVE_DECAY_HOURS, - DEFAULT_DORMANT_DECAY_DAYS, }; // Multi-channel importance signaling (Neuromodulator-inspired) @@ -174,6 +181,8 @@ pub use hippocampal_index::{ HippocampalIndex, HippocampalIndexConfig, HippocampalIndexError, + // Constants + INDEX_EMBEDDING_DIM, ImportanceFlags, IndexLink, IndexMatch, @@ -187,40 +196,39 @@ pub use hippocampal_index::{ MigrationResult, StorageLocation, TemporalMarker, - // Constants - INDEX_EMBEDDING_DIM, }; // Predictive memory retrieval (Free Energy Principle - Friston, 2010) pub use predictive_retrieval::{ // Backward-compatible aliases ContextualPredictor, - Prediction, - PredictionConfidence, - PredictiveConfig, - PredictiveRetriever, - SequencePredictor, - TemporalPredictor, // Enhanced types (Friston's Active Inference) PredictedMemory, + Prediction, + PredictionConfidence, PredictionOutcome, PredictionReason, + PredictiveConfig, PredictiveMemory, PredictiveMemoryConfig, PredictiveMemoryError, + PredictiveRetriever, ProjectContext as PredictiveProjectContext, QueryPattern, + SequencePredictor, SessionContext as PredictiveSessionContext, TemporalPatterns, + TemporalPredictor, UserModel, }; // Prospective memory (Einstein & McDaniel, 1990) pub use prospective_memory::{ - // Core engine - ProspectiveMemory, - ProspectiveMemoryConfig, - ProspectiveMemoryError, + // Context monitoring + Context as ProspectiveContext, + ContextMonitor, + // Triggers and patterns + ContextPattern, // Intentions Intention, IntentionParser, @@ -229,13 +237,12 @@ pub use prospective_memory::{ IntentionStatus, IntentionTrigger, Priority, - // Triggers and patterns - ContextPattern, + // Core engine + ProspectiveMemory, + ProspectiveMemoryConfig, + ProspectiveMemoryError, RecurrencePattern, TriggerPattern, - // Context monitoring - Context as ProspectiveContext, - ContextMonitor, }; // Spreading activation (Associative Memory Network - Collins & Loftus, 1975) diff --git a/crates/vestige-core/src/neuroscience/predictive_retrieval.rs b/crates/vestige-core/src/neuroscience/predictive_retrieval.rs index 4bace7c..77934e1 100644 --- a/crates/vestige-core/src/neuroscience/predictive_retrieval.rs +++ b/crates/vestige-core/src/neuroscience/predictive_retrieval.rs @@ -582,7 +582,7 @@ impl UserModel { } // Sort by count and keep top patterns - patterns.sort_by(|a, b| b.1.cmp(&a.1)); + patterns.sort_by_key(|b| std::cmp::Reverse(b.1)); patterns.truncate(50); } @@ -915,7 +915,11 @@ impl PredictiveMemory { predictions.retain(|p| p.confidence >= self.config.min_confidence); // Sort by confidence - predictions.sort_by(|a, b| b.confidence.partial_cmp(&a.confidence).unwrap_or(std::cmp::Ordering::Equal)); + predictions.sort_by(|a, b| { + b.confidence + .partial_cmp(&a.confidence) + .unwrap_or(std::cmp::Ordering::Equal) + }); // Truncate to max predictions.truncate(self.config.max_predictions); diff --git a/crates/vestige-core/src/neuroscience/prospective_memory.rs b/crates/vestige-core/src/neuroscience/prospective_memory.rs index 868e1e5..133f17d 100644 --- a/crates/vestige-core/src/neuroscience/prospective_memory.rs +++ b/crates/vestige-core/src/neuroscience/prospective_memory.rs @@ -130,8 +130,7 @@ pub type Result = std::result::Result; // ============================================================================ /// Priority levels for intentions -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default)] pub enum Priority { /// Low priority - nice to remember Low = 1, @@ -144,7 +143,6 @@ pub enum Priority { Critical = 4, } - impl Priority { /// Get numeric value for comparison pub fn value(&self) -> u8 { @@ -178,8 +176,7 @@ impl Priority { } /// Status of an intention -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[derive(Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] pub enum IntentionStatus { /// Intention is active and being monitored #[default] @@ -196,7 +193,6 @@ pub enum IntentionStatus { Snoozed, } - /// Pattern for matching trigger conditions #[derive(Debug, Clone, Serialize, Deserialize)] pub enum TriggerPattern { @@ -695,15 +691,17 @@ impl Intention { // Check snoozed if let Some(snoozed_until) = self.snoozed_until - && Utc::now() < snoozed_until { - return false; - } + && Utc::now() < snoozed_until + { + return false; + } // Check minimum interval if let Some(last) = self.last_reminded_at - && (Utc::now() - last) < Duration::minutes(MIN_REMINDER_INTERVAL_MINUTES) { - return false; - } + && (Utc::now() - last) < Duration::minutes(MIN_REMINDER_INTERVAL_MINUTES) + { + return false; + } true } @@ -956,9 +954,17 @@ impl IntentionParser { let when_char_idx = text_lower[..when_byte_idx].chars().count(); let content_part: String = if text_lower.starts_with("remind me to ") { - original.chars().skip(13).take(when_char_idx.saturating_sub(13)).collect() + original + .chars() + .skip(13) + .take(when_char_idx.saturating_sub(13)) + .collect() } else if text_lower.starts_with("remind me ") { - original.chars().skip(10).take(when_char_idx.saturating_sub(10)).collect() + original + .chars() + .skip(10) + .take(when_char_idx.saturating_sub(10)) + .collect() } else { original.chars().take(when_char_idx).collect() }; @@ -1047,8 +1053,6 @@ impl IntentionParser { /// Extract content from text, removing trigger keywords fn extract_content(&self, _text_lower: &str, original: &str, keyword: &str) -> String { - - original .replace(keyword, "") .replace(&keyword.to_uppercase(), "") @@ -1267,9 +1271,10 @@ impl ProspectiveMemory { // Check if snoozed intention should wake if intention.status == IntentionStatus::Snoozed && let Some(until) = intention.snoozed_until - && Utc::now() >= until { - intention.wake(); - } + && Utc::now() >= until + { + intention.wake(); + } continue; } @@ -1277,10 +1282,11 @@ impl ProspectiveMemory { if intention .trigger .is_triggered(context, &context.recent_events) - && intention.should_remind() { - intention.mark_triggered(); - triggered.push(intention.clone()); - } + && intention.should_remind() + { + intention.mark_triggered(); + triggered.push(intention.clone()); + } // Check for deadline escalation if self.config.enable_escalation { diff --git a/crates/vestige-core/src/neuroscience/spreading_activation.rs b/crates/vestige-core/src/neuroscience/spreading_activation.rs index 55a1fad..2daf786 100644 --- a/crates/vestige-core/src/neuroscience/spreading_activation.rs +++ b/crates/vestige-core/src/neuroscience/spreading_activation.rs @@ -57,7 +57,6 @@ pub enum LinkType { UserDefined, } - // ============================================================================ // ASSOCIATION EDGE // ============================================================================ @@ -271,13 +270,7 @@ impl ActivationNetwork { } /// Add an edge between two nodes - pub fn add_edge( - &mut self, - source: String, - target: String, - link_type: LinkType, - strength: f64, - ) { + pub fn add_edge(&mut self, source: String, target: String, link_type: LinkType, strength: f64) { // Ensure both nodes exist self.add_node(source.clone()); self.add_node(target.clone()); @@ -288,9 +281,10 @@ impl ActivationNetwork { // Update node's edge list if let Some(node) = self.nodes.get_mut(&source) - && !node.edges.contains(&target) { - node.edges.push(target); - } + && !node.edges.contains(&target) + { + node.edges.push(target); + } } /// Activate a node and spread activation through the network @@ -314,9 +308,10 @@ impl ActivationNetwork { while let Some((current_id, current_activation, hops, path)) = queue.pop() { // Skip if we've visited this node with higher activation if let Some(&prev_activation) = visited.get(¤t_id) - && prev_activation >= current_activation { - continue; - } + && prev_activation >= current_activation + { + continue; + } visited.insert(current_id.clone(), current_activation); // Check hop limit @@ -499,7 +494,7 @@ mod tests { #[test] fn test_activation_threshold() { let mut network = ActivationNetwork::with_config(ActivationConfig { - decay_factor: 0.1, // Very high decay + decay_factor: 0.1, // Very high decay min_threshold: 0.5, // High threshold ..Default::default() }); diff --git a/crates/vestige-core/src/neuroscience/synaptic_tagging.rs b/crates/vestige-core/src/neuroscience/synaptic_tagging.rs index ea2e38c..c5e9ca1 100644 --- a/crates/vestige-core/src/neuroscience/synaptic_tagging.rs +++ b/crates/vestige-core/src/neuroscience/synaptic_tagging.rs @@ -122,7 +122,6 @@ pub enum DecayFunction { Logarithmic, } - impl DecayFunction { /// Calculate decayed strength /// diff --git a/crates/vestige-core/src/search/hyde.rs b/crates/vestige-core/src/search/hyde.rs index 1e4ebc7..1fea1fa 100644 --- a/crates/vestige-core/src/search/hyde.rs +++ b/crates/vestige-core/src/search/hyde.rs @@ -43,8 +43,10 @@ pub fn classify_intent(query: &str) -> QueryIntent { if lower.contains("how to") || lower.starts_with("how do") || lower.starts_with("steps") { return QueryIntent::HowTo; } - if lower.starts_with("what is") || lower.starts_with("what are") - || lower.starts_with("define") || lower.starts_with("explain") + if lower.starts_with("what is") + || lower.starts_with("what are") + || lower.starts_with("define") + || lower.starts_with("explain") { return QueryIntent::Definition; } @@ -54,8 +56,11 @@ pub fn classify_intent(query: &str) -> QueryIntent { if lower.starts_with("when") || lower.contains("date") || lower.contains("timeline") { return QueryIntent::Temporal; } - if query.contains('(') || query.contains('{') || query.contains("fn ") - || query.contains("class ") || query.contains("::") + if query.contains('(') + || query.contains('{') + || query.contains("fn ") + || query.contains("class ") + || query.contains("::") { return QueryIntent::Technical; } @@ -161,23 +166,38 @@ mod tests { #[test] fn test_classify_definition() { assert_eq!(classify_intent("What is FSRS?"), QueryIntent::Definition); - assert_eq!(classify_intent("explain spaced repetition"), QueryIntent::Definition); + assert_eq!( + classify_intent("explain spaced repetition"), + QueryIntent::Definition + ); } #[test] fn test_classify_howto() { - assert_eq!(classify_intent("how to configure embeddings"), QueryIntent::HowTo); - assert_eq!(classify_intent("How do I search memories?"), QueryIntent::HowTo); + assert_eq!( + classify_intent("how to configure embeddings"), + QueryIntent::HowTo + ); + assert_eq!( + classify_intent("How do I search memories?"), + QueryIntent::HowTo + ); } #[test] fn test_classify_reasoning() { - assert_eq!(classify_intent("why does retention decay?"), QueryIntent::Reasoning); + assert_eq!( + classify_intent("why does retention decay?"), + QueryIntent::Reasoning + ); } #[test] fn test_classify_temporal() { - assert_eq!(classify_intent("when did the last consolidation run"), QueryIntent::Temporal); + assert_eq!( + classify_intent("when did the last consolidation run"), + QueryIntent::Temporal + ); } #[test] @@ -188,7 +208,10 @@ mod tests { #[test] fn test_classify_lookup() { - assert_eq!(classify_intent("vestige memory system"), QueryIntent::Lookup); + assert_eq!( + classify_intent("vestige memory system"), + QueryIntent::Lookup + ); } #[test] @@ -200,10 +223,7 @@ mod tests { #[test] fn test_centroid_embedding() { - let embeddings = vec![ - vec![1.0, 0.0, 0.0], - vec![0.0, 1.0, 0.0], - ]; + let embeddings = vec![vec![1.0, 0.0, 0.0], vec![0.0, 1.0, 0.0]]; let centroid = centroid_embedding(&embeddings); assert_eq!(centroid.len(), 3); // Should be normalized diff --git a/crates/vestige-core/src/search/mod.rs b/crates/vestige-core/src/search/mod.rs index eb79f89..45b5a63 100644 --- a/crates/vestige-core/src/search/mod.rs +++ b/crates/vestige-core/src/search/mod.rs @@ -15,21 +15,21 @@ mod temporal; mod vector; pub use vector::{ - VectorIndex, VectorIndexConfig, VectorIndexStats, VectorSearchError, DEFAULT_CONNECTIVITY, - DEFAULT_DIMENSIONS, + DEFAULT_CONNECTIVITY, DEFAULT_DIMENSIONS, VectorIndex, VectorIndexConfig, VectorIndexStats, + VectorSearchError, }; -pub use keyword::{sanitize_fts5_query, KeywordSearcher}; +pub use keyword::{KeywordSearcher, sanitize_fts5_query}; -pub use hybrid::{linear_combination, reciprocal_rank_fusion, HybridSearchConfig, HybridSearcher}; +pub use hybrid::{HybridSearchConfig, HybridSearcher, linear_combination, reciprocal_rank_fusion}; pub use temporal::TemporalSearcher; // GOD TIER 2026: Reranking for +15-20% precision pub use reranker::{ - Reranker, RerankerConfig, RerankerError, RerankedResult, - DEFAULT_RERANK_COUNT, DEFAULT_RETRIEVAL_COUNT, + DEFAULT_RERANK_COUNT, DEFAULT_RETRIEVAL_COUNT, RerankedResult, Reranker, RerankerConfig, + RerankerError, }; // v2.0: HyDE-inspired query expansion for improved semantic search -pub use hyde::{classify_intent, expand_query, centroid_embedding, QueryIntent}; +pub use hyde::{QueryIntent, centroid_embedding, classify_intent, expand_query}; diff --git a/crates/vestige-core/src/search/reranker.rs b/crates/vestige-core/src/search/reranker.rs index 5f1058a..1277cf7 100644 --- a/crates/vestige-core/src/search/reranker.rs +++ b/crates/vestige-core/src/search/reranker.rs @@ -10,6 +10,8 @@ //! Falls back to BM25-like term overlap scoring when the cross-encoder //! model is unavailable. +#[cfg(feature = "embeddings")] +use crate::embeddings::get_cache_dir; #[cfg(feature = "embeddings")] use fastembed::{RerankInitOptions, RerankerModel, TextRerank}; @@ -127,6 +129,7 @@ impl Reranker { } let options = RerankInitOptions::new(RerankerModel::JINARerankerV1TurboEn) + .with_cache_dir(get_cache_dir()) .with_show_download_progress(true); match TextRerank::try_new(options) { @@ -163,7 +166,9 @@ impl Reranker { top_k: Option, ) -> Result>, RerankerError> { if query.is_empty() { - return Err(RerankerError::InvalidInput("Query cannot be empty".to_string())); + return Err(RerankerError::InvalidInput( + "Query cannot be empty".to_string(), + )); } if candidates.is_empty() { @@ -190,7 +195,9 @@ impl Reranker { .collect(); results.sort_by(|a, b| { - b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal) + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) }); if let Some(min_score) = self.config.min_score { @@ -217,7 +224,11 @@ impl Reranker { }) .collect(); - results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); + results.sort_by(|a, b| { + b.score + .partial_cmp(&a.score) + .unwrap_or(std::cmp::Ordering::Equal) + }); if let Some(min_score) = self.config.min_score { results.retain(|r| r.score >= min_score); diff --git a/crates/vestige-core/src/search/vector.rs b/crates/vestige-core/src/search/vector.rs index d71b411..069dd9a 100644 --- a/crates/vestige-core/src/search/vector.rs +++ b/crates/vestige-core/src/search/vector.rs @@ -174,9 +174,9 @@ impl VectorIndex { /// Reserve capacity for a specified number of vectors /// This should be called before adding vectors to avoid segmentation faults pub fn reserve(&self, capacity: usize) -> Result<(), VectorSearchError> { - self.index - .reserve(capacity) - .map_err(|e| VectorSearchError::IndexCreation(format!("Failed to reserve capacity: {}", e))) + self.index.reserve(capacity).map_err(|e| { + VectorSearchError::IndexCreation(format!("Failed to reserve capacity: {}", e)) + }) } /// Add a vector with a string key diff --git a/crates/vestige-core/src/storage/migrations.rs b/crates/vestige-core/src/storage/migrations.rs index 851f65e..566561f 100644 --- a/crates/vestige-core/src/storage/migrations.rs +++ b/crates/vestige-core/src/storage/migrations.rs @@ -49,6 +49,16 @@ pub const MIGRATIONS: &[Migration] = &[ description: "v2.0.0 Cognitive Leap: emotional memory, flashbulb encoding, temporal hierarchy", up: MIGRATION_V9_UP, }, + Migration { + version: 10, + description: "v2.0.5 Intentional Amnesia: active forgetting — top-down suppression (Anderson 2025 + Davis Rac1)", + up: MIGRATION_V10_UP, + }, + Migration { + version: 11, + description: "v2.0.7 Cleanup: drop dead knowledge_edges and compressed_memories tables", + up: MIGRATION_V11_UP, + }, ]; /// A database migration @@ -315,7 +325,10 @@ const MIGRATION_V4_UP: &str = r#" -- TEMPORAL KNOWLEDGE GRAPH (Like Zep's Graphiti) -- ============================================================================ --- Knowledge edges for temporal reasoning +-- DEPRECATED (v2.0.5): knowledge_edges is unused. All graph edges use +-- memory_connections (migration V3). This table was designed for bi-temporal +-- edge support but was never wired. Retained for schema compatibility with +-- existing databases. Do NOT add queries against this table. CREATE TABLE IF NOT EXISTS knowledge_edges ( id TEXT PRIMARY KEY, source_id TEXT NOT NULL, @@ -605,6 +618,69 @@ ALTER TABLE dream_history ADD COLUMN creative_connections_found INTEGER DEFAULT UPDATE schema_version SET version = 9, applied_at = datetime('now'); "#; +/// V10: v2.0.5 Intentional Amnesia — Top-Down Active Forgetting +/// +/// Adds columns to `knowledge_nodes` for user-initiated suppression distinct +/// from passive FSRS decay and from bottom-up retrieval-induced forgetting +/// (which lives on `memory_states.suppression_until`). These columns are +/// incremented by the `suppress` MCP tool (tool #24) and consumed by the +/// search scoring stage + background Rac1 cascade worker. +/// +/// References: +/// - Anderson et al. (2025). Brain mechanisms underlying the inhibitory +/// control of thought. Nat Rev Neurosci. DOI 10.1038/s41583-025-00929-y +/// - Cervantes-Sandoval & Davis (2020). Rac1 Impairs Forgetting-Induced +/// Cellular Plasticity. Front Cell Neurosci. PMC7477079 +const MIGRATION_V10_UP: &str = r#" +-- Top-down suppression count (Suppression-Induced Forgetting, Anderson 2025). +-- Compounds with each `suppress` call, saturates via the k × count formula +-- in active_forgetting::retrieval_penalty(). +ALTER TABLE knowledge_nodes ADD COLUMN suppression_count INTEGER DEFAULT 0; + +-- Timestamp of the most recent suppression. Used for the 24h labile window +-- (reversal is allowed only while (now - suppressed_at) < labile_hours). +ALTER TABLE knowledge_nodes ADD COLUMN suppressed_at TEXT; + +-- Partial indices — only materialise rows actually involved in suppression. +CREATE INDEX IF NOT EXISTS idx_nodes_suppression_count + ON knowledge_nodes(suppression_count) + WHERE suppression_count > 0; + +CREATE INDEX IF NOT EXISTS idx_nodes_suppressed_at + ON knowledge_nodes(suppressed_at) + WHERE suppressed_at IS NOT NULL; + +UPDATE schema_version SET version = 10, applied_at = datetime('now'); +"#; + +/// V11: v2.0.7 Cleanup — drop tables that were added speculatively and never used +/// +/// Two tables from V4 were created but never had a single INSERT or SELECT in +/// the codebase: +/// +/// 1. `knowledge_edges` — an elaborate bi-temporal edge schema (valid_from, +/// valid_until, confidence, created_by). Was marked DEPRECATED in the same +/// V4 migration that created it. The real edge table is `memory_connections` +/// (V3), which is what the graph traversal code actually uses. +/// +/// 2. `compressed_memories` — a tiered-compression feature (compression_ratio, +/// semantic_fidelity, model_used). `advanced/compression.rs` operates +/// entirely in-memory and never touches this table. Dropping the schema +/// frees space for future migrations and removes dead schema debt. +/// +/// Both tables are verified single-file references (only in migrations.rs). +/// A grep across the entire crates/ tree shows zero INSERT, SELECT, or row +/// mapping against either table. Safe to drop without behaviour change. +const MIGRATION_V11_UP: &str = r#" +-- Drop the never-used bi-temporal edge table (real edges live in memory_connections). +DROP TABLE IF EXISTS knowledge_edges; + +-- Drop the never-used compression table (compression.rs is in-memory only). +DROP TABLE IF EXISTS compressed_memories; + +UPDATE schema_version SET version = 11, applied_at = datetime('now'); +"#; + /// Get current schema version from database pub fn get_current_version(conn: &rusqlite::Connection) -> rusqlite::Result { conn.query_row( @@ -645,3 +721,71 @@ pub fn apply_migrations(conn: &rusqlite::Connection) -> rusqlite::Result { Ok(applied) } + +#[cfg(test)] +mod tests { + use super::*; + + /// A fresh in-memory DB must end up at schema_version = highest migration + /// 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() { + 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 + let version = get_current_version(&conn).expect("read schema_version"); + assert_eq!(version, 11, "schema_version must be 11 after all migrations"); + + // 2. knowledge_edges is gone (V11 drops it) + let knowledge_edges_rows: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='knowledge_edges'", + [], + |row| row.get(0), + ) + .expect("query sqlite_master"); + assert_eq!( + knowledge_edges_rows, 0, + "knowledge_edges table must be dropped by V11" + ); + + // 3. compressed_memories is gone (V11 drops it) + let compressed_memories_rows: i64 = conn + .query_row( + "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='compressed_memories'", + [], + |row| row.get(0), + ) + .expect("query sqlite_master"); + assert_eq!( + compressed_memories_rows, 0, + "compressed_memories table must be dropped by V11" + ); + } + + /// V11 must be idempotent on replay — if the tables were already dropped + /// (e.g. a user ran v2.0.7, downgraded, then upgraded again), re-running + /// the migration must not error. `DROP TABLE IF EXISTS` handles this but + /// we enforce it with an explicit test so a future refactor to plain + /// `DROP TABLE` would be caught. + #[test] + fn test_v11_is_idempotent_on_replay() { + let conn = rusqlite::Connection::open_in_memory().expect("open in-memory"); + apply_migrations(&conn).expect("first apply_migrations succeeds"); + + // Force schema_version back to 10 so V11 runs again even though its + // changes are already applied. + conn.execute("UPDATE schema_version SET version = 10", []) + .expect("rewind schema_version"); + + // Replay must not error. + 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"); + } +} diff --git a/crates/vestige-core/src/storage/sqlite.rs b/crates/vestige-core/src/storage/sqlite.rs index f3a5681..398db9f 100644 --- a/crates/vestige-core/src/storage/sqlite.rs +++ b/crates/vestige-core/src/storage/sqlite.rs @@ -6,7 +6,7 @@ use chrono::{DateTime, Duration, Utc}; use directories::ProjectDirs; #[cfg(feature = "embeddings")] use lru::LruCache; -use rusqlite::{params, Connection, OptionalExtension}; +use rusqlite::{Connection, OptionalExtension, params}; #[cfg(feature = "embeddings")] use std::num::NonZeroUsize; use std::path::PathBuf; @@ -14,22 +14,20 @@ use std::sync::Mutex; use uuid::Uuid; use crate::fsrs::{ - retrievability_with_decay, DEFAULT_DECAY, - FSRSScheduler, FSRSState, LearningState, Rating, + DEFAULT_DECAY, FSRSScheduler, FSRSState, LearningState, Rating, retrievability_with_decay, }; +use crate::fts::sanitize_fts5_query; use crate::memory::{ - ConsolidationResult, IngestInput, KnowledgeNode, MemoryStats, - RecallInput, SearchMode, + ConsolidationResult, IngestInput, KnowledgeNode, MemoryStats, RecallInput, SearchMode, }; #[cfg(all(feature = "embeddings", feature = "vector-search"))] use crate::memory::{EmbeddingResult, MatchType, SearchResult, SimilarityResult}; -use crate::fts::sanitize_fts5_query; #[cfg(feature = "embeddings")] -use crate::embeddings::{matryoshka_truncate, Embedding, EmbeddingService, EMBEDDING_DIMENSIONS}; +use crate::embeddings::{EMBEDDING_DIMENSIONS, Embedding, EmbeddingService, matryoshka_truncate}; #[cfg(feature = "vector-search")] -use crate::search::{linear_combination, VectorIndex}; +use crate::search::{VectorIndex, linear_combination}; #[cfg(all(feature = "embeddings", feature = "vector-search"))] use crate::search::hyde; @@ -208,11 +206,12 @@ impl Storage { /// Load existing embeddings into vector index #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn load_embeddings_into_index(&self) -> Result<()> { - let reader = self.reader.lock() + 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 FROM node_embeddings")?; let embeddings: Vec<(String, Vec)> = stmt .query_map([], |row| Ok((row.get(0)?, row.get(1)?)))? @@ -227,6 +226,7 @@ impl Storage { .lock() .map_err(|_| StorageError::Init("Vector index lock poisoned".to_string()))?; + let mut load_failures = 0u32; for (node_id, embedding_bytes) in embeddings { if let Some(embedding) = Embedding::from_bytes(&embedding_bytes) { // Handle Matryoshka migration: old 768-dim → truncate to 256-dim @@ -236,10 +236,18 @@ impl Storage { embedding.vector }; if let Err(e) = index.add(&node_id, &vector) { + load_failures += 1; tracing::warn!("Failed to load embedding for {}: {}", node_id, e); } } } + if load_failures > 0 { + tracing::error!( + count = load_failures, + "Vector index: {} embeddings failed to load", + load_failures + ); + } Ok(()) } @@ -249,7 +257,9 @@ impl Storage { let now = Utc::now(); let id = Uuid::new_v4().to_string(); - let fsrs_state = self.scheduler.lock() + let fsrs_state = self + .scheduler + .lock() .map_err(|_| StorageError::Init("Scheduler lock poisoned".into()))? .new_card(); @@ -266,7 +276,9 @@ impl Storage { let valid_until_str = input.valid_until.map(|dt| dt.to_rfc3339()); { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT INTO knowledge_nodes ( @@ -330,10 +342,7 @@ impl Storage { /// /// This solves the "bad vs good similar memory" problem. #[cfg(all(feature = "embeddings", feature = "vector-search"))] - pub fn smart_ingest( - &self, - input: IngestInput, - ) -> Result { + pub fn smart_ingest(&self, input: IngestInput) -> Result { use crate::advanced::prediction_error::{ CandidateMemory, GateDecision, PredictionErrorGate, UpdateType, }; @@ -390,7 +399,12 @@ impl Storage { let decision = gate.evaluate(&input.content, &new_embedding.vector, &candidates); match decision { - GateDecision::Create { prediction_error, related_memory_ids, reason, .. } => { + GateDecision::Create { + prediction_error, + related_memory_ids, + reason, + .. + } => { // Create new memory let node = self.ingest(input)?; Ok(SmartIngestResult { @@ -399,15 +413,28 @@ impl Storage { superseded_id: None, similarity: None, prediction_error: Some(prediction_error), - reason: format!("Created new memory: {:?}. Related: {:?}", reason, related_memory_ids), + reason: if related_memory_ids.is_empty() { + format!("Created new memory: {:?}", reason) + } else { + format!( + "Created new memory: {:?}. Semantically similar (not linked): {:?}", + reason, related_memory_ids + ) + }, }) } - GateDecision::Update { target_id, similarity, update_type, prediction_error } => { + GateDecision::Update { + target_id, + similarity, + update_type, + prediction_error, + } => { match update_type { UpdateType::Reinforce => { // Just strengthen the existing memory self.strengthen_on_access(&target_id)?; - let node = self.get_node(&target_id)? + let node = self + .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; Ok(SmartIngestResult { decision: "reinforce".to_string(), @@ -415,12 +442,14 @@ impl Storage { superseded_id: None, similarity: Some(similarity), prediction_error: Some(prediction_error), - reason: "Content nearly identical - reinforced existing memory".to_string(), + reason: "Content nearly identical - reinforced existing memory" + .to_string(), }) } UpdateType::Merge | UpdateType::Append => { // Update the existing memory with merged content - let existing = self.get_node(&target_id)? + let existing = self + .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; let merged_content = format!( @@ -433,7 +462,8 @@ impl Storage { self.update_node_content(&target_id, &merged_content)?; self.strengthen_on_access(&target_id)?; - let node = self.get_node(&target_id)? + let node = self + .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; Ok(SmartIngestResult { @@ -448,7 +478,8 @@ impl Storage { UpdateType::Replace => { // Replace content entirely self.update_node_content(&target_id, &input.content)?; - let node = self.get_node(&target_id)? + let node = self + .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; Ok(SmartIngestResult { @@ -462,17 +493,16 @@ impl Storage { } UpdateType::AddContext => { // Add as context without modifying main content - let existing = self.get_node(&target_id)? + let existing = self + .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; - let merged_content = format!( - "{}\n\n---\nContext: {}", - existing.content, - input.content - ); + let merged_content = + format!("{}\n\n---\nContext: {}", existing.content, input.content); self.update_node_content(&target_id, &merged_content)?; - let node = self.get_node(&target_id)? + let node = self + .get_node(&target_id)? .ok_or_else(|| StorageError::NotFound(target_id.clone()))?; Ok(SmartIngestResult { @@ -486,7 +516,12 @@ impl Storage { } } } - GateDecision::Supersede { old_memory_id, similarity, supersede_reason, prediction_error } => { + GateDecision::Supersede { + old_memory_id, + similarity, + supersede_reason, + prediction_error, + } => { // Demote the old memory and create new self.demote_memory(&old_memory_id)?; @@ -502,7 +537,11 @@ impl Storage { reason: format!("New memory supersedes old: {:?}", supersede_reason), }) } - GateDecision::Merge { memory_ids, avg_similarity, strategy } => { + GateDecision::Merge { + memory_ids, + avg_similarity, + strategy, + } => { // For now, create new and link to existing let node = self.ingest(input)?; @@ -512,7 +551,11 @@ impl Storage { superseded_id: None, similarity: Some(avg_similarity), prediction_error: Some(1.0 - avg_similarity), - reason: format!("Created new memory linked to {} similar memories ({:?})", memory_ids.len(), strategy), + reason: format!( + "Created new memory linked to {} similar memories ({:?})", + memory_ids.len(), + strategy + ), }) } } @@ -521,28 +564,29 @@ impl Storage { /// Get the embedding vector for a node #[cfg(all(feature = "embeddings", feature = "vector-search"))] pub fn get_node_embedding(&self, node_id: &str) -> Result>> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT embedding FROM node_embeddings WHERE node_id = ?1" - )?; + let mut stmt = + reader.prepare("SELECT embedding FROM node_embeddings WHERE node_id = ?1")?; let embedding_bytes: Option> = stmt .query_row(params![node_id], |row| row.get(0)) .optional()?; - Ok(embedding_bytes.and_then(|bytes| { - crate::embeddings::Embedding::from_bytes(&bytes).map(|e| e.vector) - })) + Ok(embedding_bytes + .and_then(|bytes| crate::embeddings::Embedding::from_bytes(&bytes).map(|e| e.vector))) } /// Get all embedding vectors for duplicate detection #[cfg(all(feature = "embeddings", feature = "vector-search"))] pub fn get_all_embeddings(&self) -> Result)>> { - let reader = self.reader.lock() + 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 FROM node_embeddings")?; let results: Vec<(String, Vec)> = stmt .query_map([], |row| { @@ -552,8 +596,7 @@ impl Storage { })? .filter_map(|r| r.ok()) .filter_map(|(id, bytes)| { - crate::embeddings::Embedding::from_bytes(&bytes) - .map(|e| (id, e.vector)) + crate::embeddings::Embedding::from_bytes(&bytes).map(|e| (id, e.vector)) }) .collect(); @@ -565,7 +608,9 @@ impl Storage { let now = Utc::now(); { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET content = ?1, updated_at = ?2 WHERE id = ?3", @@ -604,7 +649,9 @@ impl Storage { let now = Utc::now(); { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT OR REPLACE INTO node_embeddings (node_id, embedding, dimensions, model, created_at) @@ -637,14 +684,13 @@ impl Storage { /// Get a node by ID pub fn get_node(&self, id: &str) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader - .prepare("SELECT * FROM knowledge_nodes WHERE id = ?1")?; + let mut stmt = reader.prepare("SELECT * FROM knowledge_nodes WHERE id = ?1")?; - let node = stmt - .query_row(params![id], Self::row_to_node) - .optional()?; + let node = stmt.query_row(params![id], Self::row_to_node).optional()?; Ok(node) } @@ -667,7 +713,13 @@ impl Storage { /// Convert a row to KnowledgeNode fn row_to_node(row: &rusqlite::Row) -> rusqlite::Result { let tags_json: String = row.get("tags")?; - let tags: Vec = serde_json::from_str(&tags_json).unwrap_or_default(); + let tags: Vec = match serde_json::from_str(&tags_json) { + Ok(t) => t, + Err(e) => { + tracing::warn!(raw = %tags_json, "Failed to deserialize tags JSON, using empty: {}", e); + Vec::new() + } + }; let created_at: String = row.get("created_at")?; let updated_at: String = row.get("updated_at")?; @@ -702,6 +754,15 @@ impl Storage { let has_embedding: Option = row.get("has_embedding").ok(); let embedding_model: Option = row.get("embedding_model").ok().flatten(); + // v2.0.5 Active Forgetting columns (Migration V10) + let suppression_count: i32 = row.get("suppression_count").unwrap_or(0); + let suppressed_at_str: Option = row.get("suppressed_at").ok().flatten(); + let suppressed_at = suppressed_at_str.and_then(|s| { + DateTime::parse_from_rfc3339(&s) + .map(|dt| dt.with_timezone(&Utc)) + .ok() + }); + Ok(KnowledgeNode { id: row.get("id")?, content: row.get("content")?, @@ -731,7 +792,13 @@ impl Storage { times_useful: row.get("times_useful").ok(), emotional_valence: row.get("emotional_valence").ok(), flashbulb: row.get::<_, Option>("flashbulb").ok().flatten(), - temporal_level: row.get::<_, Option>("temporal_level").ok().flatten(), + temporal_level: row + .get::<_, Option>("temporal_level") + .ok() + .flatten(), + // v2.0.5 Active Forgetting + suppression_count, + suppressed_at, }) } @@ -772,7 +839,9 @@ impl Storage { ) -> Result> { let sanitized_query = sanitize_fts5_query(query); - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT n.* FROM knowledge_nodes n @@ -816,7 +885,9 @@ impl Storage { scheduled_days: 0, }; - let scheduler = self.scheduler.lock() + let scheduler = self + .scheduler + .lock() .map_err(|_| StorageError::Init("Scheduler lock poisoned".into()))?; let elapsed_days = scheduler.days_since_review(¤t_state.last_review); @@ -826,8 +897,7 @@ impl Storage { None }; - let result = scheduler - .review(¤t_state, rating, elapsed_days, sentiment_boost); + let result = scheduler.review(¤t_state, rating, elapsed_days, sentiment_boost); drop(scheduler); let now = Utc::now(); @@ -844,7 +914,9 @@ impl Storage { (new_retrieval_strength * 0.7) + ((new_storage_strength / 10.0).min(1.0) * 0.3); { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET @@ -892,7 +964,9 @@ impl Storage { // Primary boost on the accessed node { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET @@ -927,7 +1001,9 @@ impl Storage { drop(index); if let Ok(neighbors) = neighbors_result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; for (neighbor_id, similarity) in neighbors { if neighbor_id == id || similarity < 0.7 { @@ -955,6 +1031,8 @@ impl Storage { pub fn strengthen_batch_on_access(&self, ids: &[&str]) -> Result<()> { for id in ids { self.strengthen_on_access(id)?; + // Also record access in memory_states for audit trail (Bug #1 fix) + let _ = self.record_memory_access(id); } Ok(()) } @@ -964,7 +1042,9 @@ impl Storage { /// /// Increments `times_useful` and recomputes `utility_score = times_useful / times_retrieved`. pub fn mark_memory_useful(&self, id: &str) -> Result<()> { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET @@ -982,7 +1062,9 @@ impl Storage { /// Log a memory access event for ACT-R activation computation fn log_access(&self, node_id: &str, access_type: &str) -> Result<()> { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT INTO memory_access_log (node_id, access_type, accessed_at) @@ -1000,7 +1082,9 @@ impl Storage { // Strong boost: +0.2 retrieval, +0.1 retention { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET @@ -1030,7 +1114,9 @@ impl Storage { // Strong penalty: -0.3 retrieval, -0.15 retention, halve stability { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET @@ -1049,11 +1135,218 @@ impl Storage { .ok_or_else(|| StorageError::NotFound(id.to_string())) } + // ======================================================================== + // Active Forgetting (v2.0.5 — Anderson 2025 + Davis Rac1) + // ======================================================================== + + /// Top-down memory suppression (Suppression-Induced Forgetting). + /// + /// Distinct from `delete` (which removes the row) and from + /// `demote_memory` (which is a single thumb-down hit). Each call + /// compounds: `suppression_count` is incremented, `suppressed_at` is + /// bumped to now, and FSRS state is dealt a strong blow: + /// + /// - `retrieval_strength -= 0.35` (stronger than demote's -0.30) + /// - `retention_strength -= 0.20` + /// - `stability *= 0.4` + /// + /// Reversible within a 24-hour labile window via + /// [`Self::reverse_suppression`]. + /// + /// Reference: Anderson et al. (2025). Brain mechanisms underlying the + /// inhibitory control of thought. *Nature Reviews Neuroscience*. + /// DOI: 10.1038/s41583-025-00929-y + pub fn suppress_memory(&self, id: &str) -> Result { + let now = Utc::now(); + { + let writer = self + .writer + .lock() + .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; + writer.execute( + "UPDATE knowledge_nodes SET + last_accessed = ?1, + suppression_count = COALESCE(suppression_count, 0) + 1, + suppressed_at = ?1, + retrieval_strength = MAX(0.05, retrieval_strength - 0.35), + retention_strength = MAX(0.05, retention_strength - 0.20), + stability = stability * 0.4 + WHERE id = ?2", + params![now.to_rfc3339(), id], + )?; + } + + let _ = self.log_access(id, "suppress"); + + self.get_node(id)? + .ok_or_else(|| StorageError::NotFound(id.to_string())) + } + + /// Reverse a previous suppression if within the 24-hour labile window. + /// + /// Returns `Err(StorageError::NotFound)` if the memory has never been + /// suppressed, or `Err(StorageError::Init)` with a "labile window expired" + /// message if more than `labile_hours` have passed. Matches Nader + /// reconsolidation semantics on a 24h axis. + pub fn reverse_suppression(&self, id: &str, labile_hours: i64) -> Result { + let node = self + .get_node(id)? + .ok_or_else(|| StorageError::NotFound(id.to_string()))?; + + let suppressed_at = node.suppressed_at.ok_or_else(|| { + StorageError::Init(format!( + "memory {} has no active suppression to reverse", + id + )) + })?; + + let elapsed = Utc::now() - suppressed_at; + if elapsed >= chrono::Duration::hours(labile_hours) { + return Err(StorageError::Init(format!( + "labile window expired ({}h since suppression; limit {}h)", + elapsed.num_hours(), + labile_hours + ))); + } + + { + let writer = self + .writer + .lock() + .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; + writer.execute( + "UPDATE knowledge_nodes SET + suppression_count = MAX(0, COALESCE(suppression_count, 0) - 1), + suppressed_at = CASE + WHEN COALESCE(suppression_count, 0) - 1 <= 0 THEN NULL + ELSE suppressed_at + END, + retrieval_strength = MIN(1.0, retrieval_strength + 0.15), + retention_strength = MIN(1.0, retention_strength + 0.10), + stability = stability * 1.25 + WHERE id = ?1", + params![id], + )?; + } + + let _ = self.log_access(id, "reverse_suppress"); + + self.get_node(id)? + .ok_or_else(|| StorageError::NotFound(id.to_string())) + } + + /// Count memories currently in a suppressed state (suppression_count > 0). + pub fn count_suppressed(&self) -> Result { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let count: i64 = reader.query_row( + "SELECT COUNT(*) FROM knowledge_nodes WHERE COALESCE(suppression_count, 0) > 0", + [], + |row| row.get(0), + )?; + Ok(count.max(0) as usize) + } + + /// Fetch memories suppressed within the last `window_hours` (still within + /// the labile window). Used by the Rac1 cascade sweep. + pub fn get_recently_suppressed(&self, window_hours: i64) -> Result> { + let cutoff = (Utc::now() - chrono::Duration::hours(window_hours)).to_rfc3339(); + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare( + "SELECT * FROM knowledge_nodes + WHERE suppressed_at IS NOT NULL AND suppressed_at >= ?1", + )?; + let rows = stmt.query_map(params![cutoff], Self::row_to_node)?; + let mut result = Vec::new(); + for row in rows { + result.push(row?); + } + Ok(result) + } + + /// Apply one-hop Rac1 cascade from a single suppressed seed memory: + /// walk `memory_connections` edges and attenuate neighbor FSRS state + /// proportional to edge strength. + /// + /// Returns the number of neighbors affected. + /// + /// Reference: Cervantes-Sandoval & Davis (2020). Rac1 Impairs Forgetting- + /// Induced Cellular Plasticity in Mushroom Body Output Neurons. + /// *Front Cell Neurosci*. PMC7477079 + pub fn apply_rac1_cascade(&self, seed_id: &str) -> Result { + use crate::neuroscience::active_forgetting::ActiveForgettingSystem; + let sys = ActiveForgettingSystem::new(); + + let edges = self.get_connections_for_memory(seed_id)?; + if edges.is_empty() { + return Ok(0); + } + + let writer = self + .writer + .lock() + .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; + + let mut affected = 0usize; + for edge in edges.iter().take(100) { + let neighbor_id = if edge.source_id == seed_id { + &edge.target_id + } else { + &edge.source_id + }; + + // Never cascade back into the suppressed seed + if neighbor_id == seed_id { + continue; + } + + let stability_factor = sys.cascade_stability_factor(edge.strength); + let retrieval_decrement = sys.cascade_retrieval_decrement(edge.strength); + + let rows = writer.execute( + "UPDATE knowledge_nodes SET + stability = MAX(0.1, stability * ?1), + retrieval_strength = MAX(0.05, retrieval_strength - ?2) + WHERE id = ?3 AND COALESCE(suppression_count, 0) = 0", + params![stability_factor, retrieval_decrement, neighbor_id], + )?; + affected += rows; + } + + Ok(affected) + } + + /// Sweep all recently-suppressed memories and apply Rac1 cascade to each. + /// Intended to run from the background consolidation loop every tick. + /// + /// Returns `(seeds_processed, neighbors_affected)`. + pub fn run_rac1_cascade_sweep(&self) -> Result<(usize, usize)> { + // 72h keeps the cascade window slightly longer than the 24h labile + // reversibility window — so suppressions that lock in continue to + // propagate for 48h after they become irreversible. + let seeds = self.get_recently_suppressed(72)?; + let mut total_affected = 0usize; + for seed in &seeds { + match self.apply_rac1_cascade(&seed.id) { + Ok(n) => total_affected += n, + Err(e) => tracing::warn!("Rac1 cascade failed for {}: {}", seed.id, e), + } + } + Ok((seeds.len(), total_affected)) + } + /// Get memories due for review pub fn get_review_queue(&self, limit: i32) -> Result> { let now = Utc::now().to_rfc3339(); - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM knowledge_nodes @@ -1093,7 +1386,9 @@ impl Storage { scheduled_days: 0, }; - let scheduler = self.scheduler.lock() + let scheduler = self + .scheduler + .lock() .map_err(|_| StorageError::Init("Scheduler lock poisoned".into()))?; let elapsed_days = scheduler.days_since_review(¤t_state.last_review); @@ -1104,12 +1399,13 @@ impl Storage { pub fn get_stats(&self) -> Result { let now = Utc::now().to_rfc3339(); - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let total: i64 = - reader - .query_row("SELECT COUNT(*) FROM knowledge_nodes", [], |row| row.get(0))?; + reader.query_row("SELECT COUNT(*) FROM knowledge_nodes", [], |row| row.get(0))?; let due: i64 = reader.query_row( "SELECT COUNT(*) FROM knowledge_nodes WHERE next_review <= ?1", @@ -1182,17 +1478,19 @@ impl Storage { /// Delete a node pub fn delete_node(&self, id: &str) -> Result { - let writer = self.writer.lock() + let 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 rows = writer.execute("DELETE FROM knowledge_nodes WHERE id = ?1", params![id])?; // Clean up vector index to prevent stale search results #[cfg(all(feature = "embeddings", feature = "vector-search"))] if rows > 0 - && let Ok(mut index) = self.vector_index.lock() { - let _ = index.remove(id); - } + && let Ok(mut index) = self.vector_index.lock() + { + let _ = index.remove(id); + } Ok(rows > 0) } @@ -1201,7 +1499,9 @@ impl Storage { pub fn search(&self, query: &str, limit: i32) -> Result> { let sanitized_query = sanitize_fts5_query(query); - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT n.* FROM knowledge_nodes n @@ -1220,9 +1520,43 @@ 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) + } + /// Get all nodes (paginated) pub fn get_all_nodes(&self, limit: i32, offset: i32) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM knowledge_nodes @@ -1249,7 +1583,9 @@ impl Storage { tag_filter: Option<&str>, limit: i32, ) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; match tag_filter { Some(tag) => { @@ -1320,7 +1656,9 @@ impl Storage { fn get_query_embedding(&self, query: &str) -> Result> { // Check cache first { - let mut cache = self.query_cache.lock() + let mut cache = self + .query_cache + .lock() .map_err(|_| StorageError::Init("Query cache lock poisoned".to_string()))?; if let Some(cached) = cache.get(query) { return Ok(cached.clone()); @@ -1328,12 +1666,16 @@ impl Storage { } // Not in cache, compute embedding - let embedding = self.embedding_service.embed(query) + let embedding = self + .embedding_service + .embed(query) .map_err(|e| StorageError::Init(format!("Failed to embed query: {}", e)))?; // Store in cache { - let mut cache = self.query_cache.lock() + let mut cache = self + .query_cache + .lock() .map_err(|_| StorageError::Init("Query cache lock poisoned".to_string()))?; cache.put(query.to_string(), embedding.vector.clone()); } @@ -1375,7 +1717,7 @@ impl Storage { Ok(similarity_results) } - /// Hybrid search + /// Hybrid search (delegates to hybrid_search_filtered with no type filters) #[cfg(all(feature = "embeddings", feature = "vector-search"))] pub fn hybrid_search( &self, @@ -1384,24 +1726,73 @@ impl Storage { keyword_weight: f32, semantic_weight: f32, ) -> Result> { - let keyword_results = self.keyword_search_with_scores(query, limit * 2)?; + self.hybrid_search_filtered(query, limit, keyword_weight, semantic_weight, None, None) + } + + /// Hybrid search with optional type filtering pushed into the storage layer. + /// + /// When `include_types` is `Some`, only nodes whose `node_type` matches one of + /// the given strings are returned. When `exclude_types` is `Some`, nodes whose + /// `node_type` matches are excluded. `include_types` takes precedence over + /// `exclude_types`. Both are case-sensitive and compared against the stored + /// `node_type` value. + #[cfg(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 has_type_filter = include_types.is_some() || exclude_types.is_some(); + // Over-fetch more aggressively when type filters are active so that + // after filtering we still have enough candidates to fill `limit`. + let overfetch_factor = if has_type_filter { 4 } else { 2 }; + + let keyword_results = self.keyword_search_with_scores( + query, + limit * overfetch_factor, + include_types, + exclude_types, + )?; let semantic_results = if self.embedding_service.is_ready() { - self.semantic_search_raw(query, limit * 2)? + self.semantic_search_raw(query, limit * overfetch_factor)? } else { vec![] }; let combined = if !semantic_results.is_empty() { - linear_combination(&keyword_results, &semantic_results, keyword_weight, semantic_weight) + linear_combination( + &keyword_results, + &semantic_results, + keyword_weight, + semantic_weight, + ) } else { keyword_results.clone() }; let mut results = Vec::with_capacity(limit as usize); - for (node_id, combined_score) in combined.into_iter().take(limit as usize) { + for (node_id, combined_score) in combined.into_iter() { + if results.len() >= limit as usize { + break; + } if let Some(node) = self.get_node(&node_id)? { + // Apply type filtering for results that came from semantic search + // (keyword search already filters in SQL, but semantic search cannot) + 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 keyword_score = keyword_results .iter() .find(|(id, _)| id == &node_id) @@ -1444,12 +1835,16 @@ impl Storage { // ACT-R activation as importance signal (pre-computed during consolidation) let activation: f64 = self - .reader.lock() - .map(|r| r.query_row( - "SELECT COALESCE(activation, 0.0) FROM knowledge_nodes WHERE id = ?1", - params![result.node.id], - |row| row.get(0), - ).unwrap_or(0.0)) + .reader + .lock() + .map(|r| { + r.query_row( + "SELECT COALESCE(activation, 0.0) FROM knowledge_nodes WHERE id = ?1", + params![result.node.id], + |row| row.get(0), + ) + .unwrap_or(0.0) + }) .unwrap_or(0.0); // Normalize ACT-R activation [-2, 5] → [0, 1] let importance = ((activation + 2.0) / 7.0).clamp(0.0, 1.0); @@ -1469,23 +1864,76 @@ impl Storage { Ok(results) } - /// Keyword search returning scores + /// Keyword search returning scores, with optional type filtering in the SQL query. #[cfg(all(feature = "embeddings", feature = "vector-search"))] - fn keyword_search_with_scores(&self, query: &str, limit: i32) -> Result> { - let sanitized_query = sanitize_fts5_query(query); + fn keyword_search_with_scores( + &self, + query: &str, + limit: i32, + include_types: Option<&[String]>, + exclude_types: Option<&[String]>, + ) -> Result> { + // 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![]); + }; - let reader = self.reader.lock() - .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( + // Build the type filter clause and collect parameter values. + // We use numbered parameters: ?1 = query, ?2 = limit, ?3.. = type strings. + let mut type_clause = String::new(); + let type_values: Vec<&str>; + + if let Some(includes) = include_types { + if !includes.is_empty() { + let placeholders: Vec = + (0..includes.len()).map(|i| format!("?{}", i + 3)).collect(); + type_clause = format!(" AND n.node_type IN ({})", placeholders.join(",")); + type_values = includes.iter().map(|s| s.as_str()).collect(); + } else { + type_values = vec![]; + } + } else if let Some(excludes) = exclude_types { + if !excludes.is_empty() { + let placeholders: Vec = + (0..excludes.len()).map(|i| format!("?{}", i + 3)).collect(); + type_clause = format!(" AND n.node_type NOT IN ({})", placeholders.join(",")); + type_values = excludes.iter().map(|s| s.as_str()).collect(); + } else { + type_values = vec![]; + } + } else { + type_values = vec![]; + } + + let sql = format!( "SELECT n.id, rank FROM knowledge_nodes n JOIN knowledge_fts fts ON n.id = fts.id - WHERE knowledge_fts MATCH ?1 + WHERE knowledge_fts MATCH ?1{} ORDER BY rank LIMIT ?2", - )?; + type_clause + ); + + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare(&sql)?; + + // Build the parameter list: [query, limit, ...type_values] + let mut param_values: Vec> = Vec::new(); + 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())); + } + let params_ref: Vec<&dyn rusqlite::ToSql> = + param_values.iter().map(|p| p.as_ref()).collect(); let results: Vec<(String, f32)> = stmt - .query_map(params![sanitized_query, limit], |row| { + .query_map(params_ref.as_slice(), |row| { Ok((row.get::<_, String>(0)?, row.get::<_, f64>(1)? as f32)) })? .filter_map(|r| r.ok()) @@ -1562,7 +2010,9 @@ impl Storage { let mut result = EmbeddingResult::default(); let nodes: Vec<(String, String)> = { - let reader = self.reader.lock() + 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(","); @@ -1587,8 +2037,7 @@ impl Storage { } result_nodes } else if force { - let mut stmt = reader - .prepare("SELECT id, content FROM knowledge_nodes")?; + 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)?)) })?; @@ -1608,7 +2057,8 @@ impl Storage { for (id, content) in nodes { if !force { let has_emb: i32 = self - .reader.lock() + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))? .query_row( "SELECT COALESCE(has_embedding, 0) FROM knowledge_nodes WHERE id = ?1", @@ -1643,7 +2093,9 @@ impl Storage { ) -> Result> { let timestamp = point_in_time.to_rfc3339(); - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM knowledge_nodes @@ -1662,59 +2114,77 @@ impl Storage { Ok(result) } - /// Query memories created/modified in a time range + /// Query memories created/modified in a time range, optionally filtered by + /// `node_type` and/or `tags`. + /// + /// All filters are pushed into the SQL `WHERE` clause so that `LIMIT` is + /// applied AFTER filtering. If filters were applied in Rust after `LIMIT`, + /// sparse types/tags could be crowded out by a dominant set within the + /// limit window — e.g. a query for a rare tag against a corpus where + /// every day has hundreds of rows with a common tag would return 0 + /// matches after `LIMIT` crowded the rare-tag rows out. + /// + /// Tag filtering uses `tags LIKE '%"tag"%'` — an exact-match JSON pattern + /// that keys off the quote characters around each tag in the stored JSON + /// array. This avoids the substring-match false positive where `alpha` + /// would otherwise match `alphabet`. pub fn query_time_range( &self, start: Option>, end: Option>, limit: i32, + node_type: Option<&str>, + tags: Option<&[String]>, ) -> Result> { let start_str = start.map(|dt| dt.to_rfc3339()); let end_str = end.map(|dt| dt.to_rfc3339()); - let (query, params): (&str, Vec>) = match (&start_str, &end_str) { - (Some(s), Some(e)) => ( - "SELECT * FROM knowledge_nodes - WHERE created_at >= ?1 AND created_at <= ?2 - ORDER BY created_at DESC - LIMIT ?3", - vec![ - Box::new(s.clone()) as Box, - Box::new(e.clone()) as Box, - Box::new(limit) as Box, - ], - ), - (Some(s), None) => ( - "SELECT * FROM knowledge_nodes - WHERE created_at >= ?1 - ORDER BY created_at DESC - LIMIT ?2", - vec![ - Box::new(s.clone()) as Box, - Box::new(limit) as Box, - ], - ), - (None, Some(e)) => ( - "SELECT * FROM knowledge_nodes - WHERE created_at <= ?1 - ORDER BY created_at DESC - LIMIT ?2", - vec![ - Box::new(e.clone()) as Box, - Box::new(limit) as Box, - ], - ), - (None, None) => ( - "SELECT * FROM knowledge_nodes - ORDER BY created_at DESC - LIMIT ?1", - vec![Box::new(limit) as Box], - ), + let mut conditions: Vec = Vec::new(); + let mut params: Vec> = Vec::new(); + let mut idx = 1; + + if let Some(ref s) = start_str { + conditions.push(format!("created_at >= ?{}", idx)); + params.push(Box::new(s.clone()) as Box); + idx += 1; + } + if let Some(ref e) = end_str { + conditions.push(format!("created_at <= ?{}", idx)); + params.push(Box::new(e.clone()) as Box); + idx += 1; + } + if let Some(nt) = node_type { + conditions.push(format!("LOWER(node_type) = LOWER(?{})", idx)); + params.push(Box::new(nt.to_string()) as Box); + idx += 1; + } + if let Some(tag_list) = tags.filter(|t| !t.is_empty()) { + let mut tag_conditions = Vec::new(); + for tag in tag_list { + tag_conditions.push(format!("tags LIKE ?{}", idx)); + params.push(Box::new(format!("%\"{}\"%", tag)) as Box); + idx += 1; + } + conditions.push(format!("({})", tag_conditions.join(" OR "))); + } + + let where_clause = if conditions.is_empty() { + String::new() + } else { + format!("WHERE {}", conditions.join(" AND ")) }; - let reader = self.reader.lock() + let query = format!( + "SELECT * FROM knowledge_nodes {} ORDER BY created_at DESC LIMIT ?{}", + where_clause, idx + ); + params.push(Box::new(limit) as Box); + + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare(query)?; + let mut stmt = reader.prepare(&query)?; let params_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect(); let nodes = stmt.query_map(params_refs.as_slice(), Self::row_to_node)?; @@ -1743,7 +2213,9 @@ impl Storage { loop { // Read batch using reader let batch: Vec<(String, String, f64, f64, f64, f64)> = { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; reader .prepare( @@ -1775,7 +2247,9 @@ impl Storage { // Write batch using writer transaction { - let mut writer = self.writer.lock() + let mut writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let tx = writer.transaction()?; @@ -1791,12 +2265,12 @@ impl Storage { let effective_stability = stability * (1.0 + sentiment_mag * 0.5); // Real FSRS-6 retrievability with personalized w20 - let new_retrieval = retrievability_with_decay( - effective_stability, days_since, w20, - ); + let new_retrieval = + retrievability_with_decay(effective_stability, days_since, w20); // Use SleepConsolidation for retention calculation - let new_retention = sleep.calculate_retention(*storage_strength, new_retrieval); + let new_retention = + sleep.calculate_retention(*storage_strength, new_retrieval); tx.execute( "UPDATE knowledge_nodes SET retrieval_strength = ?1, retention_strength = ?2 WHERE id = ?3", @@ -1817,7 +2291,9 @@ impl Storage { /// Read personalized w20 from fsrs_config table fn get_fsrs_w20(&self) -> Result { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; reader .query_row( @@ -1851,20 +2327,24 @@ impl Storage { let mut promoted = 0i64; { let candidates: Vec<(String, f64, f64)> = { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; reader .prepare( "SELECT id, sentiment_magnitude, storage_strength FROM knowledge_nodes - WHERE storage_strength < 10.0" + WHERE storage_strength < 10.0", )? .query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))? .filter_map(|r| r.ok()) .collect() }; - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; for (id, sentiment_mag, storage_strength) in &candidates { if sleep.should_promote(*sentiment_mag, *storage_strength) { @@ -1979,24 +2459,25 @@ impl Storage { { let service = crate::neuroscience::memory_states::StateUpdateService::new(); let all_nodes = self.get_all_nodes(500, 0).unwrap_or_default(); - let mut lifecycles: Vec = all_nodes - .iter() - .map(|n| { - let mut lc = crate::neuroscience::memory_states::MemoryLifecycle::new(); - lc.last_access = n.last_accessed; - lc.access_count = n.reps as u32; - lc.state = if n.retention_strength > 0.7 { - crate::neuroscience::memory_states::MemoryState::Active - } else if n.retention_strength > 0.3 { - crate::neuroscience::memory_states::MemoryState::Dormant - } else if n.retention_strength > 0.1 { - crate::neuroscience::memory_states::MemoryState::Silent - } else { - crate::neuroscience::memory_states::MemoryState::Unavailable - }; - lc - }) - .collect(); + let mut lifecycles: Vec = + all_nodes + .iter() + .map(|n| { + let mut lc = crate::neuroscience::memory_states::MemoryLifecycle::new(); + lc.last_access = n.last_accessed; + lc.access_count = n.reps as u32; + lc.state = if n.retention_strength > 0.7 { + crate::neuroscience::memory_states::MemoryState::Active + } else if n.retention_strength > 0.3 { + crate::neuroscience::memory_states::MemoryState::Dormant + } else if n.retention_strength > 0.1 { + crate::neuroscience::memory_states::MemoryState::Silent + } else { + crate::neuroscience::memory_states::MemoryState::Unavailable + }; + lc + }) + .collect(); let batch_result = service.batch_update(&mut lifecycles); _state_transitions = batch_result.total_transitions as i64; } @@ -2032,11 +2513,12 @@ impl Storage { // 16. FTS5 index optimization — merge segments for faster keyword search // 17. Run PRAGMA optimize to refresh query planner statistics { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; - let _ = writer.execute_batch( - "INSERT INTO knowledge_fts(knowledge_fts) VALUES('optimize');" - ); + let _ = writer + .execute_batch("INSERT INTO knowledge_fts(knowledge_fts) VALUES('optimize');"); let _ = writer.execute_batch("PRAGMA optimize;"); } @@ -2082,7 +2564,9 @@ impl Storage { // Record consolidation history { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let _ = writer.execute( "INSERT INTO consolidation_history (completed_at, duration_ms, memories_replayed, duplicates_merged, activations_computed, w20_optimized) @@ -2140,8 +2624,10 @@ impl Storage { if consumed.contains(&all_embeddings[j].0) { continue; } - let sim = - crate::embeddings::cosine_similarity(&all_embeddings[i].1, &all_embeddings[j].1); + let sim = crate::embeddings::cosine_similarity( + &all_embeddings[i].1, + &all_embeddings[j].1, + ); if sim >= SIMILARITY_THRESHOLD { cluster.push((j, sim)); } @@ -2153,7 +2639,9 @@ impl Storage { // Find the strongest node (highest retention_strength) let anchor_id = &all_embeddings[i].0; - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let anchor_retention: f64 = reader .query_row( @@ -2249,7 +2737,9 @@ impl Storage { let now = Utc::now(); let node_ids: Vec = { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; reader .prepare("SELECT DISTINCT node_id FROM memory_access_log")? @@ -2263,7 +2753,9 @@ impl Storage { } let mut count = 0i64; - let mut writer = self.writer.lock() + let mut writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let tx = writer.transaction()?; @@ -2309,7 +2801,9 @@ impl Storage { /// Prune old access log entries (keep last 90 days) fn prune_access_log(&self) -> Result { let cutoff = (Utc::now() - Duration::days(90)).to_rfc3339(); - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let deleted = writer.execute( "DELETE FROM memory_access_log WHERE accessed_at < ?1", @@ -2323,15 +2817,15 @@ impl Storage { fn optimize_w20_if_ready(&self) -> Result> { use crate::fsrs::{FSRSOptimizer, ReviewLog}; - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let access_count: i64 = reader - .query_row( - "SELECT COUNT(*) FROM memory_access_log", - [], - |row| row.get(0), - ) + .query_row("SELECT COUNT(*) FROM memory_access_log", [], |row| { + row.get(0) + }) .unwrap_or(0); if access_count < 100 { @@ -2399,7 +2893,9 @@ impl Storage { // Save to config { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT OR REPLACE INTO fsrs_config (key, value, updated_at) @@ -2408,7 +2904,10 @@ impl Storage { )?; } - tracing::info!(w20 = optimized_w20, "Personalized w20 optimized from access history"); + tracing::info!( + w20 = optimized_w20, + "Personalized w20 optimized from access history" + ); Ok(Some(optimized_w20)) } @@ -2417,13 +2916,16 @@ impl Storage { #[cfg(all(feature = "embeddings", feature = "vector-search"))] fn generate_missing_embeddings(&self) -> Result { if !self.embedding_service.is_ready() - && let Err(e) = self.embedding_service.init() { - tracing::warn!("Could not initialize embedding model: {}", e); - return Ok(0); - } + && let Err(e) = self.embedding_service.init() + { + tracing::warn!("Could not initialize embedding model: {}", e); + return Ok(0); + } let nodes: Vec<(String, String)> = { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; reader .prepare( @@ -2460,7 +2962,7 @@ pub struct IntentionRecord { pub id: String, pub content: String, pub trigger_type: String, - pub trigger_data: String, // JSON + pub trigger_data: String, // JSON pub priority: i32, pub status: String, pub created_at: DateTime, @@ -2524,7 +3026,7 @@ pub struct ConnectionRecord { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct MemoryStateRecord { pub memory_id: String, - pub state: String, // 'active', 'dormant', 'silent', 'unavailable' + pub state: String, // 'active', 'dormant', 'silent', 'unavailable' pub last_access: DateTime, pub access_count: i32, pub state_entered_at: DateTime, @@ -2585,9 +3087,12 @@ impl Storage { /// Save an intention to the database pub fn save_intention(&self, intention: &IntentionRecord) -> Result<()> { let tags_json = serde_json::to_string(&intention.tags).unwrap_or_else(|_| "[]".to_string()); - let related_json = serde_json::to_string(&intention.related_memories).unwrap_or_else(|_| "[]".to_string()); + let related_json = + serde_json::to_string(&intention.related_memories).unwrap_or_else(|_| "[]".to_string()); - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT OR REPLACE INTO intentions ( @@ -2620,11 +3125,11 @@ impl Storage { /// Get an intention by ID pub fn get_intention(&self, id: &str) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM intentions WHERE id = ?1" - )?; + let mut stmt = reader.prepare("SELECT * FROM intentions WHERE id = ?1")?; stmt.query_row(params![id], Self::row_to_intention) .optional() @@ -2633,7 +3138,9 @@ impl Storage { /// Get all active intentions pub fn get_active_intentions(&self) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM intentions WHERE status = 'active' ORDER BY priority DESC, created_at ASC" @@ -2649,10 +3156,12 @@ impl Storage { /// Get intentions by status pub fn get_intentions_by_status(&self, status: &str) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( - "SELECT * FROM intentions WHERE status = ?1 ORDER BY priority DESC, created_at ASC" + "SELECT * FROM intentions WHERE status = ?1 ORDER BY priority DESC, created_at ASC", )?; let rows = stmt.query_map(params![status], Self::row_to_intention)?; @@ -2666,9 +3175,15 @@ impl Storage { /// Update intention status pub fn update_intention_status(&self, id: &str, status: &str) -> Result { let now = Utc::now(); - let fulfilled_at = if status == "fulfilled" { Some(now.to_rfc3339()) } else { None }; + let fulfilled_at = if status == "fulfilled" { + Some(now.to_rfc3339()) + } else { + None + }; - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "UPDATE intentions SET status = ?1, fulfilled_at = ?2 WHERE id = ?3", @@ -2679,7 +3194,9 @@ impl Storage { /// Delete an intention pub fn delete_intention(&self, id: &str) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute("DELETE FROM intentions WHERE id = ?1", params![id])?; Ok(rows > 0) @@ -2688,7 +3205,9 @@ impl Storage { /// Get overdue intentions pub fn get_overdue_intentions(&self) -> Result> { let now = Utc::now().to_rfc3339(); - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM intentions WHERE status = 'active' AND deadline IS NOT NULL AND deadline < ?1 ORDER BY deadline ASC" @@ -2704,7 +3223,9 @@ impl Storage { /// Snooze an intention pub fn snooze_intention(&self, id: &str, until: DateTime) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "UPDATE intentions SET status = 'snoozed', snoozed_until = ?1 WHERE id = ?2", @@ -2720,7 +3241,11 @@ impl Storage { let related: Vec = serde_json::from_str(&related_json).unwrap_or_default(); let parse_opt_dt = |s: Option| -> Option> { - s.and_then(|v| DateTime::parse_from_rfc3339(&v).ok().map(|dt| dt.with_timezone(&Utc))) + s.and_then(|v| { + DateTime::parse_from_rfc3339(&v) + .ok() + .map(|dt| dt.with_timezone(&Utc)) + }) }; Ok(IntentionRecord { @@ -2752,10 +3277,13 @@ impl Storage { /// Save an insight to the database pub fn save_insight(&self, insight: &InsightRecord) -> Result<()> { - let source_json = serde_json::to_string(&insight.source_memories).unwrap_or_else(|_| "[]".to_string()); + let source_json = + serde_json::to_string(&insight.source_memories).unwrap_or_else(|_| "[]".to_string()); let tags_json = serde_json::to_string(&insight.tags).unwrap_or_else(|_| "[]".to_string()); - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT OR REPLACE INTO insights ( @@ -2780,11 +3308,12 @@ impl Storage { /// Get insights with optional limit pub fn get_insights(&self, limit: i32) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM insights ORDER BY generated_at DESC LIMIT ?1" - )?; + let mut stmt = + reader.prepare("SELECT * FROM insights ORDER BY generated_at DESC LIMIT ?1")?; let rows = stmt.query_map(params![limit], Self::row_to_insight)?; let mut result = Vec::new(); @@ -2796,11 +3325,12 @@ impl Storage { /// Get insights without feedback (pending review) pub fn get_pending_insights(&self) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM insights WHERE feedback IS NULL ORDER BY novelty_score DESC" - )?; + let mut stmt = reader + .prepare("SELECT * FROM insights WHERE feedback IS NULL ORDER BY novelty_score DESC")?; let rows = stmt.query_map([], Self::row_to_insight)?; let mut result = Vec::new(); @@ -2812,7 +3342,9 @@ impl Storage { /// Mark insight feedback pub fn mark_insight_feedback(&self, id: &str, feedback: &str) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "UPDATE insights SET feedback = ?1 WHERE id = ?2", @@ -2823,7 +3355,9 @@ impl Storage { /// Clear all insights pub fn clear_insights(&self) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let count: i32 = writer.query_row("SELECT COUNT(*) FROM insights", [], |row| row.get(0))?; writer.execute("DELETE FROM insights", [])?; @@ -2858,7 +3392,9 @@ impl Storage { /// Save a memory connection pub fn save_connection(&self, connection: &ConnectionRecord) -> Result<()> { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT OR REPLACE INTO memory_connections ( @@ -2879,7 +3415,9 @@ impl Storage { /// Get connections for a memory pub fn get_connections_for_memory(&self, memory_id: &str) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM memory_connections WHERE source_id = ?1 OR target_id = ?1 ORDER BY strength DESC" @@ -2895,11 +3433,11 @@ impl Storage { /// Get all connections (for building activation network) pub fn get_all_connections(&self) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM memory_connections ORDER BY strength DESC" - )?; + let mut stmt = reader.prepare("SELECT * FROM memory_connections ORDER BY strength DESC")?; let rows = stmt.query_map([], Self::row_to_connection)?; let mut result = Vec::new(); @@ -2910,9 +3448,16 @@ impl Storage { } /// Strengthen a connection - pub fn strengthen_connection(&self, source_id: &str, target_id: &str, boost: f64) -> Result { + pub fn strengthen_connection( + &self, + source_id: &str, + target_id: &str, + boost: f64, + ) -> Result { let now = Utc::now().to_rfc3339(); - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "UPDATE memory_connections SET @@ -2927,7 +3472,9 @@ impl Storage { /// Apply decay to all connections pub fn apply_connection_decay(&self, decay_factor: f64) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "UPDATE memory_connections SET strength = strength * ?1", @@ -2938,7 +3485,9 @@ impl Storage { /// Prune weak connections below threshold pub fn prune_weak_connections(&self, min_strength: f64) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "DELETE FROM memory_connections WHERE strength < ?1", @@ -2969,9 +3518,12 @@ impl Storage { /// Save or update memory state pub fn save_memory_state(&self, state: &MemoryStateRecord) -> Result<()> { - let suppressed_json = serde_json::to_string(&state.suppressed_by).unwrap_or_else(|_| "[]".to_string()); + let suppressed_json = + serde_json::to_string(&state.suppressed_by).unwrap_or_else(|_| "[]".to_string()); - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT OR REPLACE INTO memory_states ( @@ -2993,11 +3545,11 @@ impl Storage { /// Get memory state pub fn get_memory_state(&self, memory_id: &str) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM memory_states WHERE memory_id = ?1" - )?; + let mut stmt = reader.prepare("SELECT * FROM memory_states WHERE memory_id = ?1")?; stmt.query_row(params![memory_id], Self::row_to_memory_state) .optional() @@ -3006,11 +3558,11 @@ impl Storage { /// Get memories by state pub fn get_memories_by_state(&self, state: &str) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT memory_id FROM memory_states WHERE state = ?1" - )?; + let mut stmt = reader.prepare("SELECT memory_id FROM memory_states WHERE state = ?1")?; let rows = stmt.query_map(params![state], |row| row.get::<_, String>(0))?; let mut result = Vec::new(); @@ -3021,13 +3573,20 @@ impl Storage { } /// Update memory state - pub fn update_memory_state(&self, memory_id: &str, new_state: &str, reason: &str) -> Result { + pub fn update_memory_state( + &self, + memory_id: &str, + new_state: &str, + reason: &str, + ) -> Result { let now = Utc::now(); // Get old state for transition record if let Some(old_record) = self.get_memory_state(memory_id)? { // Record state transition - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT INTO state_transitions (memory_id, from_state, to_state, reason_type, timestamp) @@ -3036,7 +3595,9 @@ impl Storage { )?; } - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let rows = writer.execute( "UPDATE memory_states SET state = ?1, state_entered_at = ?2 WHERE memory_id = ?3", @@ -3049,7 +3610,9 @@ impl Storage { pub fn record_memory_access(&self, memory_id: &str) -> Result<()> { let now = Utc::now(); - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; // Check if state exists (writer can read too) @@ -3084,7 +3647,11 @@ impl Storage { let suppressed_by: Vec = serde_json::from_str(&suppressed_json).unwrap_or_default(); let parse_opt_dt = |s: Option| -> Option> { - s.and_then(|v| DateTime::parse_from_rfc3339(&v).ok().map(|dt| dt.with_timezone(&Utc))) + s.and_then(|v| { + DateTime::parse_from_rfc3339(&v) + .ok() + .map(|dt| dt.with_timezone(&Utc)) + }) }; Ok(MemoryStateRecord { @@ -3094,9 +3661,11 @@ impl Storage { .map(|dt| dt.with_timezone(&Utc)) .unwrap_or_else(|_| Utc::now()), access_count: row.get("access_count").unwrap_or(1), - state_entered_at: DateTime::parse_from_rfc3339(&row.get::<_, String>("state_entered_at")?) - .map(|dt| dt.with_timezone(&Utc)) - .unwrap_or_else(|_| Utc::now()), + state_entered_at: DateTime::parse_from_rfc3339( + &row.get::<_, String>("state_entered_at")?, + ) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or_else(|_| Utc::now()), suppression_until: parse_opt_dt(row.get("suppression_until").ok().flatten()), suppressed_by, }) @@ -3108,7 +3677,9 @@ impl Storage { /// Save consolidation history record pub fn save_consolidation_history(&self, record: &ConsolidationHistoryRecord) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT INTO consolidation_history ( @@ -3130,26 +3701,34 @@ impl Storage { /// Get last consolidation timestamp pub fn get_last_consolidation(&self) -> Result>> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let result: Option = reader.query_row( - "SELECT MAX(completed_at) FROM consolidation_history", - [], - |row| row.get(0), - ).ok().flatten(); + let result: Option = reader + .query_row( + "SELECT MAX(completed_at) FROM consolidation_history", + [], + |row| row.get(0), + ) + .ok() + .flatten(); Ok(result.and_then(|s| { - DateTime::parse_from_rfc3339(&s).ok().map(|dt| dt.with_timezone(&Utc)) + DateTime::parse_from_rfc3339(&s) + .ok() + .map(|dt| dt.with_timezone(&Utc)) })) } /// Get consolidation history pub fn get_consolidation_history(&self, limit: i32) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM consolidation_history ORDER BY completed_at DESC LIMIT ?1" - )?; + let mut stmt = reader + .prepare("SELECT * FROM consolidation_history ORDER BY completed_at DESC LIMIT ?1")?; let rows = stmt.query_map(params![limit], |row| { Ok(ConsolidationHistoryRecord { @@ -3179,7 +3758,9 @@ impl Storage { /// Save a dream history record pub fn save_dream_history(&self, record: &DreamHistoryRecord) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT INTO dream_history ( @@ -3210,22 +3791,69 @@ impl Storage { /// Get last dream timestamp pub fn get_last_dream(&self) -> Result>> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let result: Option = reader.query_row( - "SELECT MAX(dreamed_at) FROM dream_history", - [], - |row| row.get(0), - ).ok().flatten(); + let result: Option = reader + .query_row("SELECT MAX(dreamed_at) FROM dream_history", [], |row| { + row.get(0) + }) + .ok() + .flatten(); Ok(result.and_then(|s| { - DateTime::parse_from_rfc3339(&s).ok().map(|dt| dt.with_timezone(&Utc)) + DateTime::parse_from_rfc3339(&s) + .ok() + .map(|dt| dt.with_timezone(&Utc)) })) } + /// Get dream history (most recent first) + pub fn get_dream_history(&self, limit: i32) -> Result> { + let reader = self + .reader + .lock() + .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; + let mut stmt = reader.prepare( + "SELECT dreamed_at, duration_ms, memories_replayed, connections_found, + insights_generated, memories_strengthened, memories_compressed, + phase_nrem1_ms, phase_nrem3_ms, phase_rem_ms, phase_integration_ms, + summaries_generated, emotional_memories_processed, creative_connections_found + FROM dream_history ORDER BY dreamed_at DESC LIMIT ?1", + )?; + let records = stmt + .query_map(params![limit], |row| { + let dreamed_at_str: String = row.get(0)?; + let dreamed_at = DateTime::parse_from_rfc3339(&dreamed_at_str) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or_else(|_| Utc::now()); + Ok(DreamHistoryRecord { + dreamed_at, + duration_ms: row.get(1)?, + memories_replayed: row.get(2)?, + connections_found: row.get(3)?, + insights_generated: row.get(4)?, + memories_strengthened: row.get(5)?, + memories_compressed: row.get(6)?, + phase_nrem1_ms: row.get(7)?, + phase_nrem3_ms: row.get(8)?, + phase_rem_ms: row.get(9)?, + phase_integration_ms: row.get(10)?, + summaries_generated: row.get(11)?, + emotional_memories_processed: row.get(12)?, + creative_connections_found: row.get(13)?, + }) + })? + .collect::, _>>()?; + Ok(records) + } + /// Count memories created since a given timestamp pub fn count_memories_since(&self, since: DateTime) -> Result { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let count: i64 = reader.query_row( "SELECT COUNT(*) FROM knowledge_nodes WHERE created_at >= ?1", @@ -3252,13 +3880,17 @@ impl Storage { let name = entry.file_name(); let name_str = name.to_string_lossy(); // Parse vestige-YYYYMMDD-HHMMSS.db - if let Some(ts_part) = name_str.strip_prefix("vestige-").and_then(|s| s.strip_suffix(".db")) - && let Ok(naive) = chrono::NaiveDateTime::parse_from_str(ts_part, "%Y%m%d-%H%M%S") { - let dt = naive.and_utc(); - if latest.as_ref().is_none_or(|l| dt > *l) { - latest = Some(dt); - } + if let Some(ts_part) = name_str + .strip_prefix("vestige-") + .and_then(|s| s.strip_suffix(".db")) + && let Ok(naive) = + chrono::NaiveDateTime::parse_from_str(ts_part, "%Y%m%d-%H%M%S") + { + let dt = naive.and_utc(); + if latest.as_ref().is_none_or(|l| dt > *l) { + latest = Some(dt); } + } } } @@ -3270,11 +3902,17 @@ impl Storage { // ======================================================================== /// Get state transitions for a memory - pub fn get_state_transitions(&self, memory_id: &str, limit: i32) -> Result> { - let reader = self.reader.lock() + pub fn get_state_transitions( + &self, + memory_id: &str, + limit: i32, + ) -> Result> { + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( - "SELECT * FROM state_transitions WHERE memory_id = ?1 ORDER BY timestamp DESC LIMIT ?2" + "SELECT * FROM state_transitions WHERE memory_id = ?1 ORDER BY timestamp DESC LIMIT ?2", )?; let rows = stmt.query_map(params![memory_id, limit], |row| { @@ -3300,14 +3938,18 @@ impl Storage { /// Create a consistent backup using VACUUM INTO pub fn backup_to(&self, path: &std::path::Path) -> Result<()> { - let path_str = path.to_str().ok_or_else(|| { - StorageError::Init("Invalid backup path encoding".to_string()) - })?; + let path_str = path + .to_str() + .ok_or_else(|| StorageError::Init("Invalid backup path encoding".to_string()))?; // Validate path: reject control characters (except tab) for defense-in-depth if path_str.bytes().any(|b| b < 0x20 && b != b'\t') { - return Err(StorageError::Init("Backup path contains invalid characters".to_string())); + return Err(StorageError::Init( + "Backup path contains invalid characters".to_string(), + )); } - let reader = self.reader.lock() + let reader = self + .reader + .lock() .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('\'', "''")))?; @@ -3320,7 +3962,9 @@ impl Storage { /// Get average retention across all memories pub fn get_avg_retention(&self) -> Result { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let avg: f64 = reader.query_row( "SELECT COALESCE(AVG(retention_strength), 0.0) FROM knowledge_nodes", @@ -3332,7 +3976,9 @@ impl Storage { /// Get retention distribution in buckets (0-20%, 20-40%, 40-60%, 60-80%, 80-100%) pub fn get_retention_distribution(&self) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT @@ -3346,7 +3992,7 @@ impl Storage { COUNT(*) as count FROM knowledge_nodes GROUP BY bucket - ORDER BY bucket" + ORDER BY bucket", )?; let rows = stmt.query_map([], |row| { @@ -3362,12 +4008,16 @@ impl Storage { /// Get retention trend (improving/declining/stable) from retention snapshots pub fn get_retention_trend(&self) -> Result { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let snapshots: Vec = reader.prepare( - "SELECT avg_retention FROM retention_snapshots ORDER BY snapshot_at DESC LIMIT 5" - )?.query_map([], |row| row.get(0))? + let snapshots: Vec = reader + .prepare( + "SELECT avg_retention FROM retention_snapshots ORDER BY snapshot_at DESC LIMIT 5", + )? + .query_map([], |row| row.get(0))? .filter_map(|r| r.ok()) .collect(); @@ -3390,8 +4040,16 @@ impl Storage { } /// Save a retention snapshot (called during consolidation) - pub fn save_retention_snapshot(&self, avg_retention: f64, total: i64, below_target: i64, gc_triggered: bool) -> Result<()> { - let writer = self.writer.lock() + pub fn save_retention_snapshot( + &self, + avg_retention: f64, + total: i64, + below_target: i64, + gc_triggered: bool, + ) -> Result<()> { + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "INSERT INTO retention_snapshots (snapshot_at, avg_retention, total_memories, memories_below_target, gc_triggered) @@ -3403,7 +4061,9 @@ impl Storage { /// Count memories below a given retention threshold pub fn count_memories_below_retention(&self, threshold: f64) -> Result { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let count: i64 = reader.query_row( "SELECT COUNT(*) FROM knowledge_nodes WHERE retention_strength < ?1", @@ -3420,7 +4080,9 @@ impl Storage { // Collect IDs first for vector index cleanup #[cfg(all(feature = "embeddings", feature = "vector-search"))] let doomed_ids: Vec = { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT id FROM knowledge_nodes WHERE retention_strength < ?1 AND created_at < ?2", @@ -3430,7 +4092,9 @@ impl Storage { .collect() }; - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let deleted = writer.execute( "DELETE FROM knowledge_nodes WHERE retention_strength < ?1 AND created_at < ?2", @@ -3441,11 +4105,12 @@ impl Storage { // Clean up vector index #[cfg(all(feature = "embeddings", feature = "vector-search"))] if deleted > 0 - && let Ok(mut index) = self.vector_index.lock() { - for id in &doomed_ids { - let _ = index.remove(id); - } + && let Ok(mut index) = self.vector_index.lock() + { + for id in &doomed_ids { + let _ = index.remove(id); } + } Ok(deleted) } @@ -3457,14 +4122,16 @@ impl Storage { // Find memories with 3+ accesses in last 24h let candidates: Vec = { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT node_id, COUNT(*) as access_count FROM memory_access_log WHERE accessed_at >= ?1 GROUP BY node_id - HAVING access_count >= 3" + HAVING access_count >= 3", )?; stmt.query_map(params![twenty_four_hours_ago], |row| row.get(0))? .filter_map(|r| r.ok()) @@ -3475,7 +4142,9 @@ impl Storage { return Ok(0); } - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let mut promoted = 0i64; for id in &candidates { @@ -3497,7 +4166,9 @@ impl Storage { /// Set waking tag on a memory (marks it for preferential dream replay) pub fn set_waking_tag(&self, memory_id: &str) -> Result<()> { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; writer.execute( "UPDATE knowledge_nodes SET waking_tag = TRUE, waking_tag_at = ?1 WHERE id = ?2", @@ -3508,7 +4179,9 @@ impl Storage { /// Clear waking tags (called after dream processes them) pub fn clear_waking_tags(&self) -> Result { - let writer = self.writer.lock() + let writer = self + .writer + .lock() .map_err(|_| StorageError::Init("Writer lock poisoned".into()))?; let cleared = writer.execute( "UPDATE knowledge_nodes SET waking_tag = FALSE, waking_tag_at = NULL WHERE waking_tag = TRUE", @@ -3519,7 +4192,9 @@ impl Storage { /// Get waking-tagged memories for preferential dream replay pub fn get_waking_tagged_memories(&self, limit: i32) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT * FROM knowledge_nodes WHERE waking_tag = TRUE ORDER BY waking_tag_at DESC LIMIT ?1" @@ -3534,21 +4209,30 @@ impl Storage { /// Get the memory with the most connections (best center node for graph visualization) pub fn get_most_connected_memory(&self) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; let mut stmt = reader.prepare( "SELECT id, COUNT(*) as cnt FROM ( SELECT source_id as id FROM memory_connections UNION ALL SELECT target_id as id FROM memory_connections - ) GROUP BY id ORDER BY cnt DESC LIMIT 1" + ) GROUP BY id ORDER BY cnt DESC LIMIT 1", )?; - let result = stmt.query_row([], |row| row.get::<_, String>(0)).optional()?; + let result = stmt + .query_row([], |row| row.get::<_, String>(0)) + .optional()?; Ok(result) } /// Get memories with their connection data for graph visualization - pub fn get_memory_subgraph(&self, center_id: &str, depth: u32, max_nodes: usize) -> Result<(Vec, Vec)> { + pub fn get_memory_subgraph( + &self, + center_id: &str, + depth: u32, + max_nodes: usize, + ) -> Result<(Vec, Vec)> { let mut visited_ids: std::collections::HashSet = std::collections::HashSet::new(); let mut frontier = vec![center_id.to_string()]; visited_ids.insert(center_id.to_string()); @@ -3559,7 +4243,11 @@ impl Storage { for id in &frontier { let connections = self.get_connections_for_memory(id)?; for conn in &connections { - let other_id = if conn.source_id == *id { &conn.target_id } else { &conn.source_id }; + let other_id = if conn.source_id == *id { + &conn.target_id + } else { + &conn.source_id + }; if visited_ids.insert(other_id.clone()) { next_frontier.push(other_id.clone()); if visited_ids.len() >= max_nodes { @@ -3597,11 +4285,12 @@ impl Storage { /// Get recent state transitions across all memories (system-wide changelog) pub fn get_recent_state_transitions(&self, limit: i32) -> Result> { - let reader = self.reader.lock() + let reader = self + .reader + .lock() .map_err(|_| StorageError::Init("Reader lock poisoned".into()))?; - let mut stmt = reader.prepare( - "SELECT * FROM state_transitions ORDER BY timestamp DESC LIMIT ?1" - )?; + let mut stmt = + reader.prepare("SELECT * FROM state_transitions ORDER BY timestamp DESC LIMIT ?1")?; let rows = stmt.query_map(params![limit], |row| { Ok(StateTransitionRecord { @@ -3763,11 +4452,13 @@ mod tests { let before = Utc::now() - Duration::seconds(10); for i in 0..5 { - storage.ingest(IngestInput { - content: format!("Count test memory {}", i), - node_type: "fact".to_string(), - ..Default::default() - }).unwrap(); + storage + .ingest(IngestInput { + content: format!("Count test memory {}", i), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); } let count = storage.count_memories_since(before).unwrap(); @@ -3783,4 +4474,156 @@ mod tests { // Static method should not panic even if no backups exist let _ = Storage::get_last_backup_timestamp(); } + + #[test] + fn test_keyword_search_with_include_types() { + let storage = create_test_storage(); + + // Ingest nodes of different types all containing the word "quantum" + storage + .ingest(IngestInput { + content: "Quantum mechanics is fundamental to physics".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + storage + .ingest(IngestInput { + content: "Quantum computing uses qubits for calculation".to_string(), + node_type: "concept".to_string(), + ..Default::default() + }) + .unwrap(); + storage + .ingest(IngestInput { + content: "Quantum entanglement was demonstrated in the lab".to_string(), + node_type: "event".to_string(), + ..Default::default() + }) + .unwrap(); + + // Search with include_types = ["fact"] — should only return the fact + let include = vec!["fact".to_string()]; + let results = storage + .hybrid_search_filtered("quantum", 10, 0.3, 0.7, Some(&include), None) + .unwrap(); + + assert!(!results.is_empty(), "should return at least one result"); + for r in &results { + assert_eq!( + r.node.node_type, "fact", + "include_types=[fact] should only return facts, got: {}", + r.node.node_type + ); + } + } + + #[test] + fn test_keyword_search_with_exclude_types() { + let storage = create_test_storage(); + + storage + .ingest(IngestInput { + content: "Photosynthesis converts sunlight to energy".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + storage + .ingest(IngestInput { + content: "Photosynthesis is a complex biochemical process".to_string(), + node_type: "reflection".to_string(), + ..Default::default() + }) + .unwrap(); + + // Search with exclude_types = ["reflection"] — should skip the reflection + let exclude = vec!["reflection".to_string()]; + let results = storage + .hybrid_search_filtered("photosynthesis", 10, 0.3, 0.7, None, Some(&exclude)) + .unwrap(); + + assert!(!results.is_empty(), "should return at least one result"); + for r in &results { + assert_ne!( + r.node.node_type, "reflection", + "exclude_types=[reflection] should not return reflections" + ); + } + } + + #[test] + fn test_include_types_takes_precedence_over_exclude() { + let storage = create_test_storage(); + + storage + .ingest(IngestInput { + content: "Gravity holds planets in orbit around stars".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + storage + .ingest(IngestInput { + content: "Gravity waves were first detected by LIGO".to_string(), + node_type: "event".to_string(), + ..Default::default() + }) + .unwrap(); + + // When both are provided, include_types wins + let include = vec!["fact".to_string()]; + let exclude = vec!["fact".to_string()]; + let results = storage + .hybrid_search_filtered("gravity", 10, 0.3, 0.7, Some(&include), Some(&exclude)) + .unwrap(); + + // include_types takes precedence — facts should be returned + assert!(!results.is_empty()); + for r in &results { + assert_eq!(r.node.node_type, "fact"); + } + } + + #[test] + fn test_type_filter_with_no_matches_returns_empty() { + let storage = create_test_storage(); + + storage + .ingest(IngestInput { + content: "DNA carries genetic information in cells".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + // Search for a type that doesn't exist among matches + let include = vec!["person".to_string()]; + let results = storage + .hybrid_search_filtered("DNA", 10, 0.3, 0.7, Some(&include), None) + .unwrap(); + + assert!( + results.is_empty(), + "filtering for a non-matching type should return empty results" + ); + } + + #[test] + fn test_hybrid_search_backward_compat() { + // Ensure the original hybrid_search (no type filters) still works + let storage = create_test_storage(); + + storage + .ingest(IngestInput { + content: "Neurons transmit electrical signals in the brain".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .unwrap(); + + let results = storage.hybrid_search("neurons", 10, 0.3, 0.7).unwrap(); + assert!(!results.is_empty()); + assert!(results[0].node.content.contains("Neurons")); + } } diff --git a/crates/vestige-mcp/Cargo.toml b/crates/vestige-mcp/Cargo.toml index 5676c72..107038e 100644 --- a/crates/vestige-mcp/Cargo.toml +++ b/crates/vestige-mcp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vestige-mcp" -version = "2.0.3" +version = "2.0.9" edition = "2024" description = "Cognitive memory MCP server for Claude - FSRS-6, spreading activation, synaptic tagging, 3D dashboard, and 130 years of memory research" authors = ["samvallad33"] @@ -10,9 +10,17 @@ 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"] +# 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"] [[bin]] name = "vestige-mcp" @@ -31,8 +39,12 @@ path = "src/bin/cli.rs" # VESTIGE CORE - The cognitive science engine # ============================================================================ # Includes: FSRS-6, spreading activation, synaptic tagging, hippocampal indexing, -# memory states, context memory, importance signals, dreams, and more -vestige-core = { version = "2.0.3", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] } +# memory states, context memory, importance signals, dreams, and more. +# +# 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.8", path = "../vestige-core", default-features = false, features = ["bundled-sqlite"] } # ============================================================================ # MCP Server Dependencies diff --git a/crates/vestige-mcp/src/autopilot.rs b/crates/vestige-mcp/src/autopilot.rs new file mode 100644 index 0000000..809e802 --- /dev/null +++ b/crates/vestige-mcp/src/autopilot.rs @@ -0,0 +1,504 @@ +//! 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::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 ecb278e..eeffe54 100644 --- a/crates/vestige-mcp/src/bin/cli.rs +++ b/crates/vestige-mcp/src/bin/cli.rs @@ -18,7 +18,9 @@ use vestige_core::{IngestInput, Storage}; #[command(author = "samvallad33")] #[command(version = env!("CARGO_PKG_VERSION"))] #[command(about = "CLI for the Vestige cognitive memory system")] -#[command(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.")] +#[command( + 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 { #[command(subcommand)] command: Commands, @@ -171,21 +173,49 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { // Basic stats println!("{}: {}", "Total Memories".white().bold(), stats.total_nodes); - println!("{}: {}", "Due for Review".white().bold(), stats.nodes_due_for_review); - println!("{}: {:.1}%", "Average Retention".white().bold(), stats.average_retention * 100.0); - println!("{}: {:.2}", "Average Storage Strength".white().bold(), stats.average_storage_strength); - println!("{}: {:.2}", "Average Retrieval Strength".white().bold(), stats.average_retrieval_strength); - println!("{}: {}", "With Embeddings".white().bold(), stats.nodes_with_embeddings); + println!( + "{}: {}", + "Due for Review".white().bold(), + stats.nodes_due_for_review + ); + println!( + "{}: {:.1}%", + "Average Retention".white().bold(), + stats.average_retention * 100.0 + ); + println!( + "{}: {:.2}", + "Average Storage Strength".white().bold(), + stats.average_storage_strength + ); + println!( + "{}: {:.2}", + "Average Retrieval Strength".white().bold(), + stats.average_retrieval_strength + ); + println!( + "{}: {}", + "With Embeddings".white().bold(), + stats.nodes_with_embeddings + ); if let Some(model) = &stats.embedding_model { println!("{}: {}", "Embedding Model".white().bold(), model); } if let Some(oldest) = stats.oldest_memory { - println!("{}: {}", "Oldest Memory".white().bold(), oldest.format("%Y-%m-%d %H:%M:%S")); + println!( + "{}: {}", + "Oldest Memory".white().bold(), + oldest.format("%Y-%m-%d %H:%M:%S") + ); } if let Some(newest) = stats.newest_memory { - println!("{}: {}", "Newest Memory".white().bold(), newest.format("%Y-%m-%d %H:%M:%S")); + println!( + "{}: {}", + "Newest Memory".white().bold(), + newest.format("%Y-%m-%d %H:%M:%S") + ); } // Embedding coverage @@ -194,7 +224,11 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { } else { 0.0 }; - println!("{}: {:.1}%", "Embedding Coverage".white().bold(), embedding_coverage); + println!( + "{}: {:.1}%", + "Embedding Coverage".white().bold(), + embedding_coverage + ); // Tagging distribution (retention levels) if show_tagging { @@ -205,9 +239,18 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { let total = memories.len(); if total > 0 { - let high = memories.iter().filter(|m| m.retention_strength >= 0.7).count(); - let medium = memories.iter().filter(|m| m.retention_strength >= 0.4 && m.retention_strength < 0.7).count(); - let low = memories.iter().filter(|m| m.retention_strength < 0.4).count(); + let high = memories + .iter() + .filter(|m| m.retention_strength >= 0.7) + .count(); + let medium = memories + .iter() + .filter(|m| m.retention_strength >= 0.4 && m.retention_strength < 0.7) + .count(); + let low = memories + .iter() + .filter(|m| m.retention_strength < 0.4) + .count(); print_distribution_bar("High (>=70%)", high, total, "green"); print_distribution_bar("Medium (40-70%)", medium, total, "yellow"); @@ -220,7 +263,10 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { // State distribution if show_states { println!(); - println!("{}", "=== Cognitive State Distribution ===".magenta().bold()); + println!( + "{}", + "=== Cognitive State Distribution ===".magenta().bold() + ); let memories = storage.get_all_nodes(500, 0)?; let total = memories.len(); @@ -248,7 +294,9 @@ fn run_stats(show_tagging: bool, show_states: bool) -> anyhow::Result<()> { } /// Compute cognitive state distribution for memories -fn compute_state_distribution(memories: &[vestige_core::KnowledgeNode]) -> (usize, usize, usize, usize) { +fn compute_state_distribution( + memories: &[vestige_core::KnowledgeNode], +) -> (usize, usize, usize, usize) { let mut active = 0; let mut dormant = 0; let mut silent = 0; @@ -297,10 +345,7 @@ fn print_distribution_bar(label: &str, count: usize, total: usize, color: &str) println!( " {:15} [{:30}] {:>4} ({:>5.1}%)", - label, - colored_bar, - count, - percentage + label, colored_bar, count, percentage ); } @@ -332,8 +377,16 @@ fn run_health() -> anyhow::Result<()> { println!("{}: {}", "Status".white().bold(), colored_status); println!("{}: {}", "Total Memories".white(), stats.total_nodes); - println!("{}: {}", "Due for Review".white(), stats.nodes_due_for_review); - println!("{}: {:.1}%", "Average Retention".white(), stats.average_retention * 100.0); + println!( + "{}: {}", + "Due for Review".white(), + stats.nodes_due_for_review + ); + println!( + "{}: {:.1}%", + "Average Retention".white(), + stats.average_retention * 100.0 + ); // Embedding coverage let embedding_coverage = if stats.total_nodes > 0 { @@ -341,15 +394,27 @@ fn run_health() -> anyhow::Result<()> { } else { 0.0 }; - println!("{}: {:.1}%", "Embedding Coverage".white(), embedding_coverage); - println!("{}: {}", "Embedding Service".white(), - if storage.is_embedding_ready() { "Ready".green() } else { "Not Ready".red() }); + println!( + "{}: {:.1}%", + "Embedding Coverage".white(), + embedding_coverage + ); + println!( + "{}: {}", + "Embedding Service".white(), + if storage.is_embedding_ready() { + "Ready".green() + } else { + "Not Ready".red() + } + ); // Warnings 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"); + warnings + .push("Low average retention - consider running consolidation or reviewing memories"); } if stats.nodes_due_for_review > 10 { @@ -376,7 +441,8 @@ fn run_health() -> anyhow::Result<()> { let mut recommendations = Vec::new(); if status == "CRITICAL" { - recommendations.push("CRITICAL: Many memories have very low retention. Review important memories."); + recommendations + .push("CRITICAL: Many memories have very low retention. Review important memories."); } if stats.nodes_due_for_review > 5 { @@ -384,7 +450,8 @@ fn run_health() -> anyhow::Result<()> { } if stats.nodes_with_embeddings < stats.total_nodes { - recommendations.push("Run 'vestige consolidate' to generate embeddings for better semantic search."); + recommendations + .push("Run 'vestige consolidate' to generate embeddings for better semantic search."); } if stats.total_nodes > 100 && stats.average_retention < 0.7 { @@ -398,8 +465,16 @@ fn run_health() -> anyhow::Result<()> { println!(); println!("{}", "Recommendations:".cyan().bold()); for rec in &recommendations { - let icon = if rec.starts_with("CRITICAL") { "!".red().bold() } else { ">".cyan() }; - let text = if rec.starts_with("CRITICAL") { rec.red().to_string() } else { rec.to_string() }; + let icon = if rec.starts_with("CRITICAL") { + "!".red().bold() + } else { + ">".cyan() + }; + let text = if rec.starts_with("CRITICAL") { + rec.red().to_string() + } else { + rec.to_string() + }; println!(" {} {}", icon, text); } @@ -416,11 +491,27 @@ fn run_consolidate() -> anyhow::Result<()> { let storage = Storage::new(None)?; let result = storage.run_consolidation()?; - println!("{}: {}", "Nodes Processed".white().bold(), result.nodes_processed); - println!("{}: {}", "Nodes Promoted".white().bold(), result.nodes_promoted); + println!( + "{}: {}", + "Nodes Processed".white().bold(), + result.nodes_processed + ); + println!( + "{}: {}", + "Nodes Promoted".white().bold(), + result.nodes_promoted + ); println!("{}: {}", "Nodes Pruned".white().bold(), result.nodes_pruned); - println!("{}: {}", "Decay Applied".white().bold(), result.decay_applied); - println!("{}: {}", "Embeddings Generated".white().bold(), result.embeddings_generated); + println!( + "{}: {}", + "Decay Applied".white().bold(), + result.decay_applied + ); + println!( + "{}: {}", + "Embeddings Generated".white().bold(), + result.embeddings_generated + ); println!("{}: {}ms", "Duration".white().bold(), result.duration_ms); println!(); @@ -523,7 +614,11 @@ fn run_restore(backup_path: PathBuf) -> anyhow::Result<()> { let stats = storage.get_stats()?; println!(); println!("{}: {}", "Total Nodes".white(), stats.total_nodes); - println!("{}: {}", "With Embeddings".white(), stats.nodes_with_embeddings); + println!( + "{}: {}", + "With Embeddings".white(), + stats.nodes_with_embeddings + ); Ok(()) } @@ -581,9 +676,10 @@ fn run_backup(output: PathBuf) -> anyhow::Result<()> { // Create parent directories if needed if let Some(parent) = output.parent() - && !parent.exists() { - std::fs::create_dir_all(parent)?; - } + && !parent.exists() + { + std::fs::create_dir_all(parent)?; + } // Copy the database file println!("Copying database..."); @@ -630,8 +726,9 @@ fn run_export( // Parse since date if provided let since_date = match &since { Some(date_str) => { - let naive = NaiveDate::parse_from_str(date_str, "%Y-%m-%d") - .map_err(|e| anyhow::anyhow!("Invalid date '{}': {}. Use YYYY-MM-DD format.", date_str, e))?; + let naive = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").map_err(|e| { + anyhow::anyhow!("Invalid date '{}': {}. Use YYYY-MM-DD format.", date_str, e) + })?; Some( naive .and_hms_opt(0, 0, 0) @@ -645,7 +742,12 @@ fn run_export( // Parse tags filter let tag_filter: Vec = tags .as_deref() - .map(|t| t.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()) + .map(|t| { + t.split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() + }) .unwrap_or_default(); let storage = Storage::new(None)?; @@ -657,9 +759,10 @@ fn run_export( .filter(|node| { // Date filter if let Some(ref since_dt) = since_date - && node.created_at < *since_dt { - return false; - } + && node.created_at < *since_dt + { + return false; + } // Tag filter: node must contain ALL specified tags if !tag_filter.is_empty() { for tag in &tag_filter { @@ -689,9 +792,10 @@ fn run_export( // Create parent directories if needed if let Some(parent) = output.parent() - && !parent.exists() { - std::fs::create_dir_all(parent)?; - } + && !parent.exists() + { + std::fs::create_dir_all(parent)?; + } let file = std::fs::File::create(&output)?; let mut writer = BufWriter::new(file); @@ -770,7 +874,11 @@ fn run_gc( }) .collect(); - println!("{}: {}", "Min retention threshold".white().bold(), min_retention); + println!( + "{}: {}", + "Min retention threshold".white().bold(), + min_retention + ); if let Some(max_days) = max_age_days { println!("{}: {} days", "Max age".white().bold(), max_days); } @@ -783,7 +891,10 @@ fn run_gc( if candidates.is_empty() { println!(); - println!("{}", "No memories match the garbage collection criteria.".green()); + println!( + "{}", + "No memories match the garbage collection criteria.".green() + ); return Ok(()); } @@ -853,7 +964,12 @@ fn run_gc( Ok(true) => deleted += 1, Ok(false) => errors += 1, // node was already gone Err(e) => { - eprintln!(" {} Failed to delete {}: {}", "ERR".red(), &node.id[..8], e); + eprintln!( + " {} Failed to delete {}: {}", + "ERR".red(), + &node.id[..8], + e + ); errors += 1; } } @@ -960,7 +1076,10 @@ fn run_ingest( fn run_dashboard(port: u16, open_browser: bool) -> anyhow::Result<()> { println!("{}", "=== Vestige Dashboard ===".cyan().bold()); println!(); - println!("Starting dashboard at {}...", format!("http://127.0.0.1:{}", port).cyan()); + println!( + "Starting dashboard at {}...", + format!("http://127.0.0.1:{}", port).cyan() + ); let storage = Storage::new(None)?; @@ -1025,8 +1144,19 @@ fn run_serve(port: u16, with_dashboard: bool, dashboard_port: u16) -> anyhow::Re let dc = Arc::clone(&cognitive); let dtx = event_tx.clone(); tokio::spawn(async move { - match vestige_mcp::dashboard::start_background_with_event_tx(ds, Some(dc), dtx, dashboard_port).await { - Ok(_) => println!(" {} Dashboard: http://127.0.0.1:{}", ">".cyan(), dashboard_port), + match vestige_mcp::dashboard::start_background_with_event_tx( + ds, + Some(dc), + dtx, + dashboard_port, + ) + .await + { + Ok(_) => println!( + " {} Dashboard: http://127.0.0.1:{}", + ">".cyan(), + dashboard_port + ), Err(e) => eprintln!(" {} Dashboard failed: {}", "!".yellow(), e), } }); @@ -1037,7 +1167,12 @@ fn run_serve(port: u16, with_dashboard: bool, dashboard_port: u16) -> anyhow::Re .map_err(|e| anyhow::anyhow!("Failed to create auth token: {}", e))?; let bind = std::env::var("VESTIGE_HTTP_BIND").unwrap_or_else(|_| "127.0.0.1".to_string()); - println!(" {} HTTP transport: http://{}:{}/mcp", ">".cyan(), bind, port); + println!( + " {} HTTP transport: http://{}:{}/mcp", + ">".cyan(), + bind, + port + ); println!(" {} Auth token: {}...", ">".cyan(), &token[..8]); println!(); println!("{}", "Press Ctrl+C to stop.".dimmed()); diff --git a/crates/vestige-mcp/src/bin/restore.rs b/crates/vestige-mcp/src/bin/restore.rs index afb7f85..6e24800 100644 --- a/crates/vestige-mcp/src/bin/restore.rs +++ b/crates/vestige-mcp/src/bin/restore.rs @@ -65,7 +65,12 @@ fn main() -> anyhow::Result<()> { match storage.ingest(input) { Ok(_node) => { success_count += 1; - println!("[{}/{}] OK: {}", i + 1, total, truncate(&memory.content, 60)); + println!( + "[{}/{}] OK: {}", + i + 1, + total, + truncate(&memory.content, 60) + ); } Err(e) => { println!("[{}/{}] FAIL: {}", i + 1, total, e); @@ -73,7 +78,10 @@ fn main() -> anyhow::Result<()> { } } - println!("\nRestore complete: {}/{} memories restored", success_count, total); + println!( + "\nRestore complete: {}/{} memories restored", + success_count, total + ); // Show stats let stats = storage.get_stats()?; diff --git a/crates/vestige-mcp/src/cognitive.rs b/crates/vestige-mcp/src/cognitive.rs index 6f9c174..86aadcf 100644 --- a/crates/vestige-mcp/src/cognitive.rs +++ b/crates/vestige-mcp/src/cognitive.rs @@ -4,24 +4,43 @@ //! Each module is initialized once at startup and shared via Arc> //! across all tool invocations. +use vestige_core::neuroscience::predictive_retrieval::PredictiveMemory; +use vestige_core::neuroscience::prospective_memory::{IntentionParser, ProspectiveMemory}; +use vestige_core::search::TemporalSearcher; use vestige_core::{ + AccessibilityCalculator, // Neuroscience modules - ActivationNetwork, SynapticTaggingSystem, HippocampalIndex, ContextMatcher, - AccessibilityCalculator, CompetitionManager, StateUpdateService, - ImportanceSignals, NoveltySignal, ArousalSignal, RewardSignal, AttentionSignal, - EmotionalMemory, LinkType, + ActivationNetwork, + ActivityTracker, + AdaptiveEmbedder, + ArousalSignal, + AttentionSignal, + CompetitionManager, + ConsolidationScheduler, + ContextMatcher, + CrossProjectLearner, + EmotionalMemory, + HippocampalIndex, + ImportanceSignals, // Advanced modules - ImportanceTracker, ReconsolidationManager, IntentDetector, ActivityTracker, - MemoryDreamer, MemoryChainBuilder, MemoryCompressor, CrossProjectLearner, - AdaptiveEmbedder, SpeculativeRetriever, ConsolidationScheduler, + ImportanceTracker, + IntentDetector, + LinkType, + MemoryChainBuilder, + MemoryCompressor, + MemoryDreamer, + NoveltySignal, + ReconsolidationManager, // Search modules - Reranker, RerankerConfig, + Reranker, + RerankerConfig, + RewardSignal, + SpeculativeRetriever, + StateUpdateService, // Storage Storage, + SynapticTaggingSystem, }; -use vestige_core::search::TemporalSearcher; -use vestige_core::neuroscience::predictive_retrieval::PredictiveMemory; -use vestige_core::neuroscience::prospective_memory::{ProspectiveMemory, IntentionParser}; /// Stateful cognitive engine holding all neuroscience modules. /// @@ -151,9 +170,9 @@ impl CognitiveEngine { #[cfg(test)] mod tests { use super::*; - use vestige_core::{ConnectionRecord, IngestInput}; use chrono::Utc; use tempfile::TempDir; + use vestige_core::{ConnectionRecord, IngestInput}; fn create_test_storage() -> (Storage, TempDir) { let dir = TempDir::new().unwrap(); @@ -162,16 +181,18 @@ mod tests { } fn ingest_memory(storage: &Storage, content: &str) -> String { - let result = storage.ingest(IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["test".to_string()], - valid_from: None, - valid_until: None, - }).unwrap(); + let result = storage + .ingest(IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); result.id } @@ -195,15 +216,17 @@ mod tests { // Save a connection between them let now = Utc::now(); - storage.save_connection(&ConnectionRecord { - source_id: id1.clone(), - target_id: id2.clone(), - strength: 0.85, - link_type: "semantic".to_string(), - created_at: now, - last_activated: now, - activation_count: 1, - }).unwrap(); + storage + .save_connection(&ConnectionRecord { + source_id: id1.clone(), + target_id: id2.clone(), + strength: 0.85, + link_type: "semantic".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }) + .unwrap(); // Hydrate engine let mut engine = CognitiveEngine::new(); @@ -211,7 +234,11 @@ mod tests { // Verify activation network has the connection let assocs = engine.activation_network.get_associations(&id1); - assert!(!assocs.is_empty(), "Hydrated engine should have associations for {}", id1); + assert!( + !assocs.is_empty(), + "Hydrated engine should have associations for {}", + id1 + ); assert!( assocs.iter().any(|a| a.memory_id == id2), "Should find connection to {}", @@ -228,29 +255,37 @@ mod tests { let id3 = ingest_memory(&storage, "Event C was caused by A"); let now = Utc::now(); - storage.save_connection(&ConnectionRecord { - source_id: id1.clone(), - target_id: id2.clone(), - strength: 0.7, - link_type: "temporal".to_string(), - created_at: now, - last_activated: now, - activation_count: 1, - }).unwrap(); - storage.save_connection(&ConnectionRecord { - source_id: id1.clone(), - target_id: id3.clone(), - strength: 0.9, - link_type: "causal".to_string(), - created_at: now, - last_activated: now, - activation_count: 1, - }).unwrap(); + storage + .save_connection(&ConnectionRecord { + source_id: id1.clone(), + target_id: id2.clone(), + strength: 0.7, + link_type: "temporal".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }) + .unwrap(); + storage + .save_connection(&ConnectionRecord { + source_id: id1.clone(), + target_id: id3.clone(), + strength: 0.9, + link_type: "causal".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }) + .unwrap(); let mut engine = CognitiveEngine::new(); engine.hydrate(&storage); let assocs = engine.activation_network.get_associations(&id1); - assert!(assocs.len() >= 2, "Should have at least 2 associations, got {}", assocs.len()); + assert!( + assocs.len() >= 2, + "Should have at least 2 associations, got {}", + assocs.len() + ); } } diff --git a/crates/vestige-mcp/src/dashboard/events.rs b/crates/vestige-mcp/src/dashboard/events.rs index 15b624c..a6807e2 100644 --- a/crates/vestige-mcp/src/dashboard/events.rs +++ b/crates/vestige-mcp/src/dashboard/events.rs @@ -38,6 +38,24 @@ pub enum VestigeEvent { new_retention: f64, timestamp: DateTime, }, + // v2.0.5: Active forgetting — top-down suppression (Anderson 2025 + Davis Rac1) + MemorySuppressed { + id: String, + suppression_count: i32, + estimated_cascade: usize, + reversible_until: DateTime, + timestamp: DateTime, + }, + MemoryUnsuppressed { + id: String, + remaining_count: i32, + timestamp: DateTime, + }, + Rac1CascadeSwept { + seeds: usize, + neighbors_affected: usize, + timestamp: DateTime, + }, // -- Search -- SearchPerformed { @@ -48,6 +66,25 @@ 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, + }, + // -- Dream -- DreamStarted { memory_count: usize, @@ -105,6 +142,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, @@ -119,6 +162,9 @@ pub enum VestigeEvent { uptime_secs: u64, memory_count: usize, avg_retention: f64, + /// v2.0.5: memories with suppression_count > 0 (actively forgetting) + #[serde(default)] + suppressed_count: usize, timestamp: DateTime, }, } diff --git a/crates/vestige-mcp/src/dashboard/handlers.rs b/crates/vestige-mcp/src/dashboard/handlers.rs index 375664a..e158c56 100644 --- a/crates/vestige-mcp/src/dashboard/handlers.rs +++ b/crates/vestige-mcp/src/dashboard/handlers.rs @@ -38,7 +38,8 @@ pub async fn list_memories( if let Some(query) = params.q.as_ref().filter(|q| !q.trim().is_empty()) { // Use hybrid search - let results = state.storage + let results = state + .storage .hybrid_search(query, limit, 0.3, 0.7) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -76,7 +77,8 @@ pub async fn list_memories( } // No search query — list all memories - let mut nodes = state.storage + let mut nodes = state + .storage .get_all_nodes(limit, offset) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -121,7 +123,8 @@ pub async fn get_memory( State(state): State, Path(id): Path, ) -> Result, StatusCode> { - let node = state.storage + let node = state + .storage .get_node(&id) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::NOT_FOUND)?; @@ -152,7 +155,8 @@ pub async fn delete_memory( State(state): State, Path(id): Path, ) -> Result, StatusCode> { - let deleted = state.storage + let deleted = state + .storage .delete_node(&id) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -172,7 +176,8 @@ pub async fn promote_memory( State(state): State, Path(id): Path, ) -> Result, StatusCode> { - let node = state.storage + let node = state + .storage .promote_memory(&id) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -194,7 +199,8 @@ pub async fn demote_memory( State(state): State, Path(id): Path, ) -> Result, StatusCode> { - let node = state.storage + let node = state + .storage .demote_memory(&id) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -211,11 +217,133 @@ pub async fn demote_memory( }))) } -/// Get system stats -pub async fn get_stats( +/// Actively suppress a memory via top-down inhibitory control. +/// +/// Optional JSON body: `{"reason": "..."}`. Each call compounds — the +/// `suppression_count` field on the memory increments, FSRS takes a hit, +/// and the background Rac1 worker fades co-activated neighbors over the +/// next 72 hours. Emits a `MemorySuppressed` event so the 3D graph plays +/// the violet implosion animation. +/// +/// Reversible within the 24-hour labile window via `unsuppress_memory`. +/// +/// Fixes the v2.0.5 UI gap: `suppress` had full graph event handlers and +/// MCP tool exposure, but zero HTTP endpoint and no dashboard trigger. +pub async fn suppress_memory( State(state): State, + Path(id): Path, + body: Option>, ) -> Result, StatusCode> { - let stats = state.storage + use vestige_core::neuroscience::active_forgetting::{ + ActiveForgettingSystem, DEFAULT_LABILE_HOURS, + }; + + let reason = body + .as_ref() + .and_then(|Json(v)| v.get("reason")) + .and_then(|r| r.as_str()) + .map(String::from); + + let sys = ActiveForgettingSystem::new(); + + // Pre-count to surface in the response + for the event payload. + let before_count = state + .storage + .get_node(&id) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? + .map(|n| n.suppression_count) + .unwrap_or(0); + + let node = state + .storage + .suppress_memory(&id) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + // Estimate cascade size for the UX; capped at 100 so the number is + // stable even on highly-connected nodes. + let estimated_cascade = state + .storage + .get_connections_for_memory(&id) + .map(|edges| edges.len().min(100)) + .unwrap_or(0); + + let reversible_until = node + .suppressed_at + .map(|t| sys.reversible_until(t)) + .unwrap_or_else(chrono::Utc::now); + let retrieval_penalty = sys.retrieval_penalty(node.suppression_count); + + tracing::info!( + id = %id, + count = node.suppression_count, + reason = reason.as_deref().unwrap_or(""), + "Memory suppressed via dashboard" + ); + + state.emit(VestigeEvent::MemorySuppressed { + id: node.id.clone(), + suppression_count: node.suppression_count, + estimated_cascade, + reversible_until, + timestamp: chrono::Utc::now(), + }); + + Ok(Json(serde_json::json!({ + "suppressed": true, + "id": node.id, + "suppressionCount": node.suppression_count, + "priorCount": before_count, + "retrievalPenalty": retrieval_penalty, + "retentionStrength": node.retention_strength, + "retrievalStrength": node.retrieval_strength, + "stability": node.stability, + "estimatedCascadeNeighbors": estimated_cascade, + "reversibleUntil": reversible_until.to_rfc3339(), + "labileWindowHours": DEFAULT_LABILE_HOURS, + "reason": reason, + }))) +} + +/// Reverse a prior suppression, if still inside the 24-hour labile +/// window. Emits `MemoryUnsuppressed` so the graph plays the rainbow +/// reversal burst. Returns the current suppression state so the UI +/// knows whether a single click fully cleared the suppression or whether +/// the memory still has compounded suppressions remaining. +pub async fn unsuppress_memory( + State(state): State, + Path(id): Path, +) -> Result, StatusCode> { + use vestige_core::neuroscience::active_forgetting::ActiveForgettingSystem; + + let sys = ActiveForgettingSystem::new(); + let node = state + .storage + .reverse_suppression(&id, sys.labile_hours) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + + let still_suppressed = node.suppression_count > 0; + + state.emit(VestigeEvent::MemoryUnsuppressed { + id: node.id.clone(), + remaining_count: node.suppression_count, + timestamp: chrono::Utc::now(), + }); + + Ok(Json(serde_json::json!({ + "unsuppressed": true, + "id": node.id, + "suppressionCount": node.suppression_count, + "stillSuppressed": still_suppressed, + "retentionStrength": node.retention_strength, + "retrievalStrength": node.retrieval_strength, + "stability": node.stability, + }))) +} + +/// Get system stats +pub async fn get_stats(State(state): State) -> Result, StatusCode> { + let stats = state + .storage .get_stats() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -254,12 +382,14 @@ pub async fn get_timeline( let limit = params.limit.unwrap_or(200).clamp(1, 500); let start = Utc::now() - Duration::days(days); - let nodes = state.storage - .query_time_range(Some(start), Some(Utc::now()), limit) + let nodes = state + .storage + .query_time_range(Some(start), Some(Utc::now()), limit, None, None) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; // Group by day - let mut by_day: std::collections::BTreeMap> = std::collections::BTreeMap::new(); + let mut by_day: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); for node in &nodes { let date = node.created_at.format("%Y-%m-%d").to_string(); let content_preview: String = { @@ -299,10 +429,9 @@ pub async fn get_timeline( } /// Health check -pub async fn health_check( - State(state): State, -) -> Result, StatusCode> { - let stats = state.storage +pub async fn health_check(State(state): State) -> Result, StatusCode> { + let stats = state + .storage .get_stats() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; @@ -339,6 +468,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) @@ -348,46 +501,57 @@ 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 { - let results = state.storage + let results = state + .storage .search(query, 1) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - results.first() + results + .first() .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.storage + 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); } // Build nodes JSON with timestamps for recency calculation - let nodes_json: Vec = nodes.iter() + let nodes_json: Vec = nodes + .iter() .map(|n| { let label = if n.content.chars().count() > 80 { format!("{}...", n.content.chars().take(77).collect::()) @@ -407,7 +571,8 @@ pub async fn get_graph( }) .collect(); - let edges_json: Vec = edges.iter() + let edges_json: Vec = edges + .iter() .map(|e| { serde_json::json!({ "source": e.source_id, @@ -428,6 +593,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) // ============================================================================ @@ -498,10 +703,11 @@ pub async fn search_memories( // ============================================================================ /// Trigger a dream cycle via CognitiveEngine -pub async fn trigger_dream( - State(state): State, -) -> Result, StatusCode> { - let cognitive = state.cognitive.as_ref().ok_or(StatusCode::SERVICE_UNAVAILABLE)?; +pub async fn trigger_dream(State(state): State) -> Result, StatusCode> { + let cognitive = state + .cognitive + .as_ref() + .ok_or(StatusCode::SERVICE_UNAVAILABLE)?; let start = std::time::Instant::now(); let memory_count: usize = 50; @@ -539,17 +745,23 @@ pub async fn trigger_dream( // Run dream through CognitiveEngine let cog = cognitive.lock().await; - let pre_dream_count = cog.dreamer.get_connections().len(); + // 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 insights = cog.dreamer.synthesize_insights(&dream_memories); let all_connections = cog.dreamer.get_connections(); drop(cog); // Persist new connections - let new_connections = &all_connections[pre_dream_count..]; + // 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 { + for conn in new_connections.iter() { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -709,9 +921,7 @@ pub async fn explore_connections( } /// Predict which memories will be needed -pub async fn predict_memories( - State(state): State, -) -> Result, StatusCode> { +pub async fn predict_memories(State(state): State) -> Result, StatusCode> { // Get recent memories as predictions based on activity let recent = state .storage @@ -750,7 +960,9 @@ pub async fn score_importance( if let Some(ref cognitive) = state.cognitive { let context = vestige_core::ImportanceContext::current(); let cog = cognitive.lock().await; - let score = cog.importance_signals.compute_importance(&req.content, &context); + let score = cog + .importance_signals + .compute_importance(&req.content, &context); drop(cog); let composite = score.composite; @@ -760,6 +972,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, @@ -783,7 +996,11 @@ pub async fn score_importance( // Fallback: basic heuristic scoring let word_count = req.content.split_whitespace().count(); let has_code = req.content.contains("```") || req.content.contains("fn "); - let composite = if has_code { 0.7 } else { (word_count as f64 / 100.0).min(0.8) }; + let composite = if has_code { + 0.7 + } else { + (word_count as f64 / 100.0).min(0.8) + }; Ok(Json(serde_json::json!({ "composite": composite, @@ -901,17 +1118,35 @@ pub async fn list_intentions( let intentions = if status_filter == "all" { // Get all statuses - let mut all = state.storage.get_active_intentions() - .unwrap_or_default(); - all.extend(state.storage.get_intentions_by_status("fulfilled").unwrap_or_default()); - all.extend(state.storage.get_intentions_by_status("cancelled").unwrap_or_default()); - all.extend(state.storage.get_intentions_by_status("snoozed").unwrap_or_default()); + let mut all = state.storage.get_active_intentions().unwrap_or_default(); + all.extend( + state + .storage + .get_intentions_by_status("fulfilled") + .unwrap_or_default(), + ); + all.extend( + state + .storage + .get_intentions_by_status("cancelled") + .unwrap_or_default(), + ); + all.extend( + state + .storage + .get_intentions_by_status("snoozed") + .unwrap_or_default(), + ); all } else if status_filter == "active" { - state.storage.get_active_intentions() + state + .storage + .get_active_intentions() .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? } else { - state.storage.get_intentions_by_status(&status_filter) + state + .storage + .get_intentions_by_status(&status_filter) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? }; @@ -922,3 +1157,260 @@ 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, 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); + } + + 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 d236041..4e14b56 100644 --- a/crates/vestige-mcp/src/dashboard/mod.rs +++ b/crates/vestige-mcp/src/dashboard/mod.rs @@ -11,8 +11,8 @@ pub mod state; pub mod static_files; pub mod websocket; -use axum::routing::{delete, get, post}; use axum::Router; +use axum::routing::{delete, get, post}; use std::net::SocketAddr; use std::sync::Arc; use tokio::sync::Mutex; @@ -47,7 +47,6 @@ pub fn build_router_with_event_tx( } fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { - #[allow(unused_mut)] let mut origins = vec![ format!("http://127.0.0.1:{}", port) @@ -61,8 +60,16 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { // SvelteKit dev server — only in debug builds #[cfg(debug_assertions)] { - origins.push("http://localhost:5173".parse::().expect("valid origin")); - origins.push("http://127.0.0.1:5173".parse::().expect("valid origin")); + origins.push( + "http://localhost:5173" + .parse::() + .expect("valid origin"), + ); + origins.push( + "http://127.0.0.1:5173" + .parse::() + .expect("valid origin"), + ); } let cors = CorsLayer::new() @@ -116,7 +123,10 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { let router = Router::new() // SvelteKit Dashboard v2.0 (embedded static build) .route("/dashboard", get(static_files::serve_dashboard_spa)) - .route("/dashboard/{*path}", get(static_files::serve_dashboard_asset)) + .route( + "/dashboard/{*path}", + get(static_files::serve_dashboard_asset), + ) // Legacy embedded HTML (keep for backward compat) .route("/", get(handlers::serve_dashboard)) .route("/graph", get(handlers::serve_graph)) @@ -128,6 +138,15 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { .route("/api/memories/{id}", delete(handlers::delete_memory)) .route("/api/memories/{id}/promote", post(handlers::promote_memory)) .route("/api/memories/{id}/demote", post(handlers::demote_memory)) + // v2.0.7: active-forgetting HTTP surface. `suppress` was MCP-only + // 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}/unsuppress", + post(handlers::unsuppress_memory), + ) // Search .route("/api/search", get(handlers::search_memories)) // Stats & health @@ -143,9 +162,19 @@ fn build_router_inner(state: AppState, port: u16) -> (Router, AppState) { .route("/api/predict", post(handlers::predict_memories)) .route("/api/importance", post(handlers::score_importance)) .route("/api/consolidate", post(handlers::trigger_consolidation)) - .route("/api/retention-distribution", get(handlers::retention_distribution)) + .route( + "/api/retention-distribution", + get(handlers::retention_distribution), + ) // 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), + ) .layer( ServiceBuilder::new() .concurrency_limit(50) diff --git a/crates/vestige-mcp/src/dashboard/state.rs b/crates/vestige-mcp/src/dashboard/state.rs index c1ae661..5a41266 100644 --- a/crates/vestige-mcp/src/dashboard/state.rs +++ b/crates/vestige-mcp/src/dashboard/state.rs @@ -2,11 +2,11 @@ use std::sync::Arc; use std::time::Instant; -use tokio::sync::{broadcast, Mutex}; +use tokio::sync::{Mutex, broadcast}; use vestige_core::Storage; -use crate::cognitive::CognitiveEngine; use super::events::VestigeEvent; +use crate::cognitive::CognitiveEngine; /// Broadcast channel capacity — how many events can buffer before old ones drop. const EVENT_CHANNEL_CAPACITY: usize = 1024; @@ -22,10 +22,7 @@ pub struct AppState { impl AppState { /// Create a new AppState with event broadcasting. - pub fn new( - storage: Arc, - cognitive: Option>>, - ) -> Self { + pub fn new(storage: Arc, cognitive: Option>>) -> Self { let (event_tx, _) = broadcast::channel(EVENT_CHANNEL_CAPACITY); Self { storage, diff --git a/crates/vestige-mcp/src/dashboard/static_files.rs b/crates/vestige-mcp/src/dashboard/static_files.rs index 1bc9ba3..c780965 100644 --- a/crates/vestige-mcp/src/dashboard/static_files.rs +++ b/crates/vestige-mcp/src/dashboard/static_files.rs @@ -4,9 +4,9 @@ //! using `include_dir!`. This serves it at `/dashboard/` prefix. use axum::extract::Path; -use axum::http::{header, StatusCode}; +use axum::http::{StatusCode, header}; use axum::response::{Html, IntoResponse, Response}; -use include_dir::{include_dir, Dir}; +use include_dir::{Dir, include_dir}; /// Embed the entire SvelteKit build output into the binary. /// Build with: cd apps/dashboard && pnpm build @@ -16,11 +16,11 @@ static DASHBOARD_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../apps/das /// Serve the SvelteKit dashboard index pub async fn serve_dashboard_spa() -> impl IntoResponse { match DASHBOARD_DIR.get_file("index.html") { - Some(file) => Html( - String::from_utf8_lossy(file.contents()).to_string(), + Some(file) => Html(String::from_utf8_lossy(file.contents()).to_string()).into_response(), + None => ( + StatusCode::NOT_FOUND, + "Dashboard not built. Run: cd apps/dashboard && pnpm build", ) - .into_response(), - None => (StatusCode::NOT_FOUND, "Dashboard not built. Run: cd apps/dashboard && pnpm build") .into_response(), } } diff --git a/crates/vestige-mcp/src/dashboard/websocket.rs b/crates/vestige-mcp/src/dashboard/websocket.rs index 88d428c..3ee6fac 100644 --- a/crates/vestige-mcp/src/dashboard/websocket.rs +++ b/crates/vestige-mcp/src/dashboard/websocket.rs @@ -3,8 +3,8 @@ //! Clients connect to `/ws` and receive all VestigeEvents as JSON. //! Also sends heartbeats every 5 seconds with system stats. -use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::extract::State; +use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; use axum::http::{HeaderMap, StatusCode}; use axum::response::IntoResponse; use chrono::Utc; @@ -26,10 +26,11 @@ pub async fn ws_handler( // Non-browser clients (curl, wscat) won't have Origin — allowed since localhost-only. match headers.get("origin").and_then(|v| v.to_str().ok()) { Some(origin) => { - let allowed = origin.starts_with("http://127.0.0.1:") - || origin.starts_with("http://localhost:"); + let allowed = + origin.starts_with("http://127.0.0.1:") || origin.starts_with("http://localhost:"); #[cfg(debug_assertions)] - let allowed = allowed || origin == "http://localhost:5173" || origin == "http://127.0.0.1:5173"; + let allowed = + allowed || origin == "http://localhost:5173" || origin == "http://127.0.0.1:5173"; if !allowed { warn!("Rejected WebSocket connection from origin: {}", origin); return StatusCode::FORBIDDEN.into_response(); @@ -85,10 +86,14 @@ async fn handle_socket(socket: WebSocket, state: AppState) { .map(|s| (s.total_nodes as usize, s.average_retention)) .unwrap_or((0, 0.0)); + // v2.0.5: live count of memories being actively forgotten + let suppressed_count = heartbeat_state.storage.count_suppressed().unwrap_or(0); + let event = VestigeEvent::Heartbeat { uptime_secs: uptime, memory_count, avg_retention, + suppressed_count, timestamp: Utc::now(), }; @@ -118,6 +123,9 @@ async fn handle_socket(socket: WebSocket, state: AppState) { msg = receiver.next() => { match msg { Some(Ok(Message::Close(_))) | None => break, + // Match guards can't move out of the bound `data` (Bytes is + // not Copy), so the nested `if` is required here. + #[allow(clippy::collapsible_match, clippy::collapsible_if)] Some(Ok(Message::Ping(data))) => { if sender.send(Message::Pong(data)).await.is_err() { break; 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 b594b0e..4527772 100644 --- a/crates/vestige-mcp/src/main.rs +++ b/crates/vestige-mcp/src/main.rs @@ -35,7 +35,7 @@ use std::io; use std::path::PathBuf; use std::sync::Arc; use tokio::sync::Mutex; -use tracing::{error, info, warn, Level}; +use tracing::{Level, error, info, warn}; use tracing_subscriber::EnvFilter; // Use vestige-core for the cognitive science engine @@ -48,6 +48,7 @@ use server::McpServer; struct Config { data_dir: Option, http_port: u16, + dashboard_enabled: bool, } /// Parse command-line arguments into a `Config`. @@ -59,6 +60,9 @@ fn parse_args() -> Config { .ok() .and_then(|s| s.parse().ok()) .unwrap_or(3928); + 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() { @@ -78,10 +82,18 @@ fn parse_args() -> Config { println!(" --http-port HTTP transport port (default: 3928)"); println!(); println!("ENVIRONMENT:"); - println!(" RUST_LOG Log level filter (e.g., debug, info, warn, error)"); - println!(" VESTIGE_AUTH_TOKEN Override the bearer token for HTTP transport"); + println!( + " RUST_LOG Log level filter (e.g., debug, info, warn, error)" + ); + println!( + " 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_DASHBOARD_PORT Dashboard port (default: 3927)"); + println!( + " VESTIGE_SYSTEM_PROMPT_MODE Inject the full composition mandate into every MCP session (minimal|full, default: minimal)" + ); println!(); println!("EXAMPLES:"); println!(" vestige-mcp"); @@ -148,7 +160,11 @@ fn parse_args() -> Config { i += 1; } - Config { data_dir, http_port } + Config { + data_dir, + http_port, + dashboard_enabled, + } } #[tokio::main] @@ -158,16 +174,16 @@ async fn main() { // Initialize logging to stderr (stdout is for JSON-RPC) tracing_subscriber::fmt() - .with_env_filter( - EnvFilter::from_default_env() - .add_directive(Level::INFO.into()) - ) + .with_env_filter(EnvFilter::from_default_env().add_directive(Level::INFO.into())) .with_writer(io::stderr) .with_target(false) .with_ansi(false) .init(); - info!("Vestige MCP Server v{} starting...", env!("CARGO_PKG_VERSION")); + info!( + "Vestige MCP Server v{} starting...", + env!("CARGO_PKG_VERSION") + ); // Initialize storage with optional custom data directory let storage = match Storage::new(config.data_dir) { @@ -180,7 +196,9 @@ async fn main() { if let Err(e) = s.init_embeddings() { error!("Failed to initialize embedding service: {}", e); error!("Smart ingest will fall back to regular ingest without deduplication"); - error!("Hint: Check FASTEMBED_CACHE_PATH or ensure ~/.fastembed_cache exists"); + error!( + "Hint: Check FASTEMBED_CACHE_PATH or ensure ~/.cache/vestige/fastembed is writable" + ); } else { info!("Embedding service initialized successfully"); } @@ -228,7 +246,10 @@ async fn main() { true } Err(e) => { - warn!("Could not read consolidation history: {} — running anyway", e); + warn!( + "Could not read consolidation history: {} — running anyway", + e + ); true } }; @@ -250,6 +271,23 @@ async fn main() { warn!("Periodic auto-consolidation failed: {}", e); } } + + // v2.0.5: Rac1 cascade sweep — walk recently-suppressed + // memories and fade their co-activated neighbors + // (Cervantes-Sandoval & Davis 2020, PMC7477079). + match storage_clone.run_rac1_cascade_sweep() { + Ok((seeds, affected)) if seeds > 0 || affected > 0 => { + info!( + suppressed_seeds = seeds, + neighbors_affected = affected, + "Rac1 cascade sweep complete" + ); + } + Ok(_) => {} + Err(e) => { + warn!("Rac1 cascade sweep failed: {}", e); + } + } } // Sleep until next check @@ -268,10 +306,25 @@ async fn main() { info!("CognitiveEngine initialized and hydrated"); // Create shared event broadcast channel for dashboard <-> MCP tool events - let (event_tx, _) = tokio::sync::broadcast::channel::(1024); + 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") .ok() .and_then(|s| s.parse::().ok()) @@ -285,7 +338,9 @@ async fn main() { Some(dashboard_cognitive), dashboard_event_tx, dashboard_port, - ).await { + ) + .await + { Ok(_state) => { info!("Dashboard started with WebSocket + CognitiveEngine + shared event bus"); } @@ -294,6 +349,8 @@ async fn main() { } } }); + } else { + info!("Dashboard disabled by VESTIGE_DASHBOARD_ENABLED=false"); } // Start HTTP MCP transport (Streamable HTTP for Claude.ai / remote clients) @@ -305,9 +362,10 @@ async fn main() { match protocol::auth::get_or_create_auth_token() { Ok(token) => { - let bind = std::env::var("VESTIGE_HTTP_BIND").unwrap_or_else(|_| "127.0.0.1".to_string()); + 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[..8]); + eprintln!("Auth token: {}...", &token[..token.len().min(8)]); tokio::spawn(async move { if let Err(e) = protocol::http::start_http_transport( http_storage, @@ -323,7 +381,10 @@ async fn main() { }); } Err(e) => { - warn!("Could not create auth token, HTTP transport disabled: {}", e); + warn!( + "Could not create auth token, HTTP transport disabled: {}", + e + ); } } } diff --git a/crates/vestige-mcp/src/protocol/http.rs b/crates/vestige-mcp/src/protocol/http.rs index b02c0d8..e90c313 100644 --- a/crates/vestige-mcp/src/protocol/http.rs +++ b/crates/vestige-mcp/src/protocol/http.rs @@ -17,17 +17,17 @@ use axum::response::IntoResponse; use axum::routing::{delete, post}; use axum::{Json, Router}; use subtle::ConstantTimeEq; -use tokio::sync::{broadcast, Mutex, RwLock}; +use tokio::sync::{Mutex, RwLock, broadcast}; use tower::ServiceBuilder; use tower::limit::ConcurrencyLimitLayer; use tower_http::cors::CorsLayer; use tracing::{info, warn}; use crate::cognitive::CognitiveEngine; +use crate::dashboard::events::VestigeEvent; use crate::protocol::types::JsonRpcRequest; use crate::server::McpServer; use vestige_core::Storage; -use crate::dashboard::events::VestigeEvent; /// Maximum concurrent sessions. const MAX_SESSIONS: usize = 100; @@ -95,7 +95,11 @@ pub async fn start_http_transport( }); let removed = before - map.len(); if removed > 0 { - info!("Session reaper: removed {} idle sessions ({} active)", removed, map.len()); + info!( + "Session reaper: removed {} idle sessions ({} active)", + removed, + map.len() + ); } } }); @@ -108,7 +112,27 @@ pub async fn start_http_transport( ServiceBuilder::new() .layer(DefaultBodyLimit::max(MAX_BODY_SIZE)) .layer(ConcurrencyLimitLayer::new(CONCURRENCY_LIMIT)) - .layer(CorsLayer::permissive()), + .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_methods([ + axum::http::Method::POST, + axum::http::Method::DELETE, + axum::http::Method::OPTIONS, + ]) + .allow_headers([ + axum::http::header::CONTENT_TYPE, + axum::http::header::AUTHORIZATION, + ]), + ), ) .with_state(state); @@ -143,9 +167,10 @@ fn validate_auth(headers: &HeaderMap, expected: &str) -> Result<(), (StatusCode, .and_then(|v| v.to_str().ok()) .ok_or((StatusCode::UNAUTHORIZED, "Missing Authorization header"))?; - let token = header - .strip_prefix("Bearer ") - .ok_or((StatusCode::UNAUTHORIZED, "Invalid Authorization scheme (expected Bearer)"))?; + let token = header.strip_prefix("Bearer ").ok_or(( + StatusCode::UNAUTHORIZED, + "Invalid Authorization scheme (expected Bearer)", + ))?; // Constant-time comparison: prevents timing side-channel attacks. // We first check lengths match (length itself is not secret since UUIDs @@ -196,11 +221,7 @@ async fn post_mcp( // Take write lock immediately to avoid TOCTOU race on MAX_SESSIONS check. let mut sessions = state.sessions.write().await; if sessions.len() >= MAX_SESSIONS { - return ( - StatusCode::SERVICE_UNAVAILABLE, - "Too many active sessions", - ) - .into_response(); + return (StatusCode::SERVICE_UNAVAILABLE, "Too many active sessions").into_response(); } let server = McpServer::new_with_events( @@ -229,13 +250,23 @@ async fn post_mcp( match response { Some(resp) => { let mut resp_headers = HeaderMap::new(); - resp_headers.insert("mcp-session-id", session_id.parse().unwrap()); + resp_headers.insert( + "mcp-session-id", + session_id + .parse() + .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), + ); (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()); + resp_headers.insert( + "mcp-session-id", + session_id + .parse() + .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), + ); (StatusCode::ACCEPTED, resp_headers).into_response() } } @@ -260,11 +291,7 @@ async fn post_mcp( let session = match session { Some(s) => s, None => { - return ( - StatusCode::NOT_FOUND, - "Session not found or expired", - ) - .into_response(); + return (StatusCode::NOT_FOUND, "Session not found or expired").into_response(); } }; @@ -275,7 +302,12 @@ async fn post_mcp( }; let mut resp_headers = HeaderMap::new(); - resp_headers.insert("mcp-session-id", session_id.parse().unwrap()); + resp_headers.insert( + "mcp-session-id", + session_id + .parse() + .unwrap_or_else(|_| axum::http::HeaderValue::from_static("invalid")), + ); match response { Some(resp) => (StatusCode::OK, resp_headers, Json(resp)).into_response(), @@ -295,7 +327,13 @@ async fn delete_mcp( let session_id = match session_id_from_headers(&headers) { Some(id) => id, - None => return (StatusCode::BAD_REQUEST, "Missing or invalid Mcp-Session-Id header").into_response(), + None => { + return ( + StatusCode::BAD_REQUEST, + "Missing or invalid Mcp-Session-Id header", + ) + .into_response(); + } }; let mut sessions = state.sessions.write().await; diff --git a/crates/vestige-mcp/src/protocol/types.rs b/crates/vestige-mcp/src/protocol/types.rs index a29355f..57f5a10 100644 --- a/crates/vestige-mcp/src/protocol/types.rs +++ b/crates/vestige-mcp/src/protocol/types.rs @@ -25,7 +25,6 @@ pub struct JsonRpcRequest { pub params: Option, } - /// JSON-RPC Response #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JsonRpcResponse { @@ -129,7 +128,10 @@ impl JsonRpcError { #[allow(dead_code)] // Reserved for future resource handling pub fn resource_not_found(uri: &str) -> Self { - Self::new(ErrorCode::ResourceNotFound, &format!("Resource not found: {}", uri)) + Self::new( + ErrorCode::ResourceNotFound, + &format!("Resource not found: {}", uri), + ) } } diff --git a/crates/vestige-mcp/src/resources/memory.rs b/crates/vestige-mcp/src/resources/memory.rs index 1f3c696..6148932 100644 --- a/crates/vestige-mcp/src/resources/memory.rs +++ b/crates/vestige-mcp/src/resources/memory.rs @@ -35,15 +35,10 @@ pub async fn read(storage: &Arc, uri: &str) -> Result { fn parse_query_param(query: Option<&str>, key: &str, default: i32) -> i32 { query .and_then(|q| { - q.split('&') - .find_map(|pair| { - let (k, v) = pair.split_once('=')?; - if k == key { - v.parse().ok() - } else { - None - } - }) + q.split('&').find_map(|pair| { + let (k, v) = pair.split_once('=')?; + if k == key { v.parse().ok() } else { None } + }) }) .unwrap_or(default) .clamp(1, 100) @@ -95,7 +90,7 @@ async fn read_recent(storage: &Arc, limit: i32) -> Result 200 { - format!("{}...", &n.content[..200]) + format!("{}...", &n.content[..n.content.floor_char_boundary(200)]) } else { n.content.clone() }, @@ -139,7 +134,7 @@ async fn read_decaying(storage: &Arc) -> Result { serde_json::json!({ "id": n.id, "summary": if n.content.len() > 200 { - format!("{}...", &n.content[..200]) + format!("{}...", &n.content[..n.content.floor_char_boundary(200)]) } else { n.content.clone() }, @@ -180,7 +175,7 @@ async fn read_due(storage: &Arc) -> Result { serde_json::json!({ "id": n.id, "summary": if n.content.len() > 200 { - format!("{}...", &n.content[..200]) + format!("{}...", &n.content[..n.content.floor_char_boundary(200)]) } else { n.content.clone() }, @@ -228,7 +223,10 @@ async fn read_intentions(storage: &Arc) -> Result { }) .collect(); - let overdue_count = items.iter().filter(|i| i["isOverdue"].as_bool().unwrap_or(false)).count(); + let overdue_count = items + .iter() + .filter(|i| i["isOverdue"].as_bool().unwrap_or(false)) + .count(); let result = serde_json::json!({ "total": intentions.len(), @@ -241,7 +239,9 @@ async fn read_intentions(storage: &Arc) -> Result { } async fn read_triggered_intentions(storage: &Arc) -> Result { - let overdue = storage.get_overdue_intentions().map_err(|e| e.to_string())?; + let overdue = storage + .get_overdue_intentions() + .map_err(|e| e.to_string())?; let now = chrono::Utc::now(); let items: Vec = overdue @@ -289,7 +289,10 @@ async fn read_insights(storage: &Arc) -> Result { let insights = storage.get_insights(50).map_err(|e| e.to_string())?; let pending: Vec<_> = insights.iter().filter(|i| i.feedback.is_none()).collect(); - let accepted: Vec<_> = insights.iter().filter(|i| i.feedback.as_deref() == Some("accepted")).collect(); + let accepted: Vec<_> = insights + .iter() + .filter(|i| i.feedback.as_deref() == Some("accepted")) + .collect(); let items: Vec = insights .iter() @@ -319,8 +322,12 @@ async fn read_insights(storage: &Arc) -> Result { } async fn read_consolidation_log(storage: &Arc) -> Result { - let history = storage.get_consolidation_history(20).map_err(|e| e.to_string())?; - let last_run = storage.get_last_consolidation().map_err(|e| e.to_string())?; + let history = storage + .get_consolidation_history(20) + .map_err(|e| e.to_string())?; + let last_run = storage + .get_last_consolidation() + .map_err(|e| e.to_string())?; let items: Vec = history .iter() diff --git a/crates/vestige-mcp/src/server.rs b/crates/vestige-mcp/src/server.rs index 10a233a..00c2bdd 100644 --- a/crates/vestige-mcp/src/server.rs +++ b/crates/vestige-mcp/src/server.rs @@ -3,25 +3,69 @@ //! Handles the main MCP server logic, routing requests to appropriate //! tool and resource handlers. -use std::collections::HashMap; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; use chrono::Utc; -use tokio::sync::{broadcast, Mutex}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use tokio::sync::{Mutex, broadcast}; use tracing::{debug, info, warn}; use crate::cognitive::CognitiveEngine; use crate::dashboard::events::VestigeEvent; use crate::protocol::messages::{ - CallToolRequest, CallToolResult, InitializeRequest, InitializeResult, - ListResourcesResult, ListToolsResult, ReadResourceRequest, ReadResourceResult, - ResourceDescription, ServerCapabilities, ServerInfo, ToolDescription, + CallToolRequest, CallToolResult, InitializeRequest, InitializeResult, ListResourcesResult, + ListToolsResult, ReadResourceRequest, ReadResourceResult, ResourceDescription, + ServerCapabilities, ServerInfo, ToolDescription, }; use crate::protocol::types::{JsonRpcError, JsonRpcRequest, JsonRpcResponse, MCP_VERSION}; use crate::resources; use crate::tools; use vestige_core::Storage; +/// Build the MCP `instructions` string injected into every connecting client's +/// system prompt. +/// +/// Default ("minimal", 3 sentences) is safe for any user: competitive coders, +/// hobbyists saving recipes, Rails devs saving bug fixes, enterprise deployments. +/// It earns its per-session token cost by telling the client *how* to use +/// 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`. +/// +/// Anything other than `full` falls back to minimal. +fn build_instructions() -> String { + let mode = std::env::var("VESTIGE_SYSTEM_PROMPT_MODE").unwrap_or_default(); + if mode.eq_ignore_ascii_case("full") { + "Vestige is your long-term cognitive memory AND reasoning engine, not a RAG database. \ + 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 \ + 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.' \ + (c) Recommendation: what the user should DO, as a concrete executable action. \ + If your draft begins 'Memory A says X. Memory B says Y.' STOP and rewrite.\ + \n\nBLOCKING PHRASE: If retrieved high-trust memories (retention > 0.7, reps > 0) \ + contradict what you were about to say, start your response with 'Vestige is blocking this:' \ + and surface the contradiction verbatim before proceeding. FSRS trust overrides fresh guesses.\ + \n\nFEEDBACK: If the user confirms a memory was helpful, call memory(action='promote'). \ + If they correct it, call memory(action='demote'). Do not ask permission, just act." + .to_string() + } else { + "Vestige is your long-term memory system. Compose retrievals into recommendations \ + rather than listing their contents when the user is making a decision. \ + On user feedback, call memory(action='promote') for helpful retrievals and \ + memory(action='demote') for wrong ones — do not ask permission, just act." + .to_string() + } +} + /// MCP Server implementation pub struct McpServer { storage: Arc, @@ -72,8 +116,14 @@ impl McpServer { debug!("Handling request: {}", request.method); // Check initialization for non-initialize requests - if !self.initialized && request.method != "initialize" && request.method != "notifications/initialized" { - warn!("Rejecting request '{}': server not initialized", request.method); + if !self.initialized + && request.method != "initialize" + && request.method != "notifications/initialized" + { + warn!( + "Rejecting request '{}': server not initialized", + request.method + ); return Some(JsonRpcResponse::error( request.id, JsonRpcError::server_not_initialized(), @@ -109,21 +159,28 @@ impl McpServer { params: Option, ) -> Result { let request: InitializeRequest = match params { - Some(p) => serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, + Some(p) => serde_json::from_value(p) + .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, None => InitializeRequest::default(), }; // 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); + info!( + "Client requested older protocol version {}, using it", + request.protocol_version + ); request.protocol_version.clone() } else { MCP_VERSION.to_string() }; self.initialized = true; - info!("MCP session initialized with protocol version {}", negotiated_version); + info!( + "MCP session initialized with protocol version {}", + negotiated_version + ); let result = InitializeResult { protocol_version: negotiated_version, @@ -144,14 +201,7 @@ impl McpServer { }), prompts: None, }, - instructions: Some( - "Vestige is your long-term memory system. Use it to remember important information, \ - recall past knowledge, and maintain context across sessions. The system uses \ - FSRS-6 spaced repetition to naturally decay memories over time. \ - \n\nFeedback Protocol: If the user explicitly confirms a memory was helpful, use \ - memory(action='promote'). If they correct a hallucination or say a memory was wrong, use \ - memory(action='demote'). Do not ask for permission - just act on their feedback.".to_string() - ), + instructions: Some(build_instructions()), }; serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(&e.to_string())) @@ -159,7 +209,9 @@ impl McpServer { /// Handle tools/list request async fn handle_tools_list(&self) -> Result { - // v1.8: 19 tools. Deprecated tools still work via redirects in handle_tools_call. + // v2.0.5+: 24 tools (verified by the `tools.len() == 24` 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![ // ================================================================ // UNIFIED TOOLS (v1.1+) @@ -293,6 +345,28 @@ impl McpServer { 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(), }, + // ================================================================ + // DEEP REFERENCE (v2.0.4+) — replaces cross_reference + // ================================================================ + ToolDescription { + 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(), + }, + 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(), + }, + // ================================================================ + // ACTIVE FORGETTING (v2.0.5) — top-down suppression + // Anderson et al. 2025 Nat Rev Neurosci + Davis Rac1 + // ================================================================ + ToolDescription { + 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(), + }, ]; let result = ListToolsResult { tools }; @@ -305,7 +379,8 @@ impl McpServer { params: Option, ) -> Result { let request: CallToolRequest = match params { - Some(p) => serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, + Some(p) => serde_json::from_value(p) + .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, None => return Err(JsonRpcError::invalid_params("Missing tool call parameters")), }; @@ -316,43 +391,68 @@ impl McpServer { } // Save args for event emission (tool dispatch consumes request.arguments) - let saved_args = if self.event_tx.is_some() { request.arguments.clone() } else { None }; + let saved_args = if self.event_tx.is_some() { + request.arguments.clone() + } else { + None + }; let result = match request.name.as_str() { // ================================================================ // UNIFIED TOOLS (v1.1+) - Preferred API // ================================================================ - "search" => tools::search_unified::execute(&self.storage, &self.cognitive, request.arguments).await, - "memory" => tools::memory_unified::execute(&self.storage, &self.cognitive, request.arguments).await, - "codebase" => tools::codebase_unified::execute(&self.storage, &self.cognitive, request.arguments).await, - "intention" => tools::intention_unified::execute(&self.storage, &self.cognitive, request.arguments).await, + "search" => { + tools::search_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await + } + "memory" => { + tools::memory_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await + } + "codebase" => { + tools::codebase_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await + } + "intention" => { + tools::intention_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await + } // ================================================================ // Core memory (v1.7: smart_ingest absorbs ingest + checkpoint) // ================================================================ - "smart_ingest" => tools::smart_ingest::execute(&self.storage, &self.cognitive, request.arguments).await, + "smart_ingest" => { + tools::smart_ingest::execute(&self.storage, &self.cognitive, request.arguments) + .await + } // ================================================================ // DEPRECATED (v1.7): ingest → smart_ingest // ================================================================ "ingest" => { warn!("Tool 'ingest' is deprecated in v1.7. Use 'smart_ingest' instead."); - tools::smart_ingest::execute(&self.storage, &self.cognitive, request.arguments).await + tools::smart_ingest::execute(&self.storage, &self.cognitive, request.arguments) + .await } // ================================================================ // DEPRECATED (v1.7): session_checkpoint → smart_ingest (batch mode) // ================================================================ "session_checkpoint" => { - warn!("Tool 'session_checkpoint' is deprecated in v1.7. Use 'smart_ingest' with 'items' parameter instead."); - tools::smart_ingest::execute(&self.storage, &self.cognitive, request.arguments).await + warn!( + "Tool 'session_checkpoint' is deprecated in v1.7. Use 'smart_ingest' with 'items' parameter instead." + ); + tools::smart_ingest::execute(&self.storage, &self.cognitive, request.arguments) + .await } // ================================================================ // DEPRECATED (v1.7): promote_memory → memory(action='promote') // ================================================================ "promote_memory" => { - warn!("Tool 'promote_memory' is deprecated in v1.7. Use 'memory' with action='promote' instead."); + warn!( + "Tool 'promote_memory' is deprecated in v1.7. Use 'memory' with action='promote' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -366,7 +466,9 @@ impl McpServer { tools::memory_unified::execute(&self.storage, &self.cognitive, unified_args).await } "demote_memory" => { - warn!("Tool 'demote_memory' is deprecated in v1.7. Use 'memory' with action='demote' instead."); + warn!( + "Tool 'demote_memory' is deprecated in v1.7. Use 'memory' with action='demote' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -385,17 +487,34 @@ impl McpServer { // ================================================================ "health_check" => { warn!("Tool 'health_check' is deprecated in v1.7. Use 'system_status' instead."); - tools::maintenance::execute_system_status(&self.storage, &self.cognitive, request.arguments).await + tools::maintenance::execute_system_status( + &self.storage, + &self.cognitive, + request.arguments, + ) + .await } "stats" => { warn!("Tool 'stats' is deprecated in v1.7. Use 'system_status' instead."); - tools::maintenance::execute_system_status(&self.storage, &self.cognitive, request.arguments).await + tools::maintenance::execute_system_status( + &self.storage, + &self.cognitive, + request.arguments, + ) + .await } // ================================================================ // SYSTEM STATUS (v1.7: replaces health_check + stats) // ================================================================ - "system_status" => tools::maintenance::execute_system_status(&self.storage, &self.cognitive, request.arguments).await, + "system_status" => { + tools::maintenance::execute_system_status( + &self.storage, + &self.cognitive, + request.arguments, + ) + .await + } "mark_reviewed" => tools::review::execute(&self.storage, request.arguments).await, @@ -403,15 +522,21 @@ impl McpServer { // DEPRECATED: Search tools - redirect to unified 'search' // ================================================================ "recall" | "semantic_search" | "hybrid_search" => { - warn!("Tool '{}' is deprecated. Use 'search' instead.", request.name); - tools::search_unified::execute(&self.storage, &self.cognitive, request.arguments).await + warn!( + "Tool '{}' is deprecated. Use 'search' instead.", + request.name + ); + tools::search_unified::execute(&self.storage, &self.cognitive, request.arguments) + .await } // ================================================================ // DEPRECATED: Memory tools - redirect to unified 'memory' // ================================================================ "get_knowledge" => { - warn!("Tool 'get_knowledge' is deprecated. Use 'memory' with action='get' instead."); + warn!( + "Tool 'get_knowledge' is deprecated. Use 'memory' with action='get' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let id = args.get("id").cloned().unwrap_or(serde_json::Value::Null); @@ -425,7 +550,9 @@ impl McpServer { tools::memory_unified::execute(&self.storage, &self.cognitive, unified_args).await } "delete_knowledge" => { - warn!("Tool 'delete_knowledge' is deprecated. Use 'memory' with action='delete' instead."); + warn!( + "Tool 'delete_knowledge' is deprecated. Use 'memory' with action='delete' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let id = args.get("id").cloned().unwrap_or(serde_json::Value::Null); @@ -439,10 +566,15 @@ impl McpServer { tools::memory_unified::execute(&self.storage, &self.cognitive, unified_args).await } "get_memory_state" => { - warn!("Tool 'get_memory_state' is deprecated. Use 'memory' with action='state' instead."); + warn!( + "Tool 'get_memory_state' is deprecated. Use 'memory' with action='state' instead." + ); let unified_args = match request.arguments { Some(ref args) => { - let id = args.get("memory_id").cloned().unwrap_or(serde_json::Value::Null); + let id = args + .get("memory_id") + .cloned() + .unwrap_or(serde_json::Value::Null); Some(serde_json::json!({ "action": "state", "id": id @@ -457,7 +589,9 @@ impl McpServer { // DEPRECATED: Codebase tools - redirect to unified 'codebase' // ================================================================ "remember_pattern" => { - warn!("Tool 'remember_pattern' is deprecated. Use 'codebase' with action='remember_pattern' instead."); + warn!( + "Tool 'remember_pattern' is deprecated. Use 'codebase' with action='remember_pattern' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -471,12 +605,17 @@ impl McpServer { tools::codebase_unified::execute(&self.storage, &self.cognitive, unified_args).await } "remember_decision" => { - warn!("Tool 'remember_decision' is deprecated. Use 'codebase' with action='remember_decision' instead."); + warn!( + "Tool 'remember_decision' is deprecated. Use 'codebase' with action='remember_decision' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); if let Some(obj) = new_args.as_object_mut() { - obj.insert("action".to_string(), serde_json::json!("remember_decision")); + obj.insert( + "action".to_string(), + serde_json::json!("remember_decision"), + ); } Some(new_args) } @@ -485,7 +624,9 @@ impl McpServer { tools::codebase_unified::execute(&self.storage, &self.cognitive, unified_args).await } "get_codebase_context" => { - warn!("Tool 'get_codebase_context' is deprecated. Use 'codebase' with action='get_context' instead."); + warn!( + "Tool 'get_codebase_context' is deprecated. Use 'codebase' with action='get_context' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -503,7 +644,9 @@ impl McpServer { // DEPRECATED: Intention tools - redirect to unified 'intention' // ================================================================ "set_intention" => { - warn!("Tool 'set_intention' is deprecated. Use 'intention' with action='set' instead."); + warn!( + "Tool 'set_intention' is deprecated. Use 'intention' with action='set' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -514,10 +657,13 @@ impl McpServer { } None => Some(serde_json::json!({"action": "set"})), }; - tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args).await + tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args) + .await } "check_intentions" => { - warn!("Tool 'check_intentions' is deprecated. Use 'intention' with action='check' instead."); + warn!( + "Tool 'check_intentions' is deprecated. Use 'intention' with action='check' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -528,13 +674,19 @@ impl McpServer { } None => Some(serde_json::json!({"action": "check"})), }; - tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args).await + tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args) + .await } "complete_intention" => { - warn!("Tool 'complete_intention' is deprecated. Use 'intention' with action='update', status='complete' instead."); + warn!( + "Tool 'complete_intention' is deprecated. Use 'intention' with action='update', status='complete' instead." + ); let unified_args = match request.arguments { Some(ref args) => { - let id = args.get("intentionId").cloned().unwrap_or(serde_json::Value::Null); + let id = args + .get("intentionId") + .cloned() + .unwrap_or(serde_json::Value::Null); Some(serde_json::json!({ "action": "update", "id": id, @@ -543,14 +695,23 @@ impl McpServer { } None => None, }; - tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args).await + tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args) + .await } "snooze_intention" => { - warn!("Tool 'snooze_intention' is deprecated. Use 'intention' with action='update', status='snooze' instead."); + warn!( + "Tool 'snooze_intention' is deprecated. Use 'intention' with action='update', status='snooze' instead." + ); let unified_args = match request.arguments { Some(ref args) => { - let id = args.get("intentionId").cloned().unwrap_or(serde_json::Value::Null); - let minutes = args.get("minutes").cloned().unwrap_or(serde_json::json!(30)); + let id = args + .get("intentionId") + .cloned() + .unwrap_or(serde_json::Value::Null); + let minutes = args + .get("minutes") + .cloned() + .unwrap_or(serde_json::json!(30)); Some(serde_json::json!({ "action": "update", "id": id, @@ -560,10 +721,13 @@ impl McpServer { } None => None, }; - tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args).await + tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args) + .await } "list_intentions" => { - warn!("Tool 'list_intentions' is deprecated. Use 'intention' with action='list' instead."); + warn!( + "Tool 'list_intentions' is deprecated. Use 'intention' with action='list' instead." + ); let unified_args = match request.arguments { Some(ref args) => { let mut new_args = args.clone(); @@ -577,15 +741,20 @@ impl McpServer { } None => Some(serde_json::json!({"action": "list"})), }; - tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args).await + tools::intention_unified::execute(&self.storage, &self.cognitive, unified_args) + .await } // ================================================================ // Neuroscience tools (internal, not in tools/list) // ================================================================ - "list_by_state" => tools::memory_states::execute_list(&self.storage, request.arguments).await, + "list_by_state" => { + tools::memory_states::execute_list(&self.storage, request.arguments).await + } "state_stats" => tools::memory_states::execute_stats(&self.storage).await, - "trigger_importance" => tools::tagging::execute_trigger(&self.storage, request.arguments).await, + "trigger_importance" => { + tools::tagging::execute_trigger(&self.storage, request.arguments).await + } "find_tagged" => tools::tagging::execute_find(&self.storage, request.arguments).await, "tagging_stats" => tools::tagging::execute_stats(&self.storage).await, "match_context" => tools::context::execute(&self.storage, request.arguments).await, @@ -593,7 +762,9 @@ impl McpServer { // ================================================================ // Feedback (internal, still used by request_feedback) // ================================================================ - "request_feedback" => tools::feedback::execute_request_feedback(&self.storage, request.arguments).await, + "request_feedback" => { + tools::feedback::execute_request_feedback(&self.storage, request.arguments).await + } // ================================================================ // TEMPORAL TOOLS (v1.2+) @@ -617,7 +788,9 @@ impl McpServer { // ================================================================ // AUTO-SAVE & DEDUP TOOLS (v1.3+) // ================================================================ - "importance_score" => tools::importance::execute(&self.storage, &self.cognitive, request.arguments).await, + "importance_score" => { + tools::importance::execute(&self.storage, &self.cognitive, request.arguments).await + } "find_duplicates" => tools::dedup::execute(&self.storage, request.arguments).await, // ================================================================ @@ -625,25 +798,45 @@ impl McpServer { // ================================================================ "dream" => { self.emit(VestigeEvent::DreamStarted { - memory_count: self.storage.get_stats().map(|s| s.total_nodes as usize).unwrap_or(0), + memory_count: self + .storage + .get_stats() + .map(|s| s.total_nodes as usize) + .unwrap_or(0), timestamp: chrono::Utc::now(), }); tools::dream::execute(&self.storage, &self.cognitive, request.arguments).await } - "explore_connections" => tools::explore::execute(&self.storage, &self.cognitive, request.arguments).await, - "predict" => tools::predict::execute(&self.storage, &self.cognitive, request.arguments).await, + "explore_connections" => { + tools::explore::execute(&self.storage, &self.cognitive, request.arguments).await + } + "predict" => { + tools::predict::execute(&self.storage, &self.cognitive, request.arguments).await + } "restore" => tools::restore::execute(&self.storage, request.arguments).await, // ================================================================ // CONTEXT PACKETS (v1.8+) // ================================================================ - "session_context" => tools::session_context::execute(&self.storage, &self.cognitive, request.arguments).await, + "session_context" => { + tools::session_context::execute(&self.storage, &self.cognitive, request.arguments) + .await + } // ================================================================ // AUTONOMIC TOOLS (v1.9+) // ================================================================ "memory_health" => tools::health::execute(&self.storage, request.arguments).await, "memory_graph" => tools::graph::execute(&self.storage, request.arguments).await, + "deep_reference" | "cross_reference" => { + tools::cross_reference::execute(&self.storage, &self.cognitive, request.arguments) + .await + } + + // ================================================================ + // ACTIVE FORGETTING (v2.0.5) — top-down suppression + // ================================================================ + "suppress" => tools::suppress::execute(&self.storage, request.arguments).await, name => { return Err(JsonRpcError::method_not_found_with_message(&format!( @@ -666,11 +859,13 @@ impl McpServer { let call_result = CallToolResult { content: vec![crate::protocol::messages::ToolResultContent { content_type: "text".to_string(), - text: serde_json::to_string_pretty(&content).unwrap_or_else(|_| content.to_string()), + text: serde_json::to_string_pretty(&content) + .unwrap_or_else(|_| content.to_string()), }], is_error: Some(false), }; - serde_json::to_value(call_result).map_err(|e| JsonRpcError::internal_error(&e.to_string())) + serde_json::to_value(call_result) + .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } Err(e) => { let call_result = CallToolResult { @@ -680,13 +875,16 @@ impl McpServer { }], is_error: Some(true), }; - serde_json::to_value(call_result).map_err(|e| JsonRpcError::internal_error(&e.to_string())) + serde_json::to_value(call_result) + .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } }; // Inline consolidation trigger: uses ConsolidationScheduler instead of fixed count let count = self.tool_call_count.fetch_add(1, Ordering::Relaxed) + 1; - let should_consolidate = self.cognitive.try_lock() + let should_consolidate = self + .cognitive + .try_lock() .ok() .map(|cog| cog.consolidation_scheduler.should_consolidate()) .unwrap_or(count.is_multiple_of(100)); // Fallback to count-based if lock unavailable @@ -785,7 +983,9 @@ impl McpServer { ResourceDescription { uri: "memory://intentions".to_string(), name: "Active Intentions".to_string(), - description: Some("Future intentions (prospective memory) waiting to be triggered".to_string()), + description: Some( + "Future intentions (prospective memory) waiting to be triggered".to_string(), + ), mime_type: Some("application/json".to_string()), }, ResourceDescription { @@ -806,15 +1006,20 @@ impl McpServer { params: Option, ) -> Result { let request: ReadResourceRequest = match params { - Some(p) => serde_json::from_value(p).map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, + Some(p) => serde_json::from_value(p) + .map_err(|e| JsonRpcError::invalid_params(&e.to_string()))?, None => return Err(JsonRpcError::invalid_params("Missing resource URI")), }; let uri = &request.uri; - let content = if uri.starts_with("memory://") { - resources::memory::read(&self.storage, uri).await - } else if uri.starts_with("codebase://") { - resources::codebase::read(&self.storage, uri).await + // Normalize URI: strip provider prefix (e.g., "vestige/") for scheme matching + // OpenCode and other MCP clients may send "vestige/memory://recent" + // but we register resources as "memory://recent" + let normalized_uri = uri.strip_prefix("vestige/").unwrap_or(uri); + let content = if normalized_uri.starts_with("memory://") { + resources::memory::read(&self.storage, normalized_uri).await + } else if normalized_uri.starts_with("codebase://") { + resources::codebase::read(&self.storage, normalized_uri).await } else { Err(format!("Unknown resource scheme: {}", uri)) }; @@ -829,7 +1034,8 @@ impl McpServer { blob: None, }], }; - serde_json::to_value(result).map_err(|e| JsonRpcError::internal_error(&e.to_string())) + serde_json::to_value(result) + .map_err(|e| JsonRpcError::internal_error(&e.to_string())) } Err(e) => Err(JsonRpcError::internal_error(&e)), } @@ -852,25 +1058,49 @@ impl McpServer { "smart_ingest" | "ingest" | "session_checkpoint" => { // Single mode: result has "decision" (create/update/supersede/reinforce/merge/replace/add_context) if let Some(decision) = result.get("decision").and_then(|a| a.as_str()) { - let id = result.get("nodeId").or(result.get("id")) - .and_then(|v| v.as_str()).unwrap_or("").to_string(); - let preview = result.get("contentPreview").or(result.get("content")) - .and_then(|v| v.as_str()).unwrap_or("").to_string(); + let id = result + .get("nodeId") + .or(result.get("id")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let preview = result + .get("contentPreview") + .or(result.get("content")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); match decision { "create" => { - let node_type = result.get("nodeType") - .and_then(|v| v.as_str()).unwrap_or("fact").to_string(); - let tags = result.get("tags") + let node_type = result + .get("nodeType") + .and_then(|v| v.as_str()) + .unwrap_or("fact") + .to_string(); + let tags = result + .get("tags") .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(|t| t.as_str().map(String::from)).collect()) + .map(|arr| { + arr.iter() + .filter_map(|t| t.as_str().map(String::from)) + .collect() + }) .unwrap_or_default(); self.emit(VestigeEvent::MemoryCreated { - id, content_preview: preview, node_type, tags, timestamp: now, + id, + content_preview: preview, + node_type, + tags, + timestamp: now, }); } - "update" | "supersede" | "reinforce" | "merge" | "replace" | "add_context" => { + "update" | "supersede" | "reinforce" | "merge" | "replace" + | "add_context" => { self.emit(VestigeEvent::MemoryUpdated { - id, content_preview: preview, field: decision.to_string(), timestamp: now, + id, + content_preview: preview, + field: decision.to_string(), + timestamp: now, }); } _ => {} @@ -880,19 +1110,31 @@ impl McpServer { if let Some(results) = result.get("results").and_then(|r| r.as_array()) { for item in results { let decision = item.get("decision").and_then(|a| a.as_str()).unwrap_or(""); - let id = item.get("nodeId").or(item.get("id")) - .and_then(|v| v.as_str()).unwrap_or("").to_string(); - let preview = item.get("contentPreview") - .and_then(|v| v.as_str()).unwrap_or("").to_string(); + let id = item + .get("nodeId") + .or(item.get("id")) + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); + let preview = item + .get("contentPreview") + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); if decision == "create" { self.emit(VestigeEvent::MemoryCreated { - id, content_preview: preview, - node_type: "fact".to_string(), tags: vec![], timestamp: now, + id, + content_preview: preview, + node_type: "fact".to_string(), + tags: vec![], + timestamp: now, }); } else if !decision.is_empty() { self.emit(VestigeEvent::MemoryUpdated { - id, content_preview: preview, - field: decision.to_string(), timestamp: now, + id, + content_preview: preview, + field: decision.to_string(), + timestamp: now, }); } } @@ -900,35 +1142,53 @@ impl McpServer { } // -- memory: get/delete/promote/demote -- - "memory" | "promote_memory" | "demote_memory" | "delete_knowledge" | "get_memory_state" => { - let action = args.as_ref() + "memory" | "promote_memory" | "demote_memory" | "delete_knowledge" + | "get_memory_state" => { + let action = args + .as_ref() .and_then(|a| a.get("action")) .and_then(|a| a.as_str()) - .unwrap_or(if tool_name == "promote_memory" { "promote" } - else if tool_name == "demote_memory" { "demote" } - else if tool_name == "delete_knowledge" { "delete" } - else { "" }); - let id = args.as_ref() + .unwrap_or(if tool_name == "promote_memory" { + "promote" + } else if tool_name == "demote_memory" { + "demote" + } else if tool_name == "delete_knowledge" { + "delete" + } else { + "" + }); + let id = args + .as_ref() .and_then(|a| a.get("id")) - .and_then(|v| v.as_str()).unwrap_or("").to_string(); + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); match action { "delete" => { self.emit(VestigeEvent::MemoryDeleted { id, timestamp: now }); } "promote" => { - let retention = result.get("newRetention") + let retention = result + .get("newRetention") .or(result.get("retrievalStrength")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); self.emit(VestigeEvent::MemoryPromoted { - id, new_retention: retention, timestamp: now, + id, + new_retention: retention, + timestamp: now, }); } "demote" => { - let retention = result.get("newRetention") + let retention = result + .get("newRetention") .or(result.get("retrievalStrength")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); self.emit(VestigeEvent::MemoryDemoted { - id, new_retention: retention, timestamp: now, + id, + new_retention: retention, + timestamp: now, }); } _ => {} @@ -937,86 +1197,145 @@ impl McpServer { // -- search -- "search" | "recall" | "semantic_search" | "hybrid_search" => { - let query = args.as_ref() + let query = args + .as_ref() .and_then(|a| a.get("query")) - .and_then(|v| v.as_str()).unwrap_or("").to_string(); + .and_then(|v| v.as_str()) + .unwrap_or("") + .to_string(); let results = result.get("results").and_then(|r| r.as_array()); let result_count = results.map(|r| r.len()).unwrap_or(0); let result_ids: Vec = results - .map(|r| r.iter() - .filter_map(|item| item.get("id").and_then(|v| v.as_str()).map(String::from)) - .collect()) + .map(|r| { + r.iter() + .filter_map(|item| { + item.get("id").and_then(|v| v.as_str()).map(String::from) + }) + .collect() + }) .unwrap_or_default(); - let duration_ms = result.get("durationMs") + let duration_ms = result + .get("durationMs") .or(result.get("duration_ms")) - .and_then(|v| v.as_u64()).unwrap_or(0); + .and_then(|v| v.as_u64()) + .unwrap_or(0); self.emit(VestigeEvent::SearchPerformed { - query, result_count, result_ids, duration_ms, timestamp: now, + query, + result_count, + result_ids, + duration_ms, + timestamp: now, }); } // -- dream -- "dream" => { - let replayed = result.get("memoriesReplayed") + let replayed = result + .get("memoriesReplayed") .or(result.get("memories_replayed")) - .and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let connections = result.get("connectionsFound") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let connections = result + .get("connectionsFound") .or(result.get("connections_found")) - .and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let insights = result.get("insightsGenerated") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let insights = result + .get("insightsGenerated") .or(result.get("insights")) - .and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0); - let duration_ms = result.get("durationMs") + .and_then(|v| v.as_array()) + .map(|a| a.len()) + .unwrap_or(0); + let duration_ms = result + .get("durationMs") .or(result.get("duration_ms")) - .and_then(|v| v.as_u64()).unwrap_or(0); + .and_then(|v| v.as_u64()) + .unwrap_or(0); self.emit(VestigeEvent::DreamCompleted { - memories_replayed: replayed, connections_found: connections, - insights_generated: insights, duration_ms, timestamp: now, + memories_replayed: replayed, + connections_found: connections, + insights_generated: insights, + duration_ms, + timestamp: now, }); } // -- consolidate -- "consolidate" => { - let processed = result.get("nodesProcessed") + let processed = result + .get("nodesProcessed") .or(result.get("nodes_processed")) - .and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let decay = result.get("decayApplied") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let decay = result + .get("decayApplied") .or(result.get("decay_applied")) - .and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let embeddings = result.get("embeddingsGenerated") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let embeddings = result + .get("embeddingsGenerated") .or(result.get("embeddings_generated")) - .and_then(|v| v.as_u64()).unwrap_or(0) as usize; - let duration_ms = result.get("durationMs") + .and_then(|v| v.as_u64()) + .unwrap_or(0) as usize; + let duration_ms = result + .get("durationMs") .or(result.get("duration_ms")) - .and_then(|v| v.as_u64()).unwrap_or(0); + .and_then(|v| v.as_u64()) + .unwrap_or(0); self.emit(VestigeEvent::ConsolidationCompleted { - nodes_processed: processed, decay_applied: decay, - embeddings_generated: embeddings, duration_ms, timestamp: now, + nodes_processed: processed, + decay_applied: decay, + embeddings_generated: embeddings, + duration_ms, + timestamp: now, }); } // -- importance_score -- "importance_score" => { - let preview = args.as_ref() + let preview = args + .as_ref() .and_then(|a| a.get("content")) .and_then(|v| v.as_str()) - .map(|s| if s.len() > 100 { format!("{}...", &s[..s.floor_char_boundary(100)]) } else { s.to_string() }) + .map(|s| { + if s.len() > 100 { + format!("{}...", &s[..s.floor_char_boundary(100)]) + } else { + s.to_string() + } + }) .unwrap_or_default(); - let composite = result.get("compositeScore") + let composite = result + .get("compositeScore") .or(result.get("composite_score")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); let channels = result.get("channels").or(result.get("breakdown")); - let novelty = channels.and_then(|c| c.get("novelty")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); - let arousal = channels.and_then(|c| c.get("arousal")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); - let reward = channels.and_then(|c| c.get("reward")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); - let attention = channels.and_then(|c| c.get("attention")) - .and_then(|v| v.as_f64()).unwrap_or(0.0); + let novelty = channels + .and_then(|c| c.get("novelty")) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let arousal = channels + .and_then(|c| c.get("arousal")) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let reward = channels + .and_then(|c| c.get("reward")) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); + let attention = channels + .and_then(|c| c.get("attention")) + .and_then(|v| v.as_f64()) + .unwrap_or(0.0); self.emit(VestigeEvent::ImportanceScored { - content_preview: preview, composite_score: composite, - novelty, arousal, reward, attention, timestamp: now, + memory_id: None, // importance_score tool runs on arbitrary content + content_preview: preview, + composite_score: composite, + novelty, + arousal, + reward, + attention, + timestamp: now, }); } @@ -1069,14 +1388,17 @@ mod tests { let (mut server, _dir) = test_server().await; assert!(!server.initialized); - let request = make_request("initialize", Some(serde_json::json!({ - "protocolVersion": "2024-11-05", - "capabilities": {}, - "clientInfo": { - "name": "test-client", - "version": "1.0.0" - } - }))); + let request = make_request( + "initialize", + Some(serde_json::json!({ + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": { + "name": "test-client", + "version": "1.0.0" + } + })), + ); let response = server.handle_request(request).await; assert!(response.is_some()); @@ -1183,13 +1505,10 @@ mod tests { let result = response.result.unwrap(); let tools = result["tools"].as_array().unwrap(); - // v1.9: 21 tools (4 unified + 1 core + 2 temporal + 5 maintenance + 2 auto-save + 3 cognitive + 1 restore + 1 session_context + 2 autonomic) - assert_eq!(tools.len(), 21, "Expected exactly 21 tools in v1.9+"); + // 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+"); - let tool_names: Vec<&str> = tools - .iter() - .map(|t| t["name"].as_str().unwrap()) - .collect(); + let tool_names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect(); // Unified tools assert!(tool_names.contains(&"search")); @@ -1199,12 +1518,24 @@ mod tests { // Core memory (smart_ingest absorbs ingest + checkpoint in v1.7) assert!(tool_names.contains(&"smart_ingest")); - assert!(!tool_names.contains(&"ingest"), "ingest should be removed in v1.7"); - assert!(!tool_names.contains(&"session_checkpoint"), "session_checkpoint should be removed in v1.7"); + assert!( + !tool_names.contains(&"ingest"), + "ingest should be removed in v1.7" + ); + assert!( + !tool_names.contains(&"session_checkpoint"), + "session_checkpoint should be removed in v1.7" + ); // Feedback merged into memory tool (v1.7) - assert!(!tool_names.contains(&"promote_memory"), "promote_memory should be removed in v1.7"); - assert!(!tool_names.contains(&"demote_memory"), "demote_memory should be removed in v1.7"); + assert!( + !tool_names.contains(&"promote_memory"), + "promote_memory should be removed in v1.7" + ); + assert!( + !tool_names.contains(&"demote_memory"), + "demote_memory should be removed in v1.7" + ); // Temporal tools (v1.2) assert!(tool_names.contains(&"memory_timeline")); @@ -1212,8 +1543,14 @@ mod tests { // Maintenance tools (v1.7: system_status replaces health_check + stats) assert!(tool_names.contains(&"system_status")); - assert!(!tool_names.contains(&"health_check"), "health_check should be removed in v1.7"); - assert!(!tool_names.contains(&"stats"), "stats should be removed in v1.7"); + assert!( + !tool_names.contains(&"health_check"), + "health_check should be removed in v1.7" + ); + assert!( + !tool_names.contains(&"stats"), + "stats should be removed in v1.7" + ); assert!(tool_names.contains(&"consolidate")); assert!(tool_names.contains(&"backup")); assert!(tool_names.contains(&"export")); @@ -1235,6 +1572,13 @@ mod tests { // Autonomic tools (v1.9) assert!(tool_names.contains(&"memory_health")); assert!(tool_names.contains(&"memory_graph")); + + // Deep reference + cross_reference alias (v2.0.4) + assert!(tool_names.contains(&"deep_reference")); + assert!(tool_names.contains(&"cross_reference")); + + // Active forgetting (v2.0.5) — Anderson 2025 + Davis Rac1 + assert!(tool_names.contains(&"suppress")); } #[tokio::test] @@ -1252,8 +1596,14 @@ mod tests { for tool in tools { assert!(tool["name"].is_string(), "Tool should have a name"); - assert!(tool["description"].is_string(), "Tool should have a description"); - assert!(tool["inputSchema"].is_object(), "Tool should have an input schema"); + assert!( + tool["description"].is_string(), + "Tool should have a description" + ); + assert!( + tool["inputSchema"].is_object(), + "Tool should have an input schema" + ); } } @@ -1306,7 +1656,10 @@ mod tests { for resource in resources { assert!(resource["uri"].is_string(), "Resource should have a URI"); assert!(resource["name"].is_string(), "Resource should have a name"); - assert!(resource["description"].is_string(), "Resource should have a description"); + assert!( + resource["description"].is_string(), + "Resource should have a description" + ); } } @@ -1338,10 +1691,13 @@ mod tests { let init_request = make_request("initialize", None); server.handle_request(init_request).await; - let request = make_request("tools/call", Some(serde_json::json!({ - "name": "nonexistent_tool", - "arguments": {} - }))); + let request = make_request( + "tools/call", + Some(serde_json::json!({ + "name": "nonexistent_tool", + "arguments": {} + })), + ); let response = server.handle_request(request).await.unwrap(); assert!(response.error.is_some()); @@ -1392,9 +1748,12 @@ mod tests { let init_request = make_request("initialize", None); server.handle_request(init_request).await; - let request = make_request("tools/call", Some(serde_json::json!({ - "invalid": "params" - }))); + let request = make_request( + "tools/call", + Some(serde_json::json!({ + "invalid": "params" + })), + ); let response = server.handle_request(request).await.unwrap(); assert!(response.error.is_some()); diff --git a/crates/vestige-mcp/src/tools/changelog.rs b/crates/vestige-mcp/src/tools/changelog.rs index e98adef..8a2e746 100644 --- a/crates/vestige-mcp/src/tools/changelog.rs +++ b/crates/vestige-mcp/src/tools/changelog.rs @@ -46,18 +46,13 @@ pub fn schema() -> Value { struct ChangelogArgs { #[serde(alias = "memory_id")] memory_id: Option, - #[allow(dead_code)] start: Option, - #[allow(dead_code)] end: Option, limit: Option, } /// Execute memory_changelog tool -pub async fn execute( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute(storage: &Arc, args: Option) -> Result { let args: ChangelogArgs = match args { Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, None => ChangelogArgs { @@ -70,21 +65,41 @@ pub async fn execute( let limit = args.limit.unwrap_or(20).clamp(1, 100); + // Parse the optional ISO-8601 time bounds. These were advertised in the + // schema since v1.7 but silently ignored until v2.0.7 — any caller that + // set them was getting unfiltered results with no warning. + let start_ts = parse_iso_bound(args.start.as_deref(), "start")?; + let end_ts = parse_iso_bound(args.end.as_deref(), "end")?; + if let Some(ref memory_id) = args.memory_id { - // Per-memory mode: state transitions for a specific memory + // Per-memory mode: state transitions for a specific memory. + // start/end are only meaningful in system-wide mode; ignored here. execute_per_memory(storage, memory_id, limit) } else { - // System-wide mode: consolidations + recent transitions - execute_system_wide(storage, limit) + // System-wide mode: consolidations + recent transitions, optionally + // time-bounded by start/end. + execute_system_wide(storage, limit, start_ts, end_ts) + } +} + +/// Parse an ISO-8601 timestamp bound, returning a helpful error on bad input +/// instead of silently dropping the filter. +fn parse_iso_bound(raw: Option<&str>, field: &str) -> Result>, String> { + match raw { + Some(s) if !s.is_empty() => DateTime::parse_from_rfc3339(s) + .map(|dt| Some(dt.with_timezone(&Utc))) + .map_err(|e| { + format!( + "Invalid {} timestamp {:?}: {}. Use ISO-8601 / RFC-3339 (e.g. 2026-04-19T12:00:00Z).", + field, s, e + ) + }), + _ => Ok(None), } } /// Per-memory changelog: state transition audit trail -fn execute_per_memory( - storage: &Storage, - memory_id: &str, - limit: i32, -) -> Result { +fn execute_per_memory(storage: &Storage, memory_id: &str, limit: i32) -> Result { // Validate UUID format Uuid::parse_str(memory_id) .map_err(|_| format!("Invalid memory_id '{}'. Must be a valid UUID.", memory_id))?; @@ -125,21 +140,43 @@ fn execute_per_memory( })) } -/// System-wide changelog: consolidations + recent state transitions +/// System-wide changelog: consolidations + recent state transitions. +/// +/// `start`/`end` optionally bound the returned events to an inclusive +/// time window. Filtering happens in Rust after the DB reads because +/// the underlying storage helpers don't yet take time parameters — +/// moving the filter into SQL is a v2.1+ optimisation (tracked in the +/// v2.0.7 scope notes). For now we over-fetch (up to 4× `limit`) when +/// a window is supplied so filtering doesn't starve the result set. fn execute_system_wide( storage: &Storage, limit: i32, + start: Option>, + end: Option>, ) -> Result { + // When a window is supplied we can't predict how many events fall inside, + // so over-fetch to reduce the chance of returning an empty page on a + // long-tail time range. 4× is arbitrary; revisit if it becomes a cost + // on very large changelog tables. + let fetch_limit = if start.is_some() || end.is_some() { + limit.saturating_mul(4) + } else { + limit + }; + // Get consolidation history let consolidations = storage - .get_consolidation_history(limit) + .get_consolidation_history(fetch_limit) .map_err(|e| e.to_string())?; // Get recent state transitions across all memories let transitions = storage - .get_recent_state_transitions(limit) + .get_recent_state_transitions(fetch_limit) .map_err(|e| e.to_string())?; + // Get dream history (Bug #9 fix — dreams were invisible to audit trail) + let dreams = storage.get_dream_history(fetch_limit).unwrap_or_default(); + // Build unified event list let mut events: Vec<(DateTime, Value)> = Vec::new(); @@ -174,8 +211,33 @@ fn execute_system_wide( )); } + for d in &dreams { + events.push(( + d.dreamed_at, + serde_json::json!({ + "type": "dream", + "timestamp": d.dreamed_at.to_rfc3339(), + "durationMs": d.duration_ms, + "memoriesReplayed": d.memories_replayed, + "connectionFound": d.connections_found, + "insightsGenerated": d.insights_generated, + }), + )); + } + + // Apply the optional [start, end] window. `start` is inclusive, `end` + // is inclusive — matches "show me events between these two wall-clock + // instants" user expectation. + if start.is_some() || end.is_some() { + events.retain(|(ts, _)| { + let after_start = start.is_none_or(|s| *ts >= s); + let before_end = end.is_none_or(|e| *ts <= e); + after_start && before_end + }); + } + // Sort by timestamp descending - events.sort_by(|a, b| b.0.cmp(&a.0)); + events.sort_by_key(|b| std::cmp::Reverse(b.0)); // Truncate to limit events.truncate(limit as usize); @@ -187,6 +249,10 @@ fn execute_system_wide( "mode": "system_wide", "totalEvents": formatted_events.len(), "events": formatted_events, + "filter": serde_json::json!({ + "start": start.map(|s| s.to_rfc3339()), + "end": end.map(|e| e.to_rfc3339()), + }), })) } @@ -277,8 +343,7 @@ mod tests { #[tokio::test] async fn test_changelog_per_memory_nonexistent() { let (storage, _dir) = test_storage().await; - let args = - serde_json::json!({ "memory_id": "00000000-0000-0000-0000-000000000000" }); + let args = serde_json::json!({ "memory_id": "00000000-0000-0000-0000-000000000000" }); let result = execute(&storage, Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("not found")); @@ -310,4 +375,39 @@ mod tests { assert_eq!(value["totalTransitions"], 0); assert!(value["transitions"].as_array().unwrap().is_empty()); } + + /// v2.0.7 hygiene: malformed `start` must return a helpful error instead + /// of silently dropping the filter (the pre-v2.0.7 behavior was to + /// `#[allow(dead_code)]` the field entirely). Guards against a regression + /// where someone unwraps the parse and triggers a panic on bad input. + #[tokio::test] + async fn test_changelog_malformed_start_returns_error() { + let (storage, _dir) = test_storage().await; + let args = serde_json::json!({ "start": "not-a-date" }); + let result = execute(&storage, Some(args)).await; + assert!(result.is_err(), "malformed start should error"); + let err = result.unwrap_err(); + assert!( + err.contains("Invalid start"), + "error should name the offending field; got: {}", + err + ); + assert!( + err.contains("ISO-8601") || err.contains("RFC-3339"), + "error should hint at the expected format; got: {}", + err + ); + } + + /// The response must echo the applied `start` bound so callers can confirm + /// the window was honored. Empty store so filter narrows to 0 events. + #[tokio::test] + async fn test_changelog_filter_field_echoes_start() { + let (storage, _dir) = test_storage().await; + let args = serde_json::json!({ "start": "2026-04-19T00:00:00Z" }); + let result = execute(&storage, Some(args)).await; + let value = result.unwrap(); + assert_eq!(value["filter"]["start"], "2026-04-19T00:00:00+00:00"); + assert!(value["filter"]["end"].is_null()); + } } diff --git a/crates/vestige-mcp/src/tools/checkpoint.rs b/crates/vestige-mcp/src/tools/checkpoint.rs deleted file mode 100644 index b31becc..0000000 --- a/crates/vestige-mcp/src/tools/checkpoint.rs +++ /dev/null @@ -1,368 +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 22fe3d4..0000000 --- a/crates/vestige-mcp/src/tools/codebase.rs +++ /dev/null @@ -1,301 +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/codebase_unified.rs b/crates/vestige-mcp/src/tools/codebase_unified.rs index 61c7536..726ab4b 100644 --- a/crates/vestige-mcp/src/tools/codebase_unified.rs +++ b/crates/vestige-mcp/src/tools/codebase_unified.rs @@ -161,7 +161,8 @@ async fn execute_remember_pattern( // ==================================================================== if let Ok(cog) = cognitive.try_lock() { let codebase_name = args.codebase.as_deref().unwrap_or("default"); - cog.cross_project.record_project_memory(&node_id, codebase_name, None); + cog.cross_project + .record_project_memory(&node_id, codebase_name, None); // Also index in hippocampal index for fast retrieval let _ = cog.hippocampal_index.index_memory( @@ -256,7 +257,8 @@ async fn execute_remember_decision( // ==================================================================== if let Ok(cog) = cognitive.try_lock() { let codebase_name = args.codebase.as_deref().unwrap_or("default"); - cog.cross_project.record_project_memory(&node_id, codebase_name, None); + cog.cross_project + .record_project_memory(&node_id, codebase_name, None); // Index in hippocampal index let _ = cog.hippocampal_index.index_memory( @@ -285,10 +287,7 @@ async fn execute_get_context( let limit = args.limit.unwrap_or(10).clamp(1, 50); // Build tag filter for codebase - let tag_filter = args - .codebase - .as_ref() - .map(|cb| format!("codebase:{}", cb)); + let tag_filter = args.codebase.as_ref().map(|cb| format!("codebase:{}", cb)); // Query patterns by node_type and tag let patterns = storage @@ -377,18 +376,24 @@ mod tests { // Check action enum values let action_enum = &schema["properties"]["action"]["enum"]; - assert!(action_enum - .as_array() - .unwrap() - .contains(&serde_json::json!("remember_pattern"))); - assert!(action_enum - .as_array() - .unwrap() - .contains(&serde_json::json!("remember_decision"))); - assert!(action_enum - .as_array() - .unwrap() - .contains(&serde_json::json!("get_context"))); + assert!( + action_enum + .as_array() + .unwrap() + .contains(&serde_json::json!("remember_pattern")) + ); + assert!( + action_enum + .as_array() + .unwrap() + .contains(&serde_json::json!("remember_decision")) + ); + assert!( + action_enum + .as_array() + .unwrap() + .contains(&serde_json::json!("get_context")) + ); } // === INTEGRATION TESTS === 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/context.rs b/crates/vestige-mcp/src/tools/context.rs index 211c2c5..20b3fd1 100644 --- a/crates/vestige-mcp/src/tools/context.rs +++ b/crates/vestige-mcp/src/tools/context.rs @@ -7,7 +7,6 @@ use chrono::Utc; use serde_json::Value; use std::sync::Arc; - use vestige_core::{RecallInput, SearchMode, Storage}; /// Input schema for match_context tool @@ -50,19 +49,18 @@ pub fn schema() -> Value { }) } -pub async fn execute( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute(storage: &Arc, args: Option) -> Result { let args = args.ok_or("Missing arguments")?; - let query = args["query"] - .as_str() - .ok_or("query is required")?; + let query = args["query"].as_str().ok_or("query is required")?; let topics: Vec = args["topics"] .as_array() - .map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .map(|arr| { + arr.iter() + .filter_map(|v| v.as_str().map(String::from)) + .collect() + }) .unwrap_or_default(); let project = args["project"].as_str().map(String::from); @@ -83,11 +81,11 @@ pub async fn execute( search_mode: SearchMode::Hybrid, valid_at: None, }; - let candidates = storage.recall(recall_input) - .map_err(|e| e.to_string())?; + let candidates = storage.recall(recall_input).map_err(|e| e.to_string())?; // Score by context match (simplified implementation) - let mut scored_results: Vec<_> = candidates.into_iter() + let mut scored_results: Vec<_> = candidates + .into_iter() .map(|mem| { // Calculate context score based on: // 1. Temporal proximity (how recent) @@ -98,8 +96,14 @@ pub async fn execute( let tag_overlap = if topics.is_empty() { 0.5 // Neutral if no topics specified } else { - let matching = mem.tags.iter() - .filter(|t| topics.iter().any(|topic| topic.to_lowercase().contains(&t.to_lowercase()))) + let matching = mem + .tags + .iter() + .filter(|t| { + topics + .iter() + .any(|topic| topic.to_lowercase().contains(&t.to_lowercase())) + }) .count(); matching as f64 / topics.len().max(1) as f64 }; @@ -136,7 +140,8 @@ pub async fn execute( scored_results.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)); scored_results.truncate(limit as usize); - let results: Vec = scored_results.into_iter() + let results: Vec = scored_results + .into_iter() .map(|(mem, ctx_score, combined)| { serde_json::json!({ "id": mem.id, diff --git a/crates/vestige-mcp/src/tools/cross_reference.rs b/crates/vestige-mcp/src/tools/cross_reference.rs new file mode 100644 index 0000000..fd2f377 --- /dev/null +++ b/crates/vestige-mcp/src/tools/cross_reference.rs @@ -0,0 +1,1250 @@ +//! Deep Reference Tool (v2.0.4) +//! +//! Cognitive reasoning engine across memories. Combines: +//! 1. Broad retrieval (hybrid search + reranking) +//! 2. Spreading activation expansion (connected memories) +//! 3. FSRS-6 trust scoring (retention, stability, reps, lapses) +//! 4. Temporal supersession (newer = current truth) +//! 5. Contradiction analysis (trust-weighted) +//! 6. Dream insight integration (persisted insights) +//! 7. Structured synthesis (recommended answer + evidence) +//! +//! Research grounding: MAGMA (multi-graph), Kumiho (AGM belief revision), +//! InfMem (System-2 memory control), D-Mem (dual-process retrieval). +//! +//! Replaces cross_reference with full cognitive reasoning. cross_reference +//! is kept as a backward-compatible alias. + +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::Storage; + +/// Input schema for deep_reference / cross_reference tool +pub fn schema() -> Value { + serde_json::json!({ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "The question, claim, or topic to reason about across all memories" + }, + "depth": { + "type": "integer", + "description": "How many memories to analyze (default: 20, max: 50). Higher = more thorough.", + "default": 20, + "minimum": 5, + "maximum": 50 + } + }, + "required": ["query"] + }) +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +struct DeepRefArgs { + query: String, + depth: Option, +} + +// ============================================================================ +// FSRS-6 Trust Score +// ============================================================================ + +/// 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 { + 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; + let lapses_penalty = (1.0 - (lapses as f64 / 5.0)).max(0.0) * 0.2; + (retention_factor + stability_factor + reps_factor + lapses_penalty).clamp(0.0, 1.0) +} + +// ============================================================================ +// SYSTEM 1: Intent Classification (MAGMA-inspired query routing) +// ============================================================================ + +#[derive(Debug, Clone, PartialEq)] +enum QueryIntent { + FactCheck, // "Is X true?" → find support/contradiction evidence + Timeline, // "When did X happen?" → temporal ordering + pattern detection + RootCause, // "Why did X happen?" → causal chain backward + Comparison, // "How does X differ from Y?" → diff two memory clusters + Synthesis, // Default: "What do I know about X?" → cluster + best per cluster +} + +fn classify_intent(query: &str) -> QueryIntent { + let q = query.to_lowercase(); + let patterns: &[(QueryIntent, &[&str])] = &[ + ( + QueryIntent::RootCause, + &[ + "why did", + "root cause", + "what caused", + "because of", + "reason for", + "why is", + "why was", + ], + ), + ( + QueryIntent::Timeline, + &[ + "when did", + "timeline", + "history of", + "over time", + "how has", + "evolution of", + "sequence of", + ], + ), + ( + QueryIntent::Comparison, + &[ + "differ", + "compare", + "versus", + " vs ", + "difference between", + "changed from", + ], + ), + ( + QueryIntent::FactCheck, + &[ + "is it true", + "did i", + "was there", + "verify", + "confirm", + "is this correct", + "should i use", + "should we", + ], + ), + ]; + for (intent, keywords) in patterns { + if keywords.iter().any(|kw| q.contains(kw)) { + return intent.clone(); + } + } + QueryIntent::Synthesis +} + +// ============================================================================ +// SYSTEM 2: Relation Assessment (embedding similarity + sentiment + temporal) +// ============================================================================ + +#[derive(Debug, Clone)] +enum Relation { + Supports, + Contradicts, + Supersedes, + Irrelevant, +} + +#[derive(Debug, Clone)] +#[allow(dead_code)] +struct RelationAssessment { + relation: Relation, + confidence: f64, + reasoning: String, +} + +/// Assess the relationship between two memories using embedding similarity, +/// correction signals, temporal ordering, and trust comparison. +/// No LLM needed — pure algorithmic assessment. +fn assess_relation( + a_content: &str, + b_content: &str, + a_trust: f64, + b_trust: f64, + a_date: chrono::DateTime, + b_date: chrono::DateTime, + topic_sim: f32, +) -> RelationAssessment { + // Irrelevant: different topics + if topic_sim < 0.15 { + return RelationAssessment { + relation: Relation::Irrelevant, + confidence: 1.0 - topic_sim as f64, + reasoning: format!("Different topics (similarity {:.2})", topic_sim), + }; + } + + let time_delta_days = (b_date - a_date).num_days().abs(); + let trust_diff = b_trust - a_trust; + let has_correction = appears_contradictory(a_content, b_content); + + // Supersession: same topic + newer + higher trust + if topic_sim > 0.4 && time_delta_days > 0 && trust_diff > 0.05 && !has_correction { + let (newer, older) = if b_date > a_date { + ("B", "A") + } else { + ("A", "B") + }; + return RelationAssessment { + relation: Relation::Supersedes, + confidence: topic_sim as f64 * (0.5 + trust_diff.min(0.5)), + reasoning: format!( + "{} supersedes {} (newer by {}d, trust +{:.0}%)", + newer, + older, + time_delta_days, + trust_diff * 100.0 + ), + }; + } + + // 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, + reasoning: format!( + "Contradiction detected (similarity {:.2}, correction signals present)", + topic_sim + ), + }; + } + + // Support: same topic + no contradiction + if topic_sim > 0.3 { + return RelationAssessment { + relation: Relation::Supports, + confidence: topic_sim as f64, + reasoning: format!( + "Topically aligned (similarity {:.2}), consistent stance", + topic_sim + ), + }; + } + + RelationAssessment { + relation: Relation::Irrelevant, + confidence: 0.3, + reasoning: "Weak relationship".to_string(), + } +} + +// ============================================================================ +// SYSTEM 3: Template Reasoning Chain Generator (no LLM needed) +// ============================================================================ + +/// Generate a natural language reasoning chain from structured evidence. +/// The AI reads this and validates/extends it — System 1 prepares, System 2 refines. +fn generate_reasoning_chain( + query: &str, + intent: &QueryIntent, + primary: &ScoredMemory, + relations: &[(String, f64, RelationAssessment)], // (preview, trust, relation) + confidence: f64, +) -> String { + let mut chain = String::new(); + + // Intent-specific opening + match intent { + QueryIntent::FactCheck => { + chain.push_str(&format!("FACT CHECK: \"{}\"\n\n", query)); + } + QueryIntent::Timeline => { + chain.push_str(&format!("TIMELINE: \"{}\"\n\n", query)); + } + QueryIntent::RootCause => { + chain.push_str(&format!("ROOT CAUSE ANALYSIS: \"{}\"\n\n", query)); + } + QueryIntent::Comparison => { + chain.push_str(&format!("COMPARISON: \"{}\"\n\n", query)); + } + QueryIntent::Synthesis => { + chain.push_str(&format!("SYNTHESIS: \"{}\"\n\n", query)); + } + } + + // Primary finding + chain.push_str(&format!( + "PRIMARY FINDING (trust {:.0}%, {}): {}\n", + primary.trust * 100.0, + primary.updated_at.format("%b %d, %Y"), + primary.content.chars().take(300).collect::(), + )); + + // Superseded memories — with reasoning arrows + let superseded: Vec<_> = relations + .iter() + .filter(|(_, _, r)| matches!(r.relation, Relation::Supersedes)) + .collect(); + for (preview, trust, rel) in &superseded { + chain.push_str(&format!( + " SUPERSEDES (trust {:.0}%): \"{}\"\n -> {}\n", + trust * 100.0, + preview.chars().take(100).collect::(), + rel.reasoning, + )); + } + + // Supporting evidence + let supporting: Vec<_> = relations + .iter() + .filter(|(_, _, r)| matches!(r.relation, Relation::Supports)) + .collect(); + if !supporting.is_empty() { + chain.push_str(&format!( + "SUPPORTED BY {} MEMOR{}:\n", + supporting.len(), + if supporting.len() == 1 { "Y" } else { "IES" }, + )); + for (preview, trust, _) in supporting.iter().take(5) { + chain.push_str(&format!( + " + (trust {:.0}%): \"{}\"\n", + trust * 100.0, + preview.chars().take(100).collect::(), + )); + } + } + + // Contradicting evidence + let contradicting: Vec<_> = relations + .iter() + .filter(|(_, _, r)| matches!(r.relation, Relation::Contradicts)) + .collect(); + if !contradicting.is_empty() { + chain.push_str(&format!( + "CONTRADICTING EVIDENCE ({}):\n", + contradicting.len() + )); + for (preview, trust, rel) in contradicting.iter().take(3) { + chain.push_str(&format!( + " ! (trust {:.0}%): \"{}\"\n -> {}\n", + trust * 100.0, + preview.chars().take(100).collect::(), + rel.reasoning, + )); + } + } + + // If no relations found, still provide useful output + if superseded.is_empty() && supporting.is_empty() && contradicting.is_empty() { + chain.push_str("NO CONTRADICTIONS DETECTED. Evidence is consistent.\n"); + } + + chain.push_str(&format!("OVERALL CONFIDENCE: {:.0}%\n", confidence * 100.0)); + + 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"), + ("avoid", "use"), + ("wrong", "right"), + ("incorrect", "correct"), + ("deprecated", "recommended"), + ("outdated", "current"), + ("removed", "added"), + ("disabled", "enabled"), +]; + +const CORRECTION_SIGNALS: &[&str] = &[ + "actually", + "correction", + "update:", + "updated:", + "fixed", + "was wrong", + "changed to", + "now uses", + "replaced by", + "superseded", + "no longer", + "instead of", + "switched to", + "migrated to", +]; + +fn appears_contradictory(a: &str, b: &str) -> bool { + let a_lower = a.to_lowercase(); + let b_lower = b.to_lowercase(); + + let a_words: std::collections::HashSet<&str> = + a_lower.split_whitespace().filter(|w| w.len() > 3).collect(); + let b_words: std::collections::HashSet<&str> = + b_lower.split_whitespace().filter(|w| w.len() > 3).collect(); + let shared_words = a_words.intersection(&b_words).count(); + + // 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; + } + + // 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; + } + } + // 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 { + let a_lower = a.to_lowercase(); + let b_lower = b.to_lowercase(); + let a_words: std::collections::HashSet<&str> = + a_lower.split_whitespace().filter(|w| w.len() > 3).collect(); + let b_words: std::collections::HashSet<&str> = + b_lower.split_whitespace().filter(|w| w.len() > 3).collect(); + if a_words.is_empty() || b_words.is_empty() { + return 0.0; + } + let intersection = a_words.intersection(&b_words).count(); + let union = a_words.union(&b_words).count(); + if union == 0 { + 0.0 + } else { + intersection as f32 / union as f32 + } +} + +// ============================================================================ +// Scored Memory (used across pipeline stages) +// ============================================================================ + +#[allow(dead_code)] +struct ScoredMemory { + id: String, + content: String, + tags: Vec, + trust: f64, + updated_at: chrono::DateTime, + created_at: chrono::DateTime, + retention: f64, + combined_score: f32, +} + +// ============================================================================ +// Main Execute — 8-Stage Pipeline +// ============================================================================ + +pub async fn execute( + storage: &Arc, + cognitive: &Arc>, + args: Option, +) -> Result { + let args: DeepRefArgs = 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 depth = args.depth.unwrap_or(20).clamp(5, 50) as usize; + + // ==================================================================== + // STAGE 0: Intent Classification (MAGMA-inspired query routing) + // ==================================================================== + let intent = classify_intent(&args.query); + + // ==================================================================== + // STAGE 1: Broad Retrieval + Reranking + // ==================================================================== + let results = storage + .hybrid_search(&args.query, depth as i32, 0.3, 0.7) + .map_err(|e| e.to_string())?; + + if results.is_empty() { + return Ok(serde_json::json!({ + "query": args.query, + "status": "no_memories", + "confidence": 0.0, + "guidance": "No memories found. Use smart_ingest to add memories.", + "memoriesAnalyzed": 0, + })); + } + + let mut ranked = results; + if let Ok(mut cog) = cognitive.try_lock() { + let candidates: Vec<_> = ranked + .iter() + .map(|r| (r.clone(), r.node.content.clone())) + .collect(); + if let Ok(reranked) = cog.reranker.rerank(&args.query, candidates, Some(depth)) { + ranked = reranked.into_iter().map(|rr| rr.item).collect(); + } + } + + // ==================================================================== + // STAGE 2: Spreading Activation Expansion + // ==================================================================== + let mut activation_expanded = 0usize; + let existing_ids: std::collections::HashSet = + ranked.iter().map(|r| r.node.id.clone()).collect(); + + if let Ok(mut cog) = cognitive.try_lock() { + let mut expanded_ids = Vec::new(); + for r in ranked.iter().take(3) { + let activated = cog.activation_network.activate(&r.node.id, 1.0); + for a in activated.iter().take(3) { + if !existing_ids.contains(&a.memory_id) && !expanded_ids.contains(&a.memory_id) { + expanded_ids.push(a.memory_id.clone()); + } + } + } + // Fetch expanded memories from storage + for id in &expanded_ids { + if let Ok(Some(node)) = storage.get_node(id) { + // Create a minimal SearchResult-like entry + ranked.push(vestige_core::SearchResult { + node, + combined_score: 0.3, // lower score since these are expanded, not direct matches + keyword_score: None, + semantic_score: None, + match_type: vestige_core::MatchType::Semantic, + }); + activation_expanded += 1; + } + } + } + + // ==================================================================== + // STAGE 3: FSRS-6 Trust Scoring + // ==================================================================== + + let scored: Vec = ranked + .iter() + .map(|r| { + let trust = compute_trust( + r.node.retention_strength, + r.node.stability, + r.node.reps, + r.node.lapses, + ); + ScoredMemory { + id: r.node.id.clone(), + content: r.node.content.clone(), + tags: r.node.tags.clone(), + trust, + updated_at: r.node.updated_at, + created_at: r.node.created_at, + retention: r.node.retention_strength, + combined_score: r.combined_score, + } + }) + .collect(); + + // ==================================================================== + // STAGE 4: Temporal Supersession + // ==================================================================== + let mut superseded: Vec = Vec::new(); + let mut superseded_ids: std::collections::HashSet = std::collections::HashSet::new(); + + // Sort by date descending for supersession + let mut by_date = scored.iter().collect::>(); + by_date.sort_by_key(|b| std::cmp::Reverse(b.updated_at)); + + for i in 0..by_date.len() { + for j in (i + 1)..by_date.len() { + let newer = by_date[i]; + let older = by_date[j]; + let overlap = topic_overlap(&newer.content, &older.content); + if overlap > 0.3 && newer.trust > older.trust && !superseded_ids.contains(&older.id) { + superseded_ids.insert(older.id.clone()); + superseded.push(serde_json::json!({ + "id": older.id, + "preview": older.content.chars().take(150).collect::(), + "trust": (older.trust * 100.0).round() / 100.0, + "date": older.updated_at.to_rfc3339(), + "superseded_by": newer.id, + })); + } + } + } + + // ==================================================================== + // STAGE 5: Trust-Weighted Contradiction Analysis + // ==================================================================== + let mut contradictions: Vec = Vec::new(); + + for i in 0..scored.len() { + for j in (i + 1)..scored.len() { + let a = &scored[i]; + let b = &scored[j]; + let overlap = topic_overlap(&a.content, &b.content); + // 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; + } + + let is_contradiction = appears_contradictory(&a.content, &b.content); + if !is_contradiction { + continue; + } + + // Only flag as real contradiction if BOTH have decent trust + let min_trust = a.trust.min(b.trust); + if min_trust < 0.3 { + continue; + } // Low-trust memory isn't worth flagging + + let (stronger, weaker) = if a.trust >= b.trust { (a, b) } else { (b, a) }; + contradictions.push(serde_json::json!({ + "stronger": { + "id": stronger.id, + "preview": stronger.content.chars().take(150).collect::(), + "trust": (stronger.trust * 100.0).round() / 100.0, + "date": stronger.updated_at.to_rfc3339(), + }, + "weaker": { + "id": weaker.id, + "preview": weaker.content.chars().take(150).collect::(), + "trust": (weaker.trust * 100.0).round() / 100.0, + "date": weaker.updated_at.to_rfc3339(), + }, + "topic_overlap": overlap, + })); + } + } + + // ==================================================================== + // STAGE 6: Dream Insight Integration + // ==================================================================== + let mut related_insights: Vec = Vec::new(); + if let Ok(insights) = storage.get_insights(20) { + let memory_ids: std::collections::HashSet<&str> = + scored.iter().map(|s| s.id.as_str()).collect(); + for insight in insights { + let overlaps = insight + .source_memories + .iter() + .any(|src_id| memory_ids.contains(src_id.as_str())); + if overlaps { + related_insights.push(serde_json::json!({ + "insight": insight.insight, + "type": insight.insight_type, + "confidence": insight.confidence, + "source_memories": insight.source_memories, + })); + } + } + } + + // ==================================================================== + // 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. a Nightvision 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) = 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 + let sim = topic_overlap(&primary.content, &other.content); + let effective_sim = if other.combined_score > 0.2 { + sim.max(0.3) + } else { + sim + }; + let rel = assess_relation( + &primary.content, + &other.content, + primary.trust, + other.trust, + primary.updated_at, + other.updated_at, + effective_sim, + ); + if !matches!(rel.relation, Relation::Irrelevant) { + pair_relations.push((other.content.chars().take(100).collect(), other.trust, rel)); + } + } + } + + // ==================================================================== + // STAGE 8: Synthesis + Reasoning Chain Generation + // ==================================================================== + // `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> = non_superseded_all.clone(); + non_superseded.sort_by(|a, b| { + composite(b) + .partial_cmp(&composite(a)) + .unwrap_or(std::cmp::Ordering::Equal) + }); + let evidence: Vec = non_superseded + .iter() + .take(10) + .enumerate() + .map(|(i, s)| { + serde_json::json!({ + "id": s.id, + "preview": s.content.chars().take(200).collect::(), + "trust": (s.trust * 100.0).round() / 100.0, + "date": s.updated_at.to_rfc3339(), + "role": if i == 0 { "primary" } else { "supporting" }, + }) + }) + .collect(); + + // Build evolution timeline + let mut evolution: Vec = by_date + .iter() + .rev() + .map(|s| { + serde_json::json!({ + "date": s.updated_at.format("%b %d, %Y").to_string(), + "preview": s.content.chars().take(100).collect::(), + "trust": (s.trust * 100.0).round() / 100.0, + }) + }) + .collect(); + evolution.truncate(15); // cap timeline length + + // Confidence scoring: derived from the same composite as `recommended`, + // so confidence actually moves with query relevance instead of being a + // function of trust + corpus size alone. + let base_confidence = recommended.map(composite).unwrap_or(0.0); + let agreement_boost = (evidence.len() as f64 * 0.03).min(0.2); + let contradiction_penalty = contradictions.len() as f64 * 0.1; + let confidence = (base_confidence + agreement_boost - contradiction_penalty).clamp(0.0, 1.0); + + let status = if contradictions.is_empty() && confidence > 0.7 { + "resolved" + } else if !contradictions.is_empty() { + "contradictions_found" + } else if scored.is_empty() { + "no_evidence" + } else { + "partial_evidence" + }; + + let guidance = if let Some(rec) = recommended { + if contradictions.is_empty() { + format!( + "High confidence ({:.0}%). Recommended memory (trust {:.0}%, {}) is the most reliable source.", + confidence * 100.0, + rec.trust * 100.0, + rec.updated_at.format("%b %d, %Y") + ) + } else { + format!( + "WARNING: {} contradiction(s) detected. Recommended memory has trust {:.0}% but conflicts exist. Review contradictions below.", + contradictions.len(), + rec.trust * 100.0 + ) + } + } else { + "No strong evidence found. Verify with external sources.".to_string() + }; + + // Auto-strengthen accessed memories (Testing Effect) + let ids: Vec<&str> = scored.iter().map(|s| s.id.as_str()).collect(); + let _ = storage.strengthen_batch_on_access(&ids); + + // Generate reasoning chain (the key differentiator — no LLM needed) + let reasoning_chain = if let Some(rec) = recommended { + generate_reasoning_chain(&args.query, &intent, rec, &pair_relations, confidence) + } else { + "No strong evidence found for reasoning.".to_string() + }; + + // Build response + let mut response = serde_json::json!({ + "query": args.query, + "intent": format!("{:?}", intent), + "status": status, + "confidence": (confidence * 100.0).round() / 100.0, + "reasoning": reasoning_chain, + "guidance": guidance, + "memoriesAnalyzed": scored.len(), + "activationExpanded": activation_expanded, + }); + + if let Some(rec) = recommended { + response["recommended"] = serde_json::json!({ + "answer_preview": rec.content.chars().take(300).collect::(), + "memory_id": rec.id, + "trust_score": (rec.trust * 100.0).round() / 100.0, + "date": rec.updated_at.to_rfc3339(), + }); + } + + if !evidence.is_empty() { + response["evidence"] = serde_json::json!(evidence); + } + if !contradictions.is_empty() { + response["contradictions"] = serde_json::json!(contradictions); + } + if !superseded.is_empty() { + response["superseded"] = serde_json::json!(superseded); + } + if !evolution.is_empty() { + response["evolution"] = serde_json::json!(evolution); + } + if !related_insights.is_empty() { + response["related_insights"] = serde_json::json!(related_insights); + } + + Ok(response) +} + +// ============================================================================ +// TESTS +// ============================================================================ + +#[cfg(test)] +mod tests { + use super::*; + use crate::cognitive::CognitiveEngine; + use std::sync::Arc; + use tempfile::TempDir; + use tokio::sync::Mutex; + use vestige_core::Storage; + + fn test_cognitive() -> Arc> { + Arc::new(Mutex::new(CognitiveEngine::new())) + } + + 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) + } + + async fn ingest_one(storage: &Arc, content: &str, tags: &[&str]) -> String { + storage + .ingest(vestige_core::IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: tags.iter().map(|s| s.to_string()).collect(), + valid_from: None, + valid_until: None, + }) + .unwrap() + .id + } + + // ======================================================================== + // BUG A: `recommended` is picked by FSRS trust only, ignoring query relevance. + // ======================================================================== + #[tokio::test] + async fn test_recommended_uses_query_relevance_not_just_trust() { + let (storage, _dir) = test_storage().await; + + let id_a = ingest_one( + &storage, + "PostgreSQL connection pooling with pgbouncer transaction mode \ + requires careful tuning of max_connections and pool_mode settings.", + &["postgres", "database"], + ) + .await; + + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + + let _id_b = ingest_one( + &storage, + "Making sourdough bread requires a mature starter, long bulk \ + fermentation, and attention to dough hydration levels.", + &["baking", "bread"], + ) + .await; + + let args = serde_json::json!({ + "query": "PostgreSQL connection pooling pgbouncer max_connections" + }); + let result = execute(&storage, &test_cognitive(), Some(args)) + .await + .expect("execute should succeed"); + + assert_eq!( + result["recommended"]["memory_id"].as_str(), + Some(id_a.as_str()), + "Expected recommended={} (matches query). Got {:?}. \ + Root cause: lines 565-572 select `recommended` by trust only, \ + discarding the combined_score signal from hybrid_search + reranker.", + id_a, + result["recommended"]["memory_id"] + ); + } + + // ======================================================================== + // Confidence sanity: must vary with query relevance. + // ======================================================================== + #[tokio::test] + async fn test_confidence_varies_with_query_relevance() { + let (storage, _dir) = test_storage().await; + + ingest_one( + &storage, + "The Borrow Checker enforces Rust's ownership rules at compile time, \ + preventing data races and use-after-free without a garbage collector.", + &["rust"], + ) + .await; + + let relevant = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ + "query": "Rust borrow checker ownership compile time" + })), + ) + .await + .expect("execute should succeed"); + + let irrelevant = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ + "query": "18th century Dutch maritime trade routes" + })), + ) + .await + .expect("execute should succeed"); + + let rel_conf = relevant["confidence"].as_f64().unwrap_or(0.0); + let irr_conf = irrelevant["confidence"].as_f64().unwrap_or(0.0); + + assert!( + rel_conf > irr_conf, + "Confidence should be higher for a relevant query. Got \ + relevant={}, irrelevant={}. Currently `confidence` derives from \ + recommended.trust + evidence count (lines 602-605), both \ + invariant under query changes.", + rel_conf, + irr_conf + ); + } + + #[test] + fn test_schema_structure() { + let s = schema(); + assert!(s["properties"]["query"].is_object()); + assert!(s["properties"]["depth"].is_object()); + assert_eq!(s["required"], serde_json::json!(["query"])); + } + + #[test] + fn test_trust_score_high() { + // High retention, high stability, many reps, no lapses → high trust + let trust = compute_trust(0.95, 60.0, 20, 0); + assert!(trust > 0.8, "Expected >0.8, got {}", trust); + } + + #[test] + fn test_trust_score_low() { + // Low retention, low stability, few reps, many lapses → low trust + let trust = compute_trust(0.2, 1.0, 1, 10); + assert!(trust < 0.3, "Expected <0.3, got {}", trust); + } + + #[test] + fn test_trust_score_medium() { + // Medium everything + let trust = compute_trust(0.6, 15.0, 5, 2); + assert!( + trust > 0.4 && trust < 0.7, + "Expected 0.4-0.7, got {}", + trust + ); + } + + #[test] + fn test_trust_score_clamped() { + // Even extreme values stay in [0, 1] + assert!(compute_trust(1.0, 1000.0, 100, 0) <= 1.0); + assert!(compute_trust(0.0, 0.0, 0, 100) >= 0.0); + } + + #[test] + fn test_contradiction_requires_shared_words() { + assert!(!appears_contradictory( + "not sure about weather", + "Rust is fast" + )); + } + + #[test] + fn test_contradiction_with_shared_context() { + assert!(appears_contradictory( + "Don't use FAISS for vector search in production", + "Use FAISS for vector search in production always" + )); + } + + #[test] + fn test_topic_overlap_similar() { + let overlap = topic_overlap( + "Vestige uses USearch for vector search", + "Vestige vector search powered by USearch HNSW", + ); + assert!(overlap > 0.3); + } + + #[test] + fn test_topic_overlap_different() { + let overlap = topic_overlap("The weather is sunny today", "Rust compile times improving"); + assert!(overlap < 0.15); + } + + #[test] + fn test_depth_clamped() { + let s = schema(); + assert_eq!(s["properties"]["depth"]["minimum"], 5); + assert_eq!(s["properties"]["depth"]["maximum"], 50); + } + + // === Intent Classification Tests === + + #[test] + fn test_intent_fact_check() { + assert_eq!( + classify_intent("Is it true that Vestige uses USearch?"), + QueryIntent::FactCheck + ); + assert_eq!( + classify_intent("Did I switch to port 3002?"), + QueryIntent::FactCheck + ); + assert_eq!( + classify_intent("Should I use prefix caching?"), + QueryIntent::FactCheck + ); + } + + #[test] + fn test_intent_timeline() { + assert_eq!( + classify_intent("When did the port change happen?"), + QueryIntent::Timeline + ); + assert_eq!( + classify_intent("How has the AIMO3 score evolved over time?"), + QueryIntent::Timeline + ); + } + + #[test] + fn test_intent_root_cause() { + assert_eq!( + classify_intent("Why did the build fail?"), + QueryIntent::RootCause + ); + assert_eq!( + classify_intent("What caused the score regression?"), + QueryIntent::RootCause + ); + } + + #[test] + fn test_intent_comparison() { + assert_eq!( + classify_intent("How does USearch differ from FAISS?"), + QueryIntent::Comparison + ); + assert_eq!( + classify_intent("Compare FSRS versus SM-2"), + QueryIntent::Comparison + ); + } + + #[test] + fn test_intent_synthesis_default() { + assert_eq!( + classify_intent("Tell me about Sam's projects"), + QueryIntent::Synthesis + ); + assert_eq!(classify_intent("What is Vestige?"), QueryIntent::Synthesis); + } + + // === Relation Assessment Tests === + + #[test] + fn test_relation_irrelevant() { + let rel = assess_relation( + "Rust is fast", + "The weather is nice", + 0.8, + 0.8, + Utc::now(), + Utc::now(), + 0.05, + ); + assert!(matches!(rel.relation, Relation::Irrelevant)); + } + + #[test] + fn test_relation_supports() { + let rel = assess_relation( + "Vestige uses USearch for vector search", + "USearch provides fast HNSW indexing for Vestige", + 0.8, + 0.7, + Utc::now(), + Utc::now(), + 0.6, + ); + assert!(matches!(rel.relation, Relation::Supports)); + } + + #[test] + fn test_relation_contradicts() { + let rel = assess_relation( + "Don't use FAISS for vector search in production anymore", + "Use FAISS for vector search in production always", + 0.8, + 0.5, + Utc::now(), + Utc::now(), + 0.7, + ); + assert!(matches!(rel.relation, Relation::Contradicts)); + } +} diff --git a/crates/vestige-mcp/src/tools/dedup.rs b/crates/vestige-mcp/src/tools/dedup.rs index c982091..8456629 100644 --- a/crates/vestige-mcp/src/tools/dedup.rs +++ b/crates/vestige-mcp/src/tools/dedup.rs @@ -9,7 +9,6 @@ use serde_json::Value; use std::collections::HashMap; use std::sync::Arc; - use vestige_core::Storage; #[cfg(all(feature = "embeddings", feature = "vector-search"))] use vestige_core::cosine_similarity; @@ -45,6 +44,7 @@ pub fn schema() -> Value { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DedupArgs { + #[serde(alias = "similarity_threshold")] similarity_threshold: Option, limit: Option, tags: Option>, @@ -88,10 +88,7 @@ impl UnionFind { } } -pub async fn execute( - storage: &Arc, - args: Option, -) -> Result { +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 { @@ -107,7 +104,6 @@ pub async fn execute( #[cfg(all(feature = "embeddings", feature = "vector-search"))] { - // Load all embeddings let all_embeddings = storage .get_all_embeddings() @@ -190,10 +186,8 @@ pub async fn execute( } // Only keep clusters with >1 member, sorted by size descending - let mut clusters: Vec> = cluster_map - .into_values() - .filter(|c| c.len() > 1) - .collect(); + let mut clusters: Vec> = + cluster_map.into_values().filter(|c| c.len() > 1).collect(); clusters.sort_by_key(|b| std::cmp::Reverse(b.len())); clusters.truncate(limit); diff --git a/crates/vestige-mcp/src/tools/dream.rs b/crates/vestige-mcp/src/tools/dream.rs index f84f8d5..357315f 100644 --- a/crates/vestige-mcp/src/tools/dream.rs +++ b/crates/vestige-mcp/src/tools/dream.rs @@ -4,9 +4,9 @@ use std::sync::Arc; use tokio::sync::Mutex; -use chrono::Utc; use crate::cognitive::CognitiveEngine; -use vestige_core::{DreamHistoryRecord, LinkType, Storage}; +use chrono::Utc; +use vestige_core::{DreamHistoryRecord, InsightRecord, LinkType, Storage}; pub fn schema() -> serde_json::Value { serde_json::json!({ @@ -30,24 +30,28 @@ pub async fn execute( .as_ref() .and_then(|a| a.get("memory_count")) .and_then(|v| v.as_u64()) - .unwrap_or(50) as usize; + .unwrap_or(50) + .min(500) as usize; // Cap at 500 to prevent O(N^2) hang // v1.9.0: Waking SWR tagging — preferential replay of tagged memories (70/30 split) - let tagged_nodes = storage.get_waking_tagged_memories(memory_count as i32) + let tagged_nodes = storage + .get_waking_tagged_memories(memory_count as i32) .unwrap_or_default(); let tagged_count = tagged_nodes.len(); // Calculate how many tagged vs random to include let tagged_target = (memory_count * 7 / 10).min(tagged_count); // 70% tagged - let _random_target = memory_count.saturating_sub(tagged_target); // 30% random (used for logging) + let _random_target = memory_count.saturating_sub(tagged_target); // 30% random (used for logging) // Build the dream memory set: tagged memories first, then fill with random - let tagged_ids: std::collections::HashSet = tagged_nodes.iter() + let tagged_ids: std::collections::HashSet = tagged_nodes + .iter() .take(tagged_target) .map(|n| n.id.clone()) .collect(); - let random_nodes = storage.get_all_nodes(memory_count as i32, 0) + let random_nodes = storage + .get_all_nodes(memory_count as i32, 0) .map_err(|e| format!("Failed to load memories: {}", e))?; let mut all_nodes: Vec<_> = tagged_nodes.into_iter().take(tagged_target).collect(); @@ -58,8 +62,10 @@ pub async fn execute( } // If still under capacity (e.g., all memories are tagged), fill from remaining tagged if all_nodes.len() < memory_count { - let used_ids: std::collections::HashSet = all_nodes.iter().map(|n| n.id.clone()).collect(); - let remaining_tagged = storage.get_waking_tagged_memories(memory_count as i32) + let used_ids: std::collections::HashSet = + all_nodes.iter().map(|n| n.id.clone()).collect(); + let remaining_tagged = storage + .get_waking_tagged_memories(memory_count as i32) .unwrap_or_default(); for node in remaining_tagged { if !used_ids.contains(&node.id) && all_nodes.len() < memory_count { @@ -76,30 +82,61 @@ pub async fn execute( })); } - let dream_memories: Vec = all_nodes.iter().map(|n| { - vestige_core::DreamMemory { + let dream_memories: Vec = all_nodes + .iter() + .map(|n| vestige_core::DreamMemory { id: n.id.clone(), content: n.content.clone(), embedding: storage.get_node_embedding(&n.id).ok().flatten(), tags: n.tags.clone(), created_at: n.created_at, access_count: n.reps as u32, - } - }).collect(); + }) + .collect(); let cog = cognitive.lock().await; - let pre_dream_count = cog.dreamer.get_connections().len(); + // 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 insights = cog.dreamer.synthesize_insights(&dream_memories); let all_connections = cog.dreamer.get_connections(); drop(cog); - // v1.9.0: Persist only NEW connections from this dream (skip accumulated ones) - let new_connections = &all_connections[pre_dream_count..]; + // v2.1.0: Persist dream insights to database (Bug #4 fix) + let mut insights_persisted = 0u64; + for insight in &insights { + let record = InsightRecord { + id: insight.id.clone(), + insight: insight.insight.clone(), + source_memories: insight.source_memories.clone(), + confidence: insight.confidence, + novelty_score: insight.novelty_score, + insight_type: format!("{:?}", insight.insight_type), + generated_at: insight.generated_at, + tags: insight.tags.clone(), + feedback: None, + applied_count: 0, + }; + if storage.save_insight(&record).is_ok() { + insights_persisted += 1; + } + } + + // 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 { + for conn in new_connections.iter() { let link_type = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => "semantic", vestige_core::DiscoveredConnectionType::SharedConcept => "shared_concepts", @@ -141,7 +178,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 { + for conn in new_connections.iter() { let link_type_enum = match conn.connection_type { vestige_core::DiscoveredConnectionType::Semantic => LinkType::Semantic, vestige_core::DiscoveredConnectionType::SharedConcept => LinkType::Semantic, @@ -197,9 +234,11 @@ pub async fn execute( "novelty_score": i.novelty_score, })).collect::>(), "connectionsPersisted": connections_persisted, + "insightsPersisted": insights_persisted, "stats": { "new_connections_found": dream_result.new_connections_found, "connections_persisted": connections_persisted, + "insights_persisted": insights_persisted, "memories_strengthened": dream_result.memories_strengthened, "memories_compressed": dream_result.memories_compressed, "insights_generated": dream_result.insights_generated.len(), @@ -226,17 +265,18 @@ mod tests { async fn ingest_n_memories(storage: &Arc, n: usize) { for i in 0..n { - storage.ingest(vestige_core::IngestInput { - content: format!("Dream test memory number {}", i), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["dream-test".to_string()], - valid_from: None, - valid_until: None, - }) - .unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: format!("Dream test memory number {}", i), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["dream-test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); } } @@ -335,7 +375,10 @@ mod tests { // After dream: dream history should exist { let last = storage.get_last_dream().unwrap(); - assert!(last.is_some(), "Dream should have been persisted to database"); + assert!( + last.is_some(), + "Dream should have been persisted to database" + ); } } @@ -346,20 +389,28 @@ mod tests { // Create enough diverse memories to trigger connection discovery for i in 0..15 { - storage.ingest(vestige_core::IngestInput { - content: format!( - "Memory {} about topic {}: detailed content for connection discovery", - i, - if i % 3 == 0 { "rust" } else if i % 3 == 1 { "cargo" } else { "testing" } - ), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["dream-roundtrip".to_string()], - valid_from: None, - valid_until: None, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: format!( + "Memory {} about topic {}: detailed content for connection discovery", + i, + if i % 3 == 0 { + "rust" + } else if i % 3 == 1 { + "cargo" + } else { + "testing" + } + ), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["dream-roundtrip".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); } let cognitive = test_cognitive(); @@ -370,7 +421,10 @@ mod tests { if persisted > 0 { // Verify connections are queryable from storage let all_conns = storage.get_all_connections().unwrap(); - assert!(!all_conns.is_empty(), "Persisted connections should be queryable"); + assert!( + !all_conns.is_empty(), + "Persisted connections should be queryable" + ); // Verify connection IDs reference valid memories let all_nodes = storage.get_all_nodes(100, 0).unwrap(); @@ -392,7 +446,9 @@ mod tests { // Verify live cognitive engine was hydrated let cog = cognitive.lock().await; let first_conn = &all_conns[0]; - let assocs = cog.activation_network.get_associations(&first_conn.source_id); + let assocs = cog + .activation_network + .get_associations(&first_conn.source_id); assert!( !assocs.is_empty(), "Live cognitive engine should have been hydrated with dream connections" @@ -408,16 +464,18 @@ mod tests { // Ingest memories and collect their IDs let mut ids = Vec::new(); for i in 0..5 { - let result = storage.ingest(vestige_core::IngestInput { - content: format!("Save connection test memory {}", i), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["save-conn-test".to_string()], - valid_from: None, - valid_until: None, - }).unwrap(); + let result = storage + .ingest(vestige_core::IngestInput { + content: format!("Save connection test memory {}", i), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["save-conn-test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); ids.push(result.id); } @@ -426,7 +484,7 @@ mod tests { let mut saved = 0u32; let mut errors = Vec::new(); for i in 0..ids.len() { - for j in (i+1)..ids.len() { + for j in (i + 1)..ids.len() { let record = vestige_core::ConnectionRecord { source_id: ids[i].clone(), target_id: ids[j].clone(), @@ -438,10 +496,7 @@ mod tests { }; match storage.save_connection(&record) { Ok(_) => saved += 1, - Err(e) => errors.push(format!( - "{} -> {}: {}", - ids[i], ids[j], e - )), + Err(e) => errors.push(format!("{} -> {}: {}", ids[i], ids[j], e)), } } } @@ -477,34 +532,62 @@ mod tests { // Ingest memories with known high-similarity content (shared tags + similar text) let topics = [ - ("Rust borrow checker prevents data races at compile time", vec!["rust", "safety"]), - ("Rust ownership model ensures memory safety without GC", vec!["rust", "safety"]), - ("Cargo is the Rust package manager and build system", vec!["rust", "cargo"]), - ("Cargo.toml defines dependencies for Rust projects", vec!["rust", "cargo"]), - ("Unit tests in Rust use #[test] attribute", vec!["rust", "testing"]), - ("Integration tests in Rust live in the tests/ directory", vec!["rust", "testing"]), - ("Clippy is a Rust linter that catches common mistakes", vec!["rust", "tooling"]), - ("Rustfmt formats Rust code according to style guidelines", vec!["rust", "tooling"]), + ( + "Rust borrow checker prevents data races at compile time", + vec!["rust", "safety"], + ), + ( + "Rust ownership model ensures memory safety without GC", + vec!["rust", "safety"], + ), + ( + "Cargo is the Rust package manager and build system", + vec!["rust", "cargo"], + ), + ( + "Cargo.toml defines dependencies for Rust projects", + vec!["rust", "cargo"], + ), + ( + "Unit tests in Rust use #[test] attribute", + vec!["rust", "testing"], + ), + ( + "Integration tests in Rust live in the tests/ directory", + vec!["rust", "testing"], + ), + ( + "Clippy is a Rust linter that catches common mistakes", + vec!["rust", "tooling"], + ), + ( + "Rustfmt formats Rust code according to style guidelines", + vec!["rust", "tooling"], + ), ]; for (content, tags) in &topics { - storage.ingest(vestige_core::IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: tags.iter().map(|t| t.to_string()).collect(), - valid_from: None, - valid_until: None, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: tags.iter().map(|t| t.to_string()).collect(), + valid_from: None, + valid_until: None, + }) + .unwrap(); } let cognitive = test_cognitive(); let result = execute(&storage, &cognitive, None).await.unwrap(); assert_eq!(result["status"], "dreamed"); - let found = result["stats"]["new_connections_found"].as_u64().unwrap_or(0); + let found = result["stats"]["new_connections_found"] + .as_u64() + .unwrap_or(0); let persisted = result["connectionsPersisted"].as_u64().unwrap_or(0); // Dream should discover connections between these related memories @@ -532,4 +615,95 @@ mod tests { persisted ); } + + #[tokio::test] + async fn test_dream_persists_insights() { + let (storage, _dir) = test_storage().await; + + // Create diverse tagged memories to encourage insight generation + let topics = [ + ( + "Rust borrow checker prevents data races", + vec!["rust", "safety"], + ), + ( + "Rust ownership model ensures memory safety", + vec!["rust", "safety"], + ), + ( + "Cargo manages Rust project dependencies", + vec!["rust", "cargo"], + ), + ( + "Cargo.toml defines project configuration", + vec!["rust", "cargo"], + ), + ( + "Unit tests use the #[test] attribute", + vec!["rust", "testing"], + ), + ( + "Integration tests live in the tests directory", + vec!["rust", "testing"], + ), + ( + "Clippy catches common Rust mistakes", + vec!["rust", "tooling"], + ), + ( + "Rustfmt automatically formats code", + vec!["rust", "tooling"], + ), + ]; + for (content, tags) in &topics { + storage + .ingest(vestige_core::IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: tags.iter().map(|t| t.to_string()).collect(), + valid_from: None, + valid_until: None, + }) + .unwrap(); + } + + let result = execute(&storage, &test_cognitive(), None).await.unwrap(); + assert_eq!(result["status"], "dreamed"); + + let response_insights = result["insights"].as_array().unwrap(); + let persisted_count = result["insightsPersisted"].as_u64().unwrap_or(0); + + // If insights were generated, they should be persisted + if !response_insights.is_empty() { + assert!( + persisted_count > 0, + "Generated insights should be persisted to database" + ); + let stored = storage.get_insights(100).unwrap(); + assert_eq!( + stored.len(), + persisted_count as usize, + "All {} persisted insights should be retrievable", + persisted_count + ); + // Verify insight fields + for insight in &stored { + assert!(!insight.id.is_empty(), "Insight ID should not be empty"); + assert!( + !insight.insight.is_empty(), + "Insight text should not be empty" + ); + assert!(insight.confidence >= 0.0 && insight.confidence <= 1.0); + assert!(insight.novelty_score >= 0.0); + assert!( + insight.feedback.is_none(), + "Fresh insight should have no feedback" + ); + assert_eq!(insight.applied_count, 0); + } + } + } } diff --git a/crates/vestige-mcp/src/tools/explore.rs b/crates/vestige-mcp/src/tools/explore.rs index 8b9e5af..441afd3 100644 --- a/crates/vestige-mcp/src/tools/explore.rs +++ b/crates/vestige-mcp/src/tools/explore.rs @@ -6,6 +6,7 @@ use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; use vestige_core::Storage; +use vestige_core::advanced::{Connection, ConnectionType, MemoryChainBuilder, MemoryNode}; pub fn schema() -> serde_json::Value { serde_json::json!({ @@ -40,8 +41,14 @@ pub async fn execute( args: Option, ) -> Result { let args = args.ok_or("Missing arguments")?; - let action = args.get("action").and_then(|v| v.as_str()).ok_or("Missing 'action'")?; - let from = args.get("from").and_then(|v| v.as_str()).ok_or("Missing 'from'")?; + let action = args + .get("action") + .and_then(|v| v.as_str()) + .ok_or("Missing 'action'")?; + let from = args + .get("from") + .and_then(|v| v.as_str()) + .ok_or("Missing 'from'")?; let to = args.get("to").and_then(|v| v.as_str()); let limit = args.get("limit").and_then(|v| v.as_u64()).unwrap_or(10) as usize; @@ -50,38 +57,50 @@ pub async fn execute( match action { "chain" => { let to_id = to.ok_or("'to' is required for chain action")?; - match cog.chain_builder.build_chain(from, to_id) { - Some(chain) => { - Ok(serde_json::json!({ - "action": "chain", - "from": from, - "to": to_id, - "steps": chain.steps.iter().map(|s| serde_json::json!({ - "memory_id": s.memory_id, - "memory_preview": s.memory_preview, - "connection_type": format!("{:?}", s.connection_type), - "connection_strength": s.connection_strength, - "reasoning": s.reasoning, - })).collect::>(), - "confidence": chain.confidence, - "total_hops": chain.total_hops, - })) - } - None => { - Ok(serde_json::json!({ - "action": "chain", - "from": from, - "to": to_id, - "steps": [], - "message": "No chain found between these memories" - })) - } + let chain_result = cog.chain_builder.build_chain(from, to_id); + let from_owned = from.to_string(); + let to_owned = to_id.to_string(); + drop(cog); // release lock before potential storage fallback + + let chain_opt = if chain_result.is_some() { + chain_result + } else { + // Storage fallback: build temporary chain from persisted connections + build_chain_from_storage(storage, &from_owned, &to_owned) + }; + + match chain_opt { + Some(chain) => Ok(serde_json::json!({ + "action": "chain", + "from": from_owned, + "to": to_owned, + "steps": chain.steps.iter().map(|s| serde_json::json!({ + "memory_id": s.memory_id, + "memory_preview": s.memory_preview, + "connection_type": format!("{:?}", s.connection_type), + "connection_strength": s.connection_strength, + "reasoning": s.reasoning, + })).collect::>(), + "confidence": chain.confidence, + "total_hops": chain.total_hops, + })), + None => Ok(serde_json::json!({ + "action": "chain", + "from": from_owned, + "to": to_owned, + "steps": [], + "message": "No chain found between these memories" + })), } } "associations" => { let activation_assocs = cog.activation_network.get_associations(from); - let hippocampal_assocs = cog.hippocampal_index.get_associations(from, 2) + let hippocampal_assocs = cog + .hippocampal_index + .get_associations(from, 2) .unwrap_or_default(); + let from_owned = from.to_string(); + drop(cog); // release lock consistently (matches chain/bridges pattern) let mut all_associations: Vec = Vec::new(); @@ -105,28 +124,27 @@ pub async fn execute( all_associations.truncate(limit); // Fallback: if in-memory modules are empty, query storage directly - if all_associations.is_empty() { - drop(cog); // release cognitive lock before storage call - if let Ok(connections) = storage.get_connections_for_memory(from) { - for conn in connections.iter().take(limit) { - let other_id = if conn.source_id == from { - &conn.target_id - } else { - &conn.source_id - }; - all_associations.push(serde_json::json!({ - "memory_id": other_id, - "strength": conn.strength, - "link_type": conn.link_type, - "source": "persistent_graph", - })); - } + if all_associations.is_empty() + && let Ok(connections) = storage.get_connections_for_memory(&from_owned) + { + for conn in connections.iter().take(limit) { + let other_id = if conn.source_id == from_owned { + &conn.target_id + } else { + &conn.source_id + }; + all_associations.push(serde_json::json!({ + "memory_id": other_id, + "strength": conn.strength, + "link_type": conn.link_type, + "source": "persistent_graph", + })); } } Ok(serde_json::json!({ "action": "associations", - "from": from, + "from": from_owned, "associations": all_associations, "count": all_associations.len(), })) @@ -134,16 +152,102 @@ pub async fn execute( "bridges" => { let to_id = to.ok_or("'to' is required for bridges action")?; let bridges = cog.chain_builder.find_bridge_memories(from, to_id); - let limited: Vec<_> = bridges.iter().take(limit).collect(); + let from_owned = from.to_string(); + let to_owned = to_id.to_string(); + drop(cog); // release lock before potential storage fallback + + let final_bridges = if !bridges.is_empty() { + bridges + } else { + // Storage fallback: build temporary graph and find bridges + let temp_builder = build_temp_chain_builder(storage, &from_owned, &to_owned); + temp_builder.find_bridge_memories(&from_owned, &to_owned) + }; + + let limited: Vec<_> = final_bridges.iter().take(limit).collect(); Ok(serde_json::json!({ "action": "bridges", - "from": from, - "to": to_id, + "from": from_owned, + "to": to_owned, "bridges": limited, "count": limited.len(), })) } - _ => Err(format!("Unknown action: '{}'. Expected: chain, associations, bridges", action)), + _ => Err(format!( + "Unknown action: '{}'. Expected: chain, associations, bridges", + action + )), + } +} + +/// Build a temporary MemoryChainBuilder from persisted connections for fallback queries. +fn build_temp_chain_builder( + storage: &Arc, + from_id: &str, + to_id: &str, +) -> MemoryChainBuilder { + let mut builder = MemoryChainBuilder::new(); + + // Load connections involving either endpoint + let mut all_conns = Vec::new(); + if let Ok(conns) = storage.get_connections_for_memory(from_id) { + all_conns.extend(conns); + } + if let Ok(conns) = storage.get_connections_for_memory(to_id) { + all_conns.extend(conns); + } + + // Deduplicate edges and load referenced memory nodes + let mut seen_edges = std::collections::HashSet::new(); + all_conns.retain(|c| seen_edges.insert((c.source_id.clone(), c.target_id.clone()))); + + let mut seen_ids = std::collections::HashSet::new(); + for conn in &all_conns { + for id in [&conn.source_id, &conn.target_id] { + if seen_ids.insert(id.clone()) + && let Ok(Some(node)) = storage.get_node(id) + { + builder.add_memory(MemoryNode { + id: node.id.clone(), + content_preview: node.content.chars().take(100).collect(), + tags: node.tags.clone(), + connections: vec![], + }); + } + } + } + + // Add edges + for conn in &all_conns { + builder.add_connection(Connection { + from_id: conn.source_id.clone(), + to_id: conn.target_id.clone(), + connection_type: link_type_to_connection_type(&conn.link_type), + strength: conn.strength, + created_at: conn.created_at, + }); + } + + builder +} + +/// Build a chain from storage when in-memory chain_builder is empty. +fn build_chain_from_storage( + storage: &Arc, + from_id: &str, + to_id: &str, +) -> Option { + let builder = build_temp_chain_builder(storage, from_id, to_id); + builder.build_chain(from_id, to_id) +} + +/// Convert storage link_type string to ConnectionType enum. +fn link_type_to_connection_type(link_type: &str) -> ConnectionType { + match link_type { + "temporal" => ConnectionType::TemporalProximity, + "causal" => ConnectionType::Causal, + "part_of" => ConnectionType::PartOf, + _ => ConnectionType::SemanticSimilarity, } } @@ -300,39 +404,47 @@ mod tests { let (storage, _dir) = test_storage().await; // Create two memories and a direct connection in storage - let id1 = storage.ingest(vestige_core::IngestInput { - content: "Memory about Rust".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["test".to_string()], - valid_from: None, - valid_until: None, - }).unwrap().id; + let id1 = storage + .ingest(vestige_core::IngestInput { + content: "Memory about Rust".to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap() + .id; - let id2 = storage.ingest(vestige_core::IngestInput { - content: "Memory about Cargo".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["test".to_string()], - valid_from: None, - valid_until: None, - }).unwrap().id; + let id2 = storage + .ingest(vestige_core::IngestInput { + content: "Memory about Cargo".to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap() + .id; // Save connection directly to storage (bypassing cognitive engine) let now = chrono::Utc::now(); - storage.save_connection(&vestige_core::ConnectionRecord { - source_id: id1.clone(), - target_id: id2.clone(), - strength: 0.9, - link_type: "semantic".to_string(), - created_at: now, - last_activated: now, - activation_count: 1, - }).unwrap(); + storage + .save_connection(&vestige_core::ConnectionRecord { + source_id: id1.clone(), + target_id: id2.clone(), + strength: 0.9, + link_type: "semantic".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }) + .unwrap(); // Execute with empty cognitive engine — should fall back to storage let cognitive = test_cognitive(); @@ -351,4 +463,102 @@ mod tests { assert_eq!(associations[0]["source"], "persistent_graph"); assert_eq!(associations[0]["memory_id"], id2); } + + #[tokio::test] + async fn test_chain_storage_fallback() { + let (storage, _dir) = test_storage().await; + + // Create 3 memories: A -> B -> C + let make = |content: &str| vestige_core::IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }; + let id_a = storage.ingest(make("Memory A about databases")).unwrap().id; + let id_b = storage.ingest(make("Memory B about indexes")).unwrap().id; + let id_c = storage + .ingest(make("Memory C about performance")) + .unwrap() + .id; + + // Save connections A->B and B->C to storage + let now = chrono::Utc::now(); + for (src, tgt) in [(&id_a, &id_b), (&id_b, &id_c)] { + storage + .save_connection(&vestige_core::ConnectionRecord { + source_id: src.clone(), + target_id: tgt.clone(), + strength: 0.9, + link_type: "semantic".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }) + .unwrap(); + } + + // Execute chain with empty cognitive engine — should fall back to storage + let args = serde_json::json!({ "action": "chain", "from": id_a, "to": id_c }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value["action"], "chain"); + let steps = value["steps"].as_array().unwrap(); + assert!( + !steps.is_empty(), + "Chain should find path A->B->C via storage fallback" + ); + } + + #[tokio::test] + async fn test_bridges_storage_fallback() { + let (storage, _dir) = test_storage().await; + + // Create 3 memories: A -> B -> C (B is the bridge) + let make = |content: &str| vestige_core::IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }; + let id_a = storage.ingest(make("Bridge test memory A")).unwrap().id; + let id_b = storage.ingest(make("Bridge test memory B")).unwrap().id; + let id_c = storage.ingest(make("Bridge test memory C")).unwrap().id; + + let now = chrono::Utc::now(); + for (src, tgt) in [(&id_a, &id_b), (&id_b, &id_c)] { + storage + .save_connection(&vestige_core::ConnectionRecord { + source_id: src.clone(), + target_id: tgt.clone(), + strength: 0.9, + link_type: "semantic".to_string(), + created_at: now, + last_activated: now, + activation_count: 1, + }) + .unwrap(); + } + + // Execute bridges with empty cognitive engine + let args = serde_json::json!({ "action": "bridges", "from": id_a, "to": id_c }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!(result.is_ok()); + let value = result.unwrap(); + assert_eq!(value["action"], "bridges"); + let bridges = value["bridges"].as_array().unwrap(); + assert!( + !bridges.is_empty(), + "Should find B as bridge between A and C via storage fallback" + ); + } } diff --git a/crates/vestige-mcp/src/tools/feedback.rs b/crates/vestige-mcp/src/tools/feedback.rs index e21cc97..438e594 100644 --- a/crates/vestige-mcp/src/tools/feedback.rs +++ b/crates/vestige-mcp/src/tools/feedback.rs @@ -73,19 +73,23 @@ pub async fn execute_promote( // Validate UUID uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?; - // Get node before for comparison - let before = storage.get_node(&args.id).map_err(|e| e.to_string())? + let before = storage + .get_node(&args.id) + .map_err(|e| e.to_string())? .ok_or_else(|| format!("Node not found: {}", args.id))?; - let node = storage.promote_memory(&args.id).map_err(|e| e.to_string())?; + let node = storage + .promote_memory(&args.id) + .map_err(|e| e.to_string())?; // ==================================================================== // COGNITIVE FEEDBACK PIPELINE (promote) // ==================================================================== if let Ok(mut cog) = cognitive.try_lock() { // 5A. Reward signal — record positive outcome - cog.reward_signal.record_outcome(&args.id, OutcomeType::Helpful); + cog.reward_signal + .record_outcome(&args.id, OutcomeType::Helpful); // 5B. Importance tracking — mark as helpful retrieval cog.importance_tracker.on_retrieved(&args.id, true); @@ -143,9 +147,10 @@ pub async fn execute_demote( // Validate UUID uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid node ID format".to_string())?; - // Get node before for comparison - let before = storage.get_node(&args.id).map_err(|e| e.to_string())? + let before = storage + .get_node(&args.id) + .map_err(|e| e.to_string())? .ok_or_else(|| format!("Node not found: {}", args.id))?; let node = storage.demote_memory(&args.id).map_err(|e| e.to_string())?; @@ -155,7 +160,8 @@ pub async fn execute_demote( // ==================================================================== if let Ok(mut cog) = cognitive.try_lock() { // 5A. Reward signal — record negative outcome - cog.reward_signal.record_outcome(&args.id, OutcomeType::NotHelpful); + cog.reward_signal + .record_outcome(&args.id, OutcomeType::NotHelpful); // 5B. Importance tracking — mark as unhelpful retrieval cog.importance_tracker.on_retrieved(&args.id, false); @@ -237,8 +243,9 @@ pub async fn execute_request_feedback( // 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())? + let node = storage + .get_node(&args.id) + .map_err(|e| e.to_string())? .ok_or_else(|| format!("Node not found: {}", args.id))?; // Truncate content for display @@ -319,10 +326,12 @@ mod tests { assert_eq!(schema["type"], "object"); assert!(schema["properties"]["id"].is_object()); assert!(schema["properties"]["reason"].is_object()); - assert!(schema["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("id"))); + assert!( + schema["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("id")) + ); } #[test] @@ -330,10 +339,12 @@ mod tests { let schema = demote_schema(); assert_eq!(schema["type"], "object"); assert!(schema["properties"]["id"].is_object()); - assert!(schema["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("id"))); + assert!( + schema["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("id")) + ); } #[test] @@ -342,10 +353,12 @@ mod tests { assert_eq!(schema["type"], "object"); assert!(schema["properties"]["id"].is_object()); assert!(schema["properties"]["context"].is_object()); - assert!(schema["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("id"))); + assert!( + schema["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("id")) + ); } // === PROMOTE TESTS === @@ -370,8 +383,7 @@ mod tests { #[tokio::test] async fn test_promote_nonexistent_node_fails() { let (storage, _dir) = test_storage().await; - let args = - serde_json::json!({ "id": "00000000-0000-0000-0000-000000000000" }); + let args = serde_json::json!({ "id": "00000000-0000-0000-0000-000000000000" }); let result = execute_promote(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Node not found")); @@ -454,8 +466,7 @@ mod tests { #[tokio::test] async fn test_demote_nonexistent_node_fails() { let (storage, _dir) = test_storage().await; - let args = - serde_json::json!({ "id": "00000000-0000-0000-0000-000000000000" }); + let args = serde_json::json!({ "id": "00000000-0000-0000-0000-000000000000" }); let result = execute_demote(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Node not found")); @@ -510,8 +521,7 @@ mod tests { #[tokio::test] async fn test_request_feedback_nonexistent_node_fails() { let (storage, _dir) = test_storage().await; - let args = - serde_json::json!({ "id": "00000000-0000-0000-0000-000000000000" }); + let args = serde_json::json!({ "id": "00000000-0000-0000-0000-000000000000" }); let result = execute_request_feedback(&storage, Some(args)).await; assert!(result.is_err()); } diff --git a/crates/vestige-mcp/src/tools/graph.rs b/crates/vestige-mcp/src/tools/graph.rs index 183c725..13ca746 100644 --- a/crates/vestige-mcp/src/tools/graph.rs +++ b/crates/vestige-mcp/src/tools/graph.rs @@ -125,52 +125,72 @@ pub async fn execute( storage: &Arc, args: Option, ) -> Result { - let depth = args.as_ref() + let depth = args + .as_ref() .and_then(|a| a.get("depth")) .and_then(|v| v.as_u64()) .unwrap_or(2) .min(3) as u32; - let max_nodes = args.as_ref() + let max_nodes = args + .as_ref() .and_then(|a| a.get("max_nodes")) .and_then(|v| v.as_u64()) .unwrap_or(50) .min(200) as usize; // Determine center node - let center_id = if let Some(id) = args.as_ref().and_then(|a| a.get("center_id")).and_then(|v| v.as_str()) { + let center_id = if let Some(id) = args + .as_ref() + .and_then(|a| a.get("center_id")) + .and_then(|v| v.as_str()) + { id.to_string() - } else if let Some(query) = args.as_ref().and_then(|a| a.get("query")).and_then(|v| v.as_str()) { + } else if let Some(query) = args + .as_ref() + .and_then(|a| a.get("query")) + .and_then(|v| v.as_str()) + { // Search for center node - let results = storage.search(query, 1) + let results = storage + .search(query, 1) .map_err(|e| format!("Search failed: {}", e))?; - results.first() + results + .first() .map(|n| n.id.clone()) .ok_or_else(|| "No memories found matching query".to_string())? } else { // Default: use the most recent memory - let recent = storage.get_all_nodes(1, 0) + let recent = storage + .get_all_nodes(1, 0) .map_err(|e| format!("Failed to get recent node: {}", e))?; - recent.first() + recent + .first() .map(|n| n.id.clone()) .ok_or_else(|| "No memories in database".to_string())? }; // Get subgraph - let (nodes, edges) = storage.get_memory_subgraph(¢er_id, depth, max_nodes) + let (nodes, edges) = storage + .get_memory_subgraph(¢er_id, depth, max_nodes) .map_err(|e| format!("Failed to get subgraph: {}", e))?; if nodes.is_empty() || !nodes.iter().any(|n| n.id == center_id) { - return Err(format!("Memory '{}' not found or has no accessible data", center_id)); + return Err(format!( + "Memory '{}' not found or has no accessible data", + center_id + )); } // Build index map for FR layout - let id_to_idx: std::collections::HashMap<&str, usize> = nodes.iter() + let id_to_idx: std::collections::HashMap<&str, usize> = nodes + .iter() .enumerate() .map(|(i, n)| (n.id.as_str(), i)) .collect(); - let layout_edges: Vec<(usize, usize, f64)> = edges.iter() + let layout_edges: Vec<(usize, usize, f64)> = edges + .iter() .filter_map(|e| { let u = id_to_idx.get(e.source_id.as_str())?; let v = id_to_idx.get(e.target_id.as_str())?; @@ -182,7 +202,8 @@ pub async fn execute( let positions = fruchterman_reingold(nodes.len(), &layout_edges, 800.0, 600.0, 50); // Build response - let nodes_json: Vec = nodes.iter() + let nodes_json: Vec = nodes + .iter() .enumerate() .map(|(i, n)| { let (x, y) = positions.get(i).copied().unwrap_or((400.0, 300.0)); @@ -199,11 +220,15 @@ pub async fn execute( "x": (x * 100.0).round() / 100.0, "y": (y * 100.0).round() / 100.0, "isCenter": n.id == center_id, + // v2.0.5 Active Forgetting — dashboard uses these to dim suppressed nodes + "suppression_count": n.suppression_count, + "suppressed_at": n.suppressed_at.map(|t| t.to_rfc3339()), }) }) .collect(); - let edges_json: Vec = edges.iter() + let edges_json: Vec = edges + .iter() .map(|e| { serde_json::json!({ "source": e.source_id, @@ -293,16 +318,18 @@ mod tests { #[tokio::test] async fn test_graph_with_center_id() { let (storage, _dir) = test_storage().await; - let node = storage.ingest(vestige_core::IngestInput { - content: "Graph test memory".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["test".to_string()], - valid_from: None, - valid_until: None, - }).unwrap(); + let node = storage + .ingest(vestige_core::IngestInput { + content: "Graph test memory".to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); let args = serde_json::json!({ "center_id": node.id }); let result = execute(&storage, Some(args)).await; @@ -318,16 +345,18 @@ mod tests { #[tokio::test] async fn test_graph_with_query() { let (storage, _dir) = test_storage().await; - storage.ingest(vestige_core::IngestInput { - content: "Quantum computing fundamentals".to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["science".to_string()], - valid_from: None, - valid_until: None, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: "Quantum computing fundamentals".to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["science".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); let args = serde_json::json!({ "query": "quantum" }); let result = execute(&storage, Some(args)).await; @@ -339,16 +368,18 @@ mod tests { #[tokio::test] async fn test_graph_node_has_position() { let (storage, _dir) = test_storage().await; - let node = storage.ingest(vestige_core::IngestInput { - content: "Position test memory".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, - }).unwrap(); + let node = storage + .ingest(vestige_core::IngestInput { + content: "Position test memory".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, + }) + .unwrap(); let args = serde_json::json!({ "center_id": node.id }); let result = execute(&storage, Some(args)).await.unwrap(); diff --git a/crates/vestige-mcp/src/tools/health.rs b/crates/vestige-mcp/src/tools/health.rs index 773438a..362f298 100644 --- a/crates/vestige-mcp/src/tools/health.rs +++ b/crates/vestige-mcp/src/tools/health.rs @@ -16,23 +16,28 @@ pub async fn execute( _args: Option, ) -> Result { // Average retention - let avg_retention = storage.get_avg_retention() + let avg_retention = storage + .get_avg_retention() .map_err(|e| format!("Failed to get avg retention: {}", e))?; // Retention distribution - let distribution = storage.get_retention_distribution() + let distribution = storage + .get_retention_distribution() .map_err(|e| format!("Failed to get retention distribution: {}", e))?; - let distribution_json: serde_json::Value = distribution.iter().map(|(bucket, count)| { - serde_json::json!({ "bucket": bucket, "count": count }) - }).collect(); + let distribution_json: serde_json::Value = distribution + .iter() + .map(|(bucket, count)| serde_json::json!({ "bucket": bucket, "count": count })) + .collect(); // Retention trend - let trend = storage.get_retention_trend() + let trend = storage + .get_retention_trend() .unwrap_or_else(|_| "unknown".to_string()); // Total memories and those below key thresholds - let stats = storage.get_stats() + let stats = storage + .get_stats() .map_err(|e| format!("Failed to get stats: {}", e))?; let below_30 = storage.count_memories_below_retention(0.3).unwrap_or(0); @@ -104,16 +109,18 @@ mod tests { let (storage, _dir) = test_storage().await; // Ingest some test memories for i in 0..5 { - storage.ingest(vestige_core::IngestInput { - content: format!("Health test memory {}", i), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["test".to_string()], - valid_from: None, - valid_until: None, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: format!("Health test memory {}", i), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); } let result = execute(&storage, None).await; @@ -127,24 +134,24 @@ mod tests { #[tokio::test] async fn test_health_distribution_buckets() { let (storage, _dir) = test_storage().await; - storage.ingest(vestige_core::IngestInput { - content: "Test memory for distribution".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, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: "Test memory for distribution".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, + }) + .unwrap(); let result = execute(&storage, None).await.unwrap(); let dist = result["distribution"].as_array().unwrap(); // Should have at least one bucket with data assert!(!dist.is_empty()); - let total: i64 = dist.iter() - .map(|b| b["count"].as_i64().unwrap_or(0)) - .sum(); + let total: i64 = dist.iter().map(|b| b["count"].as_i64().unwrap_or(0)).sum(); assert_eq!(total, 1); } } diff --git a/crates/vestige-mcp/src/tools/importance.rs b/crates/vestige-mcp/src/tools/importance.rs index 10f5bfb..c9048aa 100644 --- a/crates/vestige-mcp/src/tools/importance.rs +++ b/crates/vestige-mcp/src/tools/importance.rs @@ -70,7 +70,9 @@ pub async fn execute( // Use CognitiveEngine's persistent signals (novelty/reward/attention accumulate) let cog = cognitive.lock().await; - let score = cog.importance_signals.compute_importance(&args.content, &context); + let score = cog + .importance_signals + .compute_importance(&args.content, &context); // Also detect emotional markers for richer output let emotional_markers = cog.arousal_signal.detect_emotional_markers(&args.content); @@ -129,10 +131,12 @@ mod tests { let schema = schema(); assert_eq!(schema["type"], "object"); assert!(schema["properties"]["content"].is_object()); - assert!(schema["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("content"))); + assert!( + schema["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("content")) + ); } #[tokio::test] @@ -140,7 +144,12 @@ mod tests { let storage = Arc::new( Storage::new(Some(std::path::PathBuf::from("/tmp/test_importance.db"))).unwrap(), ); - let result = execute(&storage, &test_cognitive(), Some(serde_json::json!({ "content": "" }))).await; + let result = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ "content": "" })), + ) + .await; assert!(result.is_err()); } diff --git a/crates/vestige-mcp/src/tools/ingest.rs b/crates/vestige-mcp/src/tools/ingest.rs deleted file mode 100644 index 402e24f..0000000 --- a/crates/vestige-mcp/src/tools/ingest.rs +++ /dev/null @@ -1,434 +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/intention_unified.rs b/crates/vestige-mcp/src/tools/intention_unified.rs index 3a22d93..5e2f87e 100644 --- a/crates/vestige-mcp/src/tools/intention_unified.rs +++ b/crates/vestige-mcp/src/tools/intention_unified.rs @@ -152,8 +152,10 @@ struct TriggerSpec { #[serde(rename = "type")] trigger_type: Option, at: Option, + #[serde(alias = "in_minutes")] in_minutes: Option, codebase: Option, + #[serde(alias = "file_pattern")] file_pattern: Option, topic: Option, condition: Option, @@ -263,25 +265,34 @@ async fn execute_set( nlp_parsed = true; // Extract trigger info from parsed intention let (t_type, t_data) = match &parsed.trigger { - ProspectiveTrigger::TimeBased { .. } => { - ("time".to_string(), serde_json::json!({"type": "time"}).to_string()) - } + ProspectiveTrigger::TimeBased { .. } => ( + "time".to_string(), + serde_json::json!({"type": "time"}).to_string(), + ), ProspectiveTrigger::DurationBased { after, .. } => { let mins = after.num_minutes(); - ("time".to_string(), serde_json::json!({"type": "time", "in_minutes": mins}).to_string()) - } - ProspectiveTrigger::EventBased { condition, .. } => { - ("event".to_string(), serde_json::json!({"type": "event", "condition": condition}).to_string()) - } - ProspectiveTrigger::ContextBased { context_match } => { - ("context".to_string(), serde_json::json!({"type": "context", "topic": format!("{:?}", context_match)}).to_string()) - } - ProspectiveTrigger::Recurring { .. } => { - ("recurring".to_string(), serde_json::json!({"type": "recurring"}).to_string()) - } - _ => { - ("event".to_string(), serde_json::json!({"type": "event"}).to_string()) + ( + "time".to_string(), + serde_json::json!({"type": "time", "in_minutes": mins}).to_string(), + ) } + ProspectiveTrigger::EventBased { condition, .. } => ( + "event".to_string(), + serde_json::json!({"type": "event", "condition": condition}).to_string(), + ), + ProspectiveTrigger::ContextBased { context_match } => ( + "context".to_string(), + serde_json::json!({"type": "context", "topic": format!("{:?}", context_match)}) + .to_string(), + ), + ProspectiveTrigger::Recurring { .. } => ( + "recurring".to_string(), + serde_json::json!({"type": "recurring"}).to_string(), + ), + _ => ( + "event".to_string(), + serde_json::json!({"type": "event"}).to_string(), + ), }; nlp_trigger_type = Some(t_type); nlp_trigger_data = Some(t_data); @@ -424,9 +435,27 @@ async fn execute_check( let _ = cog.prospective_memory.update_context(prospective_ctx); } - - // Get active intentions - let intentions = storage.get_active_intentions().map_err(|e| e.to_string())?; + // Get active intentions. `include_snoozed=true` folds snoozed intentions + // back into the check pool so time/context triggers can wake them up when + // their snooze window has elapsed or a matching context appears — before + // v2.0.7 this flag was advertised in the schema but silently ignored, so + // snoozed intentions were invisible to `check` regardless of the arg. + let mut intentions = storage.get_active_intentions().map_err(|e| e.to_string())?; + if args.include_snoozed.unwrap_or(false) { + let snoozed = storage + .get_intentions_by_status("snoozed") + .map_err(|e| e.to_string())?; + // Deduplicate defensively — an intention should never be in both lists + // but we pay the O(n) hash-set cost to stay correct if storage semantics + // shift under us. + use std::collections::HashSet; + let seen: HashSet = intentions.iter().map(|i| i.id.clone()).collect(); + for s in snoozed { + if !seen.contains(&s.id) { + intentions.push(s); + } + } + } let mut triggered = Vec::new(); let mut pending = Vec::new(); @@ -488,6 +517,7 @@ async fn execute_check( let item = serde_json::json!({ "id": intention.id, "description": intention.content, + "status": intention.status, "priority": match intention.priority { 1 => "low", 3 => "high", @@ -496,6 +526,7 @@ async fn execute_check( }, "createdAt": intention.created_at.to_rfc3339(), "deadline": intention.deadline.map(|d| d.to_rfc3339()), + "snoozedUntil": intention.snoozed_until.map(|d| d.to_rfc3339()), "isOverdue": is_overdue, }); @@ -519,10 +550,7 @@ async fn execute_update( storage: &Arc, args: &UnifiedIntentionArgs, ) -> Result { - let intention_id = args - .id - .as_ref() - .ok_or("Missing 'id' for update action")?; + let intention_id = args.id.as_ref().ok_or("Missing 'id' for update action")?; let status = args .status @@ -688,7 +716,9 @@ mod tests { "action": "set", "description": description }); - let result = execute(storage, &test_cognitive(), Some(args)).await.unwrap(); + let result = execute(storage, &test_cognitive(), Some(args)) + .await + .unwrap(); result["intentionId"].as_str().unwrap().to_string() } @@ -740,10 +770,12 @@ mod tests { assert_eq!(value["success"], true); assert_eq!(value["action"], "set"); assert!(value["intentionId"].is_string()); - assert!(value["message"] - .as_str() - .unwrap() - .contains("Intention created")); + assert!( + value["message"] + .as_str() + .unwrap() + .contains("Intention created") + ); } #[tokio::test] @@ -819,6 +851,77 @@ mod tests { assert!(value["triggerAt"].is_string()); } + #[tokio::test] + async fn test_set_action_with_duration_trigger_snake_case() { + // The public JSON schema (see schema() above) declares `in_minutes` in + // snake_case. The TriggerSpec struct uses `rename_all = "camelCase"` so + // without an explicit `#[serde(alias = "in_minutes")]` the snake_case + // input is silently dropped (becomes None), `triggerAt` becomes null, + // and the time-based intention never fires. + let (storage, _dir) = test_storage().await; + let args = serde_json::json!({ + "action": "set", + "description": "Check build status", + "trigger": { + "type": "time", + "in_minutes": 30 + } + }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!(result.is_ok()); + + let value = result.unwrap(); + assert!( + value["triggerAt"].is_string(), + "snake_case in_minutes should derive triggerAt; got: {:?}", + value["triggerAt"] + ); + } + + #[tokio::test] + async fn test_set_action_with_file_pattern_snake_case() { + // The public JSON schema declares `file_pattern` in snake_case. Verify + // it survives deserialization by setting an intention with ONLY + // file_pattern (no codebase — otherwise the check-side codebase branch + // would short-circuit and mask a dropped file_pattern field). + // + // Note: file_pattern matching currently uses substring containment, not + // glob, so the "pattern" must be a plain substring of the file path. + let (storage, _dir) = test_storage().await; + let args = serde_json::json!({ + "action": "set", + "description": "Review test files", + "trigger": { + "type": "context", + "file_pattern": ".test.cjs" + } + }); + let result = execute(&storage, &test_cognitive(), Some(args)).await; + assert!( + result.is_ok(), + "set should succeed with snake_case file_pattern" + ); + + // Check should fire when a matching file is in context. + let check_args = serde_json::json!({ + "action": "check", + "context": { + "file": "tests/neural-cascade.test.cjs" + } + }); + let check = execute(&storage, &test_cognitive(), Some(check_args)) + .await + .unwrap(); + let triggered = check["triggered"].as_array().expect("triggered array"); + assert!( + !triggered.is_empty(), + "file_pattern must survive snake_case deserialization and match on file substring; \ + got triggered: {:?}, pending: {:?}", + check["triggered"], + check["pending"] + ); + } + #[tokio::test] async fn test_set_action_with_deadline() { let (storage, _dir) = test_storage().await; @@ -880,7 +983,9 @@ mod tests { "codebase": "payments" } }); - execute(&storage, &test_cognitive(), Some(set_args)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(set_args)) + .await + .unwrap(); // Check with matching context let check_args = serde_json::json!({ @@ -911,7 +1016,9 @@ mod tests { "at": past_time } }); - execute(&storage, &test_cognitive(), Some(set_args)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(set_args)) + .await + .unwrap(); let check_args = serde_json::json!({ "action": "check" }); let result = execute(&storage, &test_cognitive(), Some(check_args)).await; @@ -1110,7 +1217,9 @@ mod tests { "id": intention_id, "status": "complete" }); - execute(&storage, &test_cognitive(), Some(complete_args)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(complete_args)) + .await + .unwrap(); // Create another active one create_test_intention(&storage, "Active task").await; @@ -1120,7 +1229,9 @@ mod tests { "action": "list", "filter_status": "fulfilled" }); - let result = execute(&storage, &test_cognitive(), Some(list_args)).await.unwrap(); + let result = execute(&storage, &test_cognitive(), Some(list_args)) + .await + .unwrap(); assert_eq!(result["total"], 1); assert_eq!(result["status"], "fulfilled"); } @@ -1156,14 +1267,18 @@ mod tests { "id": intention_id, "status": "complete" }); - execute(&storage, &test_cognitive(), Some(complete_args)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(complete_args)) + .await + .unwrap(); // List all let list_args = serde_json::json!({ "action": "list", "filter_status": "all" }); - let result = execute(&storage, &test_cognitive(), Some(list_args)).await.unwrap(); + let result = execute(&storage, &test_cognitive(), Some(list_args)) + .await + .unwrap(); assert_eq!(result["total"], 2); } @@ -1180,7 +1295,9 @@ mod tests { // 2. Verify it appears in list let list_args = serde_json::json!({ "action": "list" }); - let list_result = execute(&storage, &test_cognitive(), Some(list_args)).await.unwrap(); + let list_result = execute(&storage, &test_cognitive(), Some(list_args)) + .await + .unwrap(); assert_eq!(list_result["total"], 1); // 3. Snooze it @@ -1204,7 +1321,9 @@ mod tests { // 5. Verify it's no longer active let final_list_args = serde_json::json!({ "action": "list" }); - let final_list = execute(&storage, &test_cognitive(), Some(final_list_args)).await.unwrap(); + let final_list = execute(&storage, &test_cognitive(), Some(final_list_args)) + .await + .unwrap(); assert_eq!(final_list["total"], 0); // 6. Verify it's in fulfilled list @@ -1212,7 +1331,9 @@ mod tests { "action": "list", "filter_status": "fulfilled" }); - let fulfilled_list = execute(&storage, &test_cognitive(), Some(fulfilled_args)).await.unwrap(); + let fulfilled_list = execute(&storage, &test_cognitive(), Some(fulfilled_args)) + .await + .unwrap(); assert_eq!(fulfilled_list["total"], 1); } @@ -1226,25 +1347,33 @@ mod tests { "description": "Low priority task", "priority": "low" }); - execute(&storage, &test_cognitive(), Some(args_low)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(args_low)) + .await + .unwrap(); let args_critical = serde_json::json!({ "action": "set", "description": "Critical task", "priority": "critical" }); - execute(&storage, &test_cognitive(), Some(args_critical)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(args_critical)) + .await + .unwrap(); let args_normal = serde_json::json!({ "action": "set", "description": "Normal task", "priority": "normal" }); - execute(&storage, &test_cognitive(), Some(args_normal)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(args_normal)) + .await + .unwrap(); // List and verify ordering (critical should be first due to priority DESC ordering) let list_args = serde_json::json!({ "action": "list" }); - let list_result = execute(&storage, &test_cognitive(), Some(list_args)).await.unwrap(); + let list_result = execute(&storage, &test_cognitive(), Some(list_args)) + .await + .unwrap(); let intentions = list_result["intentions"].as_array().unwrap(); assert!(intentions.len() >= 3); @@ -1262,10 +1391,12 @@ mod tests { let schema_value = schema(); assert_eq!(schema_value["type"], "object"); assert!(schema_value["properties"]["action"].is_object()); - assert!(schema_value["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("action"))); + assert!( + schema_value["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("action")) + ); } #[test] @@ -1310,4 +1441,102 @@ mod tests { assert!(schema_value["properties"]["filter_status"].is_object()); assert!(schema_value["properties"]["limit"].is_object()); } + + // ======================================================================== + // v2.0.7 REGRESSION COVERAGE — include_snoozed actually wires through + // ======================================================================== + + /// `include_snoozed=true` must fold snoozed intentions back into the + /// check pool so their triggers can still fire. Before v2.0.7 the flag + /// was schema-advertised but runtime-ignored. + #[tokio::test] + async fn test_check_includes_snoozed_when_flag_set() { + let (storage, _dir) = test_storage().await; + + // Create an intention, then snooze it. + let id = create_test_intention(&storage, "snoozed test intention").await; + let snooze_args = serde_json::json!({ + "action": "update", + "id": id, + "status": "snooze", + "snooze_minutes": 1 + }); + execute(&storage, &test_cognitive(), Some(snooze_args)) + .await + .unwrap(); + + // Check with include_snoozed=true; snoozed intention should appear + // in either triggered or pending. + let check_args = serde_json::json!({ + "action": "check", + "include_snoozed": true + }); + let result = execute(&storage, &test_cognitive(), Some(check_args)) + .await + .unwrap(); + let triggered = result["triggered"].as_array().unwrap(); + let pending = result["pending"].as_array().unwrap(); + let appears_anywhere = triggered + .iter() + .chain(pending.iter()) + .any(|v| v["id"].as_str() == Some(id.as_str())); + assert!( + appears_anywhere, + "snoozed intention should be visible when include_snoozed=true" + ); + } + + /// Default (include_snoozed omitted) must NOT surface snoozed intentions + /// — this preserves the pre-v2.0.7 behavior for every caller that + /// doesn't opt in. + #[tokio::test] + async fn test_check_excludes_snoozed_by_default() { + let (storage, _dir) = test_storage().await; + + let id = create_test_intention(&storage, "default-excluded snoozed intention").await; + let snooze_args = serde_json::json!({ + "action": "update", + "id": id, + "status": "snooze", + "snooze_minutes": 1 + }); + execute(&storage, &test_cognitive(), Some(snooze_args)) + .await + .unwrap(); + + // Default check — no include_snoozed in args. + let check_args = serde_json::json!({ "action": "check" }); + let result = execute(&storage, &test_cognitive(), Some(check_args)) + .await + .unwrap(); + let triggered = result["triggered"].as_array().unwrap(); + let pending = result["pending"].as_array().unwrap(); + let appears_anywhere = triggered + .iter() + .chain(pending.iter()) + .any(|v| v["id"].as_str() == Some(id.as_str())); + assert!( + !appears_anywhere, + "snoozed intention must NOT surface without include_snoozed=true" + ); + } + + /// v2.0.7 also adds a `status` field to each check-result item so + /// callers can tell active-triggered from snoozed-overdue. Verify the + /// field is present and reflects the real storage state. + #[tokio::test] + async fn test_check_item_exposes_status_field() { + let (storage, _dir) = test_storage().await; + let _id = create_test_intention(&storage, "status-field test").await; + let check_args = serde_json::json!({ "action": "check" }); + let result = execute(&storage, &test_cognitive(), Some(check_args)) + .await + .unwrap(); + let pending = result["pending"].as_array().unwrap(); + assert!(!pending.is_empty(), "setup should produce one pending item"); + assert_eq!( + pending[0]["status"], "active", + "freshly-created intention must report status=\"active\"" + ); + } } diff --git a/crates/vestige-mcp/src/tools/intentions.rs b/crates/vestige-mcp/src/tools/intentions.rs deleted file mode 100644 index 8060125..0000000 --- a/crates/vestige-mcp/src/tools/intentions.rs +++ /dev/null @@ -1,1050 +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, Utc, Duration}; -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 1171d76..0000000 --- a/crates/vestige-mcp/src/tools/knowledge.rs +++ /dev/null @@ -1,112 +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 9764dce..892cf0a 100644 --- a/crates/vestige-mcp/src/tools/maintenance.rs +++ b/crates/vestige-mcp/src/tools/maintenance.rs @@ -10,22 +10,12 @@ use std::sync::Arc; use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; -use vestige_core::advanced::compression::MemoryForCompression; -use vestige_core::{FSRSScheduler, MemoryLifecycle, MemoryState, Storage}; +use vestige_core::{FSRSScheduler, Storage}; // ============================================================================ // SCHEMAS // ============================================================================ -/// Deprecated in v1.7 — use system_status_schema() instead -#[allow(dead_code)] -pub fn health_check_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {} - }) -} - pub fn consolidate_schema() -> Value { serde_json::json!({ "type": "object", @@ -33,15 +23,6 @@ pub fn consolidate_schema() -> Value { }) } -/// Deprecated in v1.7 — use system_status_schema() instead -#[allow(dead_code)] -pub fn stats_schema() -> Value { - serde_json::json!({ - "type": "object", - "properties": {} - }) -} - pub fn backup_schema() -> Value { serde_json::json!({ "type": "object", @@ -159,7 +140,8 @@ pub async fn execute_system_status( let mut recommendations = Vec::new(); if status == "critical" { - recommendations.push("CRITICAL: Many memories have very low retention. Review important memories."); + recommendations + .push("CRITICAL: Many memories have very low retention. Review important memories."); } if stats.nodes_due_for_review > 5 { recommendations.push("Review due memories to strengthen retention."); @@ -253,7 +235,6 @@ pub async fn execute_system_status( }; let last_backup = Storage::get_last_backup_timestamp(); - Ok(serde_json::json!({ "tool": "system_status", // Health @@ -294,76 +275,6 @@ pub async fn execute_system_status( })) } -/// Health check tool — deprecated in v1.7, use execute_system_status() instead -#[allow(dead_code)] -pub async fn execute_health_check( - storage: &Arc, - _args: Option, -) -> Result { - let stats = storage.get_stats().map_err(|e| e.to_string())?; - - 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 embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 - } else { - 0.0 - }; - - let embedding_ready = storage.is_embedding_ready(); - - let mut warnings = Vec::new(); - if stats.average_retention < 0.5 && stats.total_nodes > 0 { - warnings.push("Low average retention - consider running consolidation"); - } - 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 embedding_coverage < 50.0 && stats.total_nodes > 10 { - warnings.push("Low embedding coverage - run consolidate to improve semantic search"); - } - - let mut recommendations = Vec::new(); - if status == "critical" { - recommendations.push("CRITICAL: Many memories have very low retention. Review important memories."); - } - 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.total_nodes > 100 && stats.average_retention < 0.7 { - recommendations.push("Consider running periodic consolidation."); - } - if status == "healthy" && recommendations.is_empty() { - recommendations.push("Memory system is healthy!"); - } - - Ok(serde_json::json!({ - "tool": "health_check", - "status": status, - "totalMemories": stats.total_nodes, - "dueForReview": stats.nodes_due_for_review, - "averageRetention": stats.average_retention, - "embeddingCoverage": format!("{:.1}%", embedding_coverage), - "embeddingReady": embedding_ready, - "warnings": warnings, - "recommendations": recommendations, - })) -} - /// Consolidate tool pub async fn execute_consolidate( storage: &Arc, @@ -385,195 +296,14 @@ pub async fn execute_consolidate( })) } -/// Stats tool — deprecated in v1.7, use execute_system_status() instead -#[allow(dead_code)] -pub async fn execute_stats( - storage: &Arc, - cognitive: &Arc>, - _args: Option, -) -> Result { - let stats = storage.get_stats().map_err(|e| e.to_string())?; - - // Compute state distribution from a sample of nodes - let nodes = storage.get_all_nodes(500, 0).map_err(|e| e.to_string())?; - let total = nodes.len(); - let (active, dormant, silent, unavailable) = if total > 0 { - let mut a = 0usize; - let mut d = 0usize; - let mut s = 0usize; - let mut u = 0usize; - for node in &nodes { - let accessibility = node.retention_strength * 0.5 - + node.retrieval_strength * 0.3 - + node.storage_strength * 0.2; - if accessibility >= 0.7 { - a += 1; - } else if accessibility >= 0.4 { - d += 1; - } else if accessibility >= 0.1 { - s += 1; - } else { - u += 1; - } - } - (a, d, s, u) - } else { - (0, 0, 0, 0) - }; - - let embedding_coverage = if stats.total_nodes > 0 { - (stats.nodes_with_embeddings as f64 / stats.total_nodes as f64) * 100.0 - } else { - 0.0 - }; - - // ==================================================================== - // FSRS Preview: Show optimal intervals for a representative memory - // ==================================================================== - let scheduler = FSRSScheduler::default(); - let fsrs_preview = if let Some(representative) = nodes.first() { - let mut state = scheduler.new_card(); - state.difficulty = representative.difficulty; - state.stability = representative.stability; - state.reps = representative.reps; - state.lapses = representative.lapses; - state.last_review = representative.last_accessed; - let elapsed = scheduler.days_since_review(&state.last_review); - let preview = scheduler.preview_reviews(&state, elapsed); - Some(serde_json::json!({ - "representativeMemoryId": representative.id, - "elapsedDays": format!("{:.1}", elapsed), - "intervalIfGood": preview.good.interval, - "intervalIfEasy": preview.easy.interval, - "intervalIfHard": preview.hard.interval, - "currentRetrievability": format!("{:.3}", preview.good.retrievability), - })) - } else { - None - }; - - // ==================================================================== - // STATE SERVICE: Proper state transitions via Bjork model - // ==================================================================== - let state_distribution_precise = if let Ok(cog) = cognitive.try_lock() { - let mut lifecycles: Vec = nodes - .iter() - .take(100) // Sample 100 for performance - .map(|node| { - let mut lc = MemoryLifecycle::new(); - lc.last_access = node.last_accessed; - lc.access_count = node.reps as u32; - lc.state = if node.retention_strength > 0.7 { - MemoryState::Active - } else if node.retention_strength > 0.3 { - MemoryState::Dormant - } else if node.retention_strength > 0.1 { - MemoryState::Silent - } else { - MemoryState::Unavailable - }; - lc - }) - .collect(); - let batch_result = cog.state_service.batch_update(&mut lifecycles); - Some(serde_json::json!({ - "totalTransitions": batch_result.total_transitions, - "activeToDormant": batch_result.active_to_dormant, - "dormantToSilent": batch_result.dormant_to_silent, - "suppressionsResolved": batch_result.suppressions_resolved, - "sampled": lifecycles.len(), - })) - } else { - None - }; - - // ==================================================================== - // COMPRESSOR: Find compressible memory groups - // ==================================================================== - let compressible_groups = if let Ok(cog) = cognitive.try_lock() { - let memories_for_compression: Vec = nodes - .iter() - .filter(|n| n.retention_strength < 0.5) // Only consider low-retention memories - .take(50) // Cap for performance - .map(|n| MemoryForCompression { - id: n.id.clone(), - content: n.content.clone(), - tags: n.tags.clone(), - created_at: n.created_at, - last_accessed: Some(n.last_accessed), - embedding: None, - }) - .collect(); - if !memories_for_compression.is_empty() { - let groups = cog.compressor.find_compressible_groups(&memories_for_compression); - Some(serde_json::json!({ - "groupCount": groups.len(), - "totalCompressible": groups.iter().map(|g| g.len()).sum::(), - })) - } else { - None - } - } else { - None - }; - - // ==================================================================== - // COGNITIVE: Module health summary - // ==================================================================== - let cognitive_health = if let Ok(cog) = cognitive.try_lock() { - let activation_count = cog.activation_network.get_associations("_probe_").len(); - let prediction_accuracy = cog.predictive_memory.prediction_accuracy().unwrap_or(0.0); - let scheduler_stats = cog.consolidation_scheduler.get_activity_stats(); - Some(serde_json::json!({ - "activationNetworkSize": activation_count, - "predictionAccuracy": format!("{:.2}", prediction_accuracy), - "modulesActive": 28, - "schedulerStats": { - "totalEvents": scheduler_stats.total_events, - "eventsPerMinute": scheduler_stats.events_per_minute, - "isIdle": scheduler_stats.is_idle, - "timeUntilNextConsolidation": format!("{:?}", cog.consolidation_scheduler.time_until_next()), - }, - })) - } else { - None - }; - - Ok(serde_json::json!({ - "tool": "stats", - "totalMemories": stats.total_nodes, - "dueForReview": stats.nodes_due_for_review, - "averageRetention": stats.average_retention, - "averageStorageStrength": stats.average_storage_strength, - "averageRetrievalStrength": stats.average_retrieval_strength, - "withEmbeddings": stats.nodes_with_embeddings, - "embeddingCoverage": format!("{:.1}%", embedding_coverage), - "embeddingModel": stats.embedding_model, - "oldestMemory": stats.oldest_memory.map(|dt| dt.to_rfc3339()), - "newestMemory": stats.newest_memory.map(|dt| dt.to_rfc3339()), - "stateDistribution": { - "active": active, - "dormant": dormant, - "silent": silent, - "unavailable": unavailable, - "sampled": total, - }, - "fsrsPreview": fsrs_preview, - "cognitiveHealth": cognitive_health, - "stateTransitions": state_distribution_precise, - "compressibleMemories": compressible_groups, - })) -} - /// Backup tool -pub async fn execute_backup( - storage: &Arc, - _args: Option, -) -> Result { +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() + let backup_dir = vestige_dir + .data_dir() + .parent() .unwrap_or(vestige_dir.data_dir()) .join("backups"); @@ -585,7 +315,8 @@ pub async fn execute_backup( // Use VACUUM INTO for a consistent backup (handles WAL properly) { - storage.backup_to(&backup_path) + storage + .backup_to(&backup_path) .map_err(|e| format!("Failed to create backup: {}", e))?; } @@ -611,10 +342,7 @@ struct ExportArgs { } /// Export tool -pub async fn execute_export( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute_export(storage: &Arc, args: Option) -> Result { let args: ExportArgs = match args { Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, None => ExportArgs { @@ -627,7 +355,10 @@ pub async fn execute_export( let format = args.format.unwrap_or_else(|| "json".to_string()); if format != "json" && format != "jsonl" { - return Err(format!("Invalid format '{}'. Must be 'json' or 'jsonl'.", format)); + return Err(format!( + "Invalid format '{}'. Must be 'json' or 'jsonl'.", + format + )); } // Parse since date @@ -648,7 +379,9 @@ pub async fn execute_export( let max_nodes = 100_000; let mut offset = 0; loop { - let batch = storage.get_all_nodes(page_size, offset).map_err(|e| e.to_string())?; + let batch = storage + .get_all_nodes(page_size, offset) + .map_err(|e| e.to_string())?; let batch_len = batch.len(); all_nodes.extend(batch); if batch_len < page_size as usize || all_nodes.len() >= max_nodes { @@ -661,7 +394,10 @@ pub async fn execute_export( let filtered: Vec<&vestige_core::KnowledgeNode> = all_nodes .iter() .filter(|node| { - if since_date.as_ref().is_some_and(|since_dt| node.created_at < *since_dt) { + if since_date + .as_ref() + .is_some_and(|since_dt| node.created_at < *since_dt) + { return false; } if !tag_filter.is_empty() { @@ -678,7 +414,9 @@ pub async fn execute_export( // 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() + let export_dir = vestige_dir + .data_dir() + .parent() .unwrap_or(vestige_dir.data_dir()) .join("exports"); std::fs::create_dir_all(&export_dir) @@ -721,11 +459,24 @@ pub async fn execute_export( writer.write_all(b"\n").map_err(|e| e.to_string())?; } } - _ => unreachable!(), + // Defensive: the `format != "json" && format != "jsonl"` early-return + // above should already catch every unsupported format, but that gate is + // at the arg-validation layer. If it ever grows a bug (e.g. case + // sensitivity drift, a new branch, refactor) we return a clean error + // instead of `unreachable!()` — no panic can reach a user via the MCP + // dispatcher. + other => { + return Err(format!( + "unsupported export format: {:?}. Expected 'json' or 'jsonl'.", + other + )); + } } writer.flush().map_err(|e| e.to_string())?; - let file_size = std::fs::metadata(&export_path).map(|m| m.len()).unwrap_or(0); + let file_size = std::fs::metadata(&export_path) + .map(|m| m.len()) + .unwrap_or(0); Ok(serde_json::json!({ "tool": "export", @@ -746,10 +497,7 @@ struct GcArgs { } /// Garbage collection tool -pub async fn execute_gc( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute_gc(storage: &Arc, args: Option) -> Result { let args: GcArgs = match args { Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, None => GcArgs { @@ -771,7 +519,9 @@ pub async fn execute_gc( let max_nodes = 100_000; let mut offset = 0; loop { - let batch = storage.get_all_nodes(page_size, offset).map_err(|e| e.to_string())?; + let batch = storage + .get_all_nodes(page_size, offset) + .map_err(|e| e.to_string())?; let batch_len = batch.len(); all_nodes.extend(batch); if batch_len < page_size as usize || all_nodes.len() >= max_nodes { @@ -903,16 +653,18 @@ mod tests { async fn test_system_status_with_memories() { let (storage, _dir) = test_storage().await; { - storage.ingest(vestige_core::IngestInput { - content: "Test memory for status".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, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: "Test memory for status".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, + }) + .unwrap(); } let result = execute_system_status(&storage, &test_cognitive(), None).await; assert!(result.is_ok()); @@ -942,7 +694,10 @@ mod tests { assert!(triggers.is_object(), "automationTriggers should be present"); assert!(triggers["lastDreamTimestamp"].is_null(), "No dreams yet"); assert_eq!(triggers["savesSinceLastDream"], 0, "Empty DB = 0 saves"); - assert!(triggers["lastConsolidationTimestamp"].is_null(), "No consolidation yet"); + assert!( + triggers["lastConsolidationTimestamp"].is_null(), + "No consolidation yet" + ); // lastBackupTimestamp depends on filesystem state, just check it exists assert!(triggers.get("lastBackupTimestamp").is_some()); } @@ -952,16 +707,18 @@ mod tests { let (storage, _dir) = test_storage().await; { for i in 0..3 { - storage.ingest(vestige_core::IngestInput { - content: format!("Automation trigger test memory {}", i), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec![], - valid_from: None, - valid_until: None, - }).unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: format!("Automation trigger test memory {}", i), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec![], + valid_from: None, + valid_until: None, + }) + .unwrap(); } } let result = execute_system_status(&storage, &test_cognitive(), None).await; diff --git a/crates/vestige-mcp/src/tools/memory_states.rs b/crates/vestige-mcp/src/tools/memory_states.rs index 92d3c76..dae2583 100644 --- a/crates/vestige-mcp/src/tools/memory_states.rs +++ b/crates/vestige-mcp/src/tools/memory_states.rs @@ -75,19 +75,14 @@ pub fn stats_schema() -> Value { } /// Get the cognitive state of a specific memory -pub async fn execute_get( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute_get(storage: &Arc, args: Option) -> Result { let args = args.ok_or("Missing arguments")?; - let memory_id = args["memory_id"] - .as_str() - .ok_or("memory_id is required")?; - + let memory_id = args["memory_id"].as_str().ok_or("memory_id is required")?; // Get the memory - let memory = storage.get_node(memory_id) + let memory = storage + .get_node(memory_id) .map_err(|e| format!("Error: {}", e))? .ok_or("Memory not found")?; @@ -128,19 +123,14 @@ pub async fn execute_get( } /// List memories by state -pub async fn execute_list( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute_list(storage: &Arc, args: Option) -> Result { let args = args.unwrap_or(serde_json::json!({})); let state_filter = args["state"].as_str(); let limit = args["limit"].as_i64().unwrap_or(20) as usize; - // Get all memories - let memories = storage.get_all_nodes(500, 0) - .map_err(|e| e.to_string())?; + let memories = storage.get_all_nodes(500, 0).map_err(|e| e.to_string())?; // Categorize by state let mut active = Vec::new(); @@ -199,19 +189,15 @@ pub async fn execute_list( "dormant": { "count": dormant.len(), "memories": dormant.into_iter().take(limit).collect::>() }, "silent": { "count": silent.len(), "memories": silent.into_iter().take(limit).collect::>() }, "unavailable": { "count": unavailable.len(), "memories": unavailable.into_iter().take(limit).collect::>() } - }) + }), }; Ok(result) } /// Get memory state statistics -pub async fn execute_stats( - storage: &Arc, -) -> Result { - - let memories = storage.get_all_nodes(1000, 0) - .map_err(|e| e.to_string())?; +pub async fn execute_stats(storage: &Arc) -> Result { + let memories = storage.get_all_nodes(1000, 0).map_err(|e| e.to_string())?; let total = memories.len(); let mut active_count = 0; @@ -237,7 +223,11 @@ pub async fn execute_stats( } } - let avg_accessibility = if total > 0 { total_accessibility / total as f64 } else { 0.0 }; + let avg_accessibility = if total > 0 { + total_accessibility / total as f64 + } else { + 0.0 + }; Ok(serde_json::json!({ "totalMemories": total, diff --git a/crates/vestige-mcp/src/tools/memory_unified.rs b/crates/vestige-mcp/src/tools/memory_unified.rs index acd5240..2d73b6b 100644 --- a/crates/vestige-mcp/src/tools/memory_unified.rs +++ b/crates/vestige-mcp/src/tools/memory_unified.rs @@ -43,12 +43,17 @@ pub fn schema() -> Value { "properties": { "action": { "type": "string", - "enum": ["get", "delete", "state", "promote", "demote", "edit"], - "description": "Action to perform: 'get' retrieves full memory node, '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", "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)" }, "id": { "type": "string", - "description": "The ID of the memory node" + "description": "The ID of the memory node (for single-memory actions)" + }, + "ids": { + "type": "array", + "items": { "type": "string" }, + "description": "Array of memory IDs (for get_batch action). Max 20 IDs per call." }, "reason": { "type": "string", @@ -59,7 +64,7 @@ pub fn schema() -> Value { "description": "New content for edit action. Replaces existing content, regenerates embedding, preserves FSRS state." } }, - "required": ["action", "id"] + "required": ["action"] }) } @@ -67,7 +72,8 @@ pub fn schema() -> Value { #[serde(rename_all = "camelCase")] struct MemoryArgs { action: String, - id: String, + id: Option, + ids: Option>, reason: Option, content: Option, } @@ -83,18 +89,34 @@ pub async fn execute( None => return Err("Missing arguments".to_string()), }; - // Validate UUID format - uuid::Uuid::parse_str(&args.id).map_err(|_| "Invalid memory ID format".to_string())?; + // get_batch uses 'ids' array, all other actions use 'id' + if args.action == "get_batch" { + let ids = args.ids.ok_or("get_batch requires 'ids' array")?; + if ids.is_empty() { + return Err("ids array cannot be empty".to_string()); + } + if ids.len() > 20 { + return Err("get_batch supports max 20 IDs per call".to_string()); + } + for id in &ids { + uuid::Uuid::parse_str(id).map_err(|_| format!("Invalid memory ID format: {}", id))?; + } + return execute_get_batch(storage, &ids).await; + } + + // All other actions require 'id' + let id = args.id.ok_or("This action requires 'id' parameter")?; + uuid::Uuid::parse_str(&id).map_err(|_| "Invalid memory ID format".to_string())?; match args.action.as_str() { - "get" => execute_get(storage, &args.id).await, - "delete" => execute_delete(storage, &args.id).await, - "state" => execute_state(storage, &args.id).await, - "promote" => execute_promote(storage, cognitive, &args.id, args.reason).await, - "demote" => execute_demote(storage, cognitive, &args.id, args.reason).await, - "edit" => execute_edit(storage, &args.id, args.content).await, + "get" => execute_get(storage, &id).await, + "delete" => execute_delete(storage, &id).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, delete, state, promote, demote, edit", + "Invalid action '{}'. Must be one of: get, get_batch, delete, state, promote, demote, edit", args.action )), } @@ -140,6 +162,49 @@ async fn execute_get(storage: &Arc, id: &str) -> Result } } +/// Get multiple full memory nodes by ID (batch retrieval for expandable IDs) +async fn execute_get_batch(storage: &Arc, ids: &[String]) -> Result { + let mut results = Vec::with_capacity(ids.len()); + let mut found_count = 0; + + for id in ids { + match storage.get_node(id) { + Ok(Some(n)) => { + found_count += 1; + results.push(serde_json::json!({ + "id": n.id, + "content": n.content, + "nodeType": n.node_type, + "createdAt": n.created_at.to_rfc3339(), + "updatedAt": n.updated_at.to_rfc3339(), + "tags": n.tags, + "retentionStrength": n.retention_strength, + "source": n.source, + })); + } + Ok(None) => { + results.push(serde_json::json!({ + "id": id, + "found": false, + })); + } + Err(e) => { + results.push(serde_json::json!({ + "id": id, + "error": e.to_string(), + })); + } + } + } + + Ok(serde_json::json!({ + "action": "get_batch", + "requested": ids.len(), + "found": found_count, + "results": results, + })) +} + /// Delete a memory and return success status async fn execute_delete(storage: &Arc, id: &str) -> Result { let deleted = storage.delete_node(id).map_err(|e| e.to_string())?; @@ -154,7 +219,6 @@ async fn execute_delete(storage: &Arc, id: &str) -> Result, id: &str) -> Result { - // Get the memory let memory = storage .get_node(id) @@ -205,8 +269,9 @@ async fn execute_promote( id: &str, reason: Option, ) -> Result { - - let before = storage.get_node(id).map_err(|e| e.to_string())? + let before = storage + .get_node(id) + .map_err(|e| e.to_string())? .ok_or_else(|| format!("Node not found: {}", id))?; let node = storage.promote_memory(id).map_err(|e| e.to_string())?; @@ -260,15 +325,17 @@ async fn execute_demote( id: &str, reason: Option, ) -> Result { - - let before = storage.get_node(id).map_err(|e| e.to_string())? + let before = storage + .get_node(id) + .map_err(|e| e.to_string())? .ok_or_else(|| format!("Node not found: {}", id))?; let node = storage.demote_memory(id).map_err(|e| e.to_string())?; // Cognitive feedback pipeline if let Ok(mut cog) = cognitive.try_lock() { - cog.reward_signal.record_outcome(id, OutcomeType::NotHelpful); + cog.reward_signal + .record_outcome(id, OutcomeType::NotHelpful); cog.importance_tracker.on_retrieved(id, false); if cog.reconsolidation.is_labile(id) { cog.reconsolidation.apply_modification( @@ -364,22 +431,34 @@ mod tests { // Test Active state let accessibility = compute_accessibility(0.9, 0.8, 0.7); assert!(accessibility >= ACCESSIBILITY_ACTIVE); - assert!(matches!(state_from_accessibility(accessibility), MemoryState::Active)); + assert!(matches!( + state_from_accessibility(accessibility), + MemoryState::Active + )); // Test Dormant state let accessibility = compute_accessibility(0.5, 0.5, 0.5); - assert!(accessibility >= ACCESSIBILITY_DORMANT && accessibility < ACCESSIBILITY_ACTIVE); - assert!(matches!(state_from_accessibility(accessibility), MemoryState::Dormant)); + assert!((ACCESSIBILITY_DORMANT..ACCESSIBILITY_ACTIVE).contains(&accessibility)); + assert!(matches!( + state_from_accessibility(accessibility), + MemoryState::Dormant + )); // Test Silent state let accessibility = compute_accessibility(0.2, 0.2, 0.2); - assert!(accessibility >= ACCESSIBILITY_SILENT && accessibility < ACCESSIBILITY_DORMANT); - assert!(matches!(state_from_accessibility(accessibility), MemoryState::Silent)); + assert!((ACCESSIBILITY_SILENT..ACCESSIBILITY_DORMANT).contains(&accessibility)); + assert!(matches!( + state_from_accessibility(accessibility), + MemoryState::Silent + )); // Test Unavailable state let accessibility = compute_accessibility(0.05, 0.05, 0.05); assert!(accessibility < ACCESSIBILITY_SILENT); - assert!(matches!(state_from_accessibility(accessibility), MemoryState::Unavailable)); + assert!(matches!( + state_from_accessibility(accessibility), + MemoryState::Unavailable + )); } #[test] @@ -388,10 +467,12 @@ mod tests { assert!(schema["properties"]["action"].is_object()); assert!(schema["properties"]["id"].is_object()); assert!(schema["properties"]["reason"].is_object()); - assert_eq!(schema["required"], serde_json::json!(["action", "id"])); - // Verify all 6 actions are in enum + assert_eq!(schema["required"], serde_json::json!(["action"])); + assert!(schema["properties"]["ids"].is_object()); // get_batch support + // Verify all 7 actions are in enum let actions = schema["properties"]["action"]["enum"].as_array().unwrap(); - assert_eq!(actions.len(), 6); + assert_eq!(actions.len(), 7); + assert!(actions.contains(&serde_json::json!("get_batch"))); assert!(actions.contains(&serde_json::json!("edit"))); assert!(actions.contains(&serde_json::json!("promote"))); assert!(actions.contains(&serde_json::json!("demote"))); @@ -471,7 +552,8 @@ mod tests { #[tokio::test] async fn test_get_nonexistent_memory() { let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "action": "get", "id": "00000000-0000-0000-0000-000000000000" }); + let args = + serde_json::json!({ "action": "get", "id": "00000000-0000-0000-0000-000000000000" }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -495,13 +577,17 @@ mod tests { async fn test_delete_nonexistent_memory() { let (storage, _dir) = test_storage().await; // Ingest+delete a throwaway memory to warm writer after WAL migration - let warmup_id = storage.ingest(vestige_core::IngestInput { - content: "warmup".to_string(), - node_type: "fact".to_string(), - ..Default::default() - }).unwrap().id; + let warmup_id = storage + .ingest(vestige_core::IngestInput { + content: "warmup".to_string(), + node_type: "fact".to_string(), + ..Default::default() + }) + .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" }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_ok()); let value = result.unwrap(); @@ -514,7 +600,9 @@ mod tests { let (storage, _dir) = test_storage().await; let id = ingest_memory(&storage).await; let del_args = serde_json::json!({ "action": "delete", "id": id }); - execute(&storage, &test_cognitive(), Some(del_args)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(del_args)) + .await + .unwrap(); let get_args = serde_json::json!({ "action": "get", "id": id }); let result = execute(&storage, &test_cognitive(), Some(get_args)).await; let value = result.unwrap(); @@ -545,7 +633,8 @@ mod tests { #[tokio::test] async fn test_state_nonexistent_memory_fails() { let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "action": "state", "id": "00000000-0000-0000-0000-000000000000" }); + let args = + serde_json::json!({ "action": "state", "id": "00000000-0000-0000-0000-000000000000" }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("not found")); @@ -562,7 +651,10 @@ mod tests { fn test_accessibility_boundary_zero() { let a = compute_accessibility(0.0, 0.0, 0.0); assert_eq!(a, 0.0); - assert!(matches!(state_from_accessibility(a), MemoryState::Unavailable)); + assert!(matches!( + state_from_accessibility(a), + MemoryState::Unavailable + )); } // ======================================================================== @@ -641,7 +733,8 @@ mod tests { #[tokio::test] async fn test_demote_nonexistent_node_fails() { let (storage, _dir) = test_storage().await; - let args = serde_json::json!({ "action": "demote", "id": "00000000-0000-0000-0000-000000000000" }); + let args = + serde_json::json!({ "action": "demote", "id": "00000000-0000-0000-0000-000000000000" }); let result = execute(&storage, &test_cognitive(), Some(args)).await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Node not found")); @@ -694,9 +787,24 @@ mod tests { assert_eq!(value["success"], true); assert_eq!(value["action"], "edit"); assert_eq!(value["nodeId"], id); - assert!(value["oldContentPreview"].as_str().unwrap().contains("Memory unified test content")); - assert!(value["newContentPreview"].as_str().unwrap().contains("Updated memory content")); - assert!(value["note"].as_str().unwrap().contains("FSRS state preserved")); + assert!( + value["oldContentPreview"] + .as_str() + .unwrap() + .contains("Memory unified test content") + ); + assert!( + value["newContentPreview"] + .as_str() + .unwrap() + .contains("Updated memory content") + ); + assert!( + value["note"] + .as_str() + .unwrap() + .contains("FSRS state preserved") + ); } #[tokio::test] @@ -713,7 +821,9 @@ mod tests { "id": id, "content": "Completely new content after edit" }); - execute(&storage, &test_cognitive(), Some(args)).await.unwrap(); + execute(&storage, &test_cognitive(), Some(args)) + .await + .unwrap(); // Verify FSRS state preserved let after = storage.get_node(&id).unwrap().unwrap(); diff --git a/crates/vestige-mcp/src/tools/mod.rs b/crates/vestige-mcp/src/tools/mod.rs index 99357cc..b1331cf 100644 --- a/crates/vestige-mcp/src/tools/mod.rs +++ b/crates/vestige-mcp/src/tools/mod.rs @@ -34,38 +34,32 @@ pub mod restore; pub mod session_context; // v1.9: Autonomic tools -pub mod health; pub mod graph; +pub mod health; -// 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; +// v2.1: Cross-reference (connect the dots) +pub mod cross_reference; + +// v2.0.5: Active Forgetting — Anderson 2025 + Davis Rac1 +pub mod suppress; + +// 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 4ebf741..32a52b2 100644 --- a/crates/vestige-mcp/src/tools/predict.rs +++ b/crates/vestige-mcp/src/tools/predict.rs @@ -57,23 +57,72 @@ pub async fn execute( project_context: context .and_then(|c| c.get("codebase")) .and_then(|v| v.as_str()) - .map(|name| vestige_core::neuroscience::predictive_retrieval::ProjectContext { - name: name.to_string(), - path: String::new(), - technologies: Vec::new(), - primary_language: None, - }), + .map( + |name| vestige_core::neuroscience::predictive_retrieval::ProjectContext { + name: name.to_string(), + path: String::new(), + technologies: Vec::new(), + primary_language: None, + }, + ), }; - // Get predictions from predictive memory - let predictions = cog.predictive_memory.predict_needed_memories(&session_ctx) - .unwrap_or_default(); - let suggestions = cog.predictive_memory.get_proactive_suggestions(0.3) - .unwrap_or_default(); - let top_interests = cog.predictive_memory.get_top_interests(10) - .unwrap_or_default(); - let accuracy = cog.predictive_memory.prediction_accuracy() - .unwrap_or(0.0); + // Get predictions from predictive memory. + // + // Each of these four calls can fail (lock poisoning, internal PredictiveMemory + // errors). Before v2.0.7 the failures were silently swallowed by + // `unwrap_or_default()`, producing an empty response that was indistinguishable + // from a genuine cold-start "I don't have any predictions yet" state. That + // ambiguity is a user-visible bug: callers can't tell "model is degraded" + // from "nothing to predict." Now we track whether ANY call errored and expose + // it as `predict_degraded: true` in the response, with per-channel errors + // logged via `tracing::warn!` for observability. + let mut degraded = false; + let predictions = cog + .predictive_memory + .predict_needed_memories(&session_ctx) + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "predict_needed_memories failed; returning empty predictions" + ); + degraded = true; + Vec::new() + }); + let suggestions = cog + .predictive_memory + .get_proactive_suggestions(0.3) + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "get_proactive_suggestions failed; returning empty suggestions" + ); + degraded = true; + Vec::new() + }); + let top_interests = cog + .predictive_memory + .get_top_interests(10) + .unwrap_or_else(|e| { + tracing::warn!( + target: "vestige::predict", + error = %e, + "get_top_interests failed; returning empty interests" + ); + 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 + }); // Build speculative context let speculative_context = vestige_core::PredictionContext { @@ -91,7 +140,9 @@ pub async fn execute( .map(PathBuf::from), timestamp: Some(chrono::Utc::now()), }; - let speculative = cog.speculative_retriever.predict_needed(&speculative_context); + let speculative = cog + .speculative_retriever + .predict_needed(&speculative_context); Ok(serde_json::json!({ "predictions": predictions.iter().map(|p| serde_json::json!({ @@ -114,6 +165,13 @@ pub async fn execute( })).collect::>(), "top_interests": top_interests, "prediction_accuracy": accuracy, + // predict_degraded == true means at least one of the four underlying + // PredictiveMemory calls errored (lock poisoning, internal failure) + // and the corresponding field above was substituted with an empty + // default. Callers should treat an empty response as "genuinely nothing + // to predict" only when predict_degraded is false. See tracing logs + // under target "vestige::predict" for per-channel error details. + "predict_degraded": degraded, })) } @@ -202,4 +260,20 @@ mod tests { let accuracy = value["prediction_accuracy"].as_f64().unwrap(); assert!(accuracy >= 0.0); } + + /// The happy path must surface `predict_degraded: false`. A regression + /// that accidentally hard-coded `true` or dropped the field entirely + /// would break downstream callers that rely on the flag to distinguish + /// "no predictions because cold start" from "no predictions because + /// the system is broken." + #[tokio::test] + async fn test_predict_degraded_false_on_happy_path() { + let (storage, _dir) = test_storage().await; + let result = execute(&storage, &test_cognitive(), None).await; + let value = result.unwrap(); + assert_eq!( + value["predict_degraded"], false, + "fresh cognitive engine should not be degraded" + ); + } } diff --git a/crates/vestige-mcp/src/tools/recall.rs b/crates/vestige-mcp/src/tools/recall.rs deleted file mode 100644 index 23c4ac7..0000000 --- a/crates/vestige-mcp/src/tools/recall.rs +++ /dev/null @@ -1,400 +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 vestige_core::IngestInput; - 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 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 90ed1df..4d77252 100644 --- a/crates/vestige-mcp/src/tools/restore.rs +++ b/crates/vestige-mcp/src/tools/restore.rs @@ -8,7 +8,6 @@ use serde::Deserialize; use serde_json::Value; use std::sync::Arc; - use vestige_core::{IngestInput, Storage}; /// Input schema for restore tool @@ -51,10 +50,7 @@ struct MemoryBackup { source: Option, } -pub async fn execute( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute(storage: &Arc, args: Option) -> Result { let args: RestoreArgs = match args { Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, None => return Err("Missing arguments".to_string()), @@ -71,25 +67,26 @@ pub async fn execute( // 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))?; - recall.results - } else { - return Err("Empty backup file".to_string()); - } - } else if let Ok(recall) = serde_json::from_str::(&backup_content) { + 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 if let Ok(nodes) = serde_json::from_str::>(&backup_content) { - nodes } else { - return Err( - "Unrecognized backup format. Expected MCP wrapper, RecallResult, or array of memories." - .to_string(), - ); - }; + 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( + "Unrecognized backup format. Expected MCP wrapper, RecallResult, or array of memories." + .to_string(), + ); + }; let total = memories.len(); if total == 0 { @@ -108,7 +105,10 @@ pub async fn execute( for memory in &memories { let input = IngestInput { content: memory.content.clone(), - node_type: memory.node_type.clone().unwrap_or_else(|| "fact".to_string()), + node_type: memory + .node_type + .clone() + .unwrap_or_else(|| "fact".to_string()), source: memory.source.clone(), sentiment_score: 0.0, sentiment_magnitude: 0.0, @@ -157,10 +157,12 @@ mod tests { let s = schema(); assert_eq!(s["type"], "object"); assert!(s["properties"]["path"].is_object()); - assert!(s["required"] - .as_array() - .unwrap() - .contains(&serde_json::json!("path"))); + assert!( + s["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("path")) + ); } #[tokio::test] diff --git a/crates/vestige-mcp/src/tools/review.rs b/crates/vestige-mcp/src/tools/review.rs index 3f13835..f5a0025 100644 --- a/crates/vestige-mcp/src/tools/review.rs +++ b/crates/vestige-mcp/src/tools/review.rs @@ -6,7 +6,6 @@ use serde::Deserialize; use serde_json::Value; use std::sync::Arc; - use vestige_core::{Rating, Storage}; /// Input schema for mark_reviewed tool @@ -37,10 +36,7 @@ struct ReviewArgs { rating: Option, } -pub async fn execute( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute(storage: &Arc, args: Option) -> Result { let args: ReviewArgs = match args { Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, None => return Err("Missing arguments".to_string()), @@ -54,15 +50,18 @@ pub async fn execute( return Err("Rating must be between 1 and 4".to_string()); } - let rating = Rating::from_i32(rating_value) - .ok_or_else(|| "Invalid rating value".to_string())?; - + let rating = + Rating::from_i32(rating_value).ok_or_else(|| "Invalid rating value".to_string())?; // Get node before review for comparison - let before = storage.get_node(&args.id).map_err(|e| e.to_string())? + let before = storage + .get_node(&args.id) + .map_err(|e| e.to_string())? .ok_or_else(|| format!("Node not found: {}", args.id))?; - let node = storage.mark_reviewed(&args.id, rating).map_err(|e| e.to_string())?; + let node = storage + .mark_reviewed(&args.id, rating) + .map_err(|e| e.to_string())?; let rating_name = match rating { Rating::Again => "Again", @@ -97,8 +96,8 @@ pub async fn execute( #[cfg(test)] mod tests { use super::*; - use vestige_core::IngestInput; use tempfile::TempDir; + use vestige_core::IngestInput; /// Create a test storage instance with a temporary database async fn test_storage() -> (Arc, TempDir) { @@ -438,7 +437,12 @@ mod tests { let schema_value = schema(); assert_eq!(schema_value["type"], "object"); assert!(schema_value["properties"]["id"].is_object()); - assert!(schema_value["required"].as_array().unwrap().contains(&serde_json::json!("id"))); + assert!( + schema_value["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("id")) + ); } #[test] diff --git a/crates/vestige-mcp/src/tools/search.rs b/crates/vestige-mcp/src/tools/search.rs deleted file mode 100644 index 9235b64..0000000 --- a/crates/vestige-mcp/src/tools/search.rs +++ /dev/null @@ -1,189 +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 bf090cc..bfe419e 100644 --- a/crates/vestige-mcp/src/tools/search_unified.rs +++ b/crates/vestige-mcp/src/tools/search_unified.rs @@ -66,11 +66,27 @@ pub fn schema() -> Value { "items": { "type": "string" }, "description": "Optional topics for context-dependent retrieval boosting" }, + "exclude_types": { + "type": "array", + "items": { "type": "string" }, + "description": "Node types to exclude from results (e.g., ['reflection']). Reflections are excluded by default to prevent polluting factual queries." + }, + "include_types": { + "type": "array", + "items": { "type": "string" }, + "description": "If set, only return nodes of these types. Overrides exclude_types." + }, "token_budget": { "type": "integer", - "description": "Max tokens for response. Server truncates content to fit budget. Use memory(action='get') for full content of specific IDs.", + "description": "Max tokens for response. Server truncates content to fit budget. Use memory(action='get') for full content of specific IDs. With 1M context models, budgets up to 100K are practical.", "minimum": 100, - "maximum": 10000 + "maximum": 100000 + }, + "retrieval_mode": { + "type": "string", + "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" } }, "required": ["query"] @@ -82,13 +98,22 @@ pub fn schema() -> Value { struct SearchArgs { query: String, limit: Option, + #[serde(alias = "min_retention")] min_retention: Option, + #[serde(alias = "min_similarity")] min_similarity: Option, #[serde(alias = "detail_level")] detail_level: Option, + #[serde(alias = "context_topics")] context_topics: Option>, + #[serde(alias = "exclude_types")] + exclude_types: Option>, + #[serde(alias = "include_types")] + include_types: Option>, #[serde(alias = "token_budget")] token_budget: Option, + #[serde(alias = "retrieval_mode")] + retrieval_mode: Option, } /// Execute unified search with 7-stage cognitive pipeline. @@ -135,17 +160,76 @@ pub async fn execute( let min_retention = args.min_retention.unwrap_or(0.0).clamp(0.0, 1.0); let min_similarity = args.min_similarity.unwrap_or(0.5).clamp(0.0, 1.0); + // Validate retrieval_mode + let retrieval_mode = match args.retrieval_mode.as_deref() { + Some("precise") => "precise", + Some("exhaustive") => "exhaustive", + Some("balanced") | None => "balanced", + Some(invalid) => { + return Err(format!( + "Invalid retrieval_mode '{}'. Must be 'precise', 'balanced', or 'exhaustive'.", + invalid + )); + } + }; + // Favor semantic search — research shows 0.3/0.7 outperforms equal weights let keyword_weight = 0.3_f32; let semantic_weight = 0.7_f32; // ==================================================================== - // STAGE 1: Hybrid search with 3x over-fetch for reranking pool + // STAGE 0: Keyword-first search (dedicated keyword-only pass) // ==================================================================== - let overfetch_limit = (limit * 3).min(100); // Cap at 100 to avoid excessive DB load + // Run a small keyword-only search to guarantee strong keyword matches + // survive into the candidate pool, even with small limits/overfetch. + // Without this, exact keyword matches (e.g. unique proper nouns) get + // buried by semantic scoring in the hybrid search. + let keyword_first_limit = 10_i32; + let keyword_priority_threshold: f32 = 0.8; + + let keyword_first_results = storage + .hybrid_search_filtered( + &args.query, + keyword_first_limit, + 1.0, // keyword_weight = 1.0 (keyword-only) + 0.0, // semantic_weight = 0.0 + args.include_types.as_deref(), + args.exclude_types.as_deref(), + ) + .map_err(|e| e.to_string())?; + + // Collect keyword-priority results (keyword_score >= threshold) + let mut keyword_priority_ids: std::collections::HashSet = + std::collections::HashSet::new(); + let mut keyword_priority_results: Vec = Vec::new(); + for r in keyword_first_results { + if r.keyword_score.unwrap_or(0.0) >= keyword_priority_threshold + && r.node.retention_strength >= min_retention + { + keyword_priority_ids.insert(r.node.id.clone()); + keyword_priority_results.push(r); + } + } + + // ==================================================================== + // STAGE 1: Hybrid search with Nx over-fetch for reranking pool + // ==================================================================== + let overfetch_multiplier = match retrieval_mode { + "precise" => 1, // No overfetch — return exactly what's asked + "exhaustive" => 5, // Deep overfetch for maximum recall + _ => 3, // Balanced default + }; + let overfetch_limit = (limit * overfetch_multiplier).min(100); // Cap at 100 to avoid excessive DB load let results = storage - .hybrid_search(&args.query, overfetch_limit, keyword_weight, semantic_weight) + .hybrid_search_filtered( + &args.query, + overfetch_limit, + keyword_weight, + semantic_weight, + args.include_types.as_deref(), + args.exclude_types.as_deref(), + ) .map_err(|e| e.to_string())?; // Filter by min_retention and min_similarity first (cheap filters) @@ -164,25 +248,93 @@ pub async fn execute( }) .collect(); + // ==================================================================== + // Dedup: merge Stage 0 keyword-priority results into Stage 1 results + // ==================================================================== + for kp in &keyword_priority_results { + if let Some(existing) = filtered_results + .iter_mut() + .find(|r| r.node.id == kp.node.id) + { + // Preserve keyword_score from Stage 0 (keyword-only search is authoritative) + if kp.keyword_score.unwrap_or(0.0) > existing.keyword_score.unwrap_or(0.0) { + existing.keyword_score = kp.keyword_score; + } + if kp.combined_score > existing.combined_score { + existing.combined_score = kp.combined_score; + } + } else { + // New result from Stage 0 not in Stage 1 — add it + filtered_results.push(kp.clone()); + } + } + // ==================================================================== // STAGE 2: Reranker (BM25-like rescoring, trim to requested limit) // ==================================================================== - if let Ok(mut cog) = cognitive.try_lock() { - let candidates: Vec<_> = filtered_results - .iter() - .map(|r| (r.clone(), r.node.content.clone())) - .collect(); + // Keyword bypass: results with strong keyword matches (>= 0.8) skip the + // cross-encoder entirely and are placed above reranked results. This + // prevents the cross-encoder from burying exact/near-exact keyword hits + // (e.g. unique proper nouns) beneath semantically-similar but unrelated + // results. + { + let keyword_bypass_threshold: f32 = 0.8; + let limit_usize = limit as usize; - if let Ok(reranked) = cog.reranker.rerank(&args.query, candidates, Some(limit as usize)) { - // Replace filtered_results with reranked items (preserves original SearchResult) - filtered_results = reranked.into_iter().map(|rr| rr.item).collect(); - } else { - // Reranker failed — fall back to original order, just truncate - filtered_results.truncate(limit as usize); + // Partition: keyword bypass vs. candidates for reranking + let mut bypass_results: Vec = Vec::new(); + let mut rerank_candidates: Vec<(vestige_core::SearchResult, String)> = Vec::new(); + + for r in filtered_results.iter() { + if r.keyword_score.unwrap_or(0.0) >= keyword_bypass_threshold { + bypass_results.push(r.clone()); + } else { + rerank_candidates.push((r.clone(), r.node.content.clone())); + } } - } else { - // Couldn't acquire cognitive lock — truncate to limit - filtered_results.truncate(limit as usize); + + // Boost bypass results so they survive later pipeline stages + // (temporal, FSRS, utility, competition) and the final re-sort. + for r in bypass_results.iter_mut() { + r.combined_score *= 2.0; + } + + bypass_results.sort_by(|a, b| { + b.combined_score + .partial_cmp(&a.combined_score) + .unwrap_or(std::cmp::Ordering::Equal) + }); + + // Rerank the remaining candidates + let reranked_results: Vec = if rerank_candidates.is_empty() { + Vec::new() + } else if let Ok(mut cog) = cognitive.try_lock() { + if let Ok(reranked) = + cog.reranker + .rerank(&args.query, rerank_candidates, Some(limit_usize)) + { + reranked.into_iter().map(|rr| rr.item).collect() + } else { + // Reranker failed — fall back to original order for non-bypass candidates + filtered_results + .iter() + .filter(|r| r.keyword_score.unwrap_or(0.0) < keyword_bypass_threshold) + .cloned() + .collect() + } + } else { + // Couldn't acquire cognitive lock — use original order + filtered_results + .iter() + .filter(|r| r.keyword_score.unwrap_or(0.0) < keyword_bypass_threshold) + .cloned() + .collect() + }; + + // Merge: bypass first, then reranked, trim to limit + filtered_results = bypass_results; + filtered_results.extend(reranked_results); + filtered_results.truncate(limit_usize); } // ==================================================================== @@ -198,8 +350,8 @@ pub async fn execute( ); // Blend: 85% relevance + 15% temporal signal let temporal_factor = recency * validity; - result.combined_score = - result.combined_score * 0.85 + (result.combined_score * temporal_factor as f32) * 0.15; + result.combined_score = result.combined_score * 0.85 + + (result.combined_score * temporal_factor as f32) * 0.15; } } @@ -215,7 +367,7 @@ pub async fn execute( // Determine state from retention strength lifecycle.state = if result.node.retention_strength > 0.7 { MemoryState::Active - } else if result.node.retention_strength > 0.3 { + } else if result.node.retention_strength > 0.4 { MemoryState::Dormant } else if result.node.retention_strength > 0.1 { MemoryState::Silent @@ -223,9 +375,21 @@ pub async fn execute( MemoryState::Unavailable }; - let adjusted = cog + let mut adjusted = cog .accessibility_calc .calculate(&lifecycle, result.combined_score as f64); + + // v2.0.5: Active forgetting penalty (Anderson 2025 SIF). + // Each prior suppress call compounds a retrieval-score penalty, + // saturating at 80%. Applied AFTER accessibility so the penalty + // stacks on top of any passive FSRS decay. + if result.node.suppression_count > 0 { + let sys = + vestige_core::neuroscience::active_forgetting::ActiveForgettingSystem::new(); + let penalty = sys.retrieval_penalty(result.node.suppression_count); + adjusted *= 1.0 - penalty; + } + result.combined_score = adjusted as f32; } } @@ -236,14 +400,16 @@ pub async fn execute( if let Some(ref topics) = args.context_topics && !topics.is_empty() { - let retrieval_ctx = EncodingContext::new() - .with_topical(TopicalContext::with_topics(topics.clone())); + let retrieval_ctx = + EncodingContext::new().with_topical(TopicalContext::with_topics(topics.clone())); if let Ok(cog) = cognitive.try_lock() { for result in &mut filtered_results { // Build encoding context from memory's tags let encoding_ctx = EncodingContext::new() .with_topical(TopicalContext::with_topics(result.node.tags.clone())); - let context_score = cog.context_matcher.match_contexts(&encoding_ctx, &retrieval_ctx); + let context_score = cog + .context_matcher + .match_contexts(&encoding_ctx, &retrieval_ctx); // Blend: context match boosts relevance up to +30% result.combined_score *= 1.0 + (context_score as f32 * 0.3); } @@ -258,7 +424,9 @@ pub async fn execute( } else { EncodingContext::new() }; - let reinstatement = cog.context_matcher.reinstate_context(&first.node.id, ¤t_ctx); + let reinstatement = cog + .context_matcher + .reinstate_context(&first.node.id, ¤t_ctx); Some(serde_json::json!({ "memoryId": reinstatement.memory_id, "temporalHint": reinstatement.temporal_hint, @@ -275,9 +443,11 @@ pub async fn execute( // ==================================================================== // STAGE 5B: Retrieval competition (Anderson et al. 1994) + // Skipped in precise mode (no need) and exhaustive mode (want all results) // ==================================================================== let mut suppressed_count = 0_usize; - if filtered_results.len() > 1 + if retrieval_mode == "balanced" + && filtered_results.len() > 1 && let Ok(mut cog) = cognitive.try_lock() { let candidates: Vec = filtered_results @@ -291,7 +461,10 @@ pub async fn execute( if let Some(result) = cog.competition_mgr.run_competition(&candidates, 0.7) { // Apply suppression: losers get penalized for suppressed_id in &result.suppressed_ids { - if let Some(r) = filtered_results.iter_mut().find(|r| &r.node.id == suppressed_id) { + if let Some(r) = filtered_results + .iter_mut() + .find(|r| &r.node.id == suppressed_id) + { r.combined_score *= 0.85; // 15% suppression penalty suppressed_count += 1; } @@ -321,21 +494,31 @@ pub async fn execute( // ==================================================================== // STAGE 6: Spreading activation (find associated memories) + // Skipped in precise mode. Deeper (5 results) in exhaustive mode. // ==================================================================== - let associations: Vec = if let Ok(mut cog) = cognitive.try_lock() { - if let Some(first) = filtered_results.first() { - let activated = cog.activation_network.activate(&first.node.id, 1.0); - activated - .iter() - .take(3) - .map(|a| { - serde_json::json!({ - "memoryId": a.memory_id, - "activation": a.activation, - "distance": a.distance, + let activation_take = match retrieval_mode { + "precise" => 0, // Skip entirely + "exhaustive" => 5, // Deeper graph traversal + _ => 3, // Balanced default + }; + let associations: Vec = if activation_take > 0 { + if let Ok(mut cog) = cognitive.try_lock() { + if let Some(first) = filtered_results.first() { + let activated = cog.activation_network.activate(&first.node.id, 1.0); + activated + .iter() + .take(activation_take) + .map(|a| { + serde_json::json!({ + "memoryId": a.memory_id, + "activation": a.activation, + "distance": a.distance, + }) }) - }) - .collect() + .collect() + } else { + vec![] + } } else { vec![] } @@ -346,7 +529,10 @@ pub async fn execute( // ==================================================================== // Auto-strengthen on access (Testing Effect) // ==================================================================== - let ids: Vec<&str> = filtered_results.iter().map(|r| r.node.id.as_str()).collect(); + let ids: Vec<&str> = filtered_results + .iter() + .map(|r| r.node.id.as_str()) + .collect(); let _ = storage.strengthen_batch_on_access(&ids); // Drop storage lock before acquiring cognitive for side effects @@ -368,9 +554,9 @@ pub async fn execute( cog.speculative_retriever.record_access( &result.node.id, - None, // file_context - Some(args.query.as_str()), // query_context - None, // was_helpful (unknown yet) + None, // file_context + Some(args.query.as_str()), // query_context + None, // was_helpful (unknown yet) ); // 7C. Mark labile for reconsolidation window (5 min) @@ -401,7 +587,7 @@ pub async fn execute( 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, 10000) as usize; + let budget = budget.clamp(100, 100000) as usize; let budget_chars = budget * 4; let mut used = 0; let mut budgeted = Vec::new(); @@ -423,16 +609,28 @@ pub async fn execute( } // Check learning mode via attention signal - let learning_mode = cognitive.try_lock().ok().map(|cog| cog.attention_signal.is_learning_mode()).unwrap_or(false); + let learning_mode = cognitive + .try_lock() + .ok() + .map(|cog| cog.attention_signal.is_learning_mode()) + .unwrap_or(false); let mut response = serde_json::json!({ "query": args.query, "method": "hybrid+cognitive", + "retrievalMode": retrieval_mode, "detailLevel": detail_level, "total": formatted.len(), "results": formatted, }); + // Helpful hint when no results found + if formatted.is_empty() { + response["hint"] = serde_json::json!( + "No memories found. Use smart_ingest to add memories, or try a broader query." + ); + } + // Include associations if any were found if !associations.is_empty() { response["associations"] = serde_json::json!(associations); @@ -499,7 +697,7 @@ fn format_search_result(r: &vestige_core::SearchResult, detail_level: &str) -> V "validUntil": r.node.valid_until.map(|dt| dt.to_rfc3339()), "matchType": format!("{:?}", r.match_type), }), - // "summary" (default) — backwards compatible + // "summary" (default) — includes dates so AI never has to guess when a memory is from _ => serde_json::json!({ "id": r.node.id, "content": r.node.content, @@ -509,6 +707,8 @@ fn format_search_result(r: &vestige_core::SearchResult, detail_level: &str) -> V "nodeType": r.node.node_type, "tags": r.node.tags, "retentionStrength": r.node.retention_strength, + "createdAt": r.node.created_at.to_rfc3339(), + "updatedAt": r.node.updated_at.to_rfc3339(), }), } } @@ -873,10 +1073,12 @@ mod tests { 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"))); + assert!( + schema_value["required"] + .as_array() + .unwrap() + .contains(&serde_json::json!("query")) + ); } #[test] @@ -1004,10 +1206,17 @@ mod tests { let results = value["results"].as_array().unwrap(); if !results.is_empty() { let first = &results[0]; - // Summary should have content but not timestamps + // Summary should have content AND timestamps (v2.1: dates always visible) assert!(first["content"].is_string()); assert!(first["id"].is_string()); - assert!(first.get("createdAt").is_none() || first["createdAt"].is_null()); + assert!( + first["createdAt"].is_string(), + "summary must include createdAt" + ); + assert!( + first["updatedAt"].is_string(), + "summary must include updatedAt" + ); } } @@ -1033,7 +1242,10 @@ mod tests { for i in 0..10 { ingest_test_content( &storage, - &format!("Budget test content number {} with some extra text to increase size.", i), + &format!( + "Budget test content number {} with some extra text to increase size.", + i + ), ) .await; } @@ -1106,6 +1318,6 @@ mod tests { let tb = &schema_value["properties"]["token_budget"]; assert!(tb.is_object()); assert_eq!(tb["minimum"], 100); - assert_eq!(tb["maximum"], 10000); + assert_eq!(tb["maximum"], 100000); } } diff --git a/crates/vestige-mcp/src/tools/session_context.rs b/crates/vestige-mcp/src/tools/session_context.rs index ada68aa..e7414eb 100644 --- a/crates/vestige-mcp/src/tools/session_context.rs +++ b/crates/vestige-mcp/src/tools/session_context.rs @@ -27,10 +27,10 @@ pub fn schema() -> Value { }, "token_budget": { "type": "integer", - "description": "Max tokens for response (default: 1000). Server truncates content to fit budget.", + "description": "Max tokens for response (default: 1000). Server truncates content to fit budget. With 1M context models, budgets up to 100K are practical.", "default": 1000, "minimum": 100, - "maximum": 10000 + "maximum": 100000 }, "context": { "type": "object", @@ -105,12 +105,14 @@ pub async fn execute( None => SessionContextArgs::default(), }; - let token_budget = args.token_budget.unwrap_or(1000).clamp(100, 10000) as usize; + let token_budget = args.token_budget.unwrap_or(1000).clamp(100, 100000) as usize; let budget_chars = token_budget * 4; let include_status = args.include_status.unwrap_or(true); let include_intentions = args.include_intentions.unwrap_or(true); let include_predictions = args.include_predictions.unwrap_or(true); - let queries = args.queries.unwrap_or_else(|| vec!["user preferences".to_string()]); + let queries = args + .queries + .unwrap_or_else(|| vec!["user preferences".to_string()]); let mut context_parts: Vec = Vec::new(); let mut expandable_ids: Vec = Vec::new(); @@ -132,7 +134,8 @@ pub async fn execute( continue; } let summary = first_sentence(&r.node.content); - let line = format!("- {}", summary); + let date_str = r.node.updated_at.format("%b %d, %Y").to_string(); + let line = format!("- ({}) {}", date_str, summary); let line_len = line.len() + 1; // +1 for newline if char_count + line_len > budget_chars { @@ -274,34 +277,33 @@ pub async fn execute( if include_predictions { let cog = cognitive.lock().await; - let session_ctx = vestige_core::neuroscience::predictive_retrieval::SessionContext { - started_at: Utc::now(), - current_focus: args - .context - .as_ref() - .and_then(|c| c.topics.as_ref()) - .and_then(|t| t.first()) - .cloned(), - active_files: args - .context - .as_ref() - .and_then(|c| c.file.as_ref()) - .map(|f| vec![f.clone()]) - .unwrap_or_default(), - accessed_memories: Vec::new(), - recent_queries: Vec::new(), - detected_intent: None, - project_context: args - .context - .as_ref() - .and_then(|c| c.codebase.as_ref()) - .map(|name| vestige_core::neuroscience::predictive_retrieval::ProjectContext { - name: name.to_string(), - path: String::new(), - technologies: Vec::new(), - primary_language: None, - }), - }; + let session_ctx = + vestige_core::neuroscience::predictive_retrieval::SessionContext { + started_at: Utc::now(), + current_focus: args + .context + .as_ref() + .and_then(|c| c.topics.as_ref()) + .and_then(|t| t.first()) + .cloned(), + active_files: args + .context + .as_ref() + .and_then(|c| c.file.as_ref()) + .map(|f| vec![f.clone()]) + .unwrap_or_default(), + accessed_memories: Vec::new(), + recent_queries: Vec::new(), + detected_intent: None, + project_context: args.context.as_ref().and_then(|c| c.codebase.as_ref()).map( + |name| vestige_core::neuroscience::predictive_retrieval::ProjectContext { + name: name.to_string(), + path: String::new(), + technologies: Vec::new(), + primary_language: None, + }, + ), + }; let predictions = cog .predictive_memory @@ -334,41 +336,45 @@ pub async fn execute( // 5. Codebase patterns/decisions (if codebase specified) // ==================================================================== if let Some(ref ctx) = args.context - && let Some(ref codebase) = ctx.codebase { - let codebase_tag = format!("codebase:{}", codebase); - let mut cb_lines: Vec = Vec::new(); + && let Some(ref codebase) = ctx.codebase + { + let codebase_tag = format!("codebase:{}", codebase); + let mut cb_lines: Vec = Vec::new(); - // Get patterns - if let Ok(patterns) = storage.get_nodes_by_type_and_tag("pattern", Some(&codebase_tag), 3) { - for p in &patterns { - let line = format!("- [pattern] {}", first_sentence(&p.content)); - let line_len = line.len() + 1; - if char_count + line_len <= budget_chars { - cb_lines.push(line); - char_count += line_len; - } + // Get patterns + if let Ok(patterns) = storage.get_nodes_by_type_and_tag("pattern", Some(&codebase_tag), 3) { + for p in &patterns { + let line = format!("- [pattern] {}", first_sentence(&p.content)); + let line_len = line.len() + 1; + if char_count + line_len <= budget_chars { + cb_lines.push(line); + char_count += line_len; } } - - // Get decisions - if let Ok(decisions) = - storage.get_nodes_by_type_and_tag("decision", Some(&codebase_tag), 3) - { - for d in &decisions { - let line = format!("- [decision] {}", first_sentence(&d.content)); - let line_len = line.len() + 1; - if char_count + line_len <= budget_chars { - cb_lines.push(line); - char_count += line_len; - } - } - } - - if !cb_lines.is_empty() { - context_parts.push(format!("**Codebase ({}):**\n{}", codebase, cb_lines.join("\n"))); - } } + // Get decisions + if let Ok(decisions) = storage.get_nodes_by_type_and_tag("decision", Some(&codebase_tag), 3) + { + for d in &decisions { + let line = format!("- [decision] {}", first_sentence(&d.content)); + let line_len = line.len() + 1; + if char_count + line_len <= budget_chars { + cb_lines.push(line); + char_count += line_len; + } + } + } + + if !cb_lines.is_empty() { + context_parts.push(format!( + "**Codebase ({}):**\n{}", + codebase, + cb_lines.join("\n") + )); + } + } + // ==================================================================== // 6. Assemble final response // ==================================================================== @@ -404,9 +410,10 @@ fn check_intention_triggered( match trigger.trigger_type.as_deref() { Some("time") => { if let Some(ref at) = trigger.at - && let Ok(trigger_time) = DateTime::parse_from_rfc3339(at) { - return trigger_time.with_timezone(&Utc) <= now; - } + && let Ok(trigger_time) = DateTime::parse_from_rfc3339(at) + { + return trigger_time.with_timezone(&Utc) <= now; + } if let Some(mins) = trigger.in_minutes { let trigger_time = intention.created_at + Duration::minutes(mins); return trigger_time <= now; @@ -419,22 +426,23 @@ fn check_intention_triggered( && current_cb .to_lowercase() .contains(&trigger_cb.to_lowercase()) - { - return true; - } + { + return true; + } // Check file pattern match if let (Some(pattern), Some(file)) = (&trigger.file_pattern, &ctx.file) - && file.contains(pattern.as_str()) { - return true; - } + && file.contains(pattern.as_str()) + { + return true; + } // Check topic match if let (Some(topic), Some(topics)) = (&trigger.topic, &ctx.topics) && topics .iter() .any(|t| t.to_lowercase().contains(&topic.to_lowercase())) - { - return true; - } + { + return true; + } false } _ => false, @@ -510,7 +518,7 @@ mod tests { let s = schema(); let tb = &s["properties"]["token_budget"]; assert_eq!(tb["minimum"], 100); - assert_eq!(tb["maximum"], 10000); + assert_eq!(tb["maximum"], 100000); assert_eq!(tb["default"], 1000); } @@ -536,7 +544,12 @@ mod tests { #[tokio::test] async fn test_with_queries() { let (storage, _dir) = test_storage().await; - ingest_test_content(&storage, "Sam prefers Rust and TypeScript for all projects.", vec![]).await; + ingest_test_content( + &storage, + "Sam prefers Rust and TypeScript for all projects.", + vec![], + ) + .await; let args = serde_json::json!({ "queries": ["Sam preferences", "project context"] @@ -573,12 +586,16 @@ mod tests { assert!(result.is_ok()); let value = result.unwrap(); - let ctx = value["context"].as_str().unwrap(); + assert!(value["context"].is_string()); // Context should be within budget (200 tokens * 4 = 800 chars + header overhead) // The actual char count of context should be reasonable let tokens_used = value["tokensUsed"].as_u64().unwrap(); // Allow some overhead for the header - assert!(tokens_used <= 300, "tokens_used {} should be near budget 200", tokens_used); + assert!( + tokens_used <= 300, + "tokens_used {} should be near budget 200", + tokens_used + ); } #[tokio::test] @@ -648,7 +665,8 @@ mod tests { let (storage, _dir) = test_storage().await; // Ingest a pattern with codebase tag let input = IngestInput { - content: "Code pattern: Use Arc> for shared state in async contexts.".to_string(), + content: "Code pattern: Use Arc> for shared state in async contexts." + .to_string(), node_type: "pattern".to_string(), source: None, sentiment_score: 0.0, @@ -680,7 +698,10 @@ mod tests { #[test] fn test_first_sentence_period() { - assert_eq!(first_sentence("Hello world. More text here."), "Hello world."); + assert_eq!( + first_sentence("Hello world. More text here."), + "Hello world." + ); } #[test] diff --git a/crates/vestige-mcp/src/tools/smart_ingest.rs b/crates/vestige-mcp/src/tools/smart_ingest.rs index 900c99f..72dc3a6 100644 --- a/crates/vestige-mcp/src/tools/smart_ingest.rs +++ b/crates/vestige-mcp/src/tools/smart_ingest.rs @@ -22,7 +22,7 @@ use tokio::sync::Mutex; use crate::cognitive::CognitiveEngine; use vestige_core::{ - ContentType, ImportanceContext, ImportanceEventType, ImportanceEvent, IngestInput, Storage, + ContentType, ImportanceContext, ImportanceEvent, ImportanceEventType, IngestInput, Storage, }; /// Input schema for smart_ingest tool @@ -136,7 +136,9 @@ pub async fn execute( } // Single mode: content is required - let content = args.content.ok_or("Missing 'content' field. Provide 'content' for single mode or 'items' for batch mode.")?; + let content = args.content.ok_or( + "Missing 'content' field. Provide 'content' for single mode or 'items' for batch mode.", + )?; // Validate content if content.trim().is_empty() { @@ -156,7 +158,9 @@ pub async fn execute( if let Ok(cog) = cognitive.try_lock() { // 4A. Full 4-channel importance scoring let context = ImportanceContext::current(); - let importance = cog.importance_signals.compute_importance(&content, &context); + let importance = cog + .importance_signals + .compute_importance(&content, &context); importance_composite = importance.composite; // 4B. Intent detection → auto-tag @@ -201,7 +205,13 @@ pub async fn execute( let has_embedding = node.has_embedding.unwrap_or(false); // Post-ingest cognitive side effects - run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite); + run_post_ingest( + cognitive, + &node_id, + &node_content, + &node_type, + importance_composite, + ); return Ok(serde_json::json!({ "success": true, @@ -225,7 +235,13 @@ pub async fn execute( let has_embedding = result.node.has_embedding.unwrap_or(false); // Post-ingest cognitive side effects - run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite); + run_post_ingest( + cognitive, + &node_id, + &node_content, + &node_type, + importance_composite, + ); Ok(serde_json::json!({ "success": true, @@ -258,7 +274,13 @@ pub async fn execute( let node_content = node.content.clone(); let node_type = node.node_type.clone(); - run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite); + run_post_ingest( + cognitive, + &node_id, + &node_content, + &node_type, + importance_composite, + ); Ok(serde_json::json!({ "success": true, @@ -331,7 +353,9 @@ async fn execute_batch( if let Ok(cog) = cognitive.try_lock() { let context = ImportanceContext::current(); - let importance = cog.importance_signals.compute_importance(&item.content, &context); + let importance = cog + .importance_signals + .compute_importance(&item.content, &context); importance_composite = importance.composite; let intent_result = cog.intent_detector.detect_intent(); @@ -373,7 +397,13 @@ async fn execute_batch( let node_type = node.node_type.clone(); created += 1; - run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite); + run_post_ingest( + cognitive, + &node_id, + &node_content, + &node_type, + importance_composite, + ); results.push(serde_json::json!({ "index": i, @@ -411,7 +441,13 @@ async fn execute_batch( } // Post-ingest cognitive side effects - run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite); + run_post_ingest( + cognitive, + &node_id, + &node_content, + &node_type, + importance_composite, + ); results.push(serde_json::json!({ "index": i, @@ -443,7 +479,13 @@ async fn execute_batch( let node_type = node.node_type.clone(); created += 1; - run_post_ingest(cognitive, &node_id, &node_content, &node_type, importance_composite); + run_post_ingest( + cognitive, + &node_id, + &node_content, + &node_type, + importance_composite, + ); results.push(serde_json::json!({ "index": i, @@ -514,7 +556,8 @@ fn run_post_ingest( ); // 4G. Cross-project pattern recording - cog.cross_project.record_project_memory(node_id, "default", None); + cog.cross_project + .record_project_memory(node_id, "default", None); } } @@ -576,8 +619,13 @@ mod tests { let value = result.unwrap(); assert_eq!(value["success"], true); assert_eq!(value["decision"], "create"); - assert!(value["reason"].as_str().unwrap().contains("Forced") || - value["reason"].as_str().unwrap().contains("Embeddings not available")); + assert!( + value["reason"].as_str().unwrap().contains("Forced") + || value["reason"] + .as_str() + .unwrap() + .contains("Embeddings not available") + ); } #[test] @@ -729,7 +777,12 @@ mod tests { #[tokio::test] async fn test_batch_empty_items_fails() { let (storage, _dir) = test_storage().await; - let result = execute(&storage, &test_cognitive(), Some(serde_json::json!({ "items": [] }))).await; + let result = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ "items": [] })), + ) + .await; assert!(result.is_err()); assert!(result.unwrap_err().contains("empty")); } @@ -738,14 +791,16 @@ mod tests { async fn test_batch_ingest() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [ { "content": "First batch item", "tags": ["test"] }, { "content": "Second batch item", "tags": ["test"] } ] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["mode"], "batch"); @@ -756,7 +811,8 @@ mod tests { async fn test_batch_skips_empty_content() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [ { "content": "Valid item" }, @@ -764,7 +820,8 @@ mod tests { { "content": "Another valid item" } ] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["summary"]["skipped"], 1); @@ -784,7 +841,12 @@ mod tests { let items: Vec = (0..21) .map(|i| serde_json::json!({ "content": format!("Item {}", i) })) .collect(); - let result = execute(&storage, &test_cognitive(), Some(serde_json::json!({ "items": items }))).await; + let result = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ "items": items })), + ) + .await; assert!(result.is_err()); assert!(result.unwrap_err().contains("Maximum 20 items")); } @@ -795,7 +857,12 @@ mod tests { let items: Vec = (0..20) .map(|i| serde_json::json!({ "content": format!("Item {}", i) })) .collect(); - let result = execute(&storage, &test_cognitive(), Some(serde_json::json!({ "items": items }))).await; + let result = execute( + &storage, + &test_cognitive(), + Some(serde_json::json!({ "items": items })), + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["summary"]["total"], 20); @@ -805,14 +872,16 @@ mod tests { async fn test_batch_skips_whitespace_only_content() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [ { "content": " \t\n " }, { "content": "Valid content" } ] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["summary"]["skipped"], 1); @@ -823,11 +892,13 @@ mod tests { async fn test_batch_single_item_succeeds() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [{ "content": "Single item" }] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["summary"]["total"], 1); @@ -838,7 +909,8 @@ mod tests { async fn test_batch_items_with_all_fields() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [{ "content": "Full fields item", @@ -847,7 +919,8 @@ mod tests { "source": "test-suite" }] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["summary"]["created"], 1); @@ -857,7 +930,8 @@ mod tests { async fn test_batch_results_array_matches_items() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [ { "content": "First" }, @@ -865,7 +939,8 @@ mod tests { { "content": "Third" } ] })), - ).await; + ) + .await; let value = result.unwrap(); let results = value["results"].as_array().unwrap(); assert_eq!(results.len(), 3); @@ -879,14 +954,16 @@ mod tests { async fn test_batch_success_true_when_only_skipped() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [ { "content": "" }, { "content": " " } ] })), - ).await; + ) + .await; let value = result.unwrap(); assert_eq!(value["success"], true); // skipped ≠ errors assert_eq!(value["summary"]["errors"], 0); @@ -897,11 +974,13 @@ mod tests { async fn test_batch_has_importance_scores() { let (storage, _dir) = test_storage().await; let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [{ "content": "Important batch memory content" }] })), - ).await; + ) + .await; let value = result.unwrap(); let results = value["results"].as_array().unwrap(); assert!(results[0]["importanceScore"].is_number()); @@ -912,7 +991,8 @@ mod tests { let (storage, _dir) = test_storage().await; // Three items with very similar content + global forceCreate let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "forceCreate": true, "items": [ @@ -921,7 +1001,8 @@ mod tests { { "content": "Physics question about quantum mechanics and wave behavior" } ] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); assert_eq!(value["mode"], "batch"); @@ -941,7 +1022,8 @@ mod tests { let (storage, _dir) = test_storage().await; // Mix of forced and non-forced items let result = execute( - &storage, &test_cognitive(), + &storage, + &test_cognitive(), Some(serde_json::json!({ "items": [ { "content": "Forced item one", "forceCreate": true }, @@ -949,7 +1031,8 @@ mod tests { { "content": "Forced item three", "forceCreate": true } ] })), - ).await; + ) + .await; assert!(result.is_ok()); let value = result.unwrap(); let results = value["results"].as_array().unwrap(); diff --git a/crates/vestige-mcp/src/tools/stats.rs b/crates/vestige-mcp/src/tools/stats.rs deleted file mode 100644 index aaf37fd..0000000 --- a/crates/vestige-mcp/src/tools/stats.rs +++ /dev/null @@ -1,120 +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/suppress.rs b/crates/vestige-mcp/src/tools/suppress.rs new file mode 100644 index 0000000..f06debc --- /dev/null +++ b/crates/vestige-mcp/src/tools/suppress.rs @@ -0,0 +1,313 @@ +//! `suppress` MCP Tool (v2.0.5) — Top-Down Active Forgetting +//! +//! Actively suppress a memory via top-down inhibitory control. Distinct from +//! `memory.delete` (which removes the row) and `memory.demote` (which is a +//! one-shot thumb-down). Each call compounds: suppression_count increments, +//! FSRS state is dealt a strong blow, and a background Rac1 cascade worker +//! (in the existing consolidation loop) will fade co-activated neighbors. +//! +//! Reversible within a 24-hour labile window via `reverse: true`. +//! +//! References: +//! - Anderson et al. (2025). Brain mechanisms underlying the inhibitory +//! control of thought. Nat Rev Neurosci. DOI 10.1038/s41583-025-00929-y +//! - Cervantes-Sandoval & Davis (2020). Rac1 Impairs Forgetting-Induced +//! Cellular Plasticity. Front Cell Neurosci. PMC7477079 + +use serde::{Deserialize, Serialize}; +use serde_json::{Value, json}; +use std::sync::Arc; + +use vestige_core::Storage; +use vestige_core::neuroscience::active_forgetting::{ActiveForgettingSystem, DEFAULT_LABILE_HOURS}; + +/// Input schema for the `suppress` tool. +pub fn schema() -> Value { + json!({ + "type": "object", + "description": "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 suppression strength. A background Rac1 worker cascades accelerated decay to co-activated neighbors over the next 72 hours. Reversible within 24 hours via reverse=true.", + "properties": { + "id": { + "type": "string", + "description": "Memory UUID to suppress (or reverse-suppress)" + }, + "reason": { + "type": "string", + "description": "Optional free-form note explaining why this memory is being suppressed. Logged for audit." + }, + "reverse": { + "type": "boolean", + "default": false, + "description": "If true, reverse a previous suppression. Only works within the 24-hour labile window." + } + }, + "required": ["id"] + }) +} + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +struct SuppressArgs { + id: String, + #[serde(default)] + reason: Option, + #[serde(default)] + reverse: bool, +} + +pub async fn execute(storage: &Arc, args: Option) -> Result { + let args: SuppressArgs = 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.id.trim().is_empty() { + return Err("'id' must not be empty".to_string()); + } + // Basic UUID sanity check — don't reject if missing, but warn + if uuid::Uuid::parse_str(&args.id).is_err() { + return Err(format!("Invalid memory ID format: {}", args.id)); + } + + let sys = ActiveForgettingSystem::new(); + + if args.reverse { + // Reverse path — only allowed within labile window. + match storage.reverse_suppression(&args.id, sys.labile_hours) { + Ok(node) => { + let still_suppressed = node.suppression_count > 0; + Ok(json!({ + "success": true, + "action": "reverse", + "id": args.id, + "suppressionCount": node.suppression_count, + "stillSuppressed": still_suppressed, + "retentionStrength": node.retention_strength, + "retrievalStrength": node.retrieval_strength, + "stability": node.stability, + "message": if still_suppressed { + format!( + "Reversal applied. {} suppression(s) remain on this memory.", + node.suppression_count + ) + } else { + "Suppression fully reversed. Memory is no longer inhibited.".to_string() + }, + })) + } + Err(e) => Err(format!("Reverse failed: {}", e)), + } + } else { + // Forward path — suppress + log reason + tell the user what will happen. + let before_count = storage + .get_node(&args.id) + .map_err(|e| format!("Failed to load memory: {}", e))? + .map(|n| n.suppression_count) + .unwrap_or(0); + + let node = storage + .suppress_memory(&args.id) + .map_err(|e| format!("Suppress failed: {}", e))?; + + // Count how many neighbors will be cascaded over the coming 72h. + // We don't run the cascade synchronously — it happens in the + // background consolidation loop via `run_rac1_cascade_sweep`. But we + // can give the user an estimate. + let edges = storage + .get_connections_for_memory(&args.id) + .unwrap_or_default(); + let estimated_cascade = edges.len().min(100); + + let reversible_until = node + .suppressed_at + .map(|t| sys.reversible_until(t)) + .unwrap_or_else(chrono::Utc::now); + let retrieval_penalty = sys.retrieval_penalty(node.suppression_count); + + tracing::info!( + id = %args.id, + count = node.suppression_count, + reason = args.reason.as_deref().unwrap_or(""), + "Memory suppressed" + ); + + Ok(json!({ + "success": true, + "action": "suppress", + "id": args.id, + "suppressionCount": node.suppression_count, + "priorCount": before_count, + "retrievalPenalty": retrieval_penalty, + "retentionStrength": node.retention_strength, + "retrievalStrength": node.retrieval_strength, + "stability": node.stability, + "estimatedCascadeNeighbors": estimated_cascade, + "reversibleUntil": reversible_until.to_rfc3339(), + "labileWindowHours": DEFAULT_LABILE_HOURS, + "reason": args.reason, + "message": format!( + "Actively forgetting. Suppression #{} applied. ~{} co-activated neighbors will fade over the next 72h via Rac1 cascade. Reversible for {}h.", + node.suppression_count, estimated_cascade, DEFAULT_LABILE_HOURS + ), + "citation": "Anderson et al. 2025, Nat Rev Neurosci, DOI: 10.1038/s41583-025-00929-y" + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + use vestige_core::IngestInput; + + 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) + } + + fn ingest(storage: &Storage, content: &str) -> String { + storage + .ingest(IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap() + .id + } + + #[test] + fn test_schema_is_valid() { + let s = schema(); + assert_eq!(s["type"], "object"); + assert!(s["properties"]["id"].is_object()); + assert!(s["properties"]["reverse"].is_object()); + assert_eq!(s["required"][0], "id"); + } + + #[tokio::test] + async fn test_suppress_missing_args() { + let (storage, _dir) = test_storage(); + let result = execute(&storage, None).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Missing arguments")); + } + + #[tokio::test] + async fn test_suppress_invalid_uuid() { + let (storage, _dir) = test_storage(); + let args = json!({"id": "not-a-uuid"}); + let result = execute(&storage, Some(args)).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Invalid memory ID")); + } + + #[tokio::test] + async fn test_suppress_increments_count() { + let (storage, _dir) = test_storage(); + let id = ingest(&storage, "Jake is my roommate"); + + // First call + let r1 = execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + assert_eq!(r1["suppressionCount"], 1); + assert_eq!(r1["priorCount"], 0); + + // Second call — compounds + let r2 = execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + assert_eq!(r2["suppressionCount"], 2); + assert_eq!(r2["priorCount"], 1); + } + + #[tokio::test] + async fn test_suppress_applies_fsrs_penalty() { + let (storage, _dir) = test_storage(); + let id = ingest(&storage, "Jake"); + + let before = storage.get_node(&id).unwrap().unwrap(); + let result = execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + + // Stability should be heavily reduced + let after_stability = result["stability"].as_f64().unwrap(); + assert!(after_stability < before.stability); + // Retention should be reduced + let after_retention = result["retentionStrength"].as_f64().unwrap(); + assert!(after_retention < before.retention_strength); + } + + #[tokio::test] + async fn test_suppress_is_not_delete() { + let (storage, _dir) = test_storage(); + let id = ingest(&storage, "Jake"); + + execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + + // Memory must still be retrievable via get_node + let node = storage.get_node(&id).unwrap(); + assert!(node.is_some(), "Suppressed memory must still exist"); + assert_eq!(node.unwrap().suppression_count, 1); + } + + #[tokio::test] + async fn test_reverse_within_window_decrements() { + let (storage, _dir) = test_storage(); + let id = ingest(&storage, "Jake"); + + execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + + // Now reverse — count should drop from 2 to 1 + let r = execute(&storage, Some(json!({"id": id.clone(), "reverse": true}))) + .await + .unwrap(); + assert_eq!(r["suppressionCount"], 1); + assert_eq!(r["stillSuppressed"], true); + + // Reverse again — should go to 0 + let r = execute(&storage, Some(json!({"id": id.clone(), "reverse": true}))) + .await + .unwrap(); + assert_eq!(r["suppressionCount"], 0); + assert_eq!(r["stillSuppressed"], false); + } + + #[tokio::test] + async fn test_reverse_without_prior_suppression_fails() { + let (storage, _dir) = test_storage(); + let id = ingest(&storage, "Fresh memory"); + + let result = execute(&storage, Some(json!({"id": id.clone(), "reverse": true}))).await; + assert!(result.is_err()); + assert!(result.unwrap_err().contains("no active suppression")); + } + + #[tokio::test] + async fn test_suppress_records_timestamp() { + let (storage, _dir) = test_storage(); + let id = ingest(&storage, "Jake"); + + execute(&storage, Some(json!({"id": id.clone()}))) + .await + .unwrap(); + + let node = storage.get_node(&id).unwrap().unwrap(); + assert!(node.suppressed_at.is_some(), "suppressed_at must be set"); + } +} diff --git a/crates/vestige-mcp/src/tools/tagging.rs b/crates/vestige-mcp/src/tools/tagging.rs index e69c020..fd38888 100644 --- a/crates/vestige-mcp/src/tools/tagging.rs +++ b/crates/vestige-mcp/src/tools/tagging.rs @@ -7,8 +7,8 @@ use serde_json::Value; use std::sync::Arc; use vestige_core::{ - CaptureWindow, ImportanceEvent, ImportanceEventType, - SynapticTaggingConfig, SynapticTaggingSystem, Storage, + CaptureWindow, ImportanceEvent, ImportanceEventType, Storage, SynapticTaggingConfig, + SynapticTaggingSystem, }; /// Input schema for trigger_importance tool @@ -69,27 +69,22 @@ pub fn stats_schema() -> Value { } /// Trigger an importance event to retroactively strengthen recent memories -pub async fn execute_trigger( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute_trigger(storage: &Arc, args: Option) -> Result { let args = args.ok_or("Missing arguments")?; let event_type_str = args["event_type"] .as_str() .ok_or("event_type is required")?; - let memory_id = args["memory_id"] - .as_str() - .ok_or("memory_id is required")?; + let memory_id = args["memory_id"].as_str().ok_or("memory_id is required")?; let description = args["description"].as_str(); let hours_back = args["hours_back"].as_f64().unwrap_or(9.0); let hours_forward = args["hours_forward"].as_f64().unwrap_or(2.0); - // Verify the trigger memory exists - let trigger_memory = storage.get_node(memory_id) + let trigger_memory = storage + .get_node(memory_id) .map_err(|e| format!("Error: {}", e))? .ok_or("Memory not found")?; @@ -121,8 +116,7 @@ pub async fn execute_trigger( let mut stc = SynapticTaggingSystem::with_config(config); // Get recent memories to tag - let recent = storage.get_all_nodes(100, 0) - .map_err(|e| e.to_string())?; + let recent = storage.get_all_nodes(100, 0).map_err(|e| e.to_string())?; // Tag all recent memories for mem in &recent { @@ -155,32 +149,30 @@ pub async fn execute_trigger( } /// Find memories with active synaptic tags -pub async fn execute_find( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute_find(storage: &Arc, args: Option) -> Result { let args = args.unwrap_or(serde_json::json!({})); let min_strength = args["min_strength"].as_f64().unwrap_or(0.3); let limit = args["limit"].as_i64().unwrap_or(20) as usize; - // Get memories with high retention (proxy for "tagged") - let memories = storage.get_all_nodes(200, 0) - .map_err(|e| e.to_string())?; + let memories = storage.get_all_nodes(200, 0).map_err(|e| e.to_string())?; // Filter by retention strength (tagged memories have higher retention) - let tagged: Vec = memories.into_iter() + let tagged: Vec = memories + .into_iter() .filter(|m| m.retention_strength >= min_strength) .take(limit) - .map(|m| serde_json::json!({ - "id": m.id, - "content": m.content, - "retentionStrength": m.retention_strength, - "storageStrength": m.storage_strength, - "lastAccessed": m.last_accessed.to_rfc3339(), - "tags": m.tags - })) + .map(|m| { + serde_json::json!({ + "id": m.id, + "content": m.content, + "retentionStrength": m.retention_strength, + "storageStrength": m.storage_strength, + "lastAccessed": m.last_accessed.to_rfc3339(), + "tags": m.tags + }) + }) .collect(); Ok(serde_json::json!({ @@ -192,17 +184,22 @@ pub async fn execute_find( } /// Get synaptic tagging statistics -pub async fn execute_stats( - storage: &Arc, -) -> Result { - - let memories = storage.get_all_nodes(500, 0) - .map_err(|e| e.to_string())?; +pub async fn execute_stats(storage: &Arc) -> Result { + let memories = storage.get_all_nodes(500, 0).map_err(|e| e.to_string())?; let total = memories.len(); - let high_retention = memories.iter().filter(|m| m.retention_strength >= 0.7).count(); - let medium_retention = memories.iter().filter(|m| m.retention_strength >= 0.4 && m.retention_strength < 0.7).count(); - let low_retention = memories.iter().filter(|m| m.retention_strength < 0.4).count(); + let high_retention = memories + .iter() + .filter(|m| m.retention_strength >= 0.7) + .count(); + let medium_retention = memories + .iter() + .filter(|m| m.retention_strength >= 0.4 && m.retention_strength < 0.7) + .count(); + let low_retention = memories + .iter() + .filter(|m| m.retention_strength < 0.4) + .count(); let avg_retention = if total > 0 { memories.iter().map(|m| m.retention_strength).sum::() / total as f64 diff --git a/crates/vestige-mcp/src/tools/timeline.rs b/crates/vestige-mcp/src/tools/timeline.rs index c049b91..f73983a 100644 --- a/crates/vestige-mcp/src/tools/timeline.rs +++ b/crates/vestige-mcp/src/tools/timeline.rs @@ -9,7 +9,6 @@ use serde_json::Value; use std::collections::BTreeMap; use std::sync::Arc; - use vestige_core::Storage; use super::search_unified::format_node; @@ -88,10 +87,7 @@ fn parse_datetime(s: &str) -> Result, String> { } /// Execute memory_timeline tool -pub async fn execute( - storage: &Arc, - args: Option, -) -> Result { +pub async fn execute(storage: &Arc, args: Option) -> Result { let args: TimelineArgs = match args { Some(v) => serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {}", e))?, None => TimelineArgs { @@ -130,20 +126,20 @@ pub async fn execute( let limit = args.limit.unwrap_or(50).clamp(1, 200); - - // Query memories in time range - let mut results = storage - .query_time_range(start, end, limit) + // Query memories in time range with filters pushed into SQL. Rust-side + // `retain` after `LIMIT` was unsafe for sparse types/tags — a dominant + // set could crowd the sparse matches out of the limit window and leave + // the retain with 0 rows to keep. + let results = storage + .query_time_range( + start, + end, + limit, + args.node_type.as_deref(), + args.tags.as_deref(), + ) .map_err(|e| e.to_string())?; - // Post-query filters - if let Some(ref node_type) = args.node_type { - results.retain(|n| n.node_type == *node_type); - } - if let Some(tags) = args.tags.as_ref().filter(|t| !t.is_empty()) { - results.retain(|n| tags.iter().any(|t| n.tags.contains(t))); - } - // Group by day let mut by_day: BTreeMap> = BTreeMap::new(); for node in &results { @@ -195,17 +191,40 @@ mod tests { } async fn ingest_test_memory(storage: &Arc, content: &str) { - storage.ingest(vestige_core::IngestInput { - content: content.to_string(), - node_type: "fact".to_string(), - source: None, - sentiment_score: 0.0, - sentiment_magnitude: 0.0, - tags: vec!["timeline-test".to_string()], - valid_from: None, - valid_until: None, - }) - .unwrap(); + storage + .ingest(vestige_core::IngestInput { + content: content.to_string(), + node_type: "fact".to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: vec!["timeline-test".to_string()], + valid_from: None, + valid_until: None, + }) + .unwrap(); + } + + /// 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], + ) { + storage + .ingest(vestige_core::IngestInput { + content: content.to_string(), + node_type: node_type.to_string(), + source: None, + sentiment_score: 0.0, + sentiment_magnitude: 0.0, + tags: tags.iter().map(|t| t.to_string()).collect(), + valid_from: None, + valid_until: None, + }) + .unwrap(); } #[test] @@ -361,4 +380,90 @@ mod tests { let value = result.unwrap(); assert_eq!(value["totalMemories"], 0); } + + /// Regression: `node_type` filter must work even when the sparse type is + /// crowded out by a dominant type within the SQL `LIMIT`. Before the fix, + /// `query_time_range` applied `LIMIT` before the Rust-side `retain`, so a + /// limit of 5 against 10 dominant + 2 sparse rows returned 5 dominant, + /// then filtered to 0 sparse. + #[tokio::test] + async fn test_timeline_node_type_filter_sparse() { + let (storage, _dir) = test_storage().await; + + // Dominant set: 10 facts + for i in 0..10 { + 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; + } + + // Limit 5 against 12 total — before the fix, `retain` on `concept` + // would operate on the 5 most recent rows (all `fact`) and find 0. + let args = serde_json::json!({ "node_type": "concept", "limit": 5 }); + let value = execute(&storage, Some(args)).await.unwrap(); + assert_eq!( + value["totalMemories"], 2, + "Both sparse concepts should survive a limit smaller than the dominant set" + ); + + // Also verify the storage layer directly, so the contract is pinned + // at the API boundary even if the tool wrapper shifts. + let nodes = storage + .query_time_range(None, None, 5, Some("concept"), None) + .unwrap(); + assert_eq!(nodes.len(), 2); + assert!(nodes.iter().all(|n| n.node_type == "concept")); + } + + /// Regression: `tags` filter must work even when the sparse tag is + /// crowded out by a dominant tag within the SQL `LIMIT`. Parallel to + /// the node_type sparse case — same `retain`-after-`LIMIT` bug. + #[tokio::test] + async fn test_timeline_tag_filter_sparse() { + let (storage, _dir) = test_storage().await; + + // Dominant set: 10 memories with tag "common" + for i in 0..10 { + ingest_typed(&storage, &format!("Common memory {}", i), "fact", &["common"]).await; + } + // Sparse set: 2 memories with tag "rare" + for i in 0..2 { + ingest_typed(&storage, &format!("Rare memory {}", i), "fact", &["rare"]).await; + } + + let args = serde_json::json!({ "tags": ["rare"], "limit": 5 }); + let value = execute(&storage, Some(args)).await.unwrap(); + assert_eq!( + value["totalMemories"], 2, + "Both sparse-tag matches should survive a limit smaller than the dominant set" + ); + + let tag_slice = vec!["rare".to_string()]; + let nodes = storage + .query_time_range(None, None, 5, None, Some(&tag_slice)) + .unwrap(); + assert_eq!(nodes.len(), 2); + assert!(nodes.iter().all(|n| n.tags.iter().any(|t| t == "rare"))); + } + + /// Regression: tag filter must match exact tags, not substrings. Without + /// the `"tag"`-wrapped `LIKE` pattern, a query for `alpha` would also + /// match rows tagged `alphabet`. The pattern `%"alpha"%` keys off the + /// JSON-array quote characters and rejects that. + #[tokio::test] + async fn test_timeline_tag_filter_exact_match() { + let (storage, _dir) = test_storage().await; + + ingest_typed(&storage, "Exact tag hit", "fact", &["alpha"]).await; + ingest_typed(&storage, "Substring decoy", "fact", &["alphabet"]).await; + + let tag_slice = vec!["alpha".to_string()]; + let nodes = storage + .query_time_range(None, None, 50, None, Some(&tag_slice)) + .unwrap(); + assert_eq!(nodes.len(), 1, "Only the exact-tag match should return"); + assert_eq!(nodes[0].content, "Exact tag hit"); + } } diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index a2ff40a..f80bf00 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -31,10 +31,16 @@ export FASTEMBED_CACHE_PATH="/custom/path" | Variable | Default | Description | |----------|---------|-------------| -| `VESTIGE_DATA_DIR` | Platform default | Custom database location | -| `VESTIGE_LOG_LEVEL` | `info` | Logging verbosity | -| `RUST_LOG` | - | Detailed tracing output | +| `RUST_LOG` | `info` (via tracing-subscriber) | Log verbosity + per-module filtering | | `FASTEMBED_CACHE_PATH` | `./.fastembed_cache` | Embedding model cache location | +| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port | +| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port | +| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address | +| `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard + MCP HTTP bearer auth | +| `VESTIGE_DASHBOARD_ENABLED` | `true` | Set `false` to disable the web dashboard | +| `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS-6 decay cycle cadence | + +> **Storage location** is controlled by the `--data-dir ` CLI flag (see below), not an env var. Default is your OS's per-user data directory: `~/Library/Application Support/com.vestige.core/` on macOS, `~/.local/share/vestige/` on Linux, `%APPDATA%\vestige\core\data\` on Windows. --- @@ -62,7 +68,21 @@ vestige restore # Restore from backup --- -## Claude Configuration +## Client Configuration + +### Codex (One-liner) + +```bash +codex mcp add vestige -- /usr/local/bin/vestige-mcp +``` + +### Codex (Manual) + +Add to `~/.codex/config.toml`: +```toml +[mcp_servers.vestige] +command = "/usr/local/bin/vestige-mcp" +``` ### Claude Code (One-liner) diff --git a/docs/INSTALL-INTEL-MAC.md b/docs/INSTALL-INTEL-MAC.md new file mode 100644 index 0000000..ee42975 --- /dev/null +++ b/docs/INSTALL-INTEL-MAC.md @@ -0,0 +1,73 @@ +# 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. Download the binary +curl -L https://github.com/samvallad33/vestige/releases/latest/download/vestige-mcp-x86_64-apple-darwin.tar.gz | tar -xz +sudo mv vestige-mcp vestige vestige-restore /usr/local/bin/ + +# 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/STORAGE.md b/docs/STORAGE.md index cefca33..8b3b8c5 100644 --- a/docs/STORAGE.md +++ b/docs/STORAGE.md @@ -169,3 +169,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 Claude client | **Supported** | The default case. Zero contention. | +| Multiple Claude clients, separate `--data-dir` | **Supported** | Each process owns its own DB file. No shared state. | +| Multiple Claude 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..0ee5821 --- /dev/null +++ b/docs/VESTIGE_STATE_AND_PLAN.md @@ -0,0 +1,1273 @@ +# Vestige: State of the Engine & Next-Phase Plan + +> **For:** AI agents planning the next phase of Vestige alongside Sam. +> **From:** Sam Valladares (compiled via multi-agent inventory of the live codebase). +> **As of:** 2026-04-19 ~22:10 CT, post-merge of v2.0.8 into `main` (CI green on all four jobs). +> **Repo:** https://github.com/samvallad33/vestige +> **Related repo (private):** `~/Developer/vestige-cloud` (Feb 12, 2026 skeleton; Part 7). +> +> This document is the single authoritative briefing of what Vestige *is today* and what *ships next*. Everything in Part 1 is verifiable against the source tree; everything in Part 3 is the committed roadmap agreed 2026-04-19. + +--- + +## Table of Contents + +0. [Executive Summary (60-second read)](#0-executive-summary-60-second-read) +1. [What Vestige Is](#1-what-vestige-is) +2. [Workspace Architecture](#2-workspace-architecture) +3. [`vestige-core` — Cognitive Engine](#3-vestige-core--cognitive-engine) +4. [`vestige-mcp` — MCP Server + Dashboard Backend](#4-vestige-mcp--mcp-server--dashboard-backend) +5. [`apps/dashboard` — SvelteKit + Three.js Frontend](#5-appsdashboard--sveltekit--threejs-frontend) +6. [Integrations & Packaging](#6-integrations--packaging) +7. [`vestige-cloud` — Current Skeleton](#7-vestige-cloud--current-skeleton) +8. [Version History (v1.0 → v2.0.8)](#8-version-history-v10--v208) +9. [The Next-Phase Plan](#9-the-next-phase-plan) +10. [Composition Map](#10-composition-map) +11. [Risks & Known Gaps](#11-risks--known-gaps) +12. [Viral / Launch / Content Plan](#12-viral--launch--content-plan) +13. [How AI Agents Should Consume This Doc](#13-how-ai-agents-should-consume-this-doc) +14. [Glossary & Citations](#14-glossary--citations) + +--- + +## 0. Executive Summary (60-second read) + +Vestige is a Rust-based MCP (Model Context Protocol) cognitive memory server that gives any AI agent persistent, structured, scientifically-grounded memory. It ships three binaries (`vestige-mcp`, `vestige`, `vestige-restore`), a 3D SvelteKit dashboard embedded into the binary, and is distributed via GitHub releases + npm. As of v2.0.7 "Visible" (tagged 2026-04-19), it has **24 MCP tools**, **29 cognitive modules** implementing real neuroscience (FSRS-6 spaced repetition, synaptic tagging, hippocampal indexing, spreading activation, reconsolidation, Anderson 2025 suppression-induced forgetting, Rac1 cascade decay), **1,292 Rust tests**, **251 dashboard tests** (80 just added for v2.0.8 colour-mode), and **402 GitHub stars**. AGPL-3.0. + +**The branch `feat/v2.0.8-memory-state-colors` was fast-forwarded into `main` tonight** adding the FSRS memory-state colour mode, a floating legend, ruthless unit coverage, the Rust 1.95 clippy-compat fix (12 sites), and the dark-glass-pill label redesign. CI on main: all 4 jobs ✅. + +**The next six releases are scoped:** v2.1 "Decide" (Qwen3 embeddings, in-flight on `feat/v2.1.0-qwen3-embed`), v2.2 "Pulse" (subconscious cross-pollination — **the viral moment**), v2.3 "Rewind" (temporal slider + pin), v2.4 "Empathy" (emotional/frustration tagging, **first Pro-tier gate candidate**), v2.5 "Grip" (neuro-feedback cluster gestures), v2.6 "Remote" (`vestige-cloud` upgrade from 5→24 MCP tools + Streamable HTTP). v3.0 "Branch" reserves CoW memory branching and multi-tenant SaaS. + +**Sam's context** (load-bearing for any strategic advice): no steady income since March 2026, Mays Business School deadline May 1 ($400K+ prizes), Orbit Wars Kaggle deadline June 23 ($5K × top 10), graduation June 13. Viral OSS growth comes first; paid tier gates second. + +--- + +## 1. What Vestige Is + +### 1.1 Mission + +Give any AI agent that speaks MCP a long-term memory and a reasoning co-processor that survives session boundaries, with retrieval ranked by scientifically-validated decay and strengthening rules — not a vector database with a nice coat of paint. + +### 1.2 Positioning vs. the competitive landscape + +| System | Vestige's angle | +|---|---| +| Zep, Cognee, Letta, claude-mem, MemPalace, HippoRAG | Vestige is **local-first + MCP-native + neuroscience-grounded**. The others are cloud-first (Zep/Cognee), RAG-wrappers (HippoRAG), or toy (claude-mem). Vestige is the only one that implements 29 stateful cognitive modules. | +| ChatGPT memory, Cursor memory | Both are opaque key-value caches owned by their vendor. Vestige is open source and the memory is yours. | +| Plain vector DBs (Chroma, Qdrant) | They retrieve by similarity. Vestige *rewires* the graph on access (testing effect), decays with FSRS-6, competes retrievals, and dreams between sessions. | + +### 1.3 The "Oh My God" surface + +1. The 3D graph that **animates in real-time** when memories are created, promoted, suppressed, or cascade-decayed. +2. The `dream()` tool that runs a 5-stage consolidation cycle and generates insights from cross-cluster replay. +3. `deep_reference` — an 8-stage cognitive reasoning pipeline with FSRS trust scoring, intent classification, contradiction analysis, and a pre-built reasoning chain. Not just retrieval — actual reasoning. +4. Active forgetting (v2.0.5 "Intentional Amnesia") — top-down inhibitory control with Rac1 cascade that spreads over 72h, reversible within a 24h labile window. +5. Cross-IDE persistence. Fix a bug in VS Code, open the project in Xcode, the agent remembers. + +### 1.4 Stats (as of 2026-04-19 post-merge) + +| Metric | Value | +|---|---| +| GitHub stars | 402 | +| Total commits (main) | 139 | +| Rust source LOC | ~42,000 (vestige-core) + ~vestige-mcp | +| Rust tests passing | 1,292 (workspace, release profile) | +| Dashboard tests passing | 251 (Vitest, 7 files, 3,291 lines) | +| MCP tools | 24 | +| Cognitive modules | 29 (16 neuroscience + 11 advanced + 2 search) | +| FSRS-6 trained parameters | 21 | +| Embedding dim (default) | 768 (nomic-embed-text-v1.5), truncatable to 256 (Matryoshka) | +| Binary targets shipped | 3 (aarch64-darwin, x86_64-linux, x86_64-windows) | +| IDE integrations documented | 8 (Claude Code, Claude Desktop, Cursor, VS Code Copilot, Codex, Xcode, JetBrains/Junie, Windsurf) | +| Latest GitHub release | v2.0.7 "Visible" (binaries up, npm pending Sam's Touch ID) | +| `main` HEAD | `30d92b5` (2026-04-19 21:52 CT) | +| CI on HEAD | All 4 jobs ✅ (Test macos, Test ubuntu, Release aarch64-darwin, Release x86_64-linux) | + +### 1.5 License + +**AGPL-3.0-only** (copyleft). If you run a modified Vestige as a network service, you must open-source your modifications. This is intentional — it protects against extract-and-host competitors while allowing a future commercial-license path for SaaS (Part 9.7). + +--- + +## 2. Workspace Architecture + +### 2.1 Repo layout + +``` +vestige/ +├── Cargo.toml # Workspace root +├── Cargo.lock +├── pnpm-workspace.yaml # pnpm monorepo marker +├── package.json # Root (v2.0.1, private) +├── .mcp.json # Self-registering MCP config +├── README.md # 22.5 KB marketing + quick-start +├── CHANGELOG.md # 31 KB, v1.0 → v2.0.7 Keep-a-Changelog format +├── CLAUDE.md # Project-level Claude instructions +├── CONTRIBUTING.md # Dev setup + test commands +├── SECURITY.md # Vuln reporting +├── LICENSE # AGPL-3.0 full text +├── crates/ +│ ├── vestige-core/ # Library crate (cognitive engine) +│ └── vestige-mcp/ # Binary crate (MCP server + dashboard backend) +├── apps/ +│ └── dashboard/ # SvelteKit 2 + Svelte 5 + Three.js frontend +├── packages/ +│ ├── vestige-mcp-npm/ # npm: vestige-mcp-server (binary wrapper) +│ ├── vestige-init/ # npm: @vestige/init (zero-config installer) +│ └── vestige-mcpb/ # legacy, appears abandoned +├── tests/ +│ └── vestige-e2e-tests/ # Integration tests over MCP protocol +├── docs/ +│ ├── CLAUDE-SETUP.md +│ ├── CONFIGURATION.md +│ ├── FAQ.md +│ ├── SCIENCE.md +│ ├── STORAGE.md +│ ├── integrations/ +│ │ ├── codex.md +│ │ ├── cursor.md +│ │ ├── jetbrains.md +│ │ ├── vscode.md +│ │ ├── windsurf.md +│ │ └── xcode.md +│ ├── launch/ +│ │ ├── UI_ROADMAP_v2.1_v2.2.md # compiled 2026-04-19 +│ │ ├── show-hn.md +│ │ ├── blog-post.md +│ │ ├── demo-script.md +│ │ └── reddit-cross-reference.md +│ └── blog/ +│ └── xcode-memory.md +├── scripts/ +│ └── xcode-setup.sh # 4.9 KB interactive installer +└── .github/ + └── workflows/ + ├── ci.yml # push-main + PR: clippy + test + ├── release.yml # tag push: binary build matrix + └── test.yml # parallel unit/e2e/journey/dashboard/coverage +``` + +### 2.2 Dependency flow + +``` +┌─────────────────────┐ +│ apps/dashboard │ Svelte 5 + Three.js → static `build/` +│ (SvelteKit 2) │ embedded via include_dir! into vestige-mcp binary +└──────────┬──────────┘ + │ HTTP / WebSocket + ▼ +┌─────────────────────┐ ┌──────────────────────┐ +│ vestige-mcp │ ────► │ vestige-core │ +│ (binary + dash BE) │ │ (cognitive engine) │ +│ Axum + JSON-RPC │ │ FSRS-6, search, │ +│ MCP stdio + HTTP │ │ embeddings, 29 │ +│ │ │ cognitive modules │ +└─────────────────────┘ └──────────────────────┘ + ▲ + │ path dep + ┌────────┴──────────┐ + │ vestige-cloud │ (separate repo, Feb 12 + │ vestige-http │ skeleton, not yet + │ (Axum + SSE) │ shipped) + └───────────────────┘ +``` + +### 2.3 Build profile + +```toml +[profile.release] +lto = true +codegen-units = 1 +panic = "abort" +strip = true +opt-level = "z" # Size-optimized; binary is ~22 MB with dashboard +``` + +### 2.4 Workspace Cargo.toml pinned version + +Workspace `version = "2.0.5"`. Crate-level `Cargo.toml` files pin `2.0.7`. Version files are pumped together on each release (5 files: `crates/vestige-core/Cargo.toml`, `crates/vestige-mcp/Cargo.toml`, `apps/dashboard/package.json`, `packages/vestige-init/package.json`, `packages/vestige-mcp-npm/package.json`). + +### 2.5 MSRV & editions + +- **Rust MSRV:** 1.91 (enforced in `rust-version`). +- **CI Rust:** stable (currently 1.95 — which introduced the `unnecessary_sort_by` + `collapsible_match` lints tonight's fixes addressed). +- **Edition:** 2024 across the entire workspace. +- **Node:** 18+ for npm packages, 22+ for dashboard dev. +- **pnpm:** 10+ for workspace. + +--- + +## 3. `vestige-core` — Cognitive Engine + +### 3.1 Purpose + +Library crate. Owns the entire cognitive engine: storage, FTS5, vector search, FSRS-6, embeddings, and the 29 cognitive modules. Has no knowledge of MCP, HTTP, or the dashboard — those live one crate up. + +### 3.2 Metadata + +```toml +name = "vestige-core" +version = "2.0.7" +edition = "2024" +rust-version = "1.91" +license = "AGPL-3.0-only" +description = "Cognitive memory engine - FSRS-6 spaced repetition, semantic embeddings, and temporal memory" +keywords = ["memory", "spaced-repetition", "fsrs", "embeddings", "knowledge-graph"] +``` + +### 3.3 Feature flags (8) + +| Flag | Default | What it turns on | Cost | +|---|---|---|---| +| `embeddings` | **yes** | `mod embeddings`, fastembed v5.11, ONNX inference | +~130MB model download on first run | +| `vector-search` | **yes** | `mod search`, USearch HNSW, hybrid BM25 + semantic | negligible | +| `bundled-sqlite` | **yes** (mutex w/ `encryption`) | SQLite bundled via rusqlite 0.38 | +~2MB binary | +| `encryption` | no | SQLCipher encrypted DB | requires system libsqlcipher | +| `qwen3-reranker` | no | Qwen3 cross-encoder reranker | +candle-core deps | +| `qwen3-embed` | **no (v2.1 scaffolding)** | Qwen3 embed backend via Candle (Metal device + CPU fallback) | +candle-core, +~500MB Qwen3 model | +| `metal` | no | Metal GPU acceleration on Apple | macOS only | +| `nomic-v2` | no | Nomic Embed v2 MoE variant | +~200MB model | +| `ort-dynamic` | no | Runtime-load ORT instead of static prebuilt | required on glibc < 2.38 | + +**Default feature set ships with embeddings + vector-search.** `qwen3-embed` is the v2.1 "Decide" scaffolding — dual-index with feature-gated `DEFAULT_DIMENSIONS` (1024 for Qwen3 vs 256 for Matryoshka-truncated Nomic). + +### 3.4 Module tree (`src/lib.rs`) + +``` +src/ +├── lib.rs # Module tree + prelude re-exports +├── prelude.rs # KnowledgeNode, IngestInput, SearchResult, etc. +├── storage/ # SQLite + FTS5 + HNSW + migrations +│ ├── mod.rs # Storage struct; public API +│ ├── sqlite.rs # Connection setup, PRAGMAs, migrations +│ ├── migrations.rs # V1..V11 migration chain (V11 dropped knowledge_edges + compressed_memories tables) +│ ├── schema.rs # CREATE TABLE statements +│ ├── node.rs # CRUD for KnowledgeNode +│ ├── edge.rs # Edge insertion + deletion +│ ├── fts.rs # FTS5 wrapper +│ ├── state_transitions.rs # Append-only audit log +│ ├── consolidation_history.rs +│ ├── dream_history.rs +│ └── intention.rs # Prospective memory persistence +├── search/ # 7-stage cognitive search pipeline +│ ├── mod.rs +│ ├── hybrid.rs # BM25 + semantic fusion +│ ├── vector.rs # USearch HNSW wrapper; DEFAULT_DIMENSIONS gated +│ ├── reranker.rs # Jina Reranker v1 Turbo (38M params) +│ ├── temporal.rs # Recency + validity window boosting +│ ├── context.rs # Tulving 1973 encoding specificity +│ ├── competition.rs # Anderson 1994 retrieval-induced forgetting +│ └── activation.rs # Spreading activation side effects +├── embeddings/ # ONNX local + Qwen3 candle +│ ├── mod.rs # EmbeddingService trait +│ ├── local.rs # Backend enum (Nomic ONNX / Qwen3 Candle); metal device selection +│ ├── adaptive.rs # AdaptiveEmbedder (Matryoshka 256/768/1024 tier) +│ ├── hyde.rs # HyDE query expansion +│ └── cache.rs # In-memory embedding LRU +├── fsrs/ # Spaced repetition (21-param Anki FSRS-6) +│ ├── mod.rs +│ ├── params.rs # Trained params +│ ├── algorithm.rs # R(t) = (1 + factor × t / S)^(-w20) +│ └── review.rs # apply_review +├── neuroscience/ # 16 modules (see §3.5) +│ ├── mod.rs +│ ├── activation.rs # ActivationNetwork (Collins & Loftus 1975) +│ ├── synaptic_tagging.rs # SynapticTaggingSystem (Frey & Morris 1997) +│ ├── hippocampal_index.rs # (Teyler & Rudy 2007) +│ ├── context_matcher.rs # (Tulving 1973) +│ ├── accessibility.rs # AccessibilityCalculator +│ ├── competition.rs # CompetitionManager (Anderson 1994) +│ ├── state_update.rs # StateUpdateService +│ ├── importance_signals.rs # 4-channel (novelty/arousal/reward/attention) +│ ├── emotional_memory.rs # Brown & Kulik 1977 flashbulb memory +│ ├── predictive_retrieval.rs # Friston Free Energy 2010 +│ ├── prospective_memory.rs # Intention fulfillment +│ ├── intention_parser.rs +│ └── memory_states.rs # Active / Dormant / Silent / Unavailable + Bjork & Bjork 1992 +├── advanced/ # 11 modules (see §3.6) +│ ├── mod.rs +│ ├── importance_tracker.rs +│ ├── reconsolidation.rs # Nader 2000 labile window (5 min, 10 mods max) +│ ├── intent_detector.rs # 9 intent types +│ ├── activity_tracker.rs +│ ├── dreaming.rs # MemoryDreamer 5-stage +│ ├── chains.rs # MemoryChainBuilder (A*-like) +│ ├── compression.rs # MemoryCompressor (30-day min age) +│ ├── cross_project.rs # CrossProjectLearner (6 pattern types) +│ ├── adaptive_embedding.rs +│ ├── speculative_retriever.rs +│ └── consolidation_scheduler.rs +├── codebase/ # CrossProjectLearner backing +│ ├── git.rs # Git history analysis +│ ├── relationships.rs # File-file co-edit patterns +│ └── types.rs +└── session/ # Session-level tracking + └── mod.rs +``` + +### 3.5 Neuroscience modules (16) + +| Module | Citation / basis | Purpose | +|---|---|---| +| `ActivationNetwork` | Collins & Loftus 1975 | Spreading activation across memory graph | +| `SynapticTaggingSystem` | Frey & Morris 1997 | Retroactive importance: memories in last 9h get boosted when big event fires | +| `HippocampalIndex` | Teyler & Rudy 2007 | Graph-level indexing; "dentate gyrus pattern separator" | +| `ContextMatcher` | Tulving 1973 | Encoding specificity — context overlap boosts retrieval by up to 30% | +| `AccessibilityCalculator` | Bjork & Bjork 1992 | `accessibility = retention × 0.5 + retrieval × 0.3 + storage × 0.2` | +| `CompetitionManager` | Anderson 1994 | Retrieval-induced forgetting — winners strengthen, competitors weaken | +| `StateUpdateService` | — | FSRS state transitions + append-only log | +| `ImportanceSignals` (4 channels) | Novelty / Arousal / Reward / Attention | Composite importance score, threshold 0.6 | +| `EmotionalMemory` | Brown & Kulik 1977 | Flashbulb memories — high-arousal events encode stronger | +| `PredictiveMemory` | Friston 2010 | Active inference — predict user needs before they ask | +| `ProspectiveMemory` | — | Intentions ("remind me when...") | +| `IntentionParser` | — | Natural-language → structured intention trigger | +| `MemoryState` (enum) | Bjork & Bjork 1992 | Active ≥0.7 / Dormant ≥0.4 / Silent ≥0.1 / Unavailable <0.1 | +| `Rac1Cascade` (v2.0.5) | Cervantes-Sandoval & Davis 2020 | Actin-destabilization-mediated forgetting of co-activated neighbors | +| `Suppression` (v2.0.5) | Anderson 2025 SIF | Top-down inhibitory control; compounds; 24h reversible labile window | +| `Reconsolidation` | Nader 2000 | 5-minute labile window after access; up to 10 modifications | + +### 3.6 Advanced modules (11) + +| Module | Purpose | Key methods | +|---|---|---| +| `ImportanceTracker` | Aggregates 4-channel score history | `record()`, `get_composite()` | +| `ReconsolidationManager` | Nader labile window state machine | `mark_labile()`, `apply_modification()`, `reconsolidate()` | +| `IntentDetector` | 9 intent types (Question, Decision, Plan, etc.) | `detect()` | +| `ActivityTracker` | Session-level active memory | `record_access()`, `recent()` | +| `MemoryDreamer` | 5-stage consolidation: Replay → Cross-reference → Strengthen → Prune → Transfer. Uses Waking SWR tagging (70% tagged + 30% random for diversity) | `dream(memory_count)` | +| `MemoryChainBuilder` | A*-like pathfinding between memories | `build_chain(from, to)` | +| `MemoryCompressor` | Semantic compression for 30+ day old memories | `compress(group)` | +| `CrossProjectLearner` | 6 pattern types (ErrorHandling, AsyncConcurrency, Testing, Architecture, Performance, Security) | `find_universal_patterns()`, `apply_to_project()` | +| `AdaptiveEmbedder` | Matryoshka-truncation tier selection | `embed_adaptive()` | +| `SpeculativeRetriever` | 6 trigger types for proactive memory fetch | `predict_needed_memories()` | +| `ConsolidationScheduler` | Runs FSRS decay cycle on interval (default 6h, env-configurable) | `start()` | + +### 3.7 Storage + +- SQLite via rusqlite 0.38, WAL mode, `Mutex` split between reader and writer. +- FTS5 for keyword search (`bm25(10.0, 5.0, 1.0)` weights). +- Migrations V1..V11. **V11 (2026-04-19)** drops the dead `knowledge_edges` and `compressed_memories` tables that were reserved but never used. +- Append-only audit logs: `state_transitions`, `consolidation_history`, `dream_history`. + +### 3.8 Embeddings + +- Default: **Nomic Embed Text v1.5** via fastembed (ONNX, 768D). +- Matryoshka truncation to 256D for fast HNSW lookups (20× faster than full 768D). +- HyDE query expansion (generate a hypothetical document, embed it, search by its embedding). +- **v2.1 scaffolding:** Qwen3 embedding backend via Candle behind `qwen3-embed` feature. `qwen3_format_query()` helper prepends the instruction prefix ("Given a web search query, retrieve relevant passages that answer the query"). +- Embedding cache: in-memory LRU; disk-warm on first run (~130MB for Nomic, ~500MB for Qwen3). + +### 3.9 Vector search + +- USearch HNSW (pinned 2.23.0; 2.24.0 regressed on MSVC per usearch#746). Int8 quantization. +- Hybrid scoring: `combined = 0.3 × BM25 + 0.7 × cosine` (default, user-tunable). +- `DEFAULT_DIMENSIONS` feature-gated: 256 on default, 1024 under `qwen3-embed`. + +### 3.10 FSRS-6 + +- 21 trained parameters (Jarrett Ye / maimemo; trained on 700M+ Anki reviews). +- `R(t) = (1 + factor × t / S)^(-w20)` — power-law forgetting curve. +- 20-30% more efficient than SM-2 (Anki's original algorithm). +- Retrievability, stability, difficulty tracked per node. +- Dual-strength (Bjork & Bjork 1992): storage strength grows monotonically, retrieval strength decays. + +### 3.11 Test count + +- **364 `#[test]` annotations in vestige-core** across 47 test-bearing files. +- Examples: `cargo test --workspace` → 1,292 passing (includes 366 core + 425 mcp + e2e + journey). + +--- + +## 4. `vestige-mcp` — MCP Server + Dashboard Backend + +### 4.1 Purpose + +Binary crate. Wraps `vestige-core` behind an MCP JSON-RPC 2.0 server, plus an embedded Axum HTTP server that hosts the dashboard, WebSocket event bus, and REST API. + +### 4.2 Binaries + +| Binary | Source | Purpose | +|---|---|---| +| `vestige-mcp` | `src/main.rs` | **Primary.** MCP JSON-RPC over stdio + optional HTTP transport. Hosts dashboard at `/dashboard/`. | +| `vestige` | `src/bin/cli.rs` | CLI: stats, consolidate, backup, restore, export, gc, dashboard launcher. | +| `vestige-restore` | `src/bin/restore.rs` | Standalone batch restore from JSON backup. | + +### 4.3 Environment variables + +| Var | Default | Purpose | +|---|---|---| +| `VESTIGE_DASHBOARD_PORT` | `3927` | Dashboard HTTP + WebSocket port | +| `VESTIGE_HTTP_PORT` | `3928` | Optional MCP-over-HTTP port | +| `VESTIGE_HTTP_BIND` | `127.0.0.1` | HTTP bind address | +| `VESTIGE_AUTH_TOKEN` | auto-generated | Dashboard bearer auth | +| `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` | `6` | FSRS decay cycle cadence | +| `VESTIGE_DASHBOARD_ENABLED` | `true` | Toggle dashboard on/off | +| `VESTIGE_SYSTEM_PROMPT_MODE` | `minimal` | `full` enables the extended `build_instructions` block | +| `RUST_LOG` | `info` | tracing filter | + +### 4.4 The 24 MCP tools + +Every tool implemented in `src/tools/*.rs`. JSON schemas are programmatically emitted from `schema()` functions on each module. + +1. **`session_context`** — one-call session init. Params: `queries[]`, `context{codebase, topics, file}`, `token_budget` (100-100000), `include_status`, `include_intentions`, `include_predictions`. Returns markdown context + `automationTriggers` (needsDream, needsBackup, needsGc) + `expandable` overflow IDs. +2. **`smart_ingest`** — single or batch ingest with Prediction Error Gating (similarity >0.92 → UPDATE, 0.75-0.92 → UPDATE/SUPERSEDE, <0.75 → CREATE). Params: `content`, `tags[]`, `node_type`, `source`, `forceCreate`, OR `items[]` (up to 20). Runs full cognitive pipeline. +3. **`search`** — 7-stage cognitive search. Params: `query`, `limit` (1-100), `min_retention`, `min_similarity`, `detail_level` (brief/summary/full), `context_topics[]`, `token_budget`, `retrieval_mode` (precise/balanced/exhaustive). **Strengthens retrieved memories via testing effect.** +4. **`memory`** — CRUD + promote/demote. `action` ∈ `{get, edit, delete, promote, demote, state, get_batch}`. `get_batch` takes up to 20 IDs. Edit preserves FSRS state, regenerates embedding. +5. **`codebase`** — Remember patterns & decisions. Actions: `remember_pattern`, `remember_decision`, `get_context`. Feeds CrossProjectLearner. +6. **`intention`** — Prospective memory. Actions: `set` (with trigger types time/context/event), `check`, `update`, `list`. Supports `include_snoozed` (v2.0.7 fix). +7. **`dream`** — 5-stage consolidation cycle. Param: `memory_count` (default 50). Returns insights, connections found, memories replayed, duration. +8. **`explore_connections`** — Graph traversal. Actions: `associations` (spreading activation), `chain` (A*-like path), `bridges` (connecting memories between two concepts). +9. **`predict`** — Proactive retrieval via SpeculativeRetriever. Param: `context{codebase, current_file, current_topics[]}`. Returns predictions with confidence + reasoning. Has a `predict_degraded` flag (v2.0.7) that surfaces warnings instead of silent empty responses. +10. **`importance_score`** — 4-channel scoring. Param: `content`, `context_topics[]`, `project`. Returns `{composite, channels{novelty, arousal, reward, attention}, recommendation}`. +11. **`find_duplicates`** — Cosine similarity clustering. Params: `similarity_threshold` (default 0.80), `limit`, `tags[]`. Returns merge/review suggestions. +12. **`memory_timeline`** — Chronological browse. Params: `start`, `end`, `node_type`, `tags[]`, `limit`, `detail_level`. +13. **`memory_changelog`** — Audit trail. Per-memory mode (by `memory_id`) or system-wide (with optional `start`/`end` ISO bounds, v2.0.7 fix adds 4× over-fetch when bounded). +14. **`memory_health`** — Retention dashboard. Returns avg retention, distribution buckets (0-20%, 20-40%, ...), trend, recommendation. +15. **`memory_graph`** — Visualization export. Params: `query` OR `center_id`, `depth` (default 2), `max_nodes` (default 50). Returns nodes with force-directed positions + edges with weights. +16. **`deep_reference`** — **★ THE killer tool.** 8-stage cognitive reasoning: + 1. Broad retrieval + cross-encoder reranking. + 2. Spreading activation expansion. + 3. FSRS-6 trust scoring (retention × stability × reps ÷ lapses). + 4. Intent classification (FactCheck / Timeline / RootCause / Comparison / Synthesis). + 5. Temporal supersession. + 6. Trust-weighted contradiction analysis. + 7. Relation assessment (Supports / Contradicts / Supersedes / Irrelevant). + 8. Template reasoning chain — pre-built natural-language conclusion the AI validates. + Returns `{intent, reasoning, recommended, evidence, contradictions, superseded, evolution, related_insights, confidence}`. +17. **`cross_reference`** — Backward-compat alias that calls `deep_reference`. Kept for v1.x users. +18. **`system_status`** — Full health + stats + warnings + recommendations. Used by `session_context` when `include_status=true`. +19. **`consolidate`** — FSRS-6 decay cycle + embedding generation pass. Returns counts. +20. **`backup`** — SQLite backup to `~/.vestige/backups/` with timestamp. +21. **`export`** — JSON/JSONL export. Params: `format`, `tags[]`, `since`. v2.0.7 defensive `Err` on unknown format (was `unreachable!()`). +22. **`gc`** — Garbage collect. Params: `min_retention` (default 0.1), `dry_run` (default true). Dry-run first, then execute. +23. **`restore`** — Restore from backup. Param: `path`. +24. **`suppress`** / **`unsuppress`** (v2.0.5 "Intentional Amnesia") — Top-down inhibition. `suppress(id, reason?)` compounds (`suppressionCount` increments); `unsuppress(id)` reverses if within 24h labile window. Also exposed as dashboard HTTP endpoints (v2.0.7: `POST /api/memories/{id}/suppress` + `/unsuppress`). + +### 4.5 MCP server internals + +- `src/server.rs` — JSON-RPC 2.0 over stdio, optional HTTP. Handles `initialize`, `tools/list`, `tools/call`. +- **`build_instructions()`** — constructs the `instructions` string returned by `initialize`. Gated on `VESTIGE_SYSTEM_PROMPT_MODE=full`. Full mode emits an extended cognitive-protocol system prompt; default is concise. +- **CognitiveEngine** (`src/cognitive/mod.rs`) — async wrapper around `Arc` + broadcast channel. Holds the WebSocket event sender. +- **Tool dispatch** — every `tools/call` invocation is routed to a `execute_*` function by tool name. + +### 4.6 Dashboard HTTP backend (`src/dashboard/`) + +- `src/dashboard/mod.rs` — Axum `Router` assembly. +- `src/dashboard/handlers.rs` — all REST handlers (~30 routes). +- `src/dashboard/static_files.rs` — embeds `apps/dashboard/build/` via `include_dir!` at compile time. +- `src/dashboard/state.rs` — `AppState { storage, event_tx, start_time }`. +- `src/dashboard/websocket.rs` — `/ws` upgrade handler with Origin validation (localhost + 127.0.0.1 + dev :5173), 64KB frame cap, 256KB message cap, heartbeat task every 5s. + +**Heartbeat payload (v2.0.7):** `{type: "Heartbeat", data: {uptime_secs, memory_count, avg_retention, suppressed_count, timestamp}}`. The `uptime_secs` is what powers the sidebar footer's `formatUptime()` display ("3d 4h" / "18m 43s"). + +### 4.7 WebSocket event bus — 19 VestigeEvent types + +Emitted from the `CognitiveEngine` broadcast channel to every connected dashboard client: + +| Event | When emitted | Dashboard visual | +|---|---|---| +| `Connected` | WebSocket upgrade complete | Cyan ripple (v2.0.6) | +| `Heartbeat` | Every 5s | Silent (updates sidebar stats) | +| `MemoryCreated` | Any ingest that produces a new node | Rainbow burst + double shockwave + ripple | +| `MemoryUpdated` | Smart_ingest UPDATE path | Pulse at node | +| `MemoryDeleted` | `memory({action: "delete"})` | Dissolution animation | +| `MemoryPromoted` | `memory({action: "promote"})` | Green pulse + sparkle | +| `MemoryDemoted` | `memory({action: "demote"})` | Orange pulse + fade | +| `MemorySuppressed` | `suppress(id)` (v2.0.5) | Violet implosion (v2.0.7) | +| `MemoryUnsuppressed` | `unsuppress(id)` (v2.0.5) | Rainbow reversal (v2.0.7) | +| `Rac1CascadeSwept` | Rac1 worker completes cascade (72h async) | Violet wave pulse (v2.0.6) | +| `SearchPerformed` | Every `search()` call | Cyan flash + PipelineVisualizer 7-stage animation in `/feed` | +| `DreamStarted` | `dream()` begins | Scene enters dream mode (2s lerp) | +| `DreamProgress` | Per-stage updates during dream | Aurora hue cycle | +| `DreamCompleted` | Dream finishes, insights generated | Scene exits dream mode | +| `ConsolidationStarted` | FSRS consolidation cycle starts | Amber warning pulse (v2.0.6) | +| `ConsolidationCompleted` | Consolidation finishes | Green confirmation pulse | +| `RetentionDecayed` | Node's retention drops below threshold during consolidation | Red decay pulse | +| `ConnectionDiscovered` | Dream or spreading activation finds new edge | **Cyan flash on edge (already fires — NOT yet surfaced as a toast; see v2.2 "Pulse")** | +| `ActivationSpread` | Spreading activation from a memory | Turquoise ripple (v2.0.6) | +| `ImportanceScored` | `importance_score()` or internal scoring event | Hot-pink pulse (v2.0.6, magenta) | + +### 4.8 Dashboard REST API + +All routes under `/api/`: + +| Method | Path | Purpose | +|---|---|---| +| GET | `/api/health` | Health check (status, version, memory count) | +| GET | `/api/stats` | Full stats (same surface as `system_status` tool) | +| GET | `/api/memories` | List memories with filters (q, node_type, tag, min_retention) | +| GET | `/api/memories/{id}` | Single memory detail | +| POST | `/api/memories` | Create memory (raw ingest) | +| DELETE | `/api/memories/{id}` | Delete | +| POST | `/api/memories/{id}/promote` | Promote (+0.20 retrieval, +0.10 retention, 1.5× stability) | +| POST | `/api/memories/{id}/demote` | Demote (−0.30 retrieval, −0.15 retention, 0.5× stability) | +| POST | `/api/memories/{id}/suppress` | v2.0.7: compound suppression | +| POST | `/api/memories/{id}/unsuppress` | v2.0.7: reverse within 24h labile window | +| POST | `/api/search` | Hybrid search (keyword + semantic weights) | +| POST | `/api/ingest` | Smart ingest (PE gating) | +| GET | `/api/graph` | Graph visualization export | +| POST | `/api/explore` | Actions: associations / chains / bridges | +| POST | `/api/dream` | Run dream cycle | +| POST | `/api/consolidate` | Run FSRS decay cycle | +| POST | `/api/predict` | Proactive predictions | +| POST | `/api/importance` | 4-channel score | +| GET | `/api/timeline` | Chronological | +| GET | `/api/intentions` | List intentions (filter by status) | +| GET | `/api/retention-distribution` | Bucketed histogram | + +WebSocket: `GET /ws` (upgrade) — one broadcast channel, any connected client gets all events. + +### 4.9 vestige-mcp feature flags + +| Flag | Purpose | Default | +|---|---|---| +| `embeddings` | Forward to vestige-core | yes | +| `vector-search` | Forward to vestige-core | yes | +| `ort-dynamic` | Forward to vestige-core | no | + +Build commands (from CONTRIBUTING.md): +- Full: `cargo install --path crates/vestige-mcp` +- No-embeddings (tiny): `cargo install --path crates/vestige-mcp --no-default-features` +- Dynamic ORT (glibc < 2.38): `cargo install --path crates/vestige-mcp --no-default-features --features ort-dynamic,vector-search` + +--- + +## 5. `apps/dashboard` — SvelteKit + Three.js Frontend + +### 5.1 Purpose + +Interactive 3D graph + CRUD + analytics dashboard. Built with SvelteKit 2 + Svelte 5 runes, embedded into the Rust binary via `include_dir!` and served at `/dashboard/`. + +### 5.2 Tech stack + +- **SvelteKit 2.53** + **Svelte 5.53** (runes: `$state`, `$props`, `$derived`, `$effect`). +- **Three.js 0.172** — WebGL, MSAA, ACESFilmic tone mapping. +- **Tailwind CSS 4.2** — custom `@theme` block (synapse, dream, memory, recall, decay colors + 8 node-type palette). +- **TypeScript 5.9** — strict mode. +- **Vite 6.4** + **Vitest 4.0.18** (251 tests). +- **@playwright/test 1.58** — E2E ready (journeys live in `tests/vestige-e2e-tests/`). + +### 5.3 Routes (SvelteKit file-based) + +Grouped under `(app)/`: + +| Route | File | Purpose | +|---|---|---| +| `/` | `+page.svelte` | Redirect to `/graph` | +| `(app)/graph` | `+page.svelte` | **Primary 3D graph** (Graph3D component + color mode toggle + time slider + right panel for detail + legend overlay v2.0.8) | +| `(app)/memories` | `+page.svelte` | Memory browser (search, filter by type/tag/retention, suppress button v2.0.7) | +| `(app)/intentions` | `+page.svelte` | Prospective memory + predictions (status tabs, trigger icons, priority labels) | +| `(app)/stats` | `+page.svelte` | Health dashboard, retention distribution, endangered memories, run-consolidation button | +| `(app)/timeline` | `+page.svelte` | Chronological browse (days dropdown, expandable day cards) | +| `(app)/feed` | `+page.svelte` | Live event stream (200-event FIFO buffer, PipelineVisualizer on SearchPerformed) | +| `(app)/explore` | `+page.svelte` | Associations / Chains / Bridges mode toggle + Importance Scorer | +| `(app)/settings` | `+page.svelte` | Operations + config + keyboard shortcuts reference | + +### 5.4 Root layout (`src/routes/+layout.svelte`) + +- Desktop sidebar (8 nav items) + mobile bottom nav (5 items). +- **Command palette (⌘K)** — opens a search bar that navigates. +- **Single-key shortcuts** — G/M/T/F/E/I/S for routes. +- **Status footer** — connection indicator, memory count, avg retention, suppressed count (v2.0.5), uptime (v2.0.7: `up {formatUptime($uptimeSeconds)}`). +- **ForgettingIndicator** — violet badge showing suppressed count. +- Ambient orb background animations (CSS). + +### 5.5 Components (`src/lib/components/`) + +| Component | Purpose | +|---|---| +| `Graph3D.svelte` | **The 3D canvas.** Props: `nodes[]`, `edges[]`, `centerId`, `events[]`, `isDreaming`, `colorMode` (v2.0.8), `onSelect`, `onGraphMutation`. Owns the Three.js scene and all module init. | +| `MemoryStateLegend.svelte` (v2.0.8) | Floating overlay explaining 4 FSRS buckets — only renders when `colorMode === 'state'`. | +| `PipelineVisualizer.svelte` | 7-stage cognitive search animation (Overfetch → Rerank → Temporal → Access → Context → Compete → Activate). Shown in `/feed` when SearchPerformed arrives. | +| `RetentionCurve.svelte` | SVG FSRS-6 decay curve in the graph right panel. `R(t) = e^(-t/S)` with predictions at Now / 1d / 7d / 30d. | +| `TimeSlider.svelte` | Temporal playback scrubber. State: enabled, playing, speed (0.5-2×), sliderValue. Callbacks `onDateChange`, `onToggle`. | +| `ForgettingIndicator.svelte` | Violet badge in sidebar showing suppressed count from Heartbeat. | + +### 5.6 Three.js graph system (`src/lib/graph/`) + +| File | Role | +|---|---| +| `nodes.ts` | `NodeManager`. Fibonacci sphere initial positions, materialize/dissolve/grow animations, shared radial-gradient glow texture (128px) that prevents square bloom artifacts (issue #31). **v2.0.8:** `ColorMode` ('type' / 'state'), `getMemoryState(retention)`, `MEMORY_STATE_COLORS`, `MEMORY_STATE_DESCRIPTIONS`, `setColorMode(mode)` idempotent in-place retint. **2026-04-19:** dark-glass-pill label redesign (dimmer `#94a3b8` slate on `rgba(10,16,28,0.82)` pill with hairline stroke). | +| `edges.ts` | `EdgeManager`. Violet `#8b5cf6` lines; opacity = 25% + 50% × weight, capped at 80%. Grow/dissolve animations. | +| `force-sim.ts` | Repulsion 500, attraction 0.01 × edge weight × distance, damping 0.9, centering 0.001α. N² but fine up to ~1000 nodes at 60fps. | +| `particles.ts` | `ParticleSystem`. Starfield (3000 points on spherical shell r=600-1000) + neural particles (500 oscillating sin-wave). | +| `effects.ts` | `EffectManager`. 12 effect types (SpawnBurst, Shockwave, RainbowBurst, RippleWave, Implosion, Pulse, ConnectionFlash, etc.). | +| `events.ts` | `mapEventToEffects()` — maps every one of the 19 VestigeEvent variants to a visual effect. Live-spawn mechanics: new nodes spawn near semantically related existing nodes (tag + type scoring), FIFO eviction at 50 nodes. | +| `scene.ts` | Scene factory. Camera 60° FOV at (0, 30, 80). ACESFilmic tone mapping, exposure 1.25, pixel ratio clamped ≤2×. **UnrealBloomPass:** strength 0.55, radius 0.6, threshold 0.2 (retuned v2.0.8 for radial-gradient sprites). OrbitControls with auto-rotate 0.3°/frame. | +| `dream-mode.ts` | Smooth 2s lerp between NORMAL (bloom 0.8, rotate 0.3, fog dense) and DREAM (bloom 1.8, rotate 0.08, nebula 1.0, chromatic 0.005). Aurora lights cycle hue in dream. | +| `temporal.ts` | `filterByDate(nodes, edges, cutoff)`, `retentionAtDate(current, stability, created, target)` using FSRS decay formula. Enables the TimeSlider preview. | +| `shaders/nebula.frag.ts` | Nebula background fragment shader (purple → cyan → magenta cycle with turbulence). | +| `shaders/post-processing.ts` | Chromatic aberration, vignette, subtle distortion. Parameters lerp with dream-mode. | + +### 5.7 Stores (`src/lib/stores/`) + +| Store | Exports | Purpose | +|---|---|---| +| `api.ts` | `api.memories.*`, `api.search`, `api.graph`, `api.explore`, `api.stats`, `api.health`, `api.retentionDistribution`, `api.timeline`, `api.dream`, `api.consolidate`, `api.predict`, `api.importance`, `api.intentions` | 23 REST client methods | +| `websocket.ts` | `websocket` (writable), `isConnected`, `eventFeed`, `heartbeat`, `memoryCount`, `avgRetention`, `suppressedCount`, `uptimeSeconds`, `formatUptime(secs)` | WebSocket connection + derived state. FIFO 200-event ring buffer. Exponential backoff reconnect (1s → 30s). | +| `graph-state.svelte.ts` | (unused artifact from v2.0.6) | — | + +### 5.8 Types (`src/lib/types/index.ts`) + +Exported: `Memory`, `SearchResult`, `MemoryListResponse`, `SystemStats`, `HealthCheck`, `RetentionDistribution`, `GraphNode`, `GraphEdge`, `GraphResponse`, `DreamResult`, `DreamInsight`, `ImportanceScore`, `ConsolidationResult`, `SuppressResult`, `UnsuppressResult`, `IntentionItem`, `VestigeEventType`, `VestigeEvent`, `NODE_TYPE_COLORS` (8 types), `EVENT_TYPE_COLORS` (19 events), `ColorMode`, `MemoryState` (v2.0.8). + +### 5.9 Tests (`src/lib/graph/__tests__/`) + +| File | Tests | Lines | Covers | +|---|---|---|---| +| `color-mode.test.ts` **(v2.0.8, new)** | 80 | 664 | `getMemoryState` boundaries (12 retentions including NaN/±∞/>1/<0), palette integrity, `getNodeColor` dispatch, `NodeManager.setColorMode` idempotence + in-place retint + userData preservation + suppression channel isolation | +| `nodes.test.ts` | 32 | 456 | NodeManager lifecycle, easings, Fibonacci distribution | +| `edges.test.ts` | 21 | 314 | EdgeManager grow/dissolve, opacity-by-weight | +| `force-sim.test.ts` | 19 | 257 | Physics convergence, add/remove | +| `effects.test.ts` | 30 | 500 | All 12 effect types | +| `events.test.ts` | 48 | 864 | Every one of the 19 event handlers + live-spawn + eviction | +| `ui-fixes.test.ts` | 21 | 236 | Bloom retuning, glow-texture gradient, fog density, regression tests for issue #31 | +| **Total** | **251** | **3,291** | | + +Infrastructure: `three-mock.ts` (Scene / Mesh / Sprite / Material mocks), `setup.ts` (canvas context mocks including `beginPath`/`closePath`/`quadraticCurveTo` added tonight for the pill redesign), `helpers.ts` (node/edge/event factories). + +### 5.10 Build + +- `pnpm run build` → static SPA in `apps/dashboard/build/`. +- Precompressed `.br` + `.gz` per asset (adapter-static). +- **Embedded into `vestige-mcp` binary** at compile time via `include_dir!("$CARGO_MANIFEST_DIR/../../apps/dashboard/build")`. Every Rust build rebakes the dashboard snapshot. + +--- + +## 6. Integrations & Packaging + +### 6.1 IDE integration matrix (`docs/integrations/*.md`) + +All 8 IDEs documented. The common install flow: (a) download `vestige-mcp` binary, (b) point IDE's MCP config at its absolute path, (c) restart IDE, (d) verify with `/context` or equivalent. + +| IDE | Config path | Notable | +|---|---|---| +| Claude Code | `~/.claude.json` or project `.mcp.json` | Inline in `CONFIGURATION.md`; one-liner install | +| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` | Inline in `CONFIGURATION.md` | +| Cursor | `~/.cursor/mcp.json` | Absolute paths required (Cursor doesn't resolve relatives reliably) | +| VS Code (Copilot) | `.vscode/mcp.json` OR User via command | **Uses `"servers"` key, NOT `"mcpServers"`** — Copilot-specific schema. Requires agent mode enabled. | +| Codex | `~/.codex/config.toml` | TOML not JSON. `codex mcp add vestige -- /usr/local/bin/vestige-mcp` helper. | +| Xcode | Project-level `.mcp.json` | **Xcode 26.3's `claudeai-mcp` feature gate blocks global config. Project-level `.mcp.json` in project root bypasses entirely.** First cognitive memory server for Xcode. Sandboxed agents do NOT inherit shell env — absolute paths mandatory. | +| JetBrains / Junie | `.junie/mcp/mcp.json` or UI config | 2025.2+. Three paths: Junie autoconfig, Junie AI config, external MCP client. | +| Windsurf | `~/.codeium/windsurf/mcp_config.json` | Supports `${env:HOME}` variable expansion. Cascade AI. | + +### 6.2 npm packages + +| Package | Version | Role | +|---|---|---| +| `vestige-mcp-server` (in `packages/vestige-mcp-npm`) | 2.0.7 | Binary wrapper — postinstall downloads the platform-appropriate release asset from GitHub. Bins: `vestige-mcp`, `vestige`. | +| `@vestige/init` (in `packages/vestige-init`) | 2.0.7 | Interactive zero-config installer. Bin: `vestige-init`. | +| `packages/vestige-mcpb/` | — | Legacy, abandoned. | + +**Publish status:** v2.0.6 is live on npm. **v2.0.7 pending Sam's Touch ID** (WebAuthn 2FA flow, not TOTP — has to be triggered from Sam's machine). + +### 6.3 GitHub release workflow (`release.yml`) + +Triggered on tag push (`v*`) OR manual `workflow_dispatch`. Matrix: + +| Target | Runner | Artifact | Status | +|---|---|---|---| +| `aarch64-apple-darwin` | macos-latest | `vestige-mcp-aarch64-apple-darwin.tar.gz` | ✅ | +| `x86_64-unknown-linux-gnu` | ubuntu-latest | `vestige-mcp-x86_64-unknown-linux-gnu.tar.gz` | ✅ | +| `x86_64-pc-windows-msvc` | windows-latest | `vestige-mcp-x86_64-pc-windows-msvc.zip` | ✅ | +| `x86_64-apple-darwin` (Intel Mac) | **DROPPED in v2.0.7** | — | ❌ `ort-sys 2.0.0-rc.11` (pinned by fastembed 5.13.2) has no Intel Mac prebuilt | + +Each artifact contains three binaries: `vestige-mcp`, `vestige`, `vestige-restore`. + +### 6.4 CI workflow (`ci.yml`) + +Triggers: push main + PR main. Runs on macos-latest + ubuntu-latest. Steps: `cargo check` → `cargo clippy --workspace -- -D warnings` → `cargo test --workspace`. **Tonight's fix:** Rust 1.95 introduced `unnecessary_sort_by` (12 sites fixed) + `collapsible_match` (1 site fixed in `memory_states.rs`, 1 `#[allow]` on `websocket.rs` because match guards can't move non-Copy `Bytes`). + +### 6.5 Test workflow (`test.yml`) + +5 parallel jobs: `unit-tests`, `mcp-tests`, `journey-tests` (depends on unit), `dashboard` (pnpm + vitest), `coverage` (LLVM + Codecov). Env: `VESTIGE_TEST_MOCK_EMBEDDINGS=1` to skip ONNX model download in CI. + +### 6.6 Xcode setup script (`scripts/xcode-setup.sh`) + +4.9 KB interactive installer. (a) detect/install binary, (b) offer project picker under `~/Developer`, (c) generate `.mcp.json`, (d) optionally batch-install to all detected projects. Supports SHA-256 checksum verification. + +--- + +## 7. `vestige-cloud` — Current Skeleton + +**Location:** `/Users/entity002/Developer/vestige-cloud` (separate git repo, private). + +**Status as of 2026-04-19:** single-commit skeleton from 2026-02-12 (8 weeks old, one feature commit `4e181a6`). ~600 LOC. + +### 7.1 Structure + +``` +vestige-cloud/ +├── Cargo.toml # workspace, path-dep on ../vestige/crates/vestige-core +├── Cargo.lock +└── crates/ + └── vestige-http/ + ├── Cargo.toml # binary: vestige-http + └── src/ + ├── main.rs # Axum server on :3927, auth + cors middleware + ├── auth.rs # Single bearer token via VESTIGE_AUTH_TOKEN env (auto-generated if unset, stored in data-dir) + ├── cors.rs # prod: allowlist vestige.dev + app.vestige.dev; dev: permissive + ├── state.rs # Arc> shared state (SINGLE TENANT) + ├── sse.rs # /mcp/sse STUB — 3 TODOs, returns one static "endpoint" event + └── handlers/ + ├── mod.rs + ├── health.rs # GET /health (version + memory count) + ├── api.rs # REST CRUD: search, list, create, get, delete, promote, demote, stats, smart_ingest + ├── mcp.rs # POST /mcp JSON-RPC 2.0 — **ONLY 5 TOOLS** (search, smart_ingest, memory, promote_memory, demote_memory) + └── sync.rs # POST /sync/push + /sync/pull (sync/pull has TODO for `since` filter) +``` + +### 7.2 Gap analysis vs. current `vestige-mcp` + +| Dimension | vestige-mcp v2.0.7 | vestige-cloud Feb skeleton | Gap | +|---|---|---|---| +| MCP tools | 24 | 5 | 19 tools missing (session_context, dream, explore_connections, predict, importance_score, find_duplicates, memory_timeline, memory_changelog, memory_health, memory_graph, deep_reference, consolidate, backup, export, gc, restore, intention, codebase, suppress/unsuppress) | +| MCP transport | stdio + HTTP | HTTP only, no Streamable HTTP | Needs full Streamable HTTP (`Mcp-Session-Id` header, bidirectional, Last-Event-ID reconnect) per 2025-06-18 spec | +| Multi-tenancy | N/A (local) | **Single tenant** (one storage, one API key) | Need per-user DB, row-level scoping, or DB-per-tenant sharding | +| Auth | Local token | Single bearer | Need JWT, OAuth, scopes, org membership, token rotation | +| Billing | N/A | none | Need Stripe, entitlement, plans, webhooks | +| Observability | `tracing` only | `tracing` only | Need Prometheus / OTLP export, dashboards, rate limits, error budget | +| Sync | N/A | lossy push + unfiltered pull | Need tombstones, incremental pull by `since`, conflict resolution | +| Deploy | binaries + npm | **none** | Need Dockerfile, fly.toml, CI, docs | + +### 7.3 Two upgrade paths + +- **Path A (v2.6.0 "Remote"):** Upgrade the Feb skeleton to match v2.0.7 surface (5 → 24 tools), implement Streamable HTTP, ship Dockerfile + fly.toml. **Keep single-tenant.** Ship as "deploy your own Vestige on a VPS." +- **Path B (v3.0.0 "Cloud"):** Multi-tenant SaaS. Weeks of work on billing, per-tenant DB, ops. Not viable until v2.6 has traction + cashflow. + +The recommendation in Part 9 is **A only** for now. B is gated on demand signal + runway. + +--- + +## 8. Version History (v1.0 → v2.0.8) + +### 8.1 Shipped releases + +| Version | Tag | Date | Theme | Headline | +|---|---|---|---|---| +| v1.0.0 | v1.0.0 | 2026-01-25 | Initial | First MCP server with FSRS-6 memory | +| v1.1.x | v1.1.0/1/2 | — | CLI separation | stats/health moved out of MCP to CLI | +| v1.3.0 | v1.3.0 | — | — | Importance scoring, session checkpoints, duplicate detection | +| v1.5.0 | v1.5.0 | — | — | Cognitive engine, memory dreaming, graph exploration, predictive retrieval | +| v1.6.0 | v1.6.0 | — | — | 6× storage reduction, neural reranking, instant startup | +| v1.7.0 | v1.7.0 | — | — | 18 tools, automation triggers, SQLite perf | +| v1.9.1 | v1.9.1 | — | Autonomic | Self-regulating memory, graph visualization | +| **v2.0.0** | v2.0.0 | **2026-02-22** | "Cognitive Leap" | 3D SvelteKit+Three.js dashboard, WebSocket event bus (16 events), HyDE query expansion, Nomic v2 MoE option, Command palette, bloom post-processing | +| v2.0.1 | v2.0.1 | — | — | Release rebuild, install fixes | +| v2.0.3 | v2.0.3 | — | — | Clippy fixes, CI alignment | +| v2.0.4 | v2.0.4 | 2026-04-09 | "Deep Reference" | **8-stage cognitive reasoning tool, `cross_reference` alias**, retrieval_mode (precise/balanced/exhaustive), token budgets raised 10K → 100K, CORS hardening | +| v2.0.5 | v2.0.5 | 2026-04-14 | "Intentional Amnesia" | **Active forgetting** — suppress tool #24, Rac1 cascade (72h async neighbour decay), 24h labile reversal window, graph node visual suppression (20% opacity, no emissive) | +| v2.0.6 | v2.0.6 | 2026-04-18 | "Composer" | 6 live graph reactions (Suppressed, Unsuppressed, Rac1, Connected, ConsolidationStarted, ImportanceScored), `VESTIGE_SYSTEM_PROMPT_MODE=full` opt-in | +| **v2.0.7** | v2.0.7 | 2026-04-19 | "Visible" | V11 migration drops dead tables; `/api/memories/{id}/suppress` + `/unsuppress` endpoints + UI button; sidebar `up 3d 4h` footer via `uptime_secs`; graph error-state split; `predict` degraded flag; `changelog` start/end honored; `intention` include_snoozed; `suppress` MCP tool (was dashboard-only); tool-count reconciled 23 → 24; Intel Mac dropped from release workflow; defensive `Err` on unknown export format | +| **v2.0.8** | *(unreleased, merged to main 2026-04-19 22:10 CT)* | — | — | FSRS memory-state colour mode (`ColorMode` type/state toggle) + floating legend + dark-glass-pill label redesign + 80 new tests + Rust 1.95 clippy compat (12 sites) | + +### 8.2 Current git state + +- **HEAD:** `main` at `30d92b5` "feat(graph): redesign node labels as dark glass pills" +- **Last 4 commits on main (v2.0.8):** + - `30d92b5` — Label pill redesign + - `d7f0fe0` — 80 new color-mode tests + - `318d4db` — Rust 1.95 clippy compat + - `4c20165` — Memory-state color mode + legend +- **Branches:** + - `main` (default, protected via CI-must-pass) + - `feat/v2.0.8-memory-state-colors` (fast-forwarded into main tonight) + - `feat/v2.1.0-qwen3-embed` (Day 2 done; Day 3 pending on Sam's M3 Max arrival) + - `chore/v2.0.7-clean` (post-v2.0.7 cleanup branch) + - `wip/v2.0.7-v11-migration` (transport branch for cross-machine stash) +- **Latest tag:** `v2.0.7` (force-updated on main after v2.0.6 rebase incident) +- **Latest CI run on main:** #24646176395 ✅ all 4 jobs (Test macos, Test ubuntu, Release aarch64-darwin, Release x86_64-linux) + +### 8.3 Open GitHub issues / PRs + +- **Closed #35** — "npm publish delay 2.0.6"; replied in v2.0.6 with one-liner install command +- **Open #36** — desaiuditd: "hooks-for-automatic-memory request" — customer conversion opportunity, not yet responded + +--- + +## 9. The Next-Phase Plan + +**Shipping cadence:** weekly minor bumps (v2.1 → v2.2 → v2.3 ...) until v3.0 which gates on multi-tenancy + CoW storage. Ships ~Monday each week with content post same day + follow-up Wednesday + YouTube Friday. + +### 9.1 v2.1.0 "Decide" — Qwen3 embeddings *(in-flight)* + +**Branch:** `feat/v2.1.0-qwen3-embed` (pushed). +**Status:** scaffolding merged; Day 3 pending. +**ETA:** ~1 week after M3 Max arrival (FedEx hold at Walgreens, pickup 2026-04-20). + +**What's in:** `qwen3-embed` feature flag gates a Candle-based Qwen3 embed backend. `qwen3_format_query()` helper for the query-instruction prefix. Metal device selection with CPU fallback. `DEFAULT_DIMENSIONS` feature-gated 256/1024. Dual-index routing scaffolded. + +**What's left (Day 3):** +- Storage write-path records `embedding_model` per node. +- `semantic_search_raw` uses `qwen3_format_query` when feature active. +- Dual-index routing: old Nomic-256 nodes stay on their HNSW, new Qwen3-1024 nodes go on a new HNSW. Search merges with trust weighting. +- End-to-end test: ingest on Qwen3 → retrieve on Qwen3 at higher accuracy than Nomic. + +**Test gate:** `cargo test --workspace --features qwen3-embed --release` green. Current baseline: 366 core + 425 mcp passing. + +### 9.2 v2.2.0 "Pulse" — Subconscious Cross-Pollination **★ VIRAL LOAD-BEARING RELEASE** + +**ETA:** 1-2 weeks after v2.1 lands. + +**What it does:** While the user is doing anything else (typing a blog post, looking at a different tab, doing nothing), Vestige's running `dream()` in the background. When dream completes with `insights_generated > 0` or a `ConnectionDiscovered` event fires from spreading activation, **the dashboard pulses a toast** on the side: *"Vestige found a connection between X and Y. Here's the synthesis."* The bridging edge in the 3D graph flashes cyan and briefly thickens. + +**Why viral:** This is the single most tweet/YouTube-friendly demo in the entire roadmap. It is the "my 3D brain is thinking for itself" moment. + +**Backend (≈2 days):** +1. `ConsolidationScheduler` gains a "pulse" hook: after each cycle, if `insights_generated > 0` emit a new `InsightSurfaced` event with `{source_memory_id, target_memory_id, synthesis_text, confidence}`. +2. The existing `ConnectionDiscovered` event gets a richer payload: include both endpoint IDs + a templated synthesis string derived from the two memories' content. +3. Rate-limit pulses: max 1 per 15 min unless user is actively using the dashboard. + +**Frontend (≈5 days):** +1. New Svelte component `InsightToast.svelte` — slides in from right, shows synthesis text + "View connection" button, auto-dismisses after 10s. +2. `events.ts` mapping: `InsightSurfaced` → locate bridging edge in graph, pulse it cyan for 2s, thicken to 2× for 500ms, play a soft chime (optional, muted by default). +3. Toast queue so rapid dreams don't flood. +4. Preference: user can toggle pulse sound / toast / edge animation independently in `/settings`. + +**Already exists (nothing to build):** +- `dream()` 5-stage cycle — YES +- `DreamCompleted` event with `insights_generated` — YES +- `ConnectionDiscovered` event + WebSocket broadcast — YES +- 3D edge animation system in `events.ts` — YES (handler exists, just doesn't emit toast) +- ConsolidationScheduler running on `VESTIGE_CONSOLIDATION_INTERVAL_HOURS` — YES + +**Never-composed alarm:** Four existing components, zero lines of composition. This feature is **~90% latent in v2.0.7**. All we do is press the button. + +**Acceptance criteria:** +- Start Vestige, idle for 10 min, verify a pulse fires from scheduled dream cycle. +- Ingest 3 semantically adjacent memories from completely different domains (e.g., F1 aerodynamics, memory leak, fluid dynamics), trigger dream, verify connection pulse fires with synthesis text mentioning both source + target. +- Dashboard test coverage: add `pulse.test.ts` with 15+ cases covering toast queue, rate limit, event shape, edge animation. + +**Launch day:** Film a 90-second screen recording. Post to Twitter + Hacker News + LinkedIn + YouTube same day. + +### 9.3 v2.3.0 "Rewind" — Time Machine + +**ETA:** 2-3 weeks after v2.2 ships. + +**What it does:** The graph page gets a horizontal time slider. Drag back in time → nodes dim based on retroactive FSRS retention, edges that were created after the slider's timestamp dissolve visibly, suppressed memories un-dim to their pre-suppression state. A "Pin" button snapshots the current slider state into a named checkpoint the user can return to. + +**Backend (≈4 days):** +1. New core API: `Storage::memory_state_at(memory_id, timestamp) -> MemorySnapshot` — reconstructs a node's FSRS state at an arbitrary past timestamp by replaying `state_transitions` forward OR applying FSRS decay backward from the current state. +2. New MCP tool: `memory_graph_at(query, depth, max_nodes, timestamp)` — the existing graph call with a time parameter. +3. New MCP tool: `pin_state(name, timestamp)` — persists a named snapshot (just a row in a new `pins` table: name, timestamp, created_at). +4. New core API: `list_pins()` + `delete_pin(name)`. + +**Frontend (≈7 days):** +1. `TimeSlider.svelte` already exists as a scaffold (listed in §5.5) — upgrade it to an HTML5 range input + play/pause + speed control. +2. Graph3D consumes a new `asOfTimestamp` prop. When set, uses `temporal.ts::retentionAtDate()` to re-project every node's opacity + size. +3. Edges: hide those with `created_at > slider`. Animate the dissolution so sliding feels organic. +4. Pin sidebar: list pinned states, click to jump, rename/delete. + +**Cut from scope: branching.** Git-like "what if I forgot my Python biases" requires CoW storage = full schema migration = v3.0 territory. Scope it out explicitly. + +**Acceptance criteria:** +- Slide back 30 days, verify node count drops to whatever existed 30 days ago. +- Slide back through a suppression event, verify node un-dims. +- Pin "before Mays deadline", verify pin jumps restore exact state. + +### 9.4 v2.4.0 "Empathy" — Emotional Context Tagging **★ FIRST PRO-TIER GATE CANDIDATE** + +**ETA:** 2-3 weeks after v2.3 ships. + +**What it does:** Vestige's MCP middleware watches tool call metadata for frustration signals — repeated retries of the same query, CAPS LOCK content, explicit correction phrases ("no that's wrong", "actually..."), rapid-fire consecutive calls. When detected, the current active memory gets an automatic `ArousalSignal` boost and a `frustration_detected_at` timestamp. Next session, when the user returns to a similar topic, the agent proactively surfaces: *"Last time we worked on this, you were frustrated with the API docs. I've pre-read them."* + +**Why Pro-tier:** Invisible to demo (so doesn't hurt OSS growth), creates deep lock-in, quantifiable value ("Vestige saved you X minutes of re-frustration this month"), clear paid-hook rationale. + +**Backend (≈4 days):** +1. New middleware layer in `vestige-mcp` between JSON-RPC dispatch and tool execution: `FrustrationDetector`. Analyzes tool args for: (a) retry pattern (same `query` field within 60s), (b) content ≥70% caps after lowercase comparison, (c) correction regex (`no\s+that|actually|wrong|fix this|try again`). +2. On detection, fire a synthesized `ArousalSignal` to `ImportanceTracker` for the most-recently-accessed memory. +3. New core API: `find_frustration_hotspots(topic, limit)` → returns memories with `arousal_score > threshold` + their `frustration_detected_at` timestamps. +4. `session_context` tool gains a new field: `frustration_warnings[]` — "Topic X had previous frustration; here's what we know." + +**Frontend (≈3 days):** +1. Memory detail pane shows an orange "Frustration" badge for high-arousal memories. +2. `/stats` adds a "Frustration hotspots" section. + +**Acceptance criteria:** +- Simulate 3 rapid retries of the same query, verify ArousalSignal boosts the active memory. +- Simulate caps-lock content, verify detection. +- Return to same topic next session, verify `session_context` surfaces warning. + +### 9.5 v2.5.0 "Grip" — Neuro-Feedback Cluster Gestures + +**ETA:** 2 weeks after v2.4 ships. + +**What it does:** In the 3D graph, drag a memory sphere to "grab" it — its cluster highlights. Squeeze (pinch gesture or modifier key + drag inward) → promotes the whole cluster. Flick away (throw gesture) → triggers decay on the cluster. + +**Backend (≈2 days):** +1. New MCP tool: `promote_cluster(memory_ids[])` — applies promote to each. +2. New MCP tool: `demote_cluster(memory_ids[])` — inverse. +3. Cluster detection helper: `find_cluster(source_id, similarity_threshold)` — leverages existing `find_duplicates` + spreading activation. + +**Frontend (≈5 days):** +1. Three.js gesture system: drag detection, cluster highlight (emissive pulse on all cluster members), squeeze detection (pointer velocity inward), flick detection (pointer velocity outward past threshold). +2. Visual feedback: green ring on squeeze (promote), red dissipation on flick (demote). +3. Accessibility: keyboard alternative — select node, press `P` / `D` to promote/demote cluster. + +### 9.6 v2.6.0 "Remote" — `vestige-cloud` Self-Host Upgrade + +**ETA:** 3 weeks after v2.5 ships. First paid-tier candidate if empathy doesn't convert first. + +**What it does:** Turns the Feb `vestige-cloud` skeleton into a shippable self-host product. One-liner install → Docker container or fly.io deploy → point Claude Desktop/Cursor/Codex at the remote URL → cloud-persistent memory across all your devices. + +**Scope:** +1. Upgrade MCP handler from 5 → 24 tools (port each tool from `crates/vestige-mcp/src/tools/`). +2. Implement **MCP Streamable HTTP transport** (spec 2025-06-18): `Mcp-Session-Id` header, bidirectional event stream, Last-Event-ID reconnect, JSON-RPC batching. +3. Per-user SQLite at `/data/$USER_ID.db` (single-tenant but scoped by `VESTIGE_USER_ID` env — "single-tenant but deploy-multiple"). +4. `Dockerfile` (multi-stage: Rust build + fastembed model baked in). +5. `fly.toml` with persistent volume mount on `/data`. +6. `docker-compose.yml` for local Postgres-if-needed (probably not — stick with SQLite for self-host). +7. `scripts/cloud-deploy.sh` one-liner installer. +8. Docs: `docs/cloud/self-host.md` step-by-step. + +**Explicitly OUT of scope for v2.6:** Stripe, multi-tenant DB, user accounts, rate limits, billing. Those are v3.0. + +### 9.7 v3.0.0 "Branch" — CoW memory branching + SaaS multi-tenancy + +**ETA:** Q3 2026 at earliest. Gated on: +- v2.6 adoption signal (≥500 self-host deployments) +- Sam's runway (needs pre-revenue or funding) +- Either Mays, Orbit Wars, or another cash injection + +**What it does:** +1. **Memory branching** — git-like CoW over SQLite. Branch a memory state, diverge freely, merge or discard. "What if I forgot all my Python biases and approached this memory as a Rust expert" becomes a one-button operation. +2. **Multi-tenant SaaS** at `vestige.dev` / `app.vestige.dev`. Per-user DB shards, JWT auth + OAuth providers, Stripe subscriptions with entitlement gates, org membership, team shared memory with role-based access. + +**Major subsystems required:** +- Storage layer rewrite for CoW semantics (or adopt Dolt/sqlcipher with branching). +- Auth: JWT + OAuth (Google, GitHub, Apple) + bcrypt fallback. +- Billing: Stripe subscriptions + webhooks + dunning. +- Admin dashboard: support, usage analytics, churn. +- Multi-region: at minimum US-east + EU (GDPR). +- Observability: Prometheus + Grafana + Sentry + Honeycomb tracing. + +**Explicitly NOT a v2.x goal.** Any earlier attempt burns runway. + +### 9.8 Summary roadmap table + +| Version | Codename | Theme | Effort | Load-bearing for | ETA | +|---|---|---|---|---|---| +| v2.1 | Decide | Qwen3 embeddings | ~1 week | Retrieval quality + differentiation vs. Nomic | Days | +| **v2.2** | **Pulse** | **Subconscious cross-pollination** | **~1 week (mostly latent)** | **★ Viral launch moment** | **~2 weeks** | +| v2.3 | Rewind | Time machine (slider + pin) | ~2 weeks | Technical moat, impressive demo | ~5 weeks | +| v2.4 | Empathy | Frustration detection → arousal boost | ~1 week | **First Pro-tier gate candidate** | ~7 weeks | +| v2.5 | Grip | Cluster gestures | ~1 week | Polish | ~9 weeks | +| v2.6 | Remote | vestige-cloud self-host (5→24 tools + Streamable HTTP + Docker) | ~3 weeks | Foundation for SaaS; secondary Pro-tier gate | ~12 weeks | +| v3.0 | Branch | CoW branching + multi-tenant SaaS | ~3 months | Revenue | Q3 2026 at earliest | + +--- + +## 10. Composition Map + +For each v2.x feature, what existing primitives does it compose? + +| Feature | Existing primitive | How composed | +|---|---|---| +| v2.2 Pulse | `dream()` + `ConsolidationScheduler` + `ConnectionDiscovered` event + Three.js `events.ts::mapEventToEffects` | Consume the already-firing events; add toast UI + richer synthesis payload | +| v2.3 Rewind slider | `state_transitions` append log + FSRS decay formula + `temporal.ts::retentionAtDate()` + existing `TimeSlider.svelte` stub | Retroactive state reconstruction + slider upgrade | +| v2.3 Rewind pins | `smart_ingest` patterns + new `pins` table | Thin new table + two new tools | +| v2.4 Empathy | `ArousalSignal` (already in ImportanceSignals 4-channel model) + middleware pattern + `ImportanceTracker` | New middleware layer feeds existing arousal channel | +| v2.5 Grip | `find_duplicates` clustering + `promote`/`demote` + v2.0.8 Three.js node picking | Cluster-level wrapper over per-node operations | +| v2.6 Remote | v2.0.7 MCP tool implementations + vestige-cloud Feb skeleton + Axum | Port tools; implement Streamable HTTP; containerize | +| v3.0 Branch | Requires new CoW storage layer — **no existing primitive composes here** | Greenfield storage rewrite | +| v3.0 SaaS | Requires new auth + billing + multi-tenancy — **no existing primitive composes** | Greenfield | + +**Key insight:** v2.2-v2.6 are all ≥60% latent in existing primitives. v3.0 is the first release that requires significant greenfield work. This is why sequencing matters: ride the existing primitives to revenue, then greenfield. + +--- + +## 11. Risks & Known Gaps + +### 11.1 Technical + +| Risk | Impact | Mitigation | +|---|---|---| +| `ort-sys 2.0.0-rc.11` prebuilt gaps (Intel Mac dropped, Windows MSVC with usearch 2.24 broken) | Fewer platforms ship | Wait for ort-sys 2.1; or migrate to Candle throughout (v2.1 Qwen3 already uses Candle) | +| `usearch` pinned to 2.23.0 (2.24 regression on MSVC) | Windows build fragility | Monitor usearch#746 | +| fastembed model download (~130MB for Nomic, ~500MB for Qwen3) on first run blocks sandboxed Xcode | UX friction | Cache at `~/Library/Caches/com.vestige.core/fastembed` — documented in Xcode guide; pre-download from terminal once | +| Tool count drift (23 vs 24 across docs) | User trust | Reconciled in v2.0.7 (`docs: tool-count reconciliation`) | +| Large build times (cargo release 2-3 min incremental, 6+ min clean) | Slow iteration | M3 Max arriving Apr 20 will halve this | +| `include_dir!` bakes dashboard build into binary at compile time | Have to rebuild Rust to update dashboard | Accept as design; HMR via `pnpm dev` for iteration | + +### 11.2 Product + +| Risk | Impact | Mitigation | +|---|---|---| +| OSS-growth-before-revenue means months of zero cash | Sam can't pay rent | Mays May 1 ($400K+), Orbit Wars June 23 ($5K × top 10), part-time Wrigley Field during Cubs season | +| `deep_reference` is the crown jewel but rarely invoked | Users don't discover it | `CLAUDE.md` flags it; v2.2 Pulse farms the viral moment to drive awareness | +| Subconscious Pulse may fire too often or too rarely | User annoyance or missed value | Rate limit: max 1 pulse per 15 min; user-adjustable in settings | +| Emotional tagging may over-fire (every caps lock = frustration?) | False positives | Require ≥2 signals (retry + caps, or retry + correction) before boost | +| v3.0 SaaS burns runway if started too early | Business-ending | Gated on v2.6 adoption + cash injection | +| Copycat risk (Zep, Cognee, etc.) cloning Vestige's features | Eroded differentiation | AGPL-3.0 protects network use; neuroscience depth is hard to fake; time slider + subconscious pulse are visible moats | +| Cross-IDE MCP standard changes (Streamable HTTP spec moved 2024-11-05 → 2025-06-18) | Breaking transport changes | v2.6 implements the newer spec; keep 2024-11-05 as backward-compat alias | + +### 11.3 Known UI gaps (`docs/launch/UI_ROADMAP_v2.1_v2.2.md`) + +- **26% of MCP tools have zero UI surface** (e.g., `codebase`, `find_duplicates`, `backup`, `export`, `gc`, `restore` — all power-user only). +- **28% of cognitive modules have no visualization** (SynapticTagging, HippocampalIndex, ContextMatcher, CrossProjectLearner, etc.). +- The rainbow-bursted Rac1 cascade in the graph has no numeric "how many neighbours did it touch" display. +- `intention` shows but doesn't let you edit/snooze from the UI. +- `deep_reference` is unreachable from the dashboard (it only surfaces via MCP tool calls). + +--- + +## 12. Viral / Launch / Content Plan + +### 12.1 Content cadence (fixed) + +**Mon–Fri till June 13 graduation:** +- 1-2 posts/day across Twitter + LinkedIn + Hacker News + Reddit r/LocalLLaMA + r/selfhosted +- Weekly YouTube long-form (Friday release) + +### 12.2 Per-release launch playbook + +For every v2.x release: +1. **Monday:** Tag + release + content drop (tweet with 30-90s demo video + HN post). +2. **Tuesday:** LinkedIn long-form + Reddit cross-post. +3. **Wednesday:** Follow-up tweet thread (deep-dive on one specific feature). +4. **Thursday:** Engage with feedback; close issues; publish patch if needed. +5. **Friday:** YouTube long-form (15-25 min walkthrough). Next week's release work continues. + +### 12.3 Viral load-bearing moments + +- **v2.2 "Pulse" launch:** The single biggest viral bet. Subconscious cross-pollination demo → HN front page → Twitter thread → YouTube 10-min walkthrough. +- **v2.3 "Rewind" time slider:** Highly tweet-friendly. Screen recording of sliding back through memory decay. +- **Jarrett Ye (FSRS creator, user L-M-Sherlock) outreach:** Already a stargazer. Email him Sunday night (US time) = Monday AM Beijing with the v2.2 Pulse demo. If he retweets → FSRS community (Anki, maimemo) amplifies. + +### 12.4 Issue #36 (hooks-for-automatic-memory) + +Outstanding from desaiuditd. Response plan: +1. Thank him publicly in the issue. +2. Acknowledge the feature as valid and scoped for v2.2/v2.3. +3. Open a linked sub-issue: "v2.2: Auto-memory hooks" tied to Pulse work. + +### 12.5 Monetization gates + +**Two candidate first-gates:** +1. **v2.4 Empathy (Emotional tagging)** — invisible to OSS demos, strong retention, clean paid-feature framing ("Vestige notices when you're frustrated; free tier gets 100 detection events/month, Pro gets unlimited + frustration hotspot analytics"). +2. **v2.6 Remote (Cloud self-host binary)** — "free binary forever; paid-tier cloud-managed deploy with backups + observability + multi-device sync." + +Pick after v2.2 viral signal tells us whether retention or convenience is the weaker link. + +--- + +## 13. How AI Agents Should Consume This Doc + +### 13.1 First-time read protocol + +If this is the first time you're seeing Vestige: +1. Read Part 0 (Executive Summary) + Part 1 (What Vestige Is). That's 3 minutes. +2. Read Part 9 (The Plan). That's 10 minutes. +3. Bookmark Parts 3-6 for reference. + +### 13.2 When Sam asks you to plan a feature + +1. Check Part 9 — is it already scoped? If yes, that section IS your spec. +2. If not, work it into the existing roadmap: which version should it ship in, what primitives does it compose (Part 10), what risks apply (Part 11)? +3. Follow the `/Users/entity002/.claude/rules/active-synthesis.md` protocol (6 mandatory behaviors): compose, don't summarize. + +### 13.3 When Sam asks you to implement + +1. Find the exact file paths in Parts 3-5. +2. Check existing test coverage (Part 5.9 for dashboard, §3.11 for core). +3. Before claiming something exists, grep or read the source — memory alone is insufficient (per `CLAUDE.md` SCOUR rule). +4. Rust 1.95 toolchain — be aware of the new lints (`unnecessary_sort_by`, `collapsible_match`). + +### 13.4 When Sam asks for strategic advice + +- Apply the `/Users/entity002/.claude/rules/cross-reference.md` rule: check evidence from the exact setup before recommending. +- Apply the `always-positive-energy` rule: recommend the BEST path, not the safest. +- This doc's Part 9 is the committed roadmap. Deviate only with explicit justification. + +### 13.5 Load-bearing files to never forget + +- `/Users/entity002/Developer/vestige/CLAUDE.md` — project-level Claude instructions. +- `/Users/entity002/.claude/rules/active-synthesis.md` — 6 mandatory synthesis behaviors. +- `/Users/entity002/.claude/rules/cross-reference.md` — exact-setup evidence rule. +- `/Users/entity002/CLAUDE.md` — global Claude instructions (SCOUR + always-positive-energy). +- `/Users/entity002/Developer/vestige/docs/launch/UI_ROADMAP_v2.1_v2.2.md` — prior UI research compilation. +- **This file** — `/Users/entity002/Developer/vestige/docs/VESTIGE_STATE_AND_PLAN.md`. + +--- + +## 14. Glossary & Citations + +### 14.1 Acronyms + +| Term | Meaning | +|---|---| +| **MCP** | Model Context Protocol — JSON-RPC protocol for AI tool integration (Anthropic, 2024) | +| **FSRS** | Free Spaced Repetition Scheduler — algorithm by Jarrett Ye (maimemo), generation 6 | +| **PE Gating** | Prediction Error Gating — decide CREATE/UPDATE/SUPERSEDE by similarity threshold | +| **SIF** | Suppression-Induced Forgetting — Anderson 2025 | +| **Rac1** | Rho-family GTPase — actin-destabilization mediator of cascade decay (Cervantes-Sandoval & Davis 2020) | +| **SWR** | Sharp-wave ripple — hippocampal replay pattern used by Vestige's dream cycle | +| **HNSW** | Hierarchical Navigable Small World — graph index for fast approximate nearest neighbour | +| **CoW** | Copy-on-write — storage technique for cheap branching | +| **AGPL** | Affero General Public License — copyleft including network use | + +### 14.2 Neuroscience citations + +- Anderson, M. C. (2025). Suppression-induced forgetting — top-down inhibitory control of retrieval. +- Anderson, M. C., Bjork, R. A., & Bjork, E. L. (1994). Remembering can cause forgetting. +- Bjork, R. A., & Bjork, E. L. (1992). A new theory of disuse and an old theory of stimulus fluctuation. — dual-strength model. +- Brown, R., & Kulik, J. (1977). Flashbulb memories. +- Cervantes-Sandoval, I., & Davis, R. L. (2020). Rac1-mediated forgetting. +- Collins, A. M., & Loftus, E. F. (1975). A spreading-activation theory of semantic processing. +- Frey, U., & Morris, R. G. M. (1997). Synaptic tagging and long-term potentiation. +- Friston, K. J. (2010). The free-energy principle: a unified brain theory. +- Nader, K., Schafe, G. E., & LeDoux, J. E. (2000). Fear memories require protein synthesis in the amygdala for reconsolidation after retrieval. +- Teyler, T. J., & Rudy, J. W. (2007). The hippocampal indexing theory. +- Tulving, E., & Thomson, D. M. (1973). Encoding specificity and retrieval processes. + +### 14.3 Technical citations + +- MCP Spec (2025-06-18 Streamable HTTP): https://modelcontextprotocol.io/specification +- FSRS-6: https://github.com/open-spaced-repetition/fsrs-rs +- Nomic Embed Text v1.5: https://huggingface.co/nomic-ai/nomic-embed-text-v1.5 +- Qwen3 Embed: https://huggingface.co/Qwen/Qwen3-Embedding-0.6B +- USearch: https://github.com/unum-cloud/usearch +- Jina Reranker v1 Turbo: https://huggingface.co/jinaai/jina-reranker-v1-turbo-en + +--- + +## 15. POST-v2.0.8 ADDENDUM — The Autonomic Turn (added 2026-04-23) + +> This section supersedes portions of sections 9.1-9.8. The April 19 roadmap (v2.1 Decide → v2.2 Pulse → v2.3 Rewind → v2.4 Empathy → v2.5 Grip → v2.6 Remote → v3.0 Branch) remains the long-arc plan but has been RESEQUENCED post-v2.0.8 ship following a three-agent audit on 2026-04-23 (web research on 2026 SOTA, Vestige code audit for active-vs-passive paths, competitor landscape). Updated sequence reflects what got absorbed into v2.0.8 and the new v2.0.9 / v2.5 / v2.6 architecture tier that replaces the old placeholder numbering. + +### 15.1 What v2.0.8 "Pulse" absorbed + +v2.0.8 shipped (commit `6a80769`, tag `v2.0.8`, 2026-04-23 07:21Z) bundled: + +- **v2.2 "Pulse" InsightToast** (from April 19 roadmap) — real-time toast stack over the WebSocket event bus; DreamCompleted / ConsolidationCompleted / ConnectionDiscovered / MemoryPromoted/Demoted/Suppressed surface automatically. +- **v2.3 "Terrarium" Memory Birth Ritual** — 60-frame elastic materialization on every `MemoryCreated` event. +- **8 new dashboard surfaces** exposing the cognitive engine: `/reasoning`, `/duplicates`, `/dreams`, `/schedule`, `/importance`, `/activation`, `/contradictions`, `/patterns`. +- **Reasoning Theater** wired to the 8-stage `deep_reference` cognitive pipeline with Cmd+K Ask palette. +- **3D graph brightness** auto-compensation + user slider (0.5×–2.5×, localStorage-persisted). +- **Intel Mac restored** via `ort-dynamic` + Homebrew onnxruntime (closes #41, sidesteps Microsoft's upstream deprecation of x86_64 macOS ONNX Runtime prebuilts). +- **Cross-reference hardening** — contradiction-detection false positives from 12→0 on an FSRS-6 query; primary-selection topic-term filter (50% relevance + 20% trust + 30% term_presence) fixes off-topic-high-trust-wins-query bug. + +Post-v2.0.8 hygiene commit `0e9b260` removed 3,091 LOC of orphan code (9 superseded tool modules + ghost env-var docs + one dead fn). + +### 15.2 The audit finding — "decorative memory" at system scale + +Three agents ran in parallel on 2026-04-23. Core diagnosis: **Vestige has 30 cognitive modules but only 2 autonomic mechanisms** (6h auto-consolidation loop + per-tool-call scheduler at `server.rs:884`). The 20-event WebSocket bus at `dashboard/events.rs` has **zero backend subscribers** — all 14 live event types flow to the dashboard and terminate. Fully-built trigger methods exist but nothing calls them: + +- `ProspectiveMemory::check_triggers()` at `prospective_memory.rs:1260` — 9h intention window, never polled. +- `SpeculativeRetriever::prefetch()` at `advanced/speculative.rs` (606 LOC) — never awaited. +- `MemoryDreamer::run_consolidation_cycle()` — instantiated on CognitiveEngine but the 6h timer at `main.rs:258` calls only `storage.run_consolidation()` (FSRS decay), never the dreamer. + +Three completely dead modules: `MemoryCompressor`, `AdaptiveEmbedder`, `EmotionalMemory` (constructed in `CognitiveEngine::new()` at `cognitive.rs:145-160`, zero call sites in vestige-mcp). `Rac1CascadeSwept`, `ActivationSpread`, `RetentionDecayed` events declared but never emitted. + +**This is the ARC-AGI-3 pattern at system scale:** storage exists, retrieval exists, memory never self-triggers during the agent's decision path because no subscriber is listening. Sam's paraphrased thesis: *"the bottleneck won't be how much the agent knows — it will be how efficiently it MANAGES what it knows."* + +### 15.3 The 2026 SOTA convergence — "retrieval is solved, management is not" + +Web-research agent surfaced the consensus. Load-bearing papers + their unshipped primitives: + +- **Titans** (arXiv 2501.00663, Google NeurIPS 2025) — test-time weight updates via surprise gradient. Active IN-MODEL. +- **A-Mem** (arXiv 2502.12110) — Zettelkasten dynamic re-linking on write. +- **Memory-R1** (arXiv 2508.19828) — RL-trained Manager with ADD/UPDATE/DELETE/NOOP on 152 QA pairs; beats baselines on LoCoMo + MSC + LongMemEval. +- **Mem-α** (arXiv 2509.25911) — RL over tripartite core/episodic/semantic memory, trained on 30k tokens, generalizes to 400k. +- **MemR³** (arXiv 2512.20237) — closed-loop router with retrieve/reflect/answer decision + evidence-gap tracking. +- **SleepGate** (arXiv 2603.14517) + **LightMem** (arXiv 2510.18866) — sleep-phase offline consolidation, timer-decoupled autonomous. +- **StageMem** (arXiv 2604.16774) + **Evidence for Limited Metacognition in LLMs** (arXiv 2509.21545) — item-level confidence separated from retention, validity-screened selective abstention. +- **Memory in the Age of AI Agents** survey (arXiv 2512.13564) — taxonomy (Forms/Functions/Dynamics); all open problems live in Dynamics. + +**Three unshipped-by-anyone concepts define the 2026 frontier:** meta-memory / confidence-gated generation (refuse to answer when load-bearing memory is cold), autonomous consolidation on surprise/drift (not on timer), write-time contradiction detection with agent-facing alerts. + +### 15.4 Competitive landscape — the white-space lanes + +Nobody ships: **confidence-gated generation, proactive contradiction flagging without query, predictive pre-warm at UserPromptSubmit, autonomic working-memory capacity enforcement.** + +- Mem0 v2 (Apr 16, 2026): auto-dedup (0.9 threshold), single-pass fact extraction. Retrieval still query-triggered. +- Letta: sleep-time agents mutate shared memory blocks asynchronously (most actively-managing shipped product). Archival/recall still query-triggered. +- Zep Graphiti: temporal invalidation via valid-until edges, community summarization. Retrieval still query-triggered. +- Pieces LTM-2: OS-level auto-OCR capture (most aggressive autonomous capture). No autonomous management. +- Anthropic Claude Code: 95%-context auto-compaction. No trust-scored memories, no scheduled dream, no confidence gating. +- Google Titans: surprise-gated memory IN-MODEL; not a server-level primitive. + +Every one of those four white-space primitives has raw material **already built** in Vestige (FSRS-6 trust scores, `deep_reference`, `predict`, `SpeculativeRetriever`, WebSocket event bus, Sanhedrin POC from April 20). The bottleneck is wiring, not features. + +### 15.5 v2.0.9 "Autopilot" — Weekend Ship (2-3 days) + +**Single architectural change**: add a backend event-subscriber task in `main.rs` (~50-100 LOC `tokio::spawn`) that consumes the existing WebSocket bus and routes events into the cognitive modules that already have trigger methods. This one commit flips 14 dormant primitives into active ones simultaneously. + +**Concrete wiring:** + +| Event | Currently emits to | Add backend routing | +|---|---|---| +| `MemoryCreated` | dashboard only | `synaptic_tagging.trigger_prp()` + `predictive_memory.record_save()` + `cross_project.record_pattern()` | +| `SearchPerformed` | dashboard only | `speculative.prefetch()` awaited in background task | +| `MemoryPromoted` | dashboard only | `activation_network.cascade_reinforce(neighbors, 0.3)` | +| `MemorySuppressed` | dashboard only | emit `Rac1CascadeSwept` (currently declared never-emitted) | +| `ImportanceScored > 0.85` | dashboard only | auto-`promote` | +| `DeepReferenceCompleted` with contradictions | dashboard only | queue a `dream()` cycle for contradiction-resolution | + +**Three additional changes:** + +1. New 60s `tokio::interval` in `main.rs` calls `cog.prospective_memory.check_triggers(current_session_context)`. On hit, emit new `IntentionFired` event + MCP sampling/createMessage notification to the client. +2. Add `cognitive.dreamer.run_consolidation_cycle()` call inside the existing 6h auto-consolidation loop at `main.rs:258` (alongside, not replacing, `storage.run_consolidation()`). +3. `find_duplicates` auto-runs when `Heartbeat.total_memories > 700`. + +**Launch narrative:** *"Vestige now acts on your memories while you sleep — 14 cognitive modules that used to wait for a query now fire autonomously on every memory event."* + +### 15.6 v2.5.0 "Autonomic" — 1 Week After v2.0.9 + +Three unshipped-by-anyone primitives land in one release. This is the category-defining drop. + +**(A) Hallucination Guillotine — Confidence-Gated Veto** + +Stop hook runs `deep_reference` on the agent's draft response, checks FSRS retention on load-bearing claims. If any required fact has retention < 0.4, exits 2 with a `VESTIGE VETO: cold memory on claim X, retrieve fresh evidence or explicitly mark uncertain` block. The Sanhedrin POC from 2026-04-20 already proves the mechanism works in real dogfooding — three consecutive drafts were vetoed by the POC. Package as a formal `vestige-guillotine` Claude Code plugin. + +Files: new `crates/vestige-mcp/src/hooks/guillotine.rs`, plugin manifest in `packages/claude-plugin/`. Composes existing `deep_reference` trust-score pipeline + the Sanhedrin dogfooding script. + +**(B) Contradiction Daemon — Write-Time Alerting** + +On every `smart_ingest` write, a fast `deep_reference` runs against the existing graph. If the new memory contradicts an existing memory with trust > 0.6, the server fires an MCP sampling/createMessage notification to the agent *in the same conversation:* *"this contradicts memory Y from \[date\]. Supersede Y, discard X, or mark both as time-bounded?"* The agent resolves the conflict in real time instead of waking up to it three sessions later. + +Files: `crates/vestige-mcp/src/tools/smart_ingest.rs` (post-write hook), `crates/vestige-mcp/src/protocol/sampling.rs` (new — MCP sampling/createMessage support). Composes existing `deep_reference` + contradiction-detection hardening from v2.0.8. + +**(C) Pulse Prefetch — Predictive Pre-Warm at UserPromptSubmit** + +UserPromptSubmit hook fires `predict(query)`, top-k results injected into agent context before the first token. The agent never has to ask; the memory is already there. Nemori did predict-calibrate; Letta does sleep-time; nobody fires at query-arrival. + +Files: `crates/vestige-mcp/src/hooks/pulse_prefetch.rs` (new), extend `SpeculativeRetriever::prefetch()`. Composes existing `predict` tool + `speculative.rs` (606 LOC, never awaited until v2.0.9 wiring). + +**Launch narrative:** *"The first MCP memory that VETOes hallucinations before the user sees them, FLAGS contradictions at write-time, and PREDICTS what the agent will need before the agent knows it needs it. Zero-shot proactive memory management."* + +### 15.7 v2.6.0 "Sleepwalking" — 2 Weeks After v2.5.0 + +Dream cycle detects high-value cross-project patterns → auto-generates and opens pull requests against the user's codebase. Zep writes text summaries; Vestige writes code. The `cross_project.find_universal_patterns()` fn already exists. Wire it via a new `sleepwalk` subcommand that invokes `gh pr create` with generated diffs. + +Files: new `crates/vestige-mcp/src/bin/sleepwalk.rs`, composes `CrossProjectLearner` + `MemoryDreamer` + existing gh CLI integration. + +**Launch narrative:** *"Your AI memory writes PRs while you sleep."* + +### 15.8 Post-v2.6 — Remaining April 19 roadmap + +After v2.6 "Sleepwalking," the April 19 placeholder roadmap reasserts with renumbered slots: + +| Slot | Codename | Scope | +|---|---|---| +| v2.7 | Decide | Qwen3 embeddings (absorbing the pre-existing `feat/v2.1.0-qwen3-embed` branch) once M3 Max Metal validates | +| v2.8 | Rewind | Temporal slider + pin, state reconstruction over time | +| v2.9 | Empathy | Apple Watch biometric flashbulb + frustration detection → arousal boost. First Pro-tier gate candidate. | +| v2.10 | Grip | Cluster gestures + manual bridging | +| v2.11 | Remote | `vestige-cloud` self-host upgrade (5→24 MCP tools + Streamable HTTP + Docker) | +| v3.0 | Branch | CoW memory branching + multi-tenant SaaS (gated on v2.11 adoption + cashflow) | + +### 15.9 Expected 30-day outcome + +Target: v2.0.9 + v2.5.0 + v2.6.0 all ship within 30 days of v2.0.8. +Stars trajectory: current 484 baseline at +12/day → +600 from v2.0.9 + +1,500 from v2.5.0 + +2,000 from v2.6.0 + 360 organic = **~5,000 stars by end of May 2026.** First paid commercial license lands during v2.5.0 launch week (the Hallucination Guillotine clip is exactly the artifact that makes enterprise DevRel reshare). MCP engineer role offer inbound during the same window. + +CCN 2027 poster abstract gets written on the v2.5 primitives; RustConf 2026 Sep 8-11 talk submission writes itself around the event-bus-subscriber architecture pattern. + +### 15.10 The one-line architectural thesis + +**Vestige's bottleneck is not feature count, not capacity, not module depth. It is one missing architectural pattern — a backend event-subscriber task that routes the 14 live WebSocket events into the cognitive modules that already have the trigger methods implemented.** Closing that single gap flips Vestige from "memory library" to "cognitive agent that acts on the host LLM." Every v2.5+ feature composes on top of that one change. + +--- + +**End of document.** Length-check: ~19,000 words / ~130 KB markdown. This is the single-page briefing that lets any AI agent plan the next phase of Vestige without having to re-read the repository. diff --git a/docs/integrations/codex.md b/docs/integrations/codex.md new file mode 100644 index 0000000..71f21bd --- /dev/null +++ b/docs/integrations/codex.md @@ -0,0 +1,132 @@ +# Codex + +> Give Codex a brain that remembers between sessions. + +Codex has native MCP support through the `codex mcp` CLI. Add Vestige once and Codex can carry project preferences, architecture decisions, and past fixes across sessions. + +--- + +## Prerequisites + +- **Codex CLI** installed and authenticated +- **vestige-mcp** binary installed ([Installation guide](../../README.md#quick-start)) + +--- + +## Setup + +### 1. Add Vestige + +```bash +codex mcp add vestige -- /usr/local/bin/vestige-mcp +``` + +> **Use an absolute path.** Run `which vestige-mcp` to find the installed binary. + +### 2. Verify + +```bash +codex mcp list +``` + +You should see a `vestige` entry with `enabled` status. + +### 3. Test it in Codex + +Start Codex and ask: + +> "What MCP tools do you have access to?" + +You should see Vestige's tools listed (`search`, `smart_ingest`, `memory`, and others). + +--- + +## First Use + +In Codex: + +> "Remember that this project uses Rust with Axum and SQLite" + +Start a **new session**, then ask: + +> "What stack does this project use?" + +It remembers. + +--- + +## Manual Configuration + +Codex stores MCP servers in `~/.codex/config.toml`. + +Minimal config: + +```toml +[mcp_servers.vestige] +command = "/usr/local/bin/vestige-mcp" +``` + +After saving, restart Codex or start a new session. + +--- + +## Project-Specific Memory + +Use `--data-dir` to isolate memory per repo or workspace: + +```bash +codex mcp remove vestige +codex mcp add vestige -- /usr/local/bin/vestige-mcp --data-dir /Users/you/projects/my-app/.vestige +``` + +Equivalent manual config: + +```toml +[mcp_servers.vestige] +command = "/usr/local/bin/vestige-mcp" +args = ["--data-dir", "/Users/you/projects/my-app/.vestige"] +``` + +--- + +## Troubleshooting + +
        +Vestige tools do not appear in Codex + +1. Verify the server is registered: + ```bash + codex mcp list + ``` +2. Check the binary path: + ```bash + which vestige-mcp + ``` +3. Ensure the config entry exists in `~/.codex/config.toml`. +4. Start a fresh Codex session after adding the server. +
        + +
        +Need to remove or re-add the server + +```bash +codex mcp remove vestige +codex mcp add vestige -- /usr/local/bin/vestige-mcp +``` +
        + +--- + +## Also Works With + +| IDE | Guide | +|-----|-------| +| Xcode 26.3 | [Setup](./xcode.md) | +| Cursor | [Setup](./cursor.md) | +| VS Code (Copilot) | [Setup](./vscode.md) | +| JetBrains | [Setup](./jetbrains.md) | +| Windsurf | [Setup](./windsurf.md) | +| Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | +| Claude Desktop | [Setup](../CONFIGURATION.md#claude-desktop-macos) | + +Your AI remembers everything, everywhere. diff --git a/docs/integrations/cursor.md b/docs/integrations/cursor.md index 1c459ec..1e22943 100644 --- a/docs/integrations/cursor.md +++ b/docs/integrations/cursor.md @@ -133,6 +133,7 @@ Cursor does not surface MCP server errors in the UI. Test by running the command | IDE | Guide | |-----|-------| | Xcode 26.3 | [Setup](./xcode.md) | +| Codex | [Setup](./codex.md) | | VS Code (Copilot) | [Setup](./vscode.md) | | JetBrains | [Setup](./jetbrains.md) | | Windsurf | [Setup](./windsurf.md) | diff --git a/docs/integrations/jetbrains.md b/docs/integrations/jetbrains.md index b3dd0bf..3424539 100644 --- a/docs/integrations/jetbrains.md +++ b/docs/integrations/jetbrains.md @@ -122,6 +122,7 @@ In **Settings > Tools > MCP Server**, click the expansion arrow next to your cli | Xcode 26.3 | [Setup](./xcode.md) | | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | +| Codex | [Setup](./codex.md) | | Windsurf | [Setup](./windsurf.md) | | Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | | Claude Desktop | [Setup](../CONFIGURATION.md#claude-desktop-macos) | diff --git a/docs/integrations/vscode.md b/docs/integrations/vscode.md index 01e132a..556e784 100644 --- a/docs/integrations/vscode.md +++ b/docs/integrations/vscode.md @@ -152,6 +152,7 @@ Every team member with Vestige installed will automatically get memory-enabled C |-----|-------| | Xcode 26.3 | [Setup](./xcode.md) | | Cursor | [Setup](./cursor.md) | +| Codex | [Setup](./codex.md) | | JetBrains | [Setup](./jetbrains.md) | | Windsurf | [Setup](./windsurf.md) | | Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | diff --git a/docs/integrations/windsurf.md b/docs/integrations/windsurf.md index 6e06205..8fd0c7f 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 19 tools, leaving plenty of room for other servers. +Windsurf has a **hard cap of 100 tools** across all MCP servers. Vestige uses 24 tools, leaving plenty of room for other servers. --- @@ -148,6 +148,7 @@ If you have many MCP servers and exceed 100 total tools, Cascade will ignore exc | Xcode 26.3 | [Setup](./xcode.md) | | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | +| Codex | [Setup](./codex.md) | | JetBrains | [Setup](./jetbrains.md) | | Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | | Claude Desktop | [Setup](../CONFIGURATION.md#claude-desktop-macos) | diff --git a/docs/integrations/xcode.md b/docs/integrations/xcode.md index fafad89..fb22dcf 100644 --- a/docs/integrations/xcode.md +++ b/docs/integrations/xcode.md @@ -51,7 +51,7 @@ Quit Xcode completely (Cmd+Q) and reopen your project. ### 4. Verify -Type `/context` in the Agent panel. You should see `vestige` listed with 19 tools. +Type `/context` in the Agent panel. You should see `vestige` listed with 24 tools. --- @@ -249,6 +249,7 @@ Vestige uses the MCP standard — the same memory works across all your tools: | IDE | Guide | |-----|-------| | Claude Code | [Setup](../CONFIGURATION.md#claude-code-one-liner) | +| Codex | [Setup](./codex.md) | | Claude Desktop | [Setup](../CONFIGURATION.md#claude-desktop-macos) | | Cursor | [Setup](./cursor.md) | | VS Code (Copilot) | [Setup](./vscode.md) | diff --git a/docs/launch/UI_ROADMAP_v2.1_v2.2.md b/docs/launch/UI_ROADMAP_v2.1_v2.2.md new file mode 100644 index 0000000..3e278b0 --- /dev/null +++ b/docs/launch/UI_ROADMAP_v2.1_v2.2.md @@ -0,0 +1,201 @@ +# Vestige UI Roadmap — v2.1.0 and v2.2.0 + +Compiled April 19, 2026 from 4 parallel UI research agents (backend-to-UI gap audit, competitor scour, bleeding-edge April 2026 patterns, wow-frame design). Local-only planning doc — not for commit to main until scope is locked. + +--- + +## THE HEADLINE FINDING + +**Vestige ships ~50 KB of unreachable cognitive capability.** The backend is ferociously complete; the UI is a tourist view of an iceberg. Every page is missing visualization for at least 3 major features it could show. + +- **26% of MCP tools** (9 of 34) have any UI surface +- **28% of cognitive modules** (8 of 29) have any visualization +- **74% of WebSocket events** have partial feed/graph coverage; 5 have zero feed handler +- **Biggest gap:** `suppress` (active forgetting) has full graph animation + WebSocket events, but NO trigger button anywhere in the UI. Users literally cannot trigger the signature v2.0.5 feature from the dashboard. + +The v2.1.0 UI story writes itself: **"Vestige v2.1 makes the invisible visible."** + +--- + +## TOP 10 CRITICAL UI GAPS (from Agent 1, ordered by user-visible impact) + +1. **`suppress` tool has zero frontend trigger.** Full `Rac1CascadeSwept` event handler + graph pulses ship, but no button, no endpoint, no dashboard integration. Users can't forget anything without raw MCP access. +2. **Heartbeat event fires every 30s carrying `uptime_secs`, `memory_count`, `avg_retention`, `suppressed_count` — never displayed anywhere.** Real-time health that costs nothing to show. +3. **`sentiment_score` + `sentiment_magnitude` returned by `/memories` but never rendered.** Emotional coloring is invisible. +4. **Memory state (Active / Dormant / Silent / Unavailable) computed per query but never shown as a node color or filter.** +5. **Intention page is list-only.** No endpoints for status change, snooze, or complete. Users can see intentions but not act on them from the dashboard. +6. **Rac1 cascade shows animation with zero data summary.** Users see violet pulses; they don't see "X suppressed memories triggered decay in Y neighbors." +7. **Synaptic tagging 9h window is invisible.** Retroactive importance boost happens silently. +8. **Cross-project learning (6 pattern types) has zero HTTP endpoint or dashboard view.** +9. **Consolidation internals hidden.** Which nodes decayed, which got new embeddings — all computed, all hidden. +10. **`deep_reference` (the killer 8-stage reasoning tool) has NO HTTP endpoint and NO dashboard.** The v2.0.4 headline feature is unreachable from the UI. + +--- + +## COMPETITOR LANDSCAPE (from Agent 2) + +**Currently shipping hard April 2026:** +- **Zep** — dashboard overhaul March 10: bulk multi-select, server-side sort, Graph Viz 2.0 (nodes sized by connection count, no render cap, click-node details). Closest competitor on graph. +- **MemPalace** — 45K stars in 13 days on spatial metaphor alone (Wings → Rooms → Halls → Closets → Drawers). 13 releases in 13 days. +- **Cognee v0.3.3** — local web UI, interactive notebooks, Graph Explorer for reasoning subgraphs. +- **Letta ADE** — 3-panel Agent Development Environment at app.letta.com. Context window viewer, memory blocks, archival search. + +**Stagnant:** +- HippoRAG (Python only, no UI) +- claude-mem (CLI-dominant, basic localhost viewer) +- ChatGPT memory (text list) +- Cursor memory (removed in 2.1) + +**What NOBODY has (unclaimed UI territory):** +1. Ambient always-on memory widget (menu bar / tray) +2. Watch / ring interface +3. Voice-first memory UI +4. Collaborative multi-user graph (Figma cursors for memory) +5. AR/VR memory palace (native Vision Pro / Quest) +6. Temporal time-scrubber (drag slider to rewind graph state) +7. Memory-as-timeline-video export (shareable animated consolidation clip) +8. Contradiction surfacing UI ("Disputes" page) +9. FSRS retention heatmap calendar (GitHub-contribution-grid style) +10. Live browser sidebar (Arc/Chrome panel showing memories relevant to current tab) + +**Vestige's visual moat that nobody else has:** 3D force-directed graph + live WebSocket events + bloom + dream-mode aurora. Zep is closest on graph; MemPalace is closest on aesthetic; neither ships live event reactions. + +--- + +## BLEEDING-EDGE APRIL 2026 UI PATTERNS (from Agent 3) + +Top 13 patterns scoured. The 5 most applicable to Vestige: + +1. **Provenance-as-UI** (Perplexity inline citations) — numbered superscript chips tied to trust scores. Vestige has FSRS trust; just doesn't surface it inline. +2. **Ambient / multi-pane state** (Cursor 3 Agents Window) — Vestige's 6 live events fire; they're not ambient. +3. **Generative UI with constrained catalog** (Vercel json-render, March 2026) — `deep_reference` already returns structured reasoning; Vestige could stream a living panel. +4. **Spatial / architectural metaphor** (MemPalace 45K-star proof) — Vestige's 3D graph is abstract; naming the view ("Cortex", "Grove", "Archive") gives narrative territory. +5. **Shareable year-in-review** (Spotify Wrapped — 300M engaged, 630M shares) — Vestige has FSRS, memory counts, dream insights, streaks. All the ingredients for a free distribution loop. + +**Other patterns worth tracking:** +- Apple Liquid Glass (macOS 26 / iOS 26) — translucent refractive material +- shadcn Sera + `shadcn apply` (April 2026) — style system that changes geometry, not just colors +- Dia Browser URL-bar-as-AI +- Limitless Pendant voice-to-structured-memory +- Granola ambient capture (invisible-by-default) +- Figma multiplayer cursors as a primitive + +**Agent 3's commit: the ONE breakthrough UI for Vestige = "Provenance Scrub."** + +Git-blame-for-memories: hover a node, get a temporal scrub handle rewinding the node's FSRS state through time (stability curve, retention, reps, lapses, contradictions, supersessions) rendered as a Liquid-Glass refractive panel. Click any point on the scrub to see memory content at that time. Inline Perplexity citations tag every fact. + +Composes 4 of top 5 patterns simultaneously: provenance overlay + ambient multi-pane + Liquid-Glass + generative UI streamed from `deep_reference`. Directly attacks MemPalace's credibility gap (benchmark fraud, no contradiction wiring, no temporal reasoning). + +Engineering cost: 9 days. Floor: 3D scrub + trust chips in 4 days as v2.1 patch. Ceiling: full Liquid-Glass + generative panel as v2.2 headlining launch. + +--- + +## WOW FRAMES (from Agent 4) — ranked by ship priority + +### Ship in v2.1.0 (5.5 engineering days, two HN thumbnails) + +**1. Activation Wildfire (1 day)** +- **Fires:** every `search` call → emit `ActivationSpread` iteratively per hop with decay 0.7. +- **Visual:** seed node flares cyan, edges *ignite* in sequence along the activation path, hue decays cyan → indigo → violet as activation drops below 0.1. +- **Neuroscience:** Collins & Loftus 1975, `spreading_activation.rs:1-58`. +- **Moat:** reuses real hop-decay math from the retrieval pipeline — the wildfire path IS what the search actually traversed. + +**2. Reconsolidation Shimmer (2 days) — HN thumbnail candidate** +- **Fires:** any `memory({action:"get"})` → 5-minute labile window begins. +- **Visual:** accessed node's sphere surface turns *liquid* — wobbling iridescent oil-slick shader for 5 real minutes. Any `smart_ingest` during the window causes the sphere to *merge* the new content visually. +- **Neuroscience:** Nader 2000, `reconsolidation.rs:405`. +- **Moat:** a memory being *editable only when recalled* is pure Nader. The shimmer is the meme shot. + +**3. Dream Stitching (2.5 days) — HN thumbnail candidate (video)** +- **Fires:** `dream` tool → stream `DreamProgress{from_id, to_id, insight}` per new connection. +- **Visual:** camera auto-orbits into existing dream-mode aurora. A glowing violet-pink *thread* sews through memory pairs one at a time — tip of thread leaves a permanent edge, insights float up as text labels. Ends with a supernova at graph centroid. +- **Neuroscience:** MemoryDreamer 5-stage consolidation. +- **Moat:** dreams *creating new edges* is Vestige-exclusive. + +### Queue for v2.2.0 + +**4. Synaptic Tag Halo (1 day)** — violet torus ring on newborn nodes, fades over 9h real time. Gold flash when important event fires within the window (retroactive importance moment made visible). `synaptic_tagging.rs`. + +**5. Competition Duel (1 day)** — top-3 search results duel. Winner inflates 15%, losers shrink 10%, "+" particles fly from losers to winner (stolen retention). Anderson 1994 retrieval-induced forgetting. + +**6. Rac1 Slow Burn (1.5 days)** — suppressed seed blackens into graphite. Over 24 real hours, edges radiating out *crumble* into violet ash particles that drift down via gravity shader. Dead branches literally fall away. + +**7. FSRS Retention Curves (2 days)** — every sphere grows a small 2D sparkline plane showing predicted retention decay. Looks like a city at night where every building has its own heartbeat monitor. Nodes approaching Dormant threshold pulse amber. + +--- + +## COMPOSED v2.1.0 AND v2.2.0 UI ROADMAP + +### v2.1.0 "Decide" (May 5-6 launch) — UI track + +On top of the already-planned v2.1.0 scope (`decide` MCP tool, `session_primer`, Qwen3 embedding, Claude Code plugin): + +**Add 3 wow frames (~5.5 days):** +1. Activation Wildfire — 1 day +2. Reconsolidation Shimmer — 2 days (HN thumbnail screenshot) +3. Dream Stitching — 2.5 days (HN thumbnail video) + +**Add 5 of the top-10 gap fixes (~5 days):** +1. `suppress` trigger button + HTTP endpoint — 1 day +2. Heartbeat display widget (uptime + avg retention + suppressed count) — 0.5 day +3. Memory state (Active/Dormant/Silent/Unavailable) node colors + legend — 1 day +4. Intention update/snooze/complete endpoints + UI — 1 day +5. `deep_reference` dashboard page (the 8-stage reasoning viewer) — 1.5 days + +**Total v2.1.0 UI scope: ~10.5 engineering days** on top of the existing 19.5 day Qwen3 + decide + plugin scope. Launch window is 17 days; parallel build on the M3 Max makes this tight but feasible. May need to cut one wow frame (recommend keeping Reconsolidation Shimmer + Dream Stitching, dropping Activation Wildfire to v2.1.1 if time-pressed). + +### v2.2.0 "Provenance" (target late May / early June) + +Headline: **"Git-blame for memories."** The Provenance Scrub compose (Agent 3's breakthrough). + +- 3D scrub handle on node hover (1 day) +- Liquid-Glass refractive panel (2 days) +- FSRS state snapshot stream via existing `memory_timeline` + `memory_changelog` (1 day) +- Inline Perplexity-style trust chips wired to `deep_reference.evidence[]` (1.5 days) +- Generative side-panel streaming `deep_reference.reasoning` json-render-style (2 days) +- Polish + demo clip (1.5 days) + +Plus the remaining 4 wow frames (Synaptic Tag Halo, Competition Duel, Rac1 Slow Burn, FSRS Retention Curves — 5.5 days). + +**Total v2.2.0 UI scope: ~14.5 days.** Ship target: June graduation week (June 13). + +### v2.3.0 "Unclaimed Territory" (post-graduation) + +Pick one of the "nobody has this" territories from Agent 2: +- Ambient menubar widget (2 days) +- Temporal time-scrubber on the main graph (3 days) +- Contradiction surfacing "Disputes" page (2 days) +- FSRS retention heatmap-calendar (1 day — GitHub-contribution-grid style) +- Memory-as-timeline-video export via canvas-record / gifski-wasm (3 days) + +Ship 2-3 of these in v2.3. Each is an unclaimed moat. + +--- + +## WHAT NOT TO DO + +- **Don't add memory palace metaphor (Wings/Rooms/Halls).** MemPalace owns that narrative territory with 45K stars. Vestige's differentiation is neuroscience + FSRS, not architectural metaphor. Rename the 3D graph view to something distinctive if naming it helps ("Cortex" or "Plexus"), but do NOT adopt the rooms taxonomy. +- **Don't chase every 2026 pattern.** Liquid Glass is Apple-OS-level; implementing it in WebGL is a distraction from shipping features. Save for v2.2 selectively. +- **Don't build mobile yet.** Adoption curve isn't there. Desktop dashboard + MCP server first. +- **Don't build multi-user.** Single-user local is the AGPL-3.0 story. Multi-tenant is vestige-cloud (proprietary), separate roadmap. + +--- + +## Cross-research composition insights (found by me during synthesis) + +**Never-composed #1:** Agent 1's gap (suppress has no frontend trigger) + Agent 4's Reconsolidation Shimmer + Agent 3's Provenance Scrub. Three pieces of the "make the invisible visible" story. Ship them together as v2.1.0 UI narrative. + +**Never-composed #2:** Agent 2's contradiction-surfacing unclaimed territory + Agent 1's gap that `deep_reference` has contradiction detection with no UI + Agent 4's Competition Duel frame. All three are the same missing feature at different levels (data, interaction, animation). Ship as v2.2 "Disputes" page + Competition Duel micro-animation together. + +**Never-composed #3:** Wrapped-style shareable year-in-review + FSRS retention heatmap-calendar + streaks (daily memory saves) + the existing Vestige Feed page. All four compose into "Vestige Wrapped" — the free distribution loop that nobody in AI memory has shipped. Ship as v2.3 "Year in Memory" — summer 2026, after launch stabilizes. + +--- + +## What this document is FOR + +- **Reference** when scoping v2.1.0 and v2.2.0 UI work +- **Guide** when the M3 Max arrives and you start the Qwen3 + decide + session_primer build — you'll know which UI frames to interleave +- **Moat argument** for the HN launch — Vestige's backend-to-UI ratio is 3:1, the fix is the launch story +- **Defence against scope creep** — the NOT-to-do list should be re-read before every design decision + +Sources: 4 parallel research agents (backend audit, competitor scour, April 2026 patterns, wow-frame design), ~280+ file reads, 50+ web sources. Full raw outputs preserved in Claude Code session logs. diff --git a/docs/launch/reddit-cross-reference.md b/docs/launch/reddit-cross-reference.md new file mode 100644 index 0000000..e6e918a --- /dev/null +++ b/docs/launch/reddit-cross-reference.md @@ -0,0 +1,233 @@ +# Reddit Launch Posts — cross_reference Tool + +## Post 1: r/ClaudeAI (Primary) + +**Title:** `I built a tool that catches when Claude's memories contradict each other before it answers. It's been wrong 0 times since.` + +--- + +I've been building Vestige — an MCP memory server that gives Claude persistent memory across sessions. FSRS-6 spaced repetition (the Anki algorithm), 29 neuroscience-inspired modules, single Rust binary. 1,111 memories stored. It works. + +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. + +The problem? I had TWO memories: +- **January**: "prefix caching crashes with our vLLM build" +- **March**: "prefix caching works with the new animsamuelk wheels" + +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. + +So I built `cross_reference`. + +Now before answering any factual question, Claude calls: + +``` +cross_reference({ query: "should I use --enable-prefix-caching with vLLM?" }) +``` + +And gets back: + +```json +{ + "status": "contradictions_found", + "confidence": 0.35, + "guidance": "WARNING: 1 contradiction detected across 12 memories. + The newer memory is likely more accurate.", + "contradictions": [ + { + "newer": { + "date": "2026-03-18", + "preview": "Switched to animsamuelk wheels which support --enable-prefix-caching..." + }, + "older": { + "date": "2026-01-15", + "preview": "prefix caching crashed with our samvalladares vLLM build..." + }, + "recommendation": "Trust the newer memory. Consider demoting the older one." + } + ], + "timeline": [ /* 12 memories sorted newest-first */ ] +} +``` + +Claude sees the contradiction BEFORE answering. Trusts the March memory. Gets it right. + +**I've been using it for 2 days. It's caught contradictions I didn't even know I had.** Old project decisions that got reversed. Config values that changed. Library versions that got upgraded. All sitting in memory, waiting to give me wrong answers. + +### How it works under the hood: + +1. **Broad retrieval** — pulls up to 50 memories related to the query +2. **Cross-encoder reranking** — filters for quality (Jina Reranker v1 Turbo) +3. **Pairwise contradiction detection** — every memory pair checked for negation patterns ("don't" vs "do", "deprecated" vs "recommended") and correction signals ("now uses", "switched to", "replaced by") +4. **Topic gating** — only compares memories that share significant words (prevents false positives between unrelated memories) +5. **Confidence scoring** — agreements boost confidence, contradictions tank it, recency helps +6. **Timeline** — everything sorted newest-first so Claude sees the evolution +7. **Superseded list** — explicitly identifies which old memories should be demoted + +### The bigger picture: + +Context windows are now 1M tokens (Claude Opus 4.6, GPT-5.4). But bigger context doesn't fix this problem. The "Lost in the Middle" research shows accuracy drops from 92% to 78% at 1M tokens. More context = more chances for contradictions to slip through. + +Memory systems need to be SMARTER, not just bigger. That's what Vestige does — 29 cognitive modules implementing real neuroscience: + +- **FSRS-6** — memories naturally decay when unused, strengthen when accessed (21 parameters trained on 700M+ reviews) +- **Prediction Error Gating** — only stores what's genuinely new (no duplicates) +- **Dream consolidation** — replays memories to discover hidden connections (yes, like sleep) +- **Spreading activation** — search for "auth bug" and find the related JWT update from last week +- **cross_reference** — the new tool that catches contradictions before they become wrong answers + +### Stats: +- 22 MCP tools +- 746 tests, 0 failures +- Zero `unsafe` code +- Clean security audit (0 findings — AgentAudit verified) +- Single 22MB Rust binary — no Docker, no PostgreSQL, no cloud +- Works with Claude Code, Cursor, VS Code, Xcode 26.3, JetBrains, Windsurf + +### 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 +sudo mv vestige-mcp /usr/local/bin/ +claude mcp add vestige vestige-mcp -s user +``` + +Then add to your CLAUDE.md: +``` +Before answering factual questions, call cross_reference({ query: "the topic" }) +to verify your memories don't contradict each other. +``` + +That's it. Your AI now fact-checks itself. + +**GitHub:** https://github.com/samvallad33/vestige + +I searched every memory MCP server out there — Mem0 (47K stars), Hindsight, Zep, Letta, OMEGA, Solitaire, MuninnDB. Some detect contradictions at ingest time (when you save). Nobody else gives the AI an on-demand tool to verify its own memories before answering, with confidence scoring and guidance on which memory to trust. + +Happy to answer questions about the neuroscience, the architecture, or how to set it up. + +--- + +## Post 2: r/LocalLLaMA (Cross-post, 2h later) + +**Title:** `Your AI has 1000 memories. Some contradict each other. It doesn't know. I built the fix — 100% local, single Rust binary, zero cloud.` + +--- + +The problem nobody talks about with AI memory: + +You use a memory system. Over months, you accumulate 1000+ memories. Your project config changed 3 times. A library got deprecated. A decision got reversed. All those old memories are still there. + +When your AI searches memory, it finds 5 results. Two of them disagree. The AI picks one — maybe the wrong one — and gives you a confident answer based on outdated information. + +I built `cross_reference` to fix this. It's tool #22 in Vestige, my cognitive memory MCP server. + +``` +cross_reference({ query: "what database does the project use?" }) +``` + +Returns: +```json +{ + "status": "contradictions_found", + "confidence": 0.35, + "contradictions": [{ + "newer": { "date": "2026-03", "preview": "Migrated to PostgreSQL..." }, + "older": { "date": "2026-01", "preview": "Using SQLite for the backend..." } + }], + "guidance": "WARNING: Trust the newer memory." +} +``` + +The AI sees the conflict. Picks the right one. Every time. + +**What makes this different from RAG/vector search:** + +| | RAG | Vestige | +|---|---|---| +| Storage | Store everything | Prediction Error Gating — only stores what's new | +| Retrieval | Nearest neighbor | 7-stage cognitive pipeline with reranking | +| Contradictions | Returns both, hopes for the best | **Detects and flags before answering** | +| Decay | Nothing expires | FSRS-6 — memories fade naturally | +| Duplicates | Manual dedup | Self-healing via semantic comparison | + +**The stack:** +- Rust 2024 edition. Single 22MB binary. No Python, no Docker, no cloud. +- FSRS-6 spaced repetition (21 parameters, 700M+ reviews) +- 29 cognitive modules (spreading activation, synaptic tagging, dream consolidation, prediction error gating) +- 3D neural dashboard at localhost:3927 (Three.js, real-time WebSocket) +- MCP server — works with any AI that speaks MCP + +**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 +sudo mv vestige-mcp /usr/local/bin/ +claude mcp add vestige vestige-mcp -s user +``` + +746 tests. Zero unsafe code. Clean security audit. AGPL-3.0. + +GitHub: https://github.com/samvallad33/vestige + +--- + +## Post 3: r/rust (Optional, technical audience) + +**Title:** `I built a 22MB Rust binary that gives AI agents a brain — FSRS-6, 29 cognitive modules, 3D dashboard, and a new contradiction detection tool. 746 tests, zero unsafe.` + +--- + +Vestige is a cognitive memory engine for AI agents. MCP server (stdio JSON-RPC + HTTP transport), SQLite WAL backend, USearch HNSW vector search, Nomic Embed v1.5 via fastembed (local ONNX, no API). + +The latest addition: `cross_reference` — pairwise contradiction detection across memories at retrieval time. The AI calls it before answering factual questions to verify its memories don't disagree. + +**Why Rust:** +- Single static binary (22MB with embedded SvelteKit dashboard via `include_dir!`) +- No runtime, no GC pauses during real-time search +- `tokio::sync::Mutex` for the cognitive engine, `std::sync::Mutex` for SQLite reader/writer split +- Zero `unsafe` blocks in the entire codebase +- `cargo test` runs 746 tests in 11 seconds + +**Architecture:** +``` +Axum 0.8 (dashboard + HTTP transport) + ↕ WebSocket event bus (tokio::broadcast, 1024 capacity) +MCP Server (stdio JSON-RPC) + → 22 tools dispatched via match on tool name + → Arc + Arc> +SQLite WAL + FTS5 + USearch HNSW + → fastembed 5.11 (Nomic Embed v1.5, 768D → 256D Matryoshka) + → Jina Reranker v1 Turbo (cross-encoder, 38M params) +``` + +**Key crates:** rusqlite 0.38, axum 0.8, tokio 1, fastembed 5.11, usearch 2, chrono 0.4, serde 1, uuid 1, include_dir 0.7 + +**What I learned building it:** +- `OnceLock, String>>` for lazy model initialization — the embedding model downloads ~130MB on first run, `OnceLock` ensures it only happens once and caches the error if it fails +- `floor_char_boundary()` saved me from a UTF-8 panic (content truncation with multi-byte chars) +- SQLite `PRAGMA journal_mode = WAL` + `synchronous = NORMAL` + `mmap_size = 268435456` gives surprisingly good concurrent read performance +- `try_lock()` pattern for cognitive features in search — if the lock is held (by dream consolidation), search falls back to simpler scoring instead of blocking + +Clean security audit. Parameterized SQL everywhere. CSP headers on the dashboard. Constant-time auth comparison (`subtle::ConstantTimeEq`). File permissions 0o600/0o700. + +GitHub: https://github.com/samvallad33/vestige +AGPL-3.0 | 746 tests | 79K+ LOC + +--- + +## Posting Strategy + +| Subreddit | When | Expected Engagement | +|-----------|------|-------------------| +| r/ClaudeAI | First — 12-14 UTC (US morning) | High — direct audience for MCP tools | +| r/LocalLLaMA | 2h after r/ClaudeAI | High — local-first angle resonates here | +| r/rust | Same day, evening UTC | Medium — technical deep dive audience | +| r/MachineLearning | Next day if first two do well | Lower but prestigious | + +**Title formula that works on Reddit:** Personal story + specific problem + "nobody else has this" + +**DO NOT do:** Product-name-first titles, marketing speak, "introducing X", "check out my project" + +**DO:** Lead with the PAIN ("my AI gave me wrong answers"), show the FIX (the JSON output), then reveal the tool. diff --git a/packages/vestige-init/package.json b/packages/vestige-init/package.json index a7a8cee..be2c337 100644 --- a/packages/vestige-init/package.json +++ b/packages/vestige-init/package.json @@ -1,6 +1,6 @@ { "name": "@vestige/init", - "version": "2.0.1", + "version": "2.0.7", "description": "Give your AI a brain in 10 seconds — zero-config Vestige v2.0 installer with 3D dashboard", "bin": { "vestige-init": "bin/init.js" diff --git a/packages/vestige-mcp-npm/.gitignore b/packages/vestige-mcp-npm/.gitignore index 7c5d56c..d5d2535 100644 --- a/packages/vestige-mcp-npm/.gitignore +++ b/packages/vestige-mcp-npm/.gitignore @@ -3,5 +3,7 @@ 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..3ca06fb 100644 --- a/packages/vestige-mcp-npm/README.md +++ b/packages/vestige-mcp-npm/README.md @@ -91,9 +91,12 @@ 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 | `./.fastembed_cache` | +| `VESTIGE_DASHBOARD_PORT` | Dashboard port | `3927` | +| `VESTIGE_AUTH_TOKEN` | Bearer auth for dashboard + HTTP MCP | auto-generated | + +Storage location is the `--data-dir ` CLI flag (defaults to your OS's per-user data directory). ## Troubleshooting diff --git a/packages/vestige-mcp-npm/package.json b/packages/vestige-mcp-npm/package.json index e50754d..9310fac 100644 --- a/packages/vestige-mcp-npm/package.json +++ b/packages/vestige-mcp-npm/package.json @@ -1,6 +1,6 @@ { "name": "vestige-mcp-server", - "version": "2.0.1", + "version": "2.0.7", "description": "Vestige MCP Server — Cognitive memory for AI with FSRS-6, 3D dashboard, and 29 brain modules", "bin": { "vestige-mcp": "bin/vestige-mcp.js", diff --git a/tests/e2e/src/assertions/mod.rs b/tests/e2e/src/assertions/mod.rs index cf8d1ae..0de53e4 100644 --- a/tests/e2e/src/assertions/mod.rs +++ b/tests/e2e/src/assertions/mod.rs @@ -271,16 +271,11 @@ macro_rules! assert_search_count { #[macro_export] macro_rules! assert_search_order { ($results:expr, $expected_first:expr) => { - assert!( - !$results.is_empty(), - "Expected non-empty search results" - ); + assert!(!$results.is_empty(), "Expected non-empty search results"); assert_eq!( - $results[0].id, - $expected_first, + $results[0].id, $expected_first, "Expected first result to be {}, got {}", - $expected_first, - $results[0].id + $expected_first, $results[0].id ); }; } diff --git a/tests/e2e/src/harness/db_manager.rs b/tests/e2e/src/harness/db_manager.rs index f5ce71c..345a94c 100644 --- a/tests/e2e/src/harness/db_manager.rs +++ b/tests/e2e/src/harness/db_manager.rs @@ -6,9 +6,9 @@ //! - Database snapshots and restoration //! - Concurrent test isolation -use vestige_core::{KnowledgeNode, Rating, Storage}; use std::path::PathBuf; use tempfile::TempDir; +use vestige_core::{KnowledgeNode, Rating, Storage}; /// Helper to create IngestInput (works around non_exhaustive) #[allow(clippy::too_many_arguments)] @@ -107,10 +107,7 @@ impl TestDatabaseManager { /// Get the number of nodes in the database pub fn node_count(&self) -> i64 { - self.storage - .get_stats() - .map(|s| s.total_nodes) - .unwrap_or(0) + self.storage.get_stats().map(|s| s.total_nodes).unwrap_or(0) } // ======================================================================== @@ -257,10 +254,7 @@ impl TestDatabaseManager { /// Take a snapshot of current database state pub fn take_snapshot(&mut self) { - let nodes = self - .storage - .get_all_nodes(10000, 0) - .unwrap_or_default(); + let nodes = self.storage.get_all_nodes(10000, 0).unwrap_or_default(); self.snapshot = Some(nodes); } @@ -322,8 +316,8 @@ impl TestDatabaseManager { let _ = std::fs::remove_file(&self.db_path); // Recreate storage - self.storage = Storage::new(Some(self.db_path.clone())) - .expect("Failed to recreate storage"); + self.storage = + Storage::new(Some(self.db_path.clone())).expect("Failed to recreate storage"); } } diff --git a/tests/e2e/src/mocks/fixtures.rs b/tests/e2e/src/mocks/fixtures.rs index 05d2a22..6929e56 100644 --- a/tests/e2e/src/mocks/fixtures.rs +++ b/tests/e2e/src/mocks/fixtures.rs @@ -183,7 +183,13 @@ impl TestDataFactory { /// Create a batch of memories pub fn create_batch(storage: &mut Storage, count: usize) -> Vec { - Self::create_batch_with_config(storage, BatchConfig { count, ..Default::default() }) + Self::create_batch_with_config( + storage, + BatchConfig { + count, + ..Default::default() + }, + ) } /// Create a batch with custom configuration @@ -212,9 +218,15 @@ impl TestDataFactory { let (valid_from, valid_until) = if config.with_temporal { let now = Utc::now(); if i % 3 == 0 { - (Some(now - Duration::days(30)), Some(now + Duration::days(30))) + ( + Some(now - Duration::days(30)), + Some(now + Duration::days(30)), + ) } else if i % 3 == 1 { - (Some(now - Duration::days(60)), Some(now - Duration::days(30))) + ( + Some(now - Duration::days(60)), + Some(now - Duration::days(30)), + ) } else { (None, None) } @@ -273,12 +285,7 @@ impl TestDataFactory { } // Emotional memory (decay should be affected by sentiment) - let emotional = Self::create_emotional_memory( - storage, - "Important life event", - 0.9, - 0.95, - ); + let emotional = Self::create_emotional_memory(storage, "Important life event", 0.9, 0.95); if let Some(node) = emotional { metadata.insert("emotional".to_string(), node.id.clone()); ids.push(node.id); @@ -445,12 +452,8 @@ impl TestDataFactory { } // No bounds (always valid) - if let Some(node) = Self::create_temporal_memory( - storage, - "Always valid memory", - None, - None, - ) { + if let Some(node) = Self::create_temporal_memory(storage, "Always valid memory", None, None) + { metadata.insert("always_valid".to_string(), node.id.clone()); ids.push(node.id); } @@ -469,8 +472,15 @@ impl TestDataFactory { /// Get a random node type pub fn random_node_type(seed: usize) -> &'static str { const TYPES: [&str; 9] = [ - "fact", "concept", "procedure", "event", "relationship", - "quote", "code", "question", "insight", + "fact", + "concept", + "procedure", + "event", + "relationship", + "quote", + "code", + "question", + "insight", ]; TYPES[seed % TYPES.len()] } @@ -478,10 +488,26 @@ impl TestDataFactory { /// Generate lorem ipsum-like content pub fn lorem_content(words: usize, seed: usize) -> String { const WORDS: [&str; 20] = [ - "the", "memory", "learning", "knowledge", "algorithm", - "data", "system", "process", "function", "method", - "class", "object", "variable", "constant", "type", - "structure", "pattern", "design", "architecture", "code", + "the", + "memory", + "learning", + "knowledge", + "algorithm", + "data", + "system", + "process", + "function", + "method", + "class", + "object", + "variable", + "constant", + "type", + "structure", + "pattern", + "design", + "architecture", + "code", ]; (0..words) @@ -493,8 +519,16 @@ impl TestDataFactory { /// Generate tags pub fn generate_tags(count: usize, seed: usize) -> Vec { const TAGS: [&str; 10] = [ - "important", "review", "todo", "concept", "fact", - "code", "note", "idea", "question", "reference", + "important", + "review", + "todo", + "concept", + "fact", + "code", + "note", + "idea", + "question", + "reference", ]; (0..count) diff --git a/tests/e2e/src/mocks/mock_embedding.rs b/tests/e2e/src/mocks/mock_embedding.rs index 93f0363..02957f5 100644 --- a/tests/e2e/src/mocks/mock_embedding.rs +++ b/tests/e2e/src/mocks/mock_embedding.rs @@ -145,7 +145,11 @@ impl MockEmbeddingService { // Map word to a sparse set of dimensions for i in 0..16 { let dim = ((word_hash >> (i * 4)) as usize) % MOCK_EMBEDDING_DIM; - let sign = if (word_hash >> (i + 48)) & 1 == 0 { 1.0 } else { -1.0 }; + let sign = if (word_hash >> (i + 48)) & 1 == 0 { + 1.0 + } else { + -1.0 + }; let magnitude = ((word_hash >> (i * 2)) as f32 % 100.0) / 100.0 + 0.5; embedding[dim] += sign * magnitude; } @@ -342,9 +346,15 @@ mod tests { let query = service.embed("programming code"); let candidates = vec![ - ("doc1".to_string(), service.embed("python programming language")), + ( + "doc1".to_string(), + service.embed("python programming language"), + ), ("doc2".to_string(), service.embed("cooking recipes")), - ("doc3".to_string(), service.embed("software development code")), + ( + "doc3".to_string(), + service.embed("software development code"), + ), ]; let result = service.find_most_similar(&query, &candidates); diff --git a/tests/e2e/tests/cognitive/comparative_benchmarks.rs b/tests/e2e/tests/cognitive/comparative_benchmarks.rs index 957feb1..bac2582 100644 --- a/tests/e2e/tests/cognitive/comparative_benchmarks.rs +++ b/tests/e2e/tests/cognitive/comparative_benchmarks.rs @@ -17,12 +17,12 @@ use chrono::{DateTime, Duration, Utc}; use std::collections::{HashMap, HashSet}; -use vestige_core::neuroscience::spreading_activation::{ - ActivatedMemory, ActivationConfig, ActivationNetwork, LinkType, -}; use vestige_core::neuroscience::hippocampal_index::{ BarcodeGenerator, ContentPointer, ContentType, HippocampalIndex, HippocampalIndexConfig, - IndexQuery, MemoryBarcode, MemoryIndex, INDEX_EMBEDDING_DIM, + INDEX_EMBEDDING_DIM, IndexQuery, MemoryBarcode, MemoryIndex, +}; +use vestige_core::neuroscience::spreading_activation::{ + ActivatedMemory, ActivationConfig, ActivationNetwork, LinkType, }; use vestige_core::neuroscience::synaptic_tagging::{ CaptureWindow, DecayFunction, ImportanceEvent, ImportanceEventType, SynapticTaggingConfig, @@ -36,9 +36,9 @@ use vestige_core::neuroscience::synaptic_tagging::{ /// SM-2 state for a card #[derive(Debug, Clone)] struct SM2State { - easiness_factor: f64, // EF, starts at 2.5 - interval: i32, // Days until next review - repetitions: i32, // Number of successful reviews + easiness_factor: f64, // EF, starts at 2.5 + interval: i32, // Days until next review + repetitions: i32, // Number of successful reviews } impl Default for SM2State { @@ -73,8 +73,9 @@ fn sm2_review(state: &SM2State, grade: SM2Grade) -> SM2State { let q = grade.as_i32(); // Update easiness factor - let mut new_ef = state.easiness_factor + (0.1 - (5 - q) as f64 * (0.08 + (5 - q) as f64 * 0.02)); - new_ef = new_ef.max(1.3); // EF never goes below 1.3 + let mut new_ef = + state.easiness_factor + (0.1 - (5 - q) as f64 * (0.08 + (5 - q) as f64 * 0.02)); + new_ef = new_ef.max(1.3); // EF never goes below 1.3 if q < 3 { // Failed - restart learning @@ -117,9 +118,8 @@ fn sm2_retention(interval: i32, elapsed_days: i32) -> f64 { /// FSRS-6 default weights const FSRS6_WEIGHTS: [f64; 21] = [ - 0.212, 1.2931, 2.3065, 8.2956, 6.4133, 0.8334, 3.0194, 0.001, - 1.8722, 0.1666, 0.796, 1.4835, 0.0614, 0.2629, 1.6483, 0.6014, - 1.8729, 0.5425, 0.0912, 0.0658, 0.1542, + 0.212, 1.2931, 2.3065, 8.2956, 6.4133, 0.8334, 3.0194, 0.001, 1.8722, 0.1666, 0.796, 1.4835, + 0.0614, 0.2629, 1.6483, 0.6014, 1.8729, 0.5425, 0.0912, 0.0658, 0.1542, ]; /// FSRS-6 state @@ -160,7 +160,9 @@ fn fsrs6_retrievability(stability: f64, elapsed_days: f64, w20: f64) -> f64 { return 1.0; } let factor = fsrs6_factor(w20); - (1.0 + factor * elapsed_days / stability).powf(-w20).clamp(0.0, 1.0) + (1.0 + factor * elapsed_days / stability) + .powf(-w20) + .clamp(0.0, 1.0) } /// FSRS-6 interval calculation @@ -183,24 +185,32 @@ fn fsrs6_review(state: &FSRS6State, grade: FSRS6Grade, elapsed_days: f64) -> FSR let new_stability = match grade { FSRS6Grade::Again => { // Lapse formula - w[11] * state.difficulty.powf(-w[12]) + w[11] + * state.difficulty.powf(-w[12]) * ((state.stability + 1.0).powf(w[13]) - 1.0) * (w[14] * (1.0 - r)).exp() } _ => { // Recall formula - let hard_penalty = if matches!(grade, FSRS6Grade::Hard) { w[15] } else { 1.0 }; - let easy_bonus = if matches!(grade, FSRS6Grade::Easy) { w[16] } else { 1.0 }; + let hard_penalty = if matches!(grade, FSRS6Grade::Hard) { + w[15] + } else { + 1.0 + }; + let easy_bonus = if matches!(grade, FSRS6Grade::Easy) { + w[16] + } else { + 1.0 + }; - state.stability * ( - w[8].exp() - * (11.0 - state.difficulty) - * state.stability.powf(-w[9]) - * ((w[10] * (1.0 - r)).exp() - 1.0) - * hard_penalty - * easy_bonus - + 1.0 - ) + state.stability + * (w[8].exp() + * (11.0 - state.difficulty) + * state.stability.powf(-w[9]) + * ((w[10] * (1.0 - r)).exp() - 1.0) + * hard_penalty + * easy_bonus + + 1.0) } }; @@ -209,8 +219,8 @@ fn fsrs6_review(state: &FSRS6State, grade: FSRS6Grade, elapsed_days: f64) -> FSR let delta = -w[6] * (g - 3.0); let mean_reversion = (10.0 - state.difficulty) / 9.0; let d0 = w[4] - (w[5] * 2.0).exp() + 1.0; - let new_difficulty = (w[7] * d0 + (1.0 - w[7]) * (state.difficulty + delta * mean_reversion)) - .clamp(1.0, 10.0); + let new_difficulty = + (w[7] * d0 + (1.0 - w[7]) * (state.difficulty + delta * mean_reversion)).clamp(1.0, 10.0); FSRS6State { difficulty: new_difficulty, @@ -226,7 +236,7 @@ fn fsrs6_review(state: &FSRS6State, grade: FSRS6Grade, elapsed_days: f64) -> FSR /// Leitner box state #[derive(Debug, Clone)] struct LeitnerState { - box_number: i32, // 1-5 + box_number: i32, // 1-5 } impl Default for LeitnerState { @@ -288,7 +298,9 @@ impl SimilaritySearch { } fn search(&self, query_embedding: &[f32], top_k: usize) -> Vec<(String, f64)> { - let mut results: Vec<(String, f64)> = self.embeddings.iter() + let mut results: Vec<(String, f64)> = self + .embeddings + .iter() .map(|(id, emb)| { let sim = cosine_similarity(query_embedding, emb); (id.clone(), sim) @@ -331,9 +343,8 @@ fn test_fsrs6_vs_sm2_efficiency() { // Simulate SM-2 let mut sm2_reviews = 0; - let mut sm2_states: Vec<(SM2State, i32)> = (0..NUM_CARDS) - .map(|_| (SM2State::default(), 0)) - .collect(); + let mut sm2_states: Vec<(SM2State, i32)> = + (0..NUM_CARDS).map(|_| (SM2State::default(), 0)).collect(); for day in 1..=DAYS { for (state, next_review) in sm2_states.iter_mut() { @@ -349,9 +360,8 @@ fn test_fsrs6_vs_sm2_efficiency() { // Simulate FSRS-6 let mut fsrs_reviews = 0; - let mut fsrs_states: Vec<(FSRS6State, i32)> = (0..NUM_CARDS) - .map(|_| (FSRS6State::default(), 0)) - .collect(); + let mut fsrs_states: Vec<(FSRS6State, i32)> = + (0..NUM_CARDS).map(|_| (FSRS6State::default(), 0)).collect(); for day in 1..=DAYS { for (state, next_review) in fsrs_states.iter_mut() { @@ -432,7 +442,7 @@ fn test_fsrs6_vs_sm2_reviews_same_retention() { // SM-2: Interval growth is linear with EF // After n successful reviews: interval ≈ previous * 2.5 - let sm2_intervals = vec![1, 6, 15, 38, 95]; // Approximate SM-2 progression + let sm2_intervals = vec![1, 6, 15, 38, 95]; // Approximate SM-2 progression // FSRS-6: Stability grows based on forgetting curve parameters // This allows for more nuanced interval optimization @@ -469,13 +479,13 @@ fn test_fsrs6_vs_sm2_reviews_same_retention() { // Test the core FSRS-6 innovation: difficulty modulation // Create a "hard" card and compare stability growth let mut hard_state = FSRS6State { - difficulty: 8.0, // Hard card + difficulty: 8.0, // Hard card stability: FSRS6State::default().stability, reps: 0, }; let mut easy_state = FSRS6State { - difficulty: 2.0, // Easy card + difficulty: 2.0, // Easy card stability: FSRS6State::default().stability, reps: 0, }; @@ -593,7 +603,7 @@ fn test_fsrs6_vs_leitner() { /// get shorter intervals, and users with flatter curves (lower w20) get longer. #[test] fn test_fsrs6_personalization_improvement() { - let default_w20 = FSRS6_WEIGHTS[20]; // 0.1542 + let default_w20 = FSRS6_WEIGHTS[20]; // 0.1542 // User with faster forgetting (higher w20 = steeper curve) let fast_forgetter_w20 = 0.35; @@ -629,7 +639,7 @@ fn test_fsrs6_personalization_improvement() { // The key insight: w20 affects optimal interval calculation // For same desired_retention (0.9), different w20 gives different intervals - let desired_retention = 0.85; // Target 85% to see interval differences + let desired_retention = 0.85; // Target 85% to see interval differences let default_interval = fsrs6_interval(stability, desired_retention, default_w20); let fast_interval = fsrs6_interval(stability, desired_retention, fast_forgetter_w20); let slow_interval = fsrs6_interval(stability, desired_retention, slow_forgetter_w20); @@ -638,7 +648,9 @@ fn test_fsrs6_personalization_improvement() { assert!( default_interval > 0 && fast_interval > 0 && slow_interval > 0, "All intervals should be positive: default={}, fast={}, slow={}", - default_interval, fast_interval, slow_interval + default_interval, + fast_interval, + slow_interval ); // The total range of intervals demonstrates personalization value @@ -747,34 +759,55 @@ fn test_fsrs6_hard_penalty_effectiveness() { fn test_spreading_vs_similarity_1_hop() { // Setup spreading activation network let mut network = ActivationNetwork::new(); - network.add_edge("rust".to_string(), "cargo".to_string(), LinkType::Semantic, 0.9); - network.add_edge("rust".to_string(), "ownership".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "rust".to_string(), + "cargo".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "rust".to_string(), + "ownership".to_string(), + LinkType::Semantic, + 0.85, + ); // Setup similarity search with similar embeddings let mut sim_search = SimilaritySearch::new(); sim_search.add("rust", vec![1.0, 0.0, 0.0]); - sim_search.add("cargo", vec![0.9, 0.1, 0.0]); // Similar to rust - sim_search.add("ownership", vec![0.85, 0.15, 0.0]); // Similar to rust - sim_search.add("python", vec![0.0, 1.0, 0.0]); // Unrelated + sim_search.add("cargo", vec![0.9, 0.1, 0.0]); // Similar to rust + sim_search.add("ownership", vec![0.85, 0.15, 0.0]); // Similar to rust + sim_search.add("python", vec![0.0, 1.0, 0.0]); // Unrelated // Spreading activation let spreading_results = network.activate("rust", 1.0); - let spreading_found: HashSet<_> = spreading_results.iter() + let spreading_found: HashSet<_> = spreading_results + .iter() .map(|r| r.memory_id.as_str()) .collect(); // Similarity search let sim_results = sim_search.search(&[1.0, 0.0, 0.0], 3); - let sim_found: HashSet<_> = sim_results.iter() + let sim_found: HashSet<_> = sim_results + .iter() .filter(|(_, score)| *score > 0.8) .map(|(id, _)| id.as_str()) .collect(); // At 1-hop, both should find the direct connections - assert!(spreading_found.contains("cargo"), "Spreading should find cargo"); - assert!(spreading_found.contains("ownership"), "Spreading should find ownership"); + assert!( + spreading_found.contains("cargo"), + "Spreading should find cargo" + ); + assert!( + spreading_found.contains("ownership"), + "Spreading should find ownership" + ); assert!(sim_found.contains("cargo"), "Similarity should find cargo"); - assert!(sim_found.contains("ownership"), "Similarity should find ownership"); + assert!( + sim_found.contains("ownership"), + "Similarity should find ownership" + ); } /// Test 2-hop: Spreading activation finds indirect connections. @@ -790,23 +823,35 @@ fn test_spreading_vs_similarity_2_hop() { // Create a chain: rust -> tokio -> async_runtime // rust and async_runtime have NO direct similarity - network.add_edge("rust".to_string(), "tokio".to_string(), LinkType::Semantic, 0.9); - network.add_edge("tokio".to_string(), "async_runtime".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "rust".to_string(), + "tokio".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "tokio".to_string(), + "async_runtime".to_string(), + LinkType::Semantic, + 0.85, + ); // Similarity search - embeddings show NO similarity between rust and async_runtime let mut sim_search = SimilaritySearch::new(); sim_search.add("rust", vec![1.0, 0.0, 0.0, 0.0]); - sim_search.add("tokio", vec![0.7, 0.7, 0.0, 0.0]); // Bridge - sim_search.add("async_runtime", vec![0.0, 1.0, 0.0, 0.0]); // No similarity to rust + sim_search.add("tokio", vec![0.7, 0.7, 0.0, 0.0]); // Bridge + sim_search.add("async_runtime", vec![0.0, 1.0, 0.0, 0.0]); // No similarity to rust // Spreading finds async_runtime through the chain let spreading_results = network.activate("rust", 1.0); - let spreading_found_async = spreading_results.iter() + let spreading_found_async = spreading_results + .iter() .any(|r| r.memory_id == "async_runtime"); // Similarity from "rust" does NOT find async_runtime let sim_results = sim_search.search(&[1.0, 0.0, 0.0, 0.0], 5); - let sim_found_async = sim_results.iter() + let sim_found_async = sim_results + .iter() .any(|(id, score)| id == "async_runtime" && *score > 0.5); assert!( @@ -832,27 +877,52 @@ fn test_spreading_vs_similarity_3_hop() { // Create 3-hop chain: A -> B -> C -> D // Each step has semantic connection, but A and D have ZERO direct similarity - network.add_edge("concept_a".to_string(), "concept_b".to_string(), LinkType::Semantic, 0.9); - network.add_edge("concept_b".to_string(), "concept_c".to_string(), LinkType::Semantic, 0.9); - network.add_edge("concept_c".to_string(), "concept_d".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "concept_a".to_string(), + "concept_b".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "concept_b".to_string(), + "concept_c".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "concept_c".to_string(), + "concept_d".to_string(), + LinkType::Semantic, + 0.9, + ); // Embeddings: A and D are orthogonal (zero similarity) let mut sim_search = SimilaritySearch::new(); sim_search.add("concept_a", vec![1.0, 0.0, 0.0, 0.0]); sim_search.add("concept_b", vec![0.7, 0.7, 0.0, 0.0]); sim_search.add("concept_c", vec![0.0, 0.7, 0.7, 0.0]); - sim_search.add("concept_d", vec![0.0, 0.0, 0.0, 1.0]); // Orthogonal to A + sim_search.add("concept_d", vec![0.0, 0.0, 0.0, 1.0]); // Orthogonal to A // Spreading finds D let spreading_results = network.activate("concept_a", 1.0); - let d_result = spreading_results.iter().find(|r| r.memory_id == "concept_d"); + let d_result = spreading_results + .iter() + .find(|r| r.memory_id == "concept_d"); - assert!(d_result.is_some(), "Spreading MUST find concept_d at 3 hops"); - assert_eq!(d_result.unwrap().distance, 3, "Should be exactly 3 hops away"); + assert!( + d_result.is_some(), + "Spreading MUST find concept_d at 3 hops" + ); + assert_eq!( + d_result.unwrap().distance, + 3, + "Should be exactly 3 hops away" + ); // Similarity CANNOT find D from A let sim_results = sim_search.search(&[1.0, 0.0, 0.0, 0.0], 10); - let sim_d_score = sim_results.iter() + let sim_d_score = sim_results + .iter() .find(|(id, _)| id == "concept_d") .map(|(_, score)| *score) .unwrap_or(0.0); @@ -873,9 +943,24 @@ fn test_spreading_finds_chains_similarity_misses() { // Chain: "memory_leak" -> "reference_counting" -> "Arc_Weak" -> "cyclic_references" // The solution (cyclic_references) is NOT semantically similar to "memory_leak" - network.add_edge("memory_leak".to_string(), "reference_counting".to_string(), LinkType::Causal, 0.9); - network.add_edge("reference_counting".to_string(), "arc_weak".to_string(), LinkType::Semantic, 0.85); - network.add_edge("arc_weak".to_string(), "cyclic_references".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "memory_leak".to_string(), + "reference_counting".to_string(), + LinkType::Causal, + 0.9, + ); + network.add_edge( + "reference_counting".to_string(), + "arc_weak".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "arc_weak".to_string(), + "cyclic_references".to_string(), + LinkType::Semantic, + 0.9, + ); // The problem: "cyclic_references" has zero direct similarity to "memory_leak" // (they use completely different vocabulary) @@ -883,16 +968,18 @@ fn test_spreading_finds_chains_similarity_misses() { sim_search.add("memory_leak", vec![1.0, 0.0, 0.0, 0.0]); sim_search.add("reference_counting", vec![0.5, 0.5, 0.0, 0.0]); sim_search.add("arc_weak", vec![0.0, 0.7, 0.3, 0.0]); - sim_search.add("cyclic_references", vec![0.0, 0.0, 0.0, 1.0]); // Totally different! + sim_search.add("cyclic_references", vec![0.0, 0.0, 0.0, 1.0]); // Totally different! // Spreading activation finds the solution let spreading_results = network.activate("memory_leak", 1.0); - let found_solution = spreading_results.iter() + let found_solution = spreading_results + .iter() .any(|r| r.memory_id == "cyclic_references"); // Similarity search cannot find it let sim_results = sim_search.search(&[1.0, 0.0, 0.0, 0.0], 10); - let sim_found = sim_results.iter() + let sim_found = sim_results + .iter() .any(|(id, score)| id == "cyclic_references" && *score > 0.3); assert!( @@ -911,26 +998,47 @@ fn test_spreading_path_quality() { let mut network = ActivationNetwork::new(); // Create a knowledge graph about Rust error handling - network.add_edge("error_handling".to_string(), "result_type".to_string(), LinkType::Semantic, 0.9); - network.add_edge("result_type".to_string(), "question_mark_operator".to_string(), LinkType::Semantic, 0.85); - network.add_edge("question_mark_operator".to_string(), "early_return".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "error_handling".to_string(), + "result_type".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "result_type".to_string(), + "question_mark_operator".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "question_mark_operator".to_string(), + "early_return".to_string(), + LinkType::Semantic, + 0.8, + ); let results = network.activate("error_handling", 1.0); // Find the path to early_return - let early_return_result = results.iter() + let early_return_result = results + .iter() .find(|r| r.memory_id == "early_return") .expect("Should find early_return"); // Verify the path makes sense - assert_eq!(early_return_result.path.len(), 4, "Path should have 4 nodes"); + assert_eq!( + early_return_result.path.len(), + 4, + "Path should have 4 nodes" + ); assert_eq!(early_return_result.path[0], "error_handling"); assert_eq!(early_return_result.path[1], "result_type"); assert_eq!(early_return_result.path[2], "question_mark_operator"); assert_eq!(early_return_result.path[3], "early_return"); // Activation should decay along the path - let result_type_activation = results.iter() + let result_type_activation = results + .iter() .find(|r| r.memory_id == "result_type") .map(|r| r.activation) .unwrap_or(0.0); @@ -1056,20 +1164,52 @@ fn test_spreading_mixed_link_types() { let mut network = ActivationNetwork::new(); // Create edges with different link types - network.add_edge("event".to_string(), "semantic_relation".to_string(), LinkType::Semantic, 0.9); - network.add_edge("event".to_string(), "temporal_relation".to_string(), LinkType::Temporal, 0.9); - network.add_edge("event".to_string(), "causal_relation".to_string(), LinkType::Causal, 0.9); - network.add_edge("event".to_string(), "spatial_relation".to_string(), LinkType::Spatial, 0.9); + network.add_edge( + "event".to_string(), + "semantic_relation".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "event".to_string(), + "temporal_relation".to_string(), + LinkType::Temporal, + 0.9, + ); + network.add_edge( + "event".to_string(), + "causal_relation".to_string(), + LinkType::Causal, + 0.9, + ); + network.add_edge( + "event".to_string(), + "spatial_relation".to_string(), + LinkType::Spatial, + 0.9, + ); let results = network.activate("event", 1.0); // Should find all related nodes let found_ids: HashSet<_> = results.iter().map(|r| r.memory_id.as_str()).collect(); - assert!(found_ids.contains("semantic_relation"), "Should find semantic relation"); - assert!(found_ids.contains("temporal_relation"), "Should find temporal relation"); - assert!(found_ids.contains("causal_relation"), "Should find causal relation"); - assert!(found_ids.contains("spatial_relation"), "Should find spatial relation"); + assert!( + found_ids.contains("semantic_relation"), + "Should find semantic relation" + ); + assert!( + found_ids.contains("temporal_relation"), + "Should find temporal relation" + ); + assert!( + found_ids.contains("causal_relation"), + "Should find causal relation" + ); + assert!( + found_ids.contains("spatial_relation"), + "Should find spatial relation" + ); // Verify link types are preserved for result in &results { @@ -1094,7 +1234,7 @@ fn test_spreading_mixed_link_types() { #[test] fn test_retroactive_vs_timestamp_importance() { let config = SynapticTaggingConfig { - capture_window: CaptureWindow::new(9.0, 2.0), // 9 hours back, 2 hours forward + capture_window: CaptureWindow::new(9.0, 2.0), // 9 hours back, 2 hours forward prp_threshold: 0.7, tag_lifetime_hours: 12.0, min_tag_strength: 0.3, @@ -1130,7 +1270,8 @@ fn test_retroactive_vs_timestamp_importance() { // (In tests, tag_memory() uses Utc::now(), so temporal_distance ~= 0) for captured in &result.captured_memories { assert!( - captured.temporal_distance_hours >= 0.0 || captured.temporal_distance_hours.abs() < 0.01, + captured.temporal_distance_hours >= 0.0 + || captured.temporal_distance_hours.abs() < 0.01, "Captured memory {} should be encoded at or before event (distance: {:.4}h)", captured.memory_id, captured.temporal_distance_hours @@ -1166,7 +1307,10 @@ fn test_retroactive_captures_related_memories() { ); // Verify cluster properties - assert!(cluster.average_importance > 0.0, "Cluster should have positive importance"); + assert!( + cluster.average_importance > 0.0, + "Cluster should have positive importance" + ); assert_eq!( cluster.trigger_event_type, ImportanceEventType::EmotionalContent @@ -1184,7 +1328,11 @@ fn test_retroactive_window_effectiveness() { (Duration::hours(1), true, "1 hour before"), (Duration::hours(4), true, "4 hours before"), (Duration::hours(8), true, "8 hours before"), - (Duration::hours(10), false, "10 hours before (outside window)"), + ( + Duration::hours(10), + false, + "10 hours before (outside window)", + ), (Duration::minutes(-30), true, "30 minutes after"), (Duration::hours(-3), false, "3 hours after (outside window)"), ]; @@ -1201,7 +1349,11 @@ fn test_retroactive_window_effectiveness() { if should_capture { let prob = window.capture_probability(memory_time, event_time); - assert!(prob.is_some(), "{} should have capture probability", description); + assert!( + prob.is_some(), + "{} should have capture probability", + description + ); assert!( prob.unwrap() > 0.0, "{} should have positive capture probability", @@ -1218,7 +1370,7 @@ fn test_retroactive_semantic_filtering() { capture_window: CaptureWindow::new(9.0, 2.0), prp_threshold: 0.7, tag_lifetime_hours: 12.0, - min_tag_strength: 0.1, // Low threshold to test strength effects + min_tag_strength: 0.1, // Low threshold to test strength effects max_cluster_size: 100, enable_clustering: true, auto_decay: true, @@ -1232,14 +1384,16 @@ fn test_retroactive_semantic_filtering() { stc.tag_memory_with_strength("highly_relevant", 0.95); stc.tag_memory_with_strength("moderately_relevant", 0.6); stc.tag_memory_with_strength("barely_relevant", 0.35); - stc.tag_memory_with_strength("irrelevant", 0.05); // Below threshold + stc.tag_memory_with_strength("irrelevant", 0.05); // Below threshold // Trigger importance event let event = ImportanceEvent::user_flag("trigger", None); let result = stc.trigger_prp(event); // Higher strength memories should be captured with higher consolidated importance - let captured_ids: HashSet<_> = result.captured_memories.iter() + let captured_ids: HashSet<_> = result + .captured_memories + .iter() .map(|c| c.memory_id.as_str()) .collect(); @@ -1253,12 +1407,16 @@ fn test_retroactive_semantic_filtering() { ); // Find consolidated importance values - let highly_relevant_importance = result.captured_memories.iter() + let highly_relevant_importance = result + .captured_memories + .iter() .find(|c| c.memory_id == "highly_relevant") .map(|c| c.consolidated_importance) .unwrap_or(0.0); - let moderately_relevant_importance = result.captured_memories.iter() + let moderately_relevant_importance = result + .captured_memories + .iter() .find(|c| c.memory_id == "moderately_relevant") .map(|c| c.consolidated_importance) .unwrap_or(0.0); @@ -1285,10 +1443,8 @@ fn test_proof_unique_to_vestige() { let mut stc = SynapticTaggingSystem::new(); // Memory 1: Ordinary conversation about vacation (time T) - let _vacation_memory = stc.tag_memory_with_context( - "vacation_mention", - "User mentioned Bob's vacation plans" - ); + let _vacation_memory = + stc.tag_memory_with_context("vacation_mention", "User mentioned Bob's vacation plans"); // Memory 2: Some other ordinary memories stc.tag_memory("unrelated_memory_1"); @@ -1300,14 +1456,16 @@ fn test_proof_unique_to_vestige() { event_type: ImportanceEventType::UserFlag, memory_id: Some("departure_announcement".to_string()), timestamp: Utc::now(), - strength: 1.0, // Maximum importance + strength: 1.0, // Maximum importance context: Some("Bob is leaving - this makes prior context important".to_string()), }; let result = stc.trigger_prp(event); // The vacation memory should be captured! - let vacation_captured = result.captured_memories.iter() + let vacation_captured = result + .captured_memories + .iter() .any(|c| c.memory_id == "vacation_mention"); assert!( @@ -1316,7 +1474,9 @@ fn test_proof_unique_to_vestige() { ); // Verify the capture details - let vacation_capture = result.captured_memories.iter() + let vacation_capture = result + .captured_memories + .iter() .find(|c| c.memory_id == "vacation_mention") .unwrap(); @@ -1324,7 +1484,8 @@ fn test_proof_unique_to_vestige() { // so temporal_distance is ~0 (but conceptually it's a "backward" capture // since the memory existed BEFORE it became important) assert!( - vacation_capture.temporal_distance_hours >= 0.0 || vacation_capture.temporal_distance_hours.abs() < 0.01, + vacation_capture.temporal_distance_hours >= 0.0 + || vacation_capture.temporal_distance_hours.abs() < 0.01, "Memory should be encoded at or before the importance event (distance: {:.4}h)", vacation_capture.temporal_distance_hours ); @@ -1406,7 +1567,7 @@ fn test_index_compression_ratio() { let full_embedding_dim = 384; // Index embedding size - let index_embedding_dim = config.summary_dimensions; // 128 by default + let index_embedding_dim = config.summary_dimensions; // 128 by default // Compression ratio let compression_ratio = full_embedding_dim as f64 / index_embedding_dim as f64; @@ -1425,7 +1586,7 @@ fn test_index_compression_ratio() { ); // Memory savings per memory - let full_size_bytes = full_embedding_dim * 4; // f32 = 4 bytes + let full_size_bytes = full_embedding_dim * 4; // f32 = 4 bytes let index_size_bytes = index_embedding_dim * 4; let savings_per_memory = full_size_bytes - index_size_bytes; @@ -1522,8 +1683,7 @@ fn test_content_pointer_accuracy() { assert_eq!(chunked_ptr.size_bytes, Some(100)); // Test with hash - let hashed_ptr = ContentPointer::sqlite("data", 1, ContentType::Text) - .with_hash(0xDEADBEEF); + let hashed_ptr = ContentPointer::sqlite("data", 1, ContentType::Text).with_hash(0xDEADBEEF); assert_eq!(hashed_ptr.content_hash, Some(0xDEADBEEF)); @@ -1531,19 +1691,24 @@ fn test_content_pointer_accuracy() { let index = HippocampalIndex::new(); let now = Utc::now(); - let barcode = index.index_memory( - "test_memory", - "Test content for pointer verification", - "fact", - now, - None, - ).unwrap(); + let barcode = index + .index_memory( + "test_memory", + "Test content for pointer verification", + "fact", + now, + None, + ) + .unwrap(); // Retrieve and verify let retrieved = index.get_index("test_memory").unwrap().unwrap(); assert_eq!(retrieved.barcode, barcode); - assert!(!retrieved.content_pointers.is_empty(), "Should have content pointer"); + assert!( + !retrieved.content_pointers.is_empty(), + "Should have content pointer" + ); // Verify the default pointer is SQLite let default_ptr = &retrieved.content_pointers[0]; diff --git a/tests/e2e/tests/cognitive/dreams_tests.rs b/tests/e2e/tests/cognitive/dreams_tests.rs index 9835dee..25084b1 100644 --- a/tests/e2e/tests/cognitive/dreams_tests.rs +++ b/tests/e2e/tests/cognitive/dreams_tests.rs @@ -15,11 +15,11 @@ //! 3. **Scheduler & Timing**: Tests for activity detection and idle triggers use chrono::{Duration, Utc}; +use std::collections::HashSet; use vestige_core::advanced::dreams::{ ActivityTracker, ConnectionGraph, ConnectionReason, ConsolidationScheduler, DreamConfig, DreamMemory, InsightType, MemoryDreamer, }; -use std::collections::HashSet; // ============================================================================ // HELPER FUNCTIONS @@ -66,7 +66,6 @@ fn make_memory_with_access( } } - // ============================================================================ // INSIGHT GENERATION TESTS (5 tests) // ============================================================================ @@ -448,11 +447,7 @@ async fn test_consolidation_decay_stage() { let replay = report.stage1_replay.as_ref().unwrap(); // Should replay memories in chronological order - assert_eq!( - replay.sequence.len(), - 3, - "Should replay all 3 memories" - ); + assert_eq!(replay.sequence.len(), 3, "Should replay all 3 memories"); // Older memory should come first in replay sequence assert_eq!( @@ -616,12 +611,7 @@ async fn test_consolidation_transfer_stage() { vec!["important"], 5, ), - make_memory_with_access( - "low_access", - "Rarely accessed memory", - vec!["minor"], - 1, - ), + make_memory_with_access("low_access", "Rarely accessed memory", vec!["minor"], 1), ]; let report = scheduler.run_consolidation_cycle(&memories).await; @@ -911,7 +901,10 @@ fn test_connection_graph_comprehensive() { // Test strengthening assert!(graph.strengthen_connection("a", "b", 0.1)); let new_strength = graph.total_connection_strength("a"); - assert!(new_strength > a_strength, "Strength should increase after reinforcement"); + assert!( + new_strength > a_strength, + "Strength should increase after reinforcement" + ); // Test decay and pruning graph.apply_decay(0.5); diff --git a/tests/e2e/tests/cognitive/neuroscience_tests.rs b/tests/e2e/tests/cognitive/neuroscience_tests.rs index 5fb2db9..8a2506f 100644 --- a/tests/e2e/tests/cognitive/neuroscience_tests.rs +++ b/tests/e2e/tests/cognitive/neuroscience_tests.rs @@ -22,19 +22,39 @@ use chrono::{Duration, Utc}; use vestige_core::{ // Advanced reconsolidation - AccessContext, AccessTrigger, Modification, ReconsolidationManager, RelationshipType, - // FSRS - Rating, retrievability, retrievability_with_decay, initial_difficulty, - next_interval, FSRSScheduler, - // Neuroscience - Synaptic Tagging - SynapticTaggingSystem, ImportanceEvent, ImportanceEventType, - CaptureWindow, DecayFunction, - // Neuroscience - Memory States - MemoryState, MemoryLifecycle, AccessibilityCalculator, - CompetitionManager, CompetitionCandidate, + AccessContext, + AccessTrigger, + AccessibilityCalculator, + ArousalSignal, + AttentionSession, + AttentionSignal, + CaptureWindow, + CompetitionCandidate, + CompetitionManager, + DecayFunction, + FSRSScheduler, + ImportanceContext, + ImportanceEvent, + ImportanceEventType, // Neuroscience - Importance Signals - ImportanceSignals, NoveltySignal, ArousalSignal, RewardSignal, AttentionSignal, - ImportanceContext, AttentionSession, OutcomeType, + ImportanceSignals, + MemoryLifecycle, + // Neuroscience - Memory States + MemoryState, + Modification, + NoveltySignal, + OutcomeType, + // FSRS + Rating, + ReconsolidationManager, + RelationshipType, + RewardSignal, + // Neuroscience - Synaptic Tagging + SynapticTaggingSystem, + initial_difficulty, + next_interval, + retrievability, + retrievability_with_decay, }; // ============================================================================ @@ -91,7 +111,12 @@ fn test_stc_prp_trigger_captures_memories() { // The tagged memory should be captured assert!(result.has_captures()); - assert!(result.captured_memories.iter().any(|c| c.memory_id == "mem-background")); + assert!( + result + .captured_memories + .iter() + .any(|c| c.memory_id == "mem-background") + ); assert!(stc.is_captured("mem-background")); } @@ -140,13 +165,20 @@ fn test_stc_capture_window_probability() { // Memory just before event - high probability (exponential decay with λ=4.605/9) let recent_before = event_time - Duration::hours(1); - let prob_recent = window.capture_probability(recent_before, event_time).unwrap(); + let prob_recent = window + .capture_probability(recent_before, event_time) + .unwrap(); // At 1h out of 9h with exponential decay: e^(-4.605/9 * 1) ≈ 0.6 - assert!(prob_recent > 0.5, "Recent memory should have high capture probability"); + assert!( + prob_recent > 0.5, + "Recent memory should have high capture probability" + ); // Memory 6 hours before event - moderate probability let medium_before = event_time - Duration::hours(6); - let prob_medium = window.capture_probability(medium_before, event_time).unwrap(); + let prob_medium = window + .capture_probability(medium_before, event_time) + .unwrap(); assert!(prob_medium > 0.0 && prob_medium < prob_recent); // Memory outside window - no capture @@ -165,14 +197,26 @@ fn test_stc_decay_functions() { let exp_at_half = exp_decay.apply(1.0, 6.0, 12.0); let exp_at_end = exp_decay.apply(1.0, 12.0, 12.0); - assert!((exp_at_zero - 1.0).abs() < 0.01, "Should be full strength at t=0"); - assert!(exp_at_half > 0.0 && exp_at_half < 0.5, "Significant decay at halfway"); + assert!( + (exp_at_zero - 1.0).abs() < 0.01, + "Should be full strength at t=0" + ); + assert!( + exp_at_half > 0.0 && exp_at_half < 0.5, + "Significant decay at halfway" + ); assert!(exp_at_end < 0.02, "Near zero at lifetime end"); // Linear decay let linear_decay = DecayFunction::Linear; - assert!((linear_decay.apply(1.0, 5.0, 10.0) - 0.5).abs() < 0.01, "Linear: 50% at halfway"); - assert!((linear_decay.apply(1.0, 10.0, 10.0) - 0.0).abs() < 0.01, "Linear: 0% at end"); + assert!( + (linear_decay.apply(1.0, 5.0, 10.0) - 0.5).abs() < 0.01, + "Linear: 50% at halfway" + ); + assert!( + (linear_decay.apply(1.0, 10.0, 10.0) - 0.0).abs() < 0.01, + "Linear: 0% at end" + ); // Power decay (matches FSRS-6) let power_decay = DecayFunction::Power; @@ -259,7 +303,10 @@ fn test_reconsolidation_marks_memory_labile() { let snapshot = vestige_core::MemorySnapshot::capture( "Test content".to_string(), vec!["test".to_string()], - 0.8, 5.0, 0.9, vec![], + 0.8, + 5.0, + 0.9, + vec![], ); manager.mark_labile("mem-123", snapshot); @@ -277,22 +324,30 @@ fn test_reconsolidation_apply_modifications() { let snapshot = vestige_core::MemorySnapshot::capture( "Original content".to_string(), vec!["original".to_string()], - 0.8, 5.0, 0.9, vec![], + 0.8, + 5.0, + 0.9, + vec![], ); manager.mark_labile("mem-123", snapshot); // Apply various modifications - let success1 = manager.apply_modification("mem-123", Modification::AddTag { - tag: "new-tag".to_string(), - }); - let success2 = manager.apply_modification("mem-123", Modification::BoostRetrieval { - boost: 0.1, - }); - let success3 = manager.apply_modification("mem-123", Modification::LinkMemory { - related_memory_id: "mem-456".to_string(), - relationship: RelationshipType::Supports, - }); + let success1 = manager.apply_modification( + "mem-123", + Modification::AddTag { + tag: "new-tag".to_string(), + }, + ); + let success2 = + manager.apply_modification("mem-123", Modification::BoostRetrieval { boost: 0.1 }); + let success3 = manager.apply_modification( + "mem-123", + Modification::LinkMemory { + related_memory_id: "mem-456".to_string(), + relationship: RelationshipType::Supports, + }, + ); assert!(success1 && success2 && success3); assert_eq!(manager.get_stats().total_modifications, 3); @@ -307,16 +362,25 @@ fn test_reconsolidation_finalizes_changes() { let snapshot = vestige_core::MemorySnapshot::capture( "Content".to_string(), vec!["tag".to_string()], - 0.8, 5.0, 0.9, vec![], + 0.8, + 5.0, + 0.9, + vec![], ); manager.mark_labile("mem-123", snapshot); - manager.apply_modification("mem-123", Modification::AddTag { - tag: "new-tag".to_string(), - }); - manager.apply_modification("mem-123", Modification::AddContext { - context: "Important meeting notes".to_string(), - }); + manager.apply_modification( + "mem-123", + Modification::AddTag { + tag: "new-tag".to_string(), + }, + ); + manager.apply_modification( + "mem-123", + Modification::AddContext { + context: "Important meeting notes".to_string(), + }, + ); let result = manager.reconsolidate("mem-123"); @@ -333,10 +397,8 @@ fn test_reconsolidation_finalizes_changes() { #[test] fn test_reconsolidation_tracks_access_context() { let mut manager = ReconsolidationManager::new(); - let snapshot = vestige_core::MemorySnapshot::capture( - "Content".to_string(), - vec![], 0.8, 5.0, 0.9, vec![], - ); + let snapshot = + vestige_core::MemorySnapshot::capture("Content".to_string(), vec![], 0.8, 5.0, 0.9, vec![]); let context = AccessContext { trigger: AccessTrigger::Search, query: Some("test query".to_string()), @@ -357,10 +419,8 @@ fn test_reconsolidation_tracks_access_context() { #[test] fn test_reconsolidation_retrieval_history() { let mut manager = ReconsolidationManager::new(); - let snapshot = vestige_core::MemorySnapshot::capture( - "Content".to_string(), - vec![], 0.8, 5.0, 0.9, vec![], - ); + let snapshot = + vestige_core::MemorySnapshot::capture("Content".to_string(), vec![], 0.8, 5.0, 0.9, vec![]); // Multiple retrievals for _ in 0..3 { @@ -417,8 +477,10 @@ fn test_fsrs_custom_decay_parameter() { let r_high_decay = retrievability_with_decay(stability, elapsed, 0.5); // Lower decay = steeper curve = lower retrievability for same time - assert!(r_low_decay < r_high_decay, - "Lower decay parameter should result in faster forgetting"); + assert!( + r_low_decay < r_high_decay, + "Lower decay parameter should result in faster forgetting" + ); } /// Test interval calculation round-trips with retrievability. @@ -436,7 +498,9 @@ fn test_fsrs_interval_retrievability_roundtrip() { assert!( (actual_r - target_r).abs() < 0.05, "Round-trip: interval={}, actual_R={:.3}, target_R={:.3}", - interval, actual_r, target_r + interval, + actual_r, + target_r ); } @@ -492,7 +556,10 @@ fn test_fsrs_difficulty_mean_reversion() { let high_d_after = result.state.difficulty; // Mean reversion should pull high difficulty down - assert!(high_d_after < high_d_before, "High difficulty should decrease"); + assert!( + high_d_after < high_d_before, + "High difficulty should decrease" + ); // Create card with low difficulty let mut low_d_card = scheduler.new_card(); @@ -502,7 +569,10 @@ fn test_fsrs_difficulty_mean_reversion() { // Again rating should increase difficulty let result = scheduler.review(&low_d_card, Rating::Again, 0.0, None); let low_d_after = result.state.difficulty; - assert!(low_d_after > low_d_before, "Again should increase low difficulty"); + assert!( + low_d_after > low_d_before, + "Again should increase low difficulty" + ); } /// Test scheduler lapse tracking. @@ -541,12 +611,18 @@ fn test_memory_state_accessibility_multipliers() { assert!((MemoryState::Unavailable.accessibility_multiplier() - 0.05).abs() < 0.001); // Active > Dormant > Silent > Unavailable - assert!(MemoryState::Active.accessibility_multiplier() > - MemoryState::Dormant.accessibility_multiplier()); - assert!(MemoryState::Dormant.accessibility_multiplier() > - MemoryState::Silent.accessibility_multiplier()); - assert!(MemoryState::Silent.accessibility_multiplier() > - MemoryState::Unavailable.accessibility_multiplier()); + assert!( + MemoryState::Active.accessibility_multiplier() + > MemoryState::Dormant.accessibility_multiplier() + ); + assert!( + MemoryState::Dormant.accessibility_multiplier() + > MemoryState::Silent.accessibility_multiplier() + ); + assert!( + MemoryState::Silent.accessibility_multiplier() + > MemoryState::Unavailable.accessibility_multiplier() + ); } /// Test state retrievability properties. @@ -589,11 +665,7 @@ fn test_memory_lifecycle_transitions() { fn test_memory_state_competition_suppression() { let mut lifecycle = MemoryLifecycle::new(); - lifecycle.suppress_from_competition( - "winner-123".to_string(), - 0.85, - Duration::hours(2), - ); + lifecycle.suppress_from_competition("winner-123".to_string(), 0.85, Duration::hours(2)); assert_eq!(lifecycle.state, MemoryState::Unavailable); assert!(!lifecycle.is_suppression_expired()); @@ -671,7 +743,10 @@ fn test_memory_state_accessibility_calculator() { assert!(active_threshold < 0.5, "Active has lower threshold"); assert!(silent_threshold > 0.5, "Silent has higher threshold"); - assert!(unavailable_threshold > 1.0, "Unavailable is effectively unreachable"); + assert!( + unavailable_threshold > 1.0, + "Unavailable is effectively unreachable" + ); } // ============================================================================ @@ -713,14 +788,19 @@ fn test_importance_arousal_signal() { let neutral_score = arousal.compute("The meeting is scheduled for tomorrow at 3pm."); // Highly emotional content - let emotional_score = arousal.compute( - "CRITICAL ERROR!!! Production database is DOWN! Data loss imminent!!!" - ); + let emotional_score = + arousal.compute("CRITICAL ERROR!!! Production database is DOWN! Data loss imminent!!!"); - assert!(emotional_score > neutral_score, + assert!( + emotional_score > neutral_score, "Emotional content should have higher arousal: {} vs {}", - emotional_score, neutral_score); - assert!(emotional_score > 0.5, "Highly emotional content should score high"); + emotional_score, + neutral_score + ); + assert!( + emotional_score > 0.5, + "Highly emotional content should score high" + ); // Detect emotional markers let markers = arousal.detect_emotional_markers("URGENT: Critical failure!!!"); @@ -740,14 +820,20 @@ fn test_importance_reward_signal() { reward.record_outcome("mem-helpful", OutcomeType::Helpful); let helpful_score = reward.compute("mem-helpful"); - assert!(helpful_score > 0.5, "Memory with positive outcomes should score high"); + assert!( + helpful_score > 0.5, + "Memory with positive outcomes should score high" + ); // Record negative outcomes reward.record_outcome("mem-unhelpful", OutcomeType::NotHelpful); reward.record_outcome("mem-unhelpful", OutcomeType::NotHelpful); let unhelpful_score = reward.compute("mem-unhelpful"); - assert!(unhelpful_score < 0.5, "Memory with negative outcomes should score low"); + assert!( + unhelpful_score < 0.5, + "Memory with negative outcomes should score low" + ); assert!(helpful_score > unhelpful_score); } @@ -768,11 +854,17 @@ fn test_importance_attention_signal() { edit_count: 2, unique_memories_accessed: 15, viewed_docs: true, - query_topics: vec!["rust".to_string(), "async".to_string(), "memory".to_string()], + query_topics: vec![ + "rust".to_string(), + "async".to_string(), + "memory".to_string(), + ], }; - assert!(attention.detect_learning_mode(&learning_session), - "Should detect learning mode from session patterns"); + assert!( + attention.detect_learning_mode(&learning_session), + "Should detect learning mode from session patterns" + ); // Non-learning session (quick edit) let quick_session = AttentionSession { @@ -786,8 +878,10 @@ fn test_importance_attention_signal() { query_topics: vec![], }; - assert!(!attention.detect_learning_mode(&quick_session), - "Quick edit session should not be learning mode"); + assert!( + !attention.detect_learning_mode(&quick_session), + "Quick edit session should not be learning mode" + ); } /// Test composite importance combines all signals. @@ -806,9 +900,15 @@ fn test_importance_composite_score() { &context, ); - assert!(score.composite > 0.4, "Important content should score moderately high"); + assert!( + score.composite > 0.4, + "Important content should score moderately high" + ); assert!(score.arousal > 0.4, "Emotional content should have arousal"); - assert!(score.encoding_boost >= 1.0, "High importance should boost encoding"); + assert!( + score.encoding_boost >= 1.0, + "High importance should boost encoding" + ); // Verify all components are present assert!(score.novelty >= 0.0 && score.novelty <= 1.0); diff --git a/tests/e2e/tests/cognitive/psychology_tests.rs b/tests/e2e/tests/cognitive/psychology_tests.rs index d89aaed..e794fde 100644 --- a/tests/e2e/tests/cognitive/psychology_tests.rs +++ b/tests/e2e/tests/cognitive/psychology_tests.rs @@ -129,15 +129,15 @@ fn test_serial_position_u_shaped_curve() { // U-shaped curve: high-low-high pattern let items = vec![ - ("pos_1", 0.90), // High (primacy) + ("pos_1", 0.90), // High (primacy) ("pos_2", 0.80), ("pos_3", 0.65), - ("pos_4", 0.55), // Low (middle) - ("pos_5", 0.50), // Low (middle) + ("pos_4", 0.55), // Low (middle) + ("pos_5", 0.50), // Low (middle) ("pos_6", 0.55), ("pos_7", 0.70), ("pos_8", 0.85), - ("pos_9", 0.95), // High (recency) + ("pos_9", 0.95), // High (recency) ]; for (item, strength) in &items { @@ -151,13 +151,35 @@ fn test_serial_position_u_shaped_curve() { let results = network.activate("list_context", 1.0); - let pos_1 = results.iter().find(|r| r.memory_id == "pos_1").map(|r| r.activation).unwrap_or(0.0); - let pos_5 = results.iter().find(|r| r.memory_id == "pos_5").map(|r| r.activation).unwrap_or(0.0); - let pos_9 = results.iter().find(|r| r.memory_id == "pos_9").map(|r| r.activation).unwrap_or(0.0); + let pos_1 = results + .iter() + .find(|r| r.memory_id == "pos_1") + .map(|r| r.activation) + .unwrap_or(0.0); + let pos_5 = results + .iter() + .find(|r| r.memory_id == "pos_5") + .map(|r| r.activation) + .unwrap_or(0.0); + let pos_9 = results + .iter() + .find(|r| r.memory_id == "pos_9") + .map(|r| r.activation) + .unwrap_or(0.0); // U-shape: ends higher than middle - assert!(pos_1 > pos_5, "First position ({}) > middle position ({})", pos_1, pos_5); - assert!(pos_9 > pos_5, "Last position ({}) > middle position ({})", pos_9, pos_5); + assert!( + pos_1 > pos_5, + "First position ({}) > middle position ({})", + pos_1, + pos_5 + ); + assert!( + pos_9 > pos_5, + "Last position ({}) > middle position ({})", + pos_9, + pos_5 + ); } /// Test that rehearsal strengthens primacy items. @@ -168,9 +190,24 @@ fn test_serial_position_rehearsal_strengthens_primacy() { let mut network = ActivationNetwork::new(); // Initial weak connections - network.add_edge("learning".to_string(), "first_concept".to_string(), LinkType::Semantic, 0.3); - network.add_edge("learning".to_string(), "middle_concept".to_string(), LinkType::Semantic, 0.3); - network.add_edge("learning".to_string(), "last_concept".to_string(), LinkType::Semantic, 0.3); + network.add_edge( + "learning".to_string(), + "first_concept".to_string(), + LinkType::Semantic, + 0.3, + ); + network.add_edge( + "learning".to_string(), + "middle_concept".to_string(), + LinkType::Semantic, + 0.3, + ); + network.add_edge( + "learning".to_string(), + "last_concept".to_string(), + LinkType::Semantic, + 0.3, + ); // Simulate rehearsal - first items get more rehearsal // (5 rehearsals for first, 2 for middle, 3 for last) @@ -217,12 +254,12 @@ fn test_serial_position_delay_eliminates_recency() { // After delay: primacy preserved, recency diminished // (modeling that working memory has cleared) let delayed_items = vec![ - ("early_1", 0.85), // Primacy preserved + ("early_1", 0.85), // Primacy preserved ("early_2", 0.75), ("middle_1", 0.50), ("middle_2", 0.45), - ("late_1", 0.40), // Recency lost after delay - ("late_2", 0.35), // (items not transferred to LTM) + ("late_1", 0.40), // Recency lost after delay + ("late_2", 0.35), // (items not transferred to LTM) ]; for (item, strength) in &delayed_items { @@ -271,7 +308,12 @@ fn test_spacing_effect_distributed_vs_massed() { let mut network = ActivationNetwork::new(); // Massed practice: all reinforcements close together (less effective) - network.add_edge("massed".to_string(), "concept_a".to_string(), LinkType::Semantic, 0.2); + network.add_edge( + "massed".to_string(), + "concept_a".to_string(), + LinkType::Semantic, + 0.2, + ); // 5 rapid reinforcements for _ in 0..5 { network.reinforce_edge("massed", "concept_a", 0.1); @@ -279,7 +321,12 @@ fn test_spacing_effect_distributed_vs_massed() { // Spaced practice: reinforcements distributed (more effective) // Simulated by giving higher reinforcement values (representing better encoding) - network.add_edge("spaced".to_string(), "concept_b".to_string(), LinkType::Semantic, 0.2); + network.add_edge( + "spaced".to_string(), + "concept_b".to_string(), + LinkType::Semantic, + 0.2, + ); // 5 spaced reinforcements with better encoding for _ in 0..5 { network.reinforce_edge("spaced", "concept_b", 0.15); // Higher value = better encoding @@ -316,12 +363,22 @@ fn test_spacing_effect_optimal_interval() { let mut network = ActivationNetwork::new(); // Short retention interval: shorter spacing optimal - network.add_edge("short_retention".to_string(), "fact_1".to_string(), LinkType::Semantic, 0.3); + network.add_edge( + "short_retention".to_string(), + "fact_1".to_string(), + LinkType::Semantic, + 0.3, + ); network.reinforce_edge("short_retention", "fact_1", 0.2); network.reinforce_edge("short_retention", "fact_1", 0.2); // Long retention interval: longer spacing optimal (simulated with stronger encoding) - network.add_edge("long_retention".to_string(), "fact_2".to_string(), LinkType::Semantic, 0.3); + network.add_edge( + "long_retention".to_string(), + "fact_2".to_string(), + LinkType::Semantic, + 0.3, + ); network.reinforce_edge("long_retention", "fact_2", 0.25); network.reinforce_edge("long_retention", "fact_2", 0.25); @@ -346,9 +403,24 @@ fn test_spacing_effect_semantic_associations() { let mut network = ActivationNetwork::new(); // Create semantic network with spaced learning - network.add_edge("programming".to_string(), "rust".to_string(), LinkType::Semantic, 0.5); - network.add_edge("rust".to_string(), "ownership".to_string(), LinkType::Semantic, 0.5); - network.add_edge("ownership".to_string(), "borrowing".to_string(), LinkType::Semantic, 0.5); + network.add_edge( + "programming".to_string(), + "rust".to_string(), + LinkType::Semantic, + 0.5, + ); + network.add_edge( + "rust".to_string(), + "ownership".to_string(), + LinkType::Semantic, + 0.5, + ); + network.add_edge( + "ownership".to_string(), + "borrowing".to_string(), + LinkType::Semantic, + 0.5, + ); // Spaced reinforcement of the path for _ in 0..3 { @@ -361,7 +433,10 @@ fn test_spacing_effect_semantic_associations() { // Should reach borrowing through the strengthened path let borrowing_result = results.iter().find(|r| r.memory_id == "borrowing"); - assert!(borrowing_result.is_some(), "Spaced learning should strengthen multi-hop paths"); + assert!( + borrowing_result.is_some(), + "Spaced learning should strengthen multi-hop paths" + ); let borrowing_activation = borrowing_result.unwrap().activation; assert!( @@ -379,7 +454,12 @@ fn test_spacing_effect_expanding_retrieval() { let mut network = ActivationNetwork::new(); // Expanding intervals: each retrieval strengthens more as intervals grow - network.add_edge("expanding".to_string(), "memory".to_string(), LinkType::Semantic, 0.2); + network.add_edge( + "expanding".to_string(), + "memory".to_string(), + LinkType::Semantic, + 0.2, + ); // Simulate expanding intervals with increasing reinforcement let expanding_reinforcements = [0.1, 0.12, 0.15, 0.18, 0.22]; // Increasing gains @@ -411,9 +491,24 @@ fn test_spacing_effect_multi_hop_paths() { let mut network = ActivationNetwork::with_config(config); // Create a learning chain - network.add_edge("topic".to_string(), "subtopic_a".to_string(), LinkType::Semantic, 0.4); - network.add_edge("subtopic_a".to_string(), "detail_1".to_string(), LinkType::Semantic, 0.4); - network.add_edge("detail_1".to_string(), "example".to_string(), LinkType::Semantic, 0.4); + network.add_edge( + "topic".to_string(), + "subtopic_a".to_string(), + LinkType::Semantic, + 0.4, + ); + network.add_edge( + "subtopic_a".to_string(), + "detail_1".to_string(), + LinkType::Semantic, + 0.4, + ); + network.add_edge( + "detail_1".to_string(), + "example".to_string(), + LinkType::Semantic, + 0.4, + ); // Spaced practice on entire chain for _ in 0..4 { @@ -426,11 +521,18 @@ fn test_spacing_effect_multi_hop_paths() { // Example should be reachable with good activation let example_result = results.iter().find(|r| r.memory_id == "example"); - assert!(example_result.is_some(), "Spaced practice should enable deep retrieval"); + assert!( + example_result.is_some(), + "Spaced practice should enable deep retrieval" + ); let example = example_result.unwrap(); assert_eq!(example.distance, 3, "Example should be 3 hops away"); - assert!(example.activation > 0.1, "Example should have sufficient activation: {}", example.activation); + assert!( + example.activation > 0.1, + "Example should have sufficient activation: {}", + example.activation + ); } // ============================================================================ @@ -448,27 +550,58 @@ fn test_context_dependent_matching_context() { let mut network = ActivationNetwork::new(); // Memory encoded in "office" context - network.add_edge("office_context".to_string(), "project_deadline".to_string(), LinkType::Semantic, 0.9); - network.add_edge("office_context".to_string(), "meeting_notes".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "office_context".to_string(), + "project_deadline".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "office_context".to_string(), + "meeting_notes".to_string(), + LinkType::Semantic, + 0.85, + ); // Memory encoded in "home" context - network.add_edge("home_context".to_string(), "grocery_list".to_string(), LinkType::Semantic, 0.9); - network.add_edge("home_context".to_string(), "family_event".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "home_context".to_string(), + "grocery_list".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "home_context".to_string(), + "family_event".to_string(), + LinkType::Semantic, + 0.85, + ); // Recall from office context let office_results = network.activate("office_context", 1.0); let home_results = network.activate("home_context", 1.0); // Office context should find office memories - let found_deadline = office_results.iter().any(|r| r.memory_id == "project_deadline"); + let found_deadline = office_results + .iter() + .any(|r| r.memory_id == "project_deadline"); let found_grocery = office_results.iter().any(|r| r.memory_id == "grocery_list"); - assert!(found_deadline, "Office context should activate office memories"); - assert!(!found_grocery, "Office context should NOT directly activate home memories"); + assert!( + found_deadline, + "Office context should activate office memories" + ); + assert!( + !found_grocery, + "Office context should NOT directly activate home memories" + ); // Home context should find home memories let home_found_grocery = home_results.iter().any(|r| r.memory_id == "grocery_list"); - assert!(home_found_grocery, "Home context should activate home memories"); + assert!( + home_found_grocery, + "Home context should activate home memories" + ); } /// Test encoding specificity principle. @@ -479,12 +612,32 @@ fn test_context_dependent_encoding_specificity() { let mut network = ActivationNetwork::new(); // Highly specific encoding context - network.add_edge("rainy_monday_morning".to_string(), "specific_memory".to_string(), LinkType::Temporal, 0.95); - network.add_edge("rainy_monday_morning".to_string(), "coffee_shop_idea".to_string(), LinkType::Temporal, 0.9); + network.add_edge( + "rainy_monday_morning".to_string(), + "specific_memory".to_string(), + LinkType::Temporal, + 0.95, + ); + network.add_edge( + "rainy_monday_morning".to_string(), + "coffee_shop_idea".to_string(), + LinkType::Temporal, + 0.9, + ); // General context (partial match) - network.add_edge("monday".to_string(), "rainy_monday_morning".to_string(), LinkType::Temporal, 0.6); - network.add_edge("morning".to_string(), "rainy_monday_morning".to_string(), LinkType::Temporal, 0.5); + network.add_edge( + "monday".to_string(), + "rainy_monday_morning".to_string(), + LinkType::Temporal, + 0.6, + ); + network.add_edge( + "morning".to_string(), + "rainy_monday_morning".to_string(), + LinkType::Temporal, + 0.5, + ); // Specific context retrieval let specific_results = network.activate("rainy_monday_morning", 1.0); @@ -520,20 +673,50 @@ fn test_context_dependent_state_dependent() { let mut network = ActivationNetwork::new(); // Memories encoded in different emotional states - network.add_edge("happy_state".to_string(), "positive_memory_1".to_string(), LinkType::Semantic, 0.9); - network.add_edge("happy_state".to_string(), "positive_memory_2".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "happy_state".to_string(), + "positive_memory_1".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "happy_state".to_string(), + "positive_memory_2".to_string(), + LinkType::Semantic, + 0.85, + ); - network.add_edge("stressed_state".to_string(), "work_problem_1".to_string(), LinkType::Semantic, 0.9); - network.add_edge("stressed_state".to_string(), "work_problem_2".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "stressed_state".to_string(), + "work_problem_1".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "stressed_state".to_string(), + "work_problem_2".to_string(), + LinkType::Semantic, + 0.85, + ); // Retrieve from happy state let happy_results = network.activate("happy_state", 1.0); - let found_positive = happy_results.iter().any(|r| r.memory_id == "positive_memory_1"); - let found_work = happy_results.iter().any(|r| r.memory_id == "work_problem_1"); + let found_positive = happy_results + .iter() + .any(|r| r.memory_id == "positive_memory_1"); + let found_work = happy_results + .iter() + .any(|r| r.memory_id == "work_problem_1"); - assert!(found_positive, "Happy state should activate positive memories"); - assert!(!found_work, "Happy state should NOT directly activate stressed memories"); + assert!( + found_positive, + "Happy state should activate positive memories" + ); + assert!( + !found_work, + "Happy state should NOT directly activate stressed memories" + ); } /// Test context reinstatement improves retrieval. @@ -544,20 +727,55 @@ fn test_context_dependent_reinstatement() { let mut network = ActivationNetwork::new(); // Memory with multiple context cues - network.add_edge("library".to_string(), "study_session".to_string(), LinkType::Temporal, 0.8); - network.add_edge("quiet".to_string(), "study_session".to_string(), LinkType::Semantic, 0.7); - network.add_edge("evening".to_string(), "study_session".to_string(), LinkType::Temporal, 0.6); + network.add_edge( + "library".to_string(), + "study_session".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "quiet".to_string(), + "study_session".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "evening".to_string(), + "study_session".to_string(), + LinkType::Temporal, + 0.6, + ); // Study session links to learned material - network.add_edge("study_session".to_string(), "learned_concept".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "study_session".to_string(), + "learned_concept".to_string(), + LinkType::Semantic, + 0.9, + ); // Single context cue let single_cue = network.activate("library", 1.0); // Multiple context cues (reinstatement) - we need to create a combined node - network.add_edge("reinstated_context".to_string(), "library".to_string(), LinkType::Semantic, 0.9); - network.add_edge("reinstated_context".to_string(), "quiet".to_string(), LinkType::Semantic, 0.9); - network.add_edge("reinstated_context".to_string(), "evening".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "reinstated_context".to_string(), + "library".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "reinstated_context".to_string(), + "quiet".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "reinstated_context".to_string(), + "evening".to_string(), + LinkType::Semantic, + 0.9, + ); let reinstated_results = network.activate("reinstated_context", 1.0); @@ -587,10 +805,20 @@ fn test_context_dependent_transfer_appropriate() { let mut network = ActivationNetwork::new(); // Semantic encoding (deep processing) - network.add_edge("meaning_focused".to_string(), "concept_meaning".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "meaning_focused".to_string(), + "concept_meaning".to_string(), + LinkType::Semantic, + 0.9, + ); // Perceptual encoding (shallow processing) - network.add_edge("appearance_focused".to_string(), "concept_appearance".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "appearance_focused".to_string(), + "concept_appearance".to_string(), + LinkType::Semantic, + 0.9, + ); // Semantic retrieval cue let semantic_results = network.activate("meaning_focused", 1.0); @@ -599,15 +827,30 @@ fn test_context_dependent_transfer_appropriate() { let perceptual_results = network.activate("appearance_focused", 1.0); // Matching encoding-retrieval processing should work best - let semantic_found = semantic_results.iter().any(|r| r.memory_id == "concept_meaning"); - let perceptual_found = perceptual_results.iter().any(|r| r.memory_id == "concept_appearance"); + let semantic_found = semantic_results + .iter() + .any(|r| r.memory_id == "concept_meaning"); + let perceptual_found = perceptual_results + .iter() + .any(|r| r.memory_id == "concept_appearance"); - assert!(semantic_found, "Semantic cue should retrieve semantically encoded info"); - assert!(perceptual_found, "Perceptual cue should retrieve perceptually encoded info"); + assert!( + semantic_found, + "Semantic cue should retrieve semantically encoded info" + ); + assert!( + perceptual_found, + "Perceptual cue should retrieve perceptually encoded info" + ); // Cross-retrieval should be weaker (not directly connected) - let cross_found = semantic_results.iter().any(|r| r.memory_id == "concept_appearance"); - assert!(!cross_found, "Semantic cue should NOT directly retrieve perceptual encoding"); + let cross_found = semantic_results + .iter() + .any(|r| r.memory_id == "concept_appearance"); + assert!( + !cross_found, + "Semantic cue should NOT directly retrieve perceptual encoding" + ); } // ============================================================================ @@ -622,26 +865,51 @@ fn test_context_dependent_transfer_appropriate() { #[test] fn test_tot_partial_activation() { let config = ActivationConfig { - decay_factor: 0.6, // Higher decay = weaker far connections + decay_factor: 0.6, // Higher decay = weaker far connections max_hops: 3, - min_threshold: 0.15, // Higher threshold = some items not retrieved + min_threshold: 0.15, // Higher threshold = some items not retrieved allow_cycles: false, }; let mut network = ActivationNetwork::with_config(config); // Target word "serendipity" with various features - network.add_edge("word_search".to_string(), "starts_with_s".to_string(), LinkType::Semantic, 0.8); - network.add_edge("word_search".to_string(), "four_syllables".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word_search".to_string(), "meaning_lucky_discovery".to_string(), LinkType::Semantic, 0.85); - network.add_edge("starts_with_s".to_string(), "serendipity".to_string(), LinkType::Semantic, 0.3); // Weak link to target + network.add_edge( + "word_search".to_string(), + "starts_with_s".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "word_search".to_string(), + "four_syllables".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word_search".to_string(), + "meaning_lucky_discovery".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "starts_with_s".to_string(), + "serendipity".to_string(), + LinkType::Semantic, + 0.3, + ); // Weak link to target let results = network.activate("word_search", 1.0); // Should find partial information let found_starts_s = results.iter().any(|r| r.memory_id == "starts_with_s"); - let found_meaning = results.iter().any(|r| r.memory_id == "meaning_lucky_discovery"); + let found_meaning = results + .iter() + .any(|r| r.memory_id == "meaning_lucky_discovery"); - assert!(found_starts_s, "Should retrieve partial info (first letter)"); + assert!( + found_starts_s, + "Should retrieve partial info (first letter)" + ); assert!(found_meaning, "Should retrieve partial info (meaning)"); // Target might not be found due to weak link and threshold @@ -669,17 +937,49 @@ fn test_tot_related_words_activated() { // Searching for "archipelago" // Related words get activated instead - network.add_edge("island_chain_concept".to_string(), "archipelago".to_string(), LinkType::Semantic, 0.4); // Weak - network.add_edge("island_chain_concept".to_string(), "peninsula".to_string(), LinkType::Semantic, 0.7); // Related, stronger - network.add_edge("island_chain_concept".to_string(), "atoll".to_string(), LinkType::Semantic, 0.65); // Related - network.add_edge("island_chain_concept".to_string(), "islands".to_string(), LinkType::Semantic, 0.8); // Generic, strong + network.add_edge( + "island_chain_concept".to_string(), + "archipelago".to_string(), + LinkType::Semantic, + 0.4, + ); // Weak + network.add_edge( + "island_chain_concept".to_string(), + "peninsula".to_string(), + LinkType::Semantic, + 0.7, + ); // Related, stronger + network.add_edge( + "island_chain_concept".to_string(), + "atoll".to_string(), + LinkType::Semantic, + 0.65, + ); // Related + network.add_edge( + "island_chain_concept".to_string(), + "islands".to_string(), + LinkType::Semantic, + 0.8, + ); // Generic, strong let results = network.activate("island_chain_concept", 1.0); // Generic/related words should be more activated than target - let archipelago_act = results.iter().find(|r| r.memory_id == "archipelago").map(|r| r.activation).unwrap_or(0.0); - let islands_act = results.iter().find(|r| r.memory_id == "islands").map(|r| r.activation).unwrap_or(0.0); - let peninsula_act = results.iter().find(|r| r.memory_id == "peninsula").map(|r| r.activation).unwrap_or(0.0); + let archipelago_act = results + .iter() + .find(|r| r.memory_id == "archipelago") + .map(|r| r.activation) + .unwrap_or(0.0); + let islands_act = results + .iter() + .find(|r| r.memory_id == "islands") + .map(|r| r.activation) + .unwrap_or(0.0); + let peninsula_act = results + .iter() + .find(|r| r.memory_id == "peninsula") + .map(|r| r.activation) + .unwrap_or(0.0); assert!( islands_act > archipelago_act, @@ -703,10 +1003,25 @@ fn test_tot_phonological_cue_resolution() { // Target: "ephemeral" // Weak semantic link - network.add_edge("temporary_concept".to_string(), "ephemeral".to_string(), LinkType::Semantic, 0.3); + network.add_edge( + "temporary_concept".to_string(), + "ephemeral".to_string(), + LinkType::Semantic, + 0.3, + ); // Strong phonological link - network.add_edge("starts_with_eph".to_string(), "ephemeral".to_string(), LinkType::Semantic, 0.85); - network.add_edge("temporary_concept".to_string(), "starts_with_eph".to_string(), LinkType::Semantic, 0.5); + network.add_edge( + "starts_with_eph".to_string(), + "ephemeral".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "temporary_concept".to_string(), + "starts_with_eph".to_string(), + LinkType::Semantic, + 0.5, + ); // Without phonological cue (just semantic) let semantic_only = network.activate("temporary_concept", 1.0); @@ -741,11 +1056,21 @@ fn test_tot_phonological_cue_resolution() { fn test_tot_age_related_increase() { // "Young" network - strong connections let mut young_network = ActivationNetwork::new(); - young_network.add_edge("cue".to_string(), "target_word".to_string(), LinkType::Semantic, 0.85); + young_network.add_edge( + "cue".to_string(), + "target_word".to_string(), + LinkType::Semantic, + 0.85, + ); // "Older" network - weakened connections let mut older_network = ActivationNetwork::new(); - older_network.add_edge("cue".to_string(), "target_word".to_string(), LinkType::Semantic, 0.45); + older_network.add_edge( + "cue".to_string(), + "target_word".to_string(), + LinkType::Semantic, + 0.45, + ); let young_results = young_network.activate("cue", 1.0); let older_results = older_network.activate("cue", 1.0); @@ -778,8 +1103,18 @@ fn test_tot_blocking_effect() { let mut network = ActivationNetwork::new(); // Target and blocker both connected to cue - network.add_edge("definition_cue".to_string(), "blocker_word".to_string(), LinkType::Semantic, 0.9); // Strong - network.add_edge("definition_cue".to_string(), "target_word".to_string(), LinkType::Semantic, 0.5); // Weaker + network.add_edge( + "definition_cue".to_string(), + "blocker_word".to_string(), + LinkType::Semantic, + 0.9, + ); // Strong + network.add_edge( + "definition_cue".to_string(), + "target_word".to_string(), + LinkType::Semantic, + 0.5, + ); // Weaker let results = network.activate("definition_cue", 1.0); @@ -819,13 +1154,15 @@ fn test_drm_basic_false_memory() { let mut network = ActivationNetwork::new(); // Study list - all semantically related to "sleep" (the critical lure) - let study_words = ["bed", "rest", "awake", "tired", "dream", "pillow", "blanket", "nap"]; + let study_words = [ + "bed", "rest", "awake", "tired", "dream", "pillow", "blanket", "nap", + ]; // Create associations from study words to the critical lure for word in &study_words { network.add_edge( word.to_string(), - "sleep".to_string(), // Critical lure (never studied) + "sleep".to_string(), // Critical lure (never studied) LinkType::Semantic, 0.7, ); @@ -833,7 +1170,12 @@ fn test_drm_basic_false_memory() { // Also link study words to a study context for word in &study_words { - network.add_edge("study_list".to_string(), word.to_string(), LinkType::Temporal, 0.8); + network.add_edge( + "study_list".to_string(), + word.to_string(), + LinkType::Temporal, + 0.8, + ); } // Activate from study context @@ -856,15 +1198,45 @@ fn test_drm_convergent_activation() { let mut network = ActivationNetwork::new(); // Multiple words converging on critical lure - network.add_edge("cold".to_string(), "hot".to_string(), LinkType::Semantic, 0.8); - network.add_edge("warm".to_string(), "hot".to_string(), LinkType::Semantic, 0.85); - network.add_edge("heat".to_string(), "hot".to_string(), LinkType::Semantic, 0.9); - network.add_edge("burn".to_string(), "hot".to_string(), LinkType::Semantic, 0.75); - network.add_edge("fire".to_string(), "hot".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "cold".to_string(), + "hot".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "warm".to_string(), + "hot".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "heat".to_string(), + "hot".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "burn".to_string(), + "hot".to_string(), + LinkType::Semantic, + 0.75, + ); + network.add_edge( + "fire".to_string(), + "hot".to_string(), + LinkType::Semantic, + 0.8, + ); // Study context for word in ["cold", "warm", "heat", "burn", "fire"] { - network.add_edge("study_context".to_string(), word.to_string(), LinkType::Temporal, 0.8); + network.add_edge( + "study_context".to_string(), + word.to_string(), + LinkType::Temporal, + 0.8, + ); } let results = network.activate("study_context", 1.0); @@ -894,20 +1266,80 @@ fn test_drm_semantic_relatedness() { let mut network = ActivationNetwork::new(); // Strongly related list - network.add_edge("strong_list".to_string(), "nurse".to_string(), LinkType::Temporal, 0.8); - network.add_edge("strong_list".to_string(), "hospital".to_string(), LinkType::Temporal, 0.8); - network.add_edge("strong_list".to_string(), "medicine".to_string(), LinkType::Temporal, 0.8); - network.add_edge("nurse".to_string(), "doctor".to_string(), LinkType::Semantic, 0.9); - network.add_edge("hospital".to_string(), "doctor".to_string(), LinkType::Semantic, 0.85); - network.add_edge("medicine".to_string(), "doctor".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "strong_list".to_string(), + "nurse".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "strong_list".to_string(), + "hospital".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "strong_list".to_string(), + "medicine".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "nurse".to_string(), + "doctor".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "hospital".to_string(), + "doctor".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "medicine".to_string(), + "doctor".to_string(), + LinkType::Semantic, + 0.8, + ); // Weakly related list - network.add_edge("weak_list".to_string(), "white".to_string(), LinkType::Temporal, 0.8); - network.add_edge("weak_list".to_string(), "smart".to_string(), LinkType::Temporal, 0.8); - network.add_edge("weak_list".to_string(), "office".to_string(), LinkType::Temporal, 0.8); - network.add_edge("white".to_string(), "doctor".to_string(), LinkType::Semantic, 0.3); // Weak - network.add_edge("smart".to_string(), "doctor".to_string(), LinkType::Semantic, 0.25); // Weak - network.add_edge("office".to_string(), "doctor".to_string(), LinkType::Semantic, 0.2); // Weak + network.add_edge( + "weak_list".to_string(), + "white".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "weak_list".to_string(), + "smart".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "weak_list".to_string(), + "office".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "white".to_string(), + "doctor".to_string(), + LinkType::Semantic, + 0.3, + ); // Weak + network.add_edge( + "smart".to_string(), + "doctor".to_string(), + LinkType::Semantic, + 0.25, + ); // Weak + network.add_edge( + "office".to_string(), + "doctor".to_string(), + LinkType::Semantic, + 0.2, + ); // Weak let strong_results = network.activate("strong_list", 1.0); let weak_results = network.activate("weak_list", 1.0); @@ -940,11 +1372,26 @@ fn test_drm_source_monitoring() { let mut network = ActivationNetwork::new(); // Studied word - network.add_edge("study_session".to_string(), "actually_studied".to_string(), LinkType::Temporal, 0.85); + network.add_edge( + "study_session".to_string(), + "actually_studied".to_string(), + LinkType::Temporal, + 0.85, + ); // Critical lure (activated through association, not direct study) - network.add_edge("study_session".to_string(), "related_word".to_string(), LinkType::Temporal, 0.8); - network.add_edge("related_word".to_string(), "critical_lure".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "study_session".to_string(), + "related_word".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "related_word".to_string(), + "critical_lure".to_string(), + LinkType::Semantic, + 0.9, + ); let results = network.activate("study_session", 1.0); @@ -963,10 +1410,16 @@ fn test_drm_source_monitoring() { // Both should have activation (source confusion) assert!(studied_activation > 0.0, "Studied word should be activated"); - assert!(lure_activation > 0.0, "Lure should also be activated, creating potential source confusion"); + assert!( + lure_activation > 0.0, + "Lure should also be activated, creating potential source confusion" + ); // The lure should have distance > 1 (indirect) but this is the only way to distinguish - let lure_result = results.iter().find(|r| r.memory_id == "critical_lure").unwrap(); + let lure_result = results + .iter() + .find(|r| r.memory_id == "critical_lure") + .unwrap(); assert!( lure_result.distance > 1, "Lure came through indirect activation (distance {}), but feels like direct memory", @@ -982,31 +1435,117 @@ fn test_drm_list_length_effect() { let mut network = ActivationNetwork::new(); // Short list - network.add_edge("short_list".to_string(), "word1".to_string(), LinkType::Temporal, 0.8); - network.add_edge("short_list".to_string(), "word2".to_string(), LinkType::Temporal, 0.8); - network.add_edge("word1".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word2".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); + network.add_edge( + "short_list".to_string(), + "word1".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "short_list".to_string(), + "word2".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "word1".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word2".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); // Long list - network.add_edge("long_list".to_string(), "word_a".to_string(), LinkType::Temporal, 0.8); - network.add_edge("long_list".to_string(), "word_b".to_string(), LinkType::Temporal, 0.8); - network.add_edge("long_list".to_string(), "word_c".to_string(), LinkType::Temporal, 0.8); - network.add_edge("long_list".to_string(), "word_d".to_string(), LinkType::Temporal, 0.8); - network.add_edge("long_list".to_string(), "word_e".to_string(), LinkType::Temporal, 0.8); - network.add_edge("long_list".to_string(), "word_f".to_string(), LinkType::Temporal, 0.8); - network.add_edge("word_a".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word_b".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word_c".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word_d".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word_e".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); - network.add_edge("word_f".to_string(), "lure".to_string(), LinkType::Semantic, 0.7); + network.add_edge( + "long_list".to_string(), + "word_a".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "long_list".to_string(), + "word_b".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "long_list".to_string(), + "word_c".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "long_list".to_string(), + "word_d".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "long_list".to_string(), + "word_e".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "long_list".to_string(), + "word_f".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "word_a".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word_b".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word_c".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word_d".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word_e".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "word_f".to_string(), + "lure".to_string(), + LinkType::Semantic, + 0.7, + ); let short_results = network.activate("short_list", 1.0); let long_results = network.activate("long_list", 1.0); // Count total activation paths to lure - let short_lure_count = short_results.iter().filter(|r| r.memory_id == "lure").count(); - let long_lure_count = long_results.iter().filter(|r| r.memory_id == "lure").count(); + let short_lure_count = short_results + .iter() + .filter(|r| r.memory_id == "lure") + .count(); + let long_lure_count = long_results + .iter() + .filter(|r| r.memory_id == "lure") + .count(); assert!( long_lure_count >= short_lure_count, @@ -1029,10 +1568,20 @@ fn test_interference_proactive() { let mut network = ActivationNetwork::new(); // Old learning (List A paired associates) - network.add_edge("cue_word".to_string(), "old_response".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "cue_word".to_string(), + "old_response".to_string(), + LinkType::Semantic, + 0.8, + ); // New learning (List B with same cues) - network.add_edge("cue_word".to_string(), "new_response".to_string(), LinkType::Semantic, 0.5); // Weaker - harder to learn + network.add_edge( + "cue_word".to_string(), + "new_response".to_string(), + LinkType::Semantic, + 0.5, + ); // Weaker - harder to learn let results = network.activate("cue_word", 1.0); @@ -1065,10 +1614,20 @@ fn test_interference_retroactive() { let mut network = ActivationNetwork::new(); // Original learning - network.add_edge("stimulus".to_string(), "original_memory".to_string(), LinkType::Semantic, 0.7); + network.add_edge( + "stimulus".to_string(), + "original_memory".to_string(), + LinkType::Semantic, + 0.7, + ); // Interpolated learning (new, stronger) - network.add_edge("stimulus".to_string(), "new_memory".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "stimulus".to_string(), + "new_memory".to_string(), + LinkType::Semantic, + 0.9, + ); let results = network.activate("stimulus", 1.0); @@ -1101,12 +1660,32 @@ fn test_interference_similarity_based() { let mut network = ActivationNetwork::new(); // Similar competing memories - network.add_edge("topic".to_string(), "similar_fact_1".to_string(), LinkType::Semantic, 0.75); - network.add_edge("topic".to_string(), "similar_fact_2".to_string(), LinkType::Semantic, 0.73); - network.add_edge("topic".to_string(), "similar_fact_3".to_string(), LinkType::Semantic, 0.71); + network.add_edge( + "topic".to_string(), + "similar_fact_1".to_string(), + LinkType::Semantic, + 0.75, + ); + network.add_edge( + "topic".to_string(), + "similar_fact_2".to_string(), + LinkType::Semantic, + 0.73, + ); + network.add_edge( + "topic".to_string(), + "similar_fact_3".to_string(), + LinkType::Semantic, + 0.71, + ); // Dissimilar memory (should be easier to distinguish) - network.add_edge("topic".to_string(), "dissimilar_fact".to_string(), LinkType::Semantic, 0.80); + network.add_edge( + "topic".to_string(), + "dissimilar_fact".to_string(), + LinkType::Semantic, + 0.80, + ); let results = network.activate("topic", 1.0); @@ -1160,13 +1739,23 @@ fn test_interference_fan_effect() { // Low fan: concept with few associations let mut low_fan_network = ActivationNetwork::with_config(config.clone()); - low_fan_network.add_edge("low_fan_concept".to_string(), "fact_1".to_string(), LinkType::Semantic, 0.9); - low_fan_network.add_edge("low_fan_concept".to_string(), "fact_2".to_string(), LinkType::Semantic, 0.85); + low_fan_network.add_edge( + "low_fan_concept".to_string(), + "fact_1".to_string(), + LinkType::Semantic, + 0.9, + ); + low_fan_network.add_edge( + "low_fan_concept".to_string(), + "fact_2".to_string(), + LinkType::Semantic, + 0.85, + ); // High fan: concept with many associations let mut high_fan_network = ActivationNetwork::with_config(config); for i in 1..=8 { - let strength = 0.9 - (i as f64 * 0.05); // Decreasing strength due to fan + let strength = 0.9 - (i as f64 * 0.05); // Decreasing strength due to fan high_fan_network.add_edge( "high_fan_concept".to_string(), format!("fact_{}", i), @@ -1179,16 +1768,16 @@ fn test_interference_fan_effect() { let high_results = high_fan_network.activate("high_fan_concept", 1.0); // Average activation for low fan - let low_avg: f64 = low_results.iter().map(|r| r.activation).sum::() - / low_results.len().max(1) as f64; + let low_avg: f64 = + low_results.iter().map(|r| r.activation).sum::() / low_results.len().max(1) as f64; // Average activation for high fan - let high_avg: f64 = high_results.iter().map(|r| r.activation).sum::() - / high_results.len().max(1) as f64; + let high_avg: f64 = + high_results.iter().map(|r| r.activation).sum::() / high_results.len().max(1) as f64; // Low fan should have higher average activation (less interference) assert!( - low_avg >= high_avg * 0.8, // Allow some tolerance + low_avg >= high_avg * 0.8, // Allow some tolerance "Low fan concept should have higher average activation: low={}, high={}", low_avg, high_avg @@ -1203,12 +1792,32 @@ fn test_interference_release_from_pi() { let mut network = ActivationNetwork::new(); // Build up PI with category A items - network.add_edge("trial_1".to_string(), "category_a_item_1".to_string(), LinkType::Temporal, 0.7); - network.add_edge("trial_2".to_string(), "category_a_item_2".to_string(), LinkType::Temporal, 0.6); // PI building - network.add_edge("trial_3".to_string(), "category_a_item_3".to_string(), LinkType::Temporal, 0.5); // More PI + network.add_edge( + "trial_1".to_string(), + "category_a_item_1".to_string(), + LinkType::Temporal, + 0.7, + ); + network.add_edge( + "trial_2".to_string(), + "category_a_item_2".to_string(), + LinkType::Temporal, + 0.6, + ); // PI building + network.add_edge( + "trial_3".to_string(), + "category_a_item_3".to_string(), + LinkType::Temporal, + 0.5, + ); // More PI // Category shift (release from PI) - network.add_edge("trial_4".to_string(), "category_b_item_1".to_string(), LinkType::Temporal, 0.85); // Recovery + network.add_edge( + "trial_4".to_string(), + "category_b_item_1".to_string(), + LinkType::Temporal, + 0.85, + ); // Recovery let trial_3_results = network.activate("trial_3", 1.0); let trial_4_results = network.activate("trial_4", 1.0); diff --git a/tests/e2e/tests/cognitive/spreading_activation_tests.rs b/tests/e2e/tests/cognitive/spreading_activation_tests.rs index 78b5fcb..2f91ffe 100644 --- a/tests/e2e/tests/cognitive/spreading_activation_tests.rs +++ b/tests/e2e/tests/cognitive/spreading_activation_tests.rs @@ -5,10 +5,10 @@ //! //! Based on Collins & Loftus (1975) spreading activation theory. +use std::collections::HashSet; use vestige_core::neuroscience::spreading_activation::{ ActivationConfig, ActivationNetwork, LinkType, }; -use std::collections::HashSet; // ============================================================================ // MULTI-HOP ASSOCIATION TESTS (6 tests) @@ -41,9 +41,7 @@ fn test_spreading_finds_hidden_chains() { let results = network.activate("rust_async", 1.0); // Should find "green_threads" through the chain - let found_green_threads = results - .iter() - .any(|r| r.memory_id == "green_threads"); + let found_green_threads = results.iter().any(|r| r.memory_id == "green_threads"); assert!( found_green_threads, @@ -71,9 +69,24 @@ fn test_spreading_3_hop_discovery() { let mut network = ActivationNetwork::with_config(config); // Create a 3-hop chain: A -> B -> C -> D - network.add_edge("memory_a".to_string(), "memory_b".to_string(), LinkType::Semantic, 0.9); - network.add_edge("memory_b".to_string(), "memory_c".to_string(), LinkType::Semantic, 0.9); - network.add_edge("memory_c".to_string(), "memory_d".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "memory_a".to_string(), + "memory_b".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "memory_b".to_string(), + "memory_c".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "memory_c".to_string(), + "memory_d".to_string(), + LinkType::Semantic, + 0.9, + ); let results = network.activate("memory_a", 1.0); @@ -147,8 +160,18 @@ fn test_spreading_beats_similarity_search() { fn test_spreading_path_tracking() { let mut network = ActivationNetwork::new(); - network.add_edge("start".to_string(), "middle".to_string(), LinkType::Semantic, 0.9); - network.add_edge("middle".to_string(), "end".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "start".to_string(), + "middle".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "middle".to_string(), + "end".to_string(), + LinkType::Semantic, + 0.9, + ); let results = network.activate("start", 1.0); @@ -167,10 +190,30 @@ fn test_spreading_convergent_activation() { let mut network = ActivationNetwork::new(); // Create convergent paths: A -> B -> D and A -> C -> D - network.add_edge("source".to_string(), "path1".to_string(), LinkType::Semantic, 0.8); - network.add_edge("source".to_string(), "path2".to_string(), LinkType::Semantic, 0.8); - network.add_edge("path1".to_string(), "target".to_string(), LinkType::Semantic, 0.8); - network.add_edge("path2".to_string(), "target".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "source".to_string(), + "path1".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "source".to_string(), + "path2".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "path1".to_string(), + "target".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "path2".to_string(), + "target".to_string(), + LinkType::Semantic, + 0.8, + ); let results = network.activate("source", 1.0); @@ -244,13 +287,35 @@ fn test_activation_decay_per_hop() { let results = network.activate("a", 1.0); - let b_activation = results.iter().find(|r| r.memory_id == "b").map(|r| r.activation).unwrap_or(0.0); - let c_activation = results.iter().find(|r| r.memory_id == "c").map(|r| r.activation).unwrap_or(0.0); - let d_activation = results.iter().find(|r| r.memory_id == "d").map(|r| r.activation).unwrap_or(0.0); + let b_activation = results + .iter() + .find(|r| r.memory_id == "b") + .map(|r| r.activation) + .unwrap_or(0.0); + let c_activation = results + .iter() + .find(|r| r.memory_id == "c") + .map(|r| r.activation) + .unwrap_or(0.0); + let d_activation = results + .iter() + .find(|r| r.memory_id == "d") + .map(|r| r.activation) + .unwrap_or(0.0); // Each hop should reduce activation by decay factor (0.7) - assert!(b_activation > c_activation, "Activation should decay: b ({}) > c ({})", b_activation, c_activation); - assert!(c_activation > d_activation, "Activation should decay: c ({}) > d ({})", c_activation, d_activation); + assert!( + b_activation > c_activation, + "Activation should decay: b ({}) > c ({})", + b_activation, + c_activation + ); + assert!( + c_activation > d_activation, + "Activation should decay: c ({}) > d ({})", + c_activation, + d_activation + ); // Verify approximate decay rate (allowing for floating point) let ratio_bc = c_activation / b_activation; @@ -289,8 +354,16 @@ fn test_activation_decay_factor_configurable() { let high_results = high_network.activate("a", 1.0); let low_results = low_network.activate("a", 1.0); - let high_c = high_results.iter().find(|r| r.memory_id == "c").map(|r| r.activation).unwrap_or(0.0); - let low_c = low_results.iter().find(|r| r.memory_id == "c").map(|r| r.activation).unwrap_or(0.0); + let high_c = high_results + .iter() + .find(|r| r.memory_id == "c") + .map(|r| r.activation) + .unwrap_or(0.0); + let low_c = low_results + .iter() + .find(|r| r.memory_id == "c") + .map(|r| r.activation) + .unwrap_or(0.0); assert!( high_c > low_c, @@ -320,10 +393,8 @@ fn test_activation_distance_law() { let results = network.activate("n0", 1.0); // Collect activations by distance - let mut activations_by_distance: Vec<(u32, f64)> = results - .iter() - .map(|r| (r.distance, r.activation)) - .collect(); + let mut activations_by_distance: Vec<(u32, f64)> = + results.iter().map(|r| (r.distance, r.activation)).collect(); activations_by_distance.sort_by_key(|(d, _)| *d); // Verify monotonic decrease with distance @@ -559,9 +630,21 @@ fn test_link_type_weights() { let results = network.activate("event", 1.0); // Verify different activations based on edge strength - let semantic_act = results.iter().find(|r| r.memory_id == "semantic_link").map(|r| r.activation).unwrap_or(0.0); - let temporal_act = results.iter().find(|r| r.memory_id == "temporal_link").map(|r| r.activation).unwrap_or(0.0); - let causal_act = results.iter().find(|r| r.memory_id == "causal_link").map(|r| r.activation).unwrap_or(0.0); + let semantic_act = results + .iter() + .find(|r| r.memory_id == "semantic_link") + .map(|r| r.activation) + .unwrap_or(0.0); + let temporal_act = results + .iter() + .find(|r| r.memory_id == "temporal_link") + .map(|r| r.activation) + .unwrap_or(0.0); + let causal_act = results + .iter() + .find(|r| r.memory_id == "causal_link") + .map(|r| r.activation) + .unwrap_or(0.0); // Semantic (0.9) > Causal (0.7) > Temporal (0.5) assert!( @@ -617,10 +700,30 @@ fn test_network_builds_from_semantic_similarity() { // These would typically be built from embedding similarity // Rust async ecosystem - network.add_edge("async_rust".to_string(), "tokio".to_string(), LinkType::Semantic, 0.9); - network.add_edge("async_rust".to_string(), "async_await".to_string(), LinkType::Semantic, 0.95); - network.add_edge("tokio".to_string(), "runtime".to_string(), LinkType::Semantic, 0.8); - network.add_edge("tokio".to_string(), "spawn".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "async_rust".to_string(), + "tokio".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "async_rust".to_string(), + "async_await".to_string(), + LinkType::Semantic, + 0.95, + ); + network.add_edge( + "tokio".to_string(), + "runtime".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "tokio".to_string(), + "spawn".to_string(), + LinkType::Semantic, + 0.85, + ); assert_eq!(network.node_count(), 5); assert_eq!(network.edge_count(), 4); @@ -642,9 +745,24 @@ fn test_network_builds_from_temporal_proximity() { // Events that happened close in time // Morning standup sequence - network.add_edge("standup".to_string(), "jira_update".to_string(), LinkType::Temporal, 0.9); - network.add_edge("jira_update".to_string(), "code_review".to_string(), LinkType::Temporal, 0.85); - network.add_edge("code_review".to_string(), "merge_pr".to_string(), LinkType::Temporal, 0.8); + network.add_edge( + "standup".to_string(), + "jira_update".to_string(), + LinkType::Temporal, + 0.9, + ); + network.add_edge( + "jira_update".to_string(), + "code_review".to_string(), + LinkType::Temporal, + 0.85, + ); + network.add_edge( + "code_review".to_string(), + "merge_pr".to_string(), + LinkType::Temporal, + 0.8, + ); // Verify temporal chain let results = network.activate("standup", 1.0); @@ -748,6 +866,13 @@ fn test_network_batch_construction() { let distance_1: Vec<_> = results.iter().filter(|r| r.distance == 1).collect(); let distance_2: Vec<_> = results.iter().filter(|r| r.distance == 2).collect(); - assert_eq!(distance_1.len(), 3, "Should have 3 nodes at distance 1 (cargo, ownership, traits)"); - assert!(distance_2.len() >= 4, "Should have at least 4 nodes at distance 2"); + assert_eq!( + distance_1.len(), + 3, + "Should have 3 nodes at distance 1 (cargo, ownership, traits)" + ); + assert!( + distance_2.len() >= 4, + "Should have at least 4 nodes at distance 2" + ); } diff --git a/tests/e2e/tests/extreme/adversarial_tests.rs b/tests/e2e/tests/extreme/adversarial_tests.rs index 2fcc042..c179d65 100644 --- a/tests/e2e/tests/extreme/adversarial_tests.rs +++ b/tests/e2e/tests/extreme/adversarial_tests.rs @@ -11,12 +11,12 @@ //! //! Based on security testing principles and fuzzing methodologies +use chrono::Utc; +use vestige_core::neuroscience::hippocampal_index::HippocampalIndex; use vestige_core::neuroscience::spreading_activation::{ ActivationConfig, ActivationNetwork, LinkType, }; use vestige_core::neuroscience::synaptic_tagging::SynapticTaggingSystem; -use vestige_core::neuroscience::hippocampal_index::HippocampalIndex; -use chrono::Utc; // ============================================================================ // MALFORMED INPUT HANDLING (2 tests) @@ -30,8 +30,18 @@ fn test_adversarial_empty_inputs() { let mut network = ActivationNetwork::new(); // Empty string node IDs - network.add_edge("".to_string(), "target".to_string(), LinkType::Semantic, 0.5); - network.add_edge("source".to_string(), "".to_string(), LinkType::Semantic, 0.5); + network.add_edge( + "".to_string(), + "target".to_string(), + LinkType::Semantic, + 0.5, + ); + network.add_edge( + "source".to_string(), + "".to_string(), + LinkType::Semantic, + 0.5, + ); network.add_edge("".to_string(), "".to_string(), LinkType::Semantic, 0.5); // Should handle gracefully @@ -40,8 +50,18 @@ fn test_adversarial_empty_inputs() { let _ = results.len(); // Whitespace-only IDs - network.add_edge(" ".to_string(), "normal".to_string(), LinkType::Semantic, 0.6); - network.add_edge("\t\n".to_string(), "normal".to_string(), LinkType::Temporal, 0.5); + network.add_edge( + " ".to_string(), + "normal".to_string(), + LinkType::Semantic, + 0.6, + ); + network.add_edge( + "\t\n".to_string(), + "normal".to_string(), + LinkType::Temporal, + 0.5, + ); let whitespace_results = network.activate(" ", 1.0); let _ = whitespace_results.len(); @@ -65,16 +85,24 @@ fn test_adversarial_extremely_long_inputs() { let long_id_1: String = "a".repeat(10000); let long_id_2: String = "b".repeat(10000); - network.add_edge(long_id_1.clone(), long_id_2.clone(), LinkType::Semantic, 0.8); + network.add_edge( + long_id_1.clone(), + long_id_2.clone(), + LinkType::Semantic, + 0.8, + ); // Should handle long IDs let results = network.activate(&long_id_1, 1.0); assert_eq!(results.len(), 1, "Should find connection to long_id_2"); - assert_eq!(results[0].memory_id, long_id_2, "Result should have correct long ID"); + assert_eq!( + results[0].memory_id, long_id_2, + "Result should have correct long ID" + ); // Test with hippocampal index let index = HippocampalIndex::new(); - let very_long_content = "word ".repeat(50000); // ~300KB of text + let very_long_content = "word ".repeat(50000); // ~300KB of text let result = index.index_memory( "long_content_memory", @@ -100,18 +128,18 @@ fn test_adversarial_unicode_handling() { // Various Unicode edge cases let unicode_ids = vec![ - "简体中文", // Chinese - "日本語テキスト", // Japanese - "한국어", // Korean - "مرحبا", // Arabic (RTL) - "שלום", // Hebrew (RTL) - "🦀🔥💯", // Emojis - "Ã̲̊", // Combining characters - "\u{200B}", // Zero-width space - "\u{FEFF}", // BOM - "a\u{0308}", // 'a' with combining umlaut - "🏳️‍🌈", // Emoji sequence with ZWJ - "\u{202E}reversed\u{202C}", // RTL override + "简体中文", // Chinese + "日本語テキスト", // Japanese + "한국어", // Korean + "مرحبا", // Arabic (RTL) + "שלום", // Hebrew (RTL) + "🦀🔥💯", // Emojis + "Ã̲̊", // Combining characters + "\u{200B}", // Zero-width space + "\u{FEFF}", // BOM + "a\u{0308}", // 'a' with combining umlaut + "🏳️‍🌈", // Emoji sequence with ZWJ + "\u{202E}reversed\u{202C}", // RTL override ]; for (i, id) in unicode_ids.iter().enumerate() { @@ -153,13 +181,13 @@ fn test_adversarial_control_characters() { // IDs with embedded control characters let control_ids = vec![ - "before\0after", // Null byte - "line1\nline2", // Newline - "tab\there", // Tab - "return\rhere", // Carriage return - "bell\x07ring", // Bell - "escape\x1B[31m", // ANSI escape - "backspace\x08x", // Backspace + "before\0after", // Null byte + "line1\nline2", // Newline + "tab\there", // Tab + "return\rhere", // Carriage return + "bell\x07ring", // Bell + "escape\x1B[31m", // ANSI escape + "backspace\x08x", // Backspace ]; for (i, id) in control_ids.iter().enumerate() { @@ -227,9 +255,11 @@ fn test_adversarial_weight_boundaries() { let results = network.activate("hub", 1.0); // Higher weights should produce higher activation - let mut activations: Vec<(&str, f64)> = weight_cases.iter() + let mut activations: Vec<(&str, f64)> = weight_cases + .iter() .filter_map(|(name, _)| { - results.iter() + results + .iter() .find(|r| r.memory_id == format!("weight_{}", name)) .map(|r| (*name, r.activation)) }) @@ -249,7 +279,8 @@ fn test_adversarial_weight_boundaries() { } // Zero weight edges might not propagate activation at all - let zero_activation = results.iter() + let zero_activation = results + .iter() .find(|r| r.memory_id == "weight_zero") .map(|r| r.activation); @@ -307,10 +338,7 @@ fn test_adversarial_config_boundaries() { zero_hops_net.add_edge("a".to_string(), "b".to_string(), LinkType::Semantic, 0.9); let zero_results = zero_hops_net.activate("a", 1.0); - assert!( - zero_results.is_empty(), - "Zero max_hops should find nothing" - ); + assert!(zero_results.is_empty(), "Zero max_hops should find nothing"); } // ============================================================================ @@ -332,9 +360,24 @@ fn test_adversarial_cyclic_graphs() { let mut no_cycle_net = ActivationNetwork::with_config(no_cycle_config); // Create a simple cycle: A -> B -> C -> A - no_cycle_net.add_edge("cycle_a".to_string(), "cycle_b".to_string(), LinkType::Semantic, 0.9); - no_cycle_net.add_edge("cycle_b".to_string(), "cycle_c".to_string(), LinkType::Semantic, 0.9); - no_cycle_net.add_edge("cycle_c".to_string(), "cycle_a".to_string(), LinkType::Semantic, 0.9); + no_cycle_net.add_edge( + "cycle_a".to_string(), + "cycle_b".to_string(), + LinkType::Semantic, + 0.9, + ); + no_cycle_net.add_edge( + "cycle_b".to_string(), + "cycle_c".to_string(), + LinkType::Semantic, + 0.9, + ); + no_cycle_net.add_edge( + "cycle_c".to_string(), + "cycle_a".to_string(), + LinkType::Semantic, + 0.9, + ); let start = std::time::Instant::now(); let results = no_cycle_net.activate("cycle_a", 1.0); @@ -387,10 +430,20 @@ fn test_adversarial_self_loops() { let mut network = ActivationNetwork::new(); // Create self-loop - network.add_edge("self_loop".to_string(), "self_loop".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "self_loop".to_string(), + "self_loop".to_string(), + LinkType::Semantic, + 0.9, + ); // Also connect to other nodes - network.add_edge("self_loop".to_string(), "other".to_string(), LinkType::Semantic, 0.7); + network.add_edge( + "self_loop".to_string(), + "other".to_string(), + LinkType::Semantic, + 0.7, + ); let start = std::time::Instant::now(); let results = network.activate("self_loop", 1.0); @@ -423,7 +476,12 @@ fn test_adversarial_special_numeric_values() { // We're testing that the system doesn't crash // Normal edge for baseline - network.add_edge("normal".to_string(), "target".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "normal".to_string(), + "target".to_string(), + LinkType::Semantic, + 0.8, + ); // Test activation with edge case values // (The implementation should clamp or validate these) @@ -457,5 +515,8 @@ fn test_adversarial_special_numeric_values() { // Edge should still exist and be valid let assoc = network.get_associations("normal"); - assert!(!assoc.is_empty(), "Edge should still exist after negative reinforce attempt"); + assert!( + !assoc.is_empty(), + "Edge should still exist after negative reinforce attempt" + ); } diff --git a/tests/e2e/tests/extreme/chaos_tests.rs b/tests/e2e/tests/extreme/chaos_tests.rs index 86f1dd6..5811f6a 100644 --- a/tests/e2e/tests/extreme/chaos_tests.rs +++ b/tests/e2e/tests/extreme/chaos_tests.rs @@ -13,15 +13,13 @@ //! Based on Chaos Engineering principles (Netflix, 2011) use chrono::{Duration, Utc}; +use vestige_core::neuroscience::hippocampal_index::{HippocampalIndex, IndexQuery}; use vestige_core::neuroscience::spreading_activation::{ ActivationConfig, ActivationNetwork, LinkType, }; use vestige_core::neuroscience::synaptic_tagging::{ CaptureWindow, ImportanceEvent, SynapticTaggingConfig, SynapticTaggingSystem, }; -use vestige_core::neuroscience::hippocampal_index::{ - HippocampalIndex, IndexQuery, -}; // ============================================================================ // RANDOM OPERATION SEQUENCE TESTS (2 tests) @@ -68,11 +66,7 @@ fn test_chaos_random_operation_sequence() { // Interleave reinforcement if i >= 7 { - network2.reinforce_edge( - &format!("node_{}", i - 7), - &format!("node_{}", i % 50), - 0.1, - ); + network2.reinforce_edge(&format!("node_{}", i - 7), &format!("node_{}", i % 50), 0.1); } } @@ -135,7 +129,10 @@ fn test_chaos_add_remove_cycles() { // Verify system still works let results = network.activate(&format!("stable_{}", cycle % 20), 1.0); - assert!(!results.is_empty(), "System should remain functional during chaos"); + assert!( + !results.is_empty(), + "System should remain functional during chaos" + ); } // Final activation should still work @@ -229,7 +226,12 @@ fn test_chaos_continuous_growth_under_load() { let mut network = ActivationNetwork::new(); // Initial seed - network.add_edge("root".to_string(), "child_0".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "root".to_string(), + "child_0".to_string(), + LinkType::Semantic, + 0.8, + ); // Continuously grow while querying for iteration in 0..500 { @@ -270,10 +272,7 @@ fn test_chaos_continuous_growth_under_load() { ); let final_results = network.activate("root", 1.0); - assert!( - !final_results.is_empty(), - "Final activation should succeed" - ); + assert!(!final_results.is_empty(), "Final activation should succeed"); } // ============================================================================ @@ -286,8 +285,8 @@ fn test_chaos_continuous_growth_under_load() { #[test] fn test_chaos_deep_chain_handling() { let config = ActivationConfig { - decay_factor: 0.95, // High to allow deep traversal - max_hops: 100, // Allow deep exploration + decay_factor: 0.95, // High to allow deep traversal + max_hops: 100, // Allow deep exploration min_threshold: 0.001, // Low threshold allow_cycles: false, }; @@ -299,7 +298,7 @@ fn test_chaos_deep_chain_handling() { format!("deep_{}", i), format!("deep_{}", i + 1), LinkType::Semantic, - 0.99, // Very strong links + 0.99, // Very strong links ); } @@ -389,21 +388,21 @@ fn test_chaos_high_fanout_handling() { /// Validates that the capture window handles edge cases correctly. #[test] fn test_chaos_capture_window_edge_cases() { - let window = CaptureWindow::new(9.0, 2.0); // 9 hours back, 2 forward + let window = CaptureWindow::new(9.0, 2.0); // 9 hours back, 2 forward let event_time = Utc::now(); // Test exact boundary conditions let test_cases = vec![ // (hours offset, expected in window) - (0.0, true), // Exactly at event - (8.99, true), // Just inside back window - (9.0, true), // At back boundary - (9.01, false), // Just outside back window - (-1.99, true), // Just inside forward window - (-2.0, true), // At forward boundary - (-2.01, false), // Just outside forward window - (100.0, false), // Way outside - (-100.0, false), // Way outside forward + (0.0, true), // Exactly at event + (8.99, true), // Just inside back window + (9.0, true), // At back boundary + (9.01, false), // Just outside back window + (-1.99, true), // Just inside forward window + (-2.0, true), // At forward boundary + (-2.01, false), // Just outside forward window + (100.0, false), // Way outside + (-100.0, false), // Way outside forward ]; for (hours_offset, expected) in test_cases { @@ -441,7 +440,7 @@ fn test_chaos_ancient_memories() { let mut stc = SynapticTaggingSystem::with_config(config); // Tag memories at various ages - stc.tag_memory("very_old"); // Will be tagged "now" for testing + stc.tag_memory("very_old"); // Will be tagged "now" for testing stc.tag_memory("old"); stc.tag_memory("recent"); @@ -478,11 +477,17 @@ fn test_chaos_isolated_subsystem_failures() { // Query non-existent node should return empty, not crash let results = network.activate("nonexistent", 1.0); - assert!(results.is_empty(), "Non-existent node should return empty results"); + assert!( + results.is_empty(), + "Non-existent node should return empty results" + ); // System should still work after "failed" query let valid_results = network.activate("a", 1.0); - assert!(!valid_results.is_empty(), "System should work after handling missing node"); + assert!( + !valid_results.is_empty(), + "System should work after handling missing node" + ); // Test 2: STC with edge case inputs let mut stc = SynapticTaggingSystem::new(); diff --git a/tests/e2e/tests/extreme/mathematical_tests.rs b/tests/e2e/tests/extreme/mathematical_tests.rs index 7a9aadc..e2d7f8c 100644 --- a/tests/e2e/tests/extreme/mathematical_tests.rs +++ b/tests/e2e/tests/extreme/mathematical_tests.rs @@ -9,14 +9,14 @@ //! //! Based on mathematical foundations of memory systems and neuroscience -use vestige_core::neuroscience::spreading_activation::{ - ActivationConfig, ActivationNetwork, LinkType, -}; +use chrono::{Duration, Utc}; +use std::collections::HashMap; use vestige_core::neuroscience::hippocampal_index::{ BarcodeGenerator, HippocampalIndex, INDEX_EMBEDDING_DIM, }; -use chrono::{Duration, Utc}; -use std::collections::HashMap; +use vestige_core::neuroscience::spreading_activation::{ + ActivationConfig, ActivationNetwork, LinkType, +}; // ============================================================================ // EXPONENTIAL DECAY VALIDATION (1 test) @@ -43,7 +43,7 @@ fn test_math_exponential_decay_law() { format!("node_{}", i), format!("node_{}", i + 1), LinkType::Semantic, - 1.0, // Unit weight to isolate decay effect + 1.0, // Unit weight to isolate decay effect ); } @@ -218,10 +218,10 @@ fn test_math_activation_bounds() { // Total activation should be bounded // (for a tree with decay d, total <= 1 / (1 - d) for geometric series) let total_activation: f64 = results.iter().map(|r| r.activation).sum(); - let theoretical_max = 1.0 / (1.0 - 0.8); // = 5.0 for infinite series + let theoretical_max = 1.0 / (1.0 - 0.8); // = 5.0 for infinite series assert!( - total_activation < theoretical_max * 3.0, // Allow margin for fan-out and multi-source + total_activation < theoretical_max * 3.0, // Allow margin for fan-out and multi-source "Total activation should be bounded: {} < {}", total_activation, theoretical_max * 3.0 @@ -276,7 +276,8 @@ fn test_math_barcode_statistics() { // Test 3: Content fingerprints should be mostly unique // (with 10000 samples, collision probability is low for good hash) - let unique_fingerprints: std::collections::HashSet = fingerprints.iter().copied().collect(); + let unique_fingerprints: std::collections::HashSet = + fingerprints.iter().copied().collect(); let uniqueness_ratio = unique_fingerprints.len() as f64 / num_barcodes as f64; assert!( @@ -322,9 +323,7 @@ fn test_math_embedding_dimensions() { let now = Utc::now(); // Create full-size embedding (384 dimensions) - let full_embedding: Vec = (0..384) - .map(|i| (i as f32 / 384.0).sin()) - .collect(); + let full_embedding: Vec = (0..384).map(|i| (i as f32 / 384.0).sin()).collect(); // Index memory with embedding let result = index.index_memory( @@ -340,8 +339,7 @@ fn test_math_embedding_dimensions() { // Verify index stats show correct dimensions let stats = index.stats(); assert_eq!( - stats.index_dimensions, - INDEX_EMBEDDING_DIM, + stats.index_dimensions, INDEX_EMBEDDING_DIM, "Index should use compressed embedding dimension ({})", INDEX_EMBEDDING_DIM ); diff --git a/tests/e2e/tests/extreme/proof_of_superiority.rs b/tests/e2e/tests/extreme/proof_of_superiority.rs index 7875a7e..a63acc1 100644 --- a/tests/e2e/tests/extreme/proof_of_superiority.rs +++ b/tests/e2e/tests/extreme/proof_of_superiority.rs @@ -10,6 +10,10 @@ //! Each test demonstrates a capability that traditional systems cannot match. use chrono::{Duration, Utc}; +use std::collections::{HashMap, HashSet}; +use vestige_core::neuroscience::hippocampal_index::{ + HippocampalIndex, INDEX_EMBEDDING_DIM, IndexQuery, +}; use vestige_core::neuroscience::spreading_activation::{ ActivationConfig, ActivationNetwork, LinkType, }; @@ -17,10 +21,6 @@ use vestige_core::neuroscience::synaptic_tagging::{ CaptureWindow, ImportanceEvent, ImportanceEventType, SynapticTaggingConfig, SynapticTaggingSystem, }; -use vestige_core::neuroscience::hippocampal_index::{ - HippocampalIndex, IndexQuery, INDEX_EMBEDDING_DIM, -}; -use std::collections::{HashMap, HashSet}; // ============================================================================ // RETROACTIVE IMPORTANCE - UNIQUE TO VESTIGE (1 test) @@ -43,7 +43,7 @@ fn test_proof_retroactive_importance_unique() { min_tag_strength: 0.2, max_cluster_size: 100, enable_clustering: true, - auto_decay: false, // Disable for test stability + auto_decay: false, // Disable for test stability cleanup_interval_hours: 24.0, }; @@ -75,7 +75,7 @@ fn test_proof_retroactive_importance_unique() { event_type: ImportanceEventType::EmotionalContent, memory_id: Some("bob_departure".to_string()), timestamp: Utc::now(), - strength: 1.0, // Maximum importance + strength: 1.0, // Maximum importance context: Some("BREAKING: Bob is leaving the company!".to_string()), }; @@ -90,7 +90,8 @@ fn test_proof_retroactive_importance_unique() { ); // 2. Earlier Bob-related memories should be captured - let captured_ids: HashSet<_> = capture_result.captured_memories + let captured_ids: HashSet<_> = capture_result + .captured_memories .iter() .map(|c| c.memory_id.as_str()) .collect(); @@ -170,20 +171,51 @@ fn test_proof_multi_hop_beats_similarity() { let mut network = ActivationNetwork::with_config(config); // Create the knowledge chain (domain knowledge graph) - network.add_edge("memory_leaks".to_string(), "reference_counting".to_string(), LinkType::Causal, 0.9); - network.add_edge("reference_counting".to_string(), "arc_weak".to_string(), LinkType::Semantic, 0.85); - network.add_edge("arc_weak".to_string(), "cyclic_references".to_string(), LinkType::Semantic, 0.9); - network.add_edge("cyclic_references".to_string(), "solution_weak_refs".to_string(), LinkType::Semantic, 0.95); + network.add_edge( + "memory_leaks".to_string(), + "reference_counting".to_string(), + LinkType::Causal, + 0.9, + ); + network.add_edge( + "reference_counting".to_string(), + "arc_weak".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "arc_weak".to_string(), + "cyclic_references".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "cyclic_references".to_string(), + "solution_weak_refs".to_string(), + LinkType::Semantic, + 0.95, + ); // Also add some direct but less relevant connections - network.add_edge("memory_leaks".to_string(), "valgrind".to_string(), LinkType::Semantic, 0.7); - network.add_edge("memory_leaks".to_string(), "profiling".to_string(), LinkType::Semantic, 0.6); + network.add_edge( + "memory_leaks".to_string(), + "valgrind".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "memory_leaks".to_string(), + "profiling".to_string(), + LinkType::Semantic, + 0.6, + ); // === SPREADING ACTIVATION SEARCH === let spreading_results = network.activate("memory_leaks", 1.0); // Collect what spreading activation found - let spreading_found: HashSet<_> = spreading_results.iter() + let spreading_found: HashSet<_> = spreading_results + .iter() .map(|r| r.memory_id.as_str()) .collect(); @@ -198,7 +230,9 @@ fn test_proof_multi_hop_beats_similarity() { impl MockSimilaritySearch { fn search(&self, query: &str, top_k: usize) -> Vec<(&str, f64)> { let query_emb = self.embeddings.get(query).unwrap(); - let mut results: Vec<_> = self.embeddings.iter() + let mut results: Vec<_> = self + .embeddings + .iter() .filter(|(k, _)| k.as_str() != query) .map(|(k, emb)| { let sim = cosine_sim(query_emb, emb); @@ -223,17 +257,27 @@ fn test_proof_multi_hop_beats_similarity() { } // Create mock embeddings where memory_leaks and cyclic_references are ORTHOGONAL - let mut mock = MockSimilaritySearch { embeddings: HashMap::new() }; - mock.embeddings.insert("memory_leaks".to_string(), vec![1.0, 0.0, 0.0, 0.0]); - mock.embeddings.insert("reference_counting".to_string(), vec![0.7, 0.7, 0.0, 0.0]); - mock.embeddings.insert("arc_weak".to_string(), vec![0.0, 0.7, 0.7, 0.0]); - mock.embeddings.insert("cyclic_references".to_string(), vec![0.0, 0.0, 0.0, 1.0]); // ORTHOGONAL! - mock.embeddings.insert("solution_weak_refs".to_string(), vec![0.0, 0.0, 0.2, 0.9]); - mock.embeddings.insert("valgrind".to_string(), vec![0.8, 0.2, 0.0, 0.0]); // Similar - mock.embeddings.insert("profiling".to_string(), vec![0.6, 0.4, 0.0, 0.0]); // Similar + let mut mock = MockSimilaritySearch { + embeddings: HashMap::new(), + }; + mock.embeddings + .insert("memory_leaks".to_string(), vec![1.0, 0.0, 0.0, 0.0]); + mock.embeddings + .insert("reference_counting".to_string(), vec![0.7, 0.7, 0.0, 0.0]); + mock.embeddings + .insert("arc_weak".to_string(), vec![0.0, 0.7, 0.7, 0.0]); + mock.embeddings + .insert("cyclic_references".to_string(), vec![0.0, 0.0, 0.0, 1.0]); // ORTHOGONAL! + mock.embeddings + .insert("solution_weak_refs".to_string(), vec![0.0, 0.0, 0.2, 0.9]); + mock.embeddings + .insert("valgrind".to_string(), vec![0.8, 0.2, 0.0, 0.0]); // Similar + mock.embeddings + .insert("profiling".to_string(), vec![0.6, 0.4, 0.0, 0.0]); // Similar let similarity_results = mock.search("memory_leaks", 10); - let similarity_found: HashSet<_> = similarity_results.iter() + let similarity_found: HashSet<_> = similarity_results + .iter() .filter(|(_, sim)| *sim > 0.3) .map(|(id, _)| *id) .collect(); @@ -257,13 +301,16 @@ fn test_proof_multi_hop_beats_similarity() { ); // Verify the discovery path - let solution_result = spreading_results.iter() + let solution_result = spreading_results + .iter() .find(|r| r.memory_id == "solution_weak_refs") .expect("Should find solution"); assert_eq!(solution_result.distance, 4, "Solution is 4 hops away"); assert!( - solution_result.path.contains(&"cyclic_references".to_string()), + solution_result + .path + .contains(&"cyclic_references".to_string()), "Path should include cyclic_references" ); } @@ -291,8 +338,12 @@ fn test_proof_hippocampal_indexing_efficiency() { let _ = index.index_memory( &format!("memory_{}", i), - &format!("This is memory number {} with content about topic {} and subtopic {}", - i, i % 50, i % 10), + &format!( + "This is memory number {} with content about topic {} and subtopic {}", + i, + i % 50, + i % 10 + ), "fact", now, Some(embedding), @@ -356,7 +407,7 @@ fn test_proof_hippocampal_indexing_efficiency() { ); // 5. Memory efficiency - let memory_per_full = 384 * 4; // 384 floats * 4 bytes + let memory_per_full = 384 * 4; // 384 floats * 4 bytes let memory_per_index = INDEX_EMBEDDING_DIM * 4; let savings_per_memory = memory_per_full - memory_per_index; let total_savings = savings_per_memory * NUM_MEMORIES; @@ -389,10 +440,10 @@ fn test_proof_temporal_capture_accuracy() { // Memories encoded BEFORE the important event can be captured let backward_tests = vec![ - (Duration::hours(1), true, 1.0), // 1h before - should be captured with high prob - (Duration::hours(4), true, 0.9), // 4h before - should be captured - (Duration::hours(8), true, 0.5), // 8h before - edge of window - (Duration::hours(9), true, 0.0), // 9h before - at boundary + (Duration::hours(1), true, 1.0), // 1h before - should be captured with high prob + (Duration::hours(4), true, 0.9), // 4h before - should be captured + (Duration::hours(8), true, 0.5), // 8h before - edge of window + (Duration::hours(9), true, 0.0), // 9h before - at boundary (Duration::hours(10), false, 0.0), // 10h before - outside window ]; @@ -401,7 +452,8 @@ fn test_proof_temporal_capture_accuracy() { let in_window = window.is_in_window(memory_time, event_time); assert_eq!( - in_window, *should_be_in_window, + in_window, + *should_be_in_window, "PROOF: Memory {}h before event: in_window={}, expected={}", offset.num_hours(), in_window, @@ -421,10 +473,10 @@ fn test_proof_temporal_capture_accuracy() { // Brief period for memories encoded shortly after let forward_tests = vec![ - (Duration::minutes(30), true), // 30min after - in window - (Duration::hours(1), true), // 1h after - in window - (Duration::hours(2), true), // 2h after - at boundary - (Duration::hours(3), false), // 3h after - outside + (Duration::minutes(30), true), // 30min after - in window + (Duration::hours(1), true), // 1h after - in window + (Duration::hours(2), true), // 2h after - at boundary + (Duration::hours(3), false), // 3h after - outside ]; for (offset, should_be_in_window) in &forward_tests { @@ -432,7 +484,8 @@ fn test_proof_temporal_capture_accuracy() { let in_window = window.is_in_window(memory_time, event_time); assert_eq!( - in_window, *should_be_in_window, + in_window, + *should_be_in_window, "PROOF: Memory {}min after event: in_window={}, expected={}", offset.num_minutes(), in_window, @@ -472,7 +525,10 @@ fn test_proof_comprehensive_capability_summary() { let result = stc.trigger_prp(event); let has_retroactive = result.has_captures(); - assert!(has_retroactive, "Capability 1: Retroactive importance - PROVEN"); + assert!( + has_retroactive, + "Capability 1: Retroactive importance - PROVEN" + ); // === CAPABILITY 2: Multi-Hop Discovery === // Traditional: NO (1-hop only) | Vestige: YES (configurable depth) @@ -492,25 +548,36 @@ fn test_proof_comprehensive_capability_summary() { let results = network.activate("a", 1.0); let max_distance = results.iter().map(|r| r.distance).max().unwrap_or(0); - assert!(max_distance >= 4, "Capability 2: Multi-hop discovery (4+ hops) - PROVEN"); + assert!( + max_distance >= 4, + "Capability 2: Multi-hop discovery (4+ hops) - PROVEN" + ); // === CAPABILITY 3: Compressed Hippocampal Index === // Traditional: Full embeddings | Vestige: Compressed index let compression = 384.0 / INDEX_EMBEDDING_DIM as f64; - assert!(compression >= 2.0, "Capability 3: Hippocampal compression ({:.1}x) - PROVEN", compression); + assert!( + compression >= 2.0, + "Capability 3: Hippocampal compression ({:.1}x) - PROVEN", + compression + ); // === CAPABILITY 4: Asymmetric Temporal Windows === // Traditional: NO temporal reasoning | Vestige: Biologically-grounded windows let window = CaptureWindow::new(9.0, 2.0); let asymmetric = 9.0 / 2.0; - assert!(asymmetric > 4.0, "Capability 4: Asymmetric capture windows ({}:1) - PROVEN", asymmetric); + assert!( + asymmetric > 4.0, + "Capability 4: Asymmetric capture windows ({}:1) - PROVEN", + asymmetric + ); // === CAPABILITY 5: Path Tracking === // Traditional: Returns items only | Vestige: Returns full association paths - let path_result = &results[results.len() - 1]; // Furthest result + let path_result = &results[results.len() - 1]; // Furthest result let has_path = !path_result.path.is_empty(); assert!(has_path, "Capability 5: Association path tracking - PROVEN"); @@ -518,10 +585,30 @@ fn test_proof_comprehensive_capability_summary() { // Traditional: Single similarity metric | Vestige: Multiple link types let mut typed_network = ActivationNetwork::new(); - typed_network.add_edge("event".to_string(), "cause".to_string(), LinkType::Causal, 0.9); - typed_network.add_edge("event".to_string(), "time".to_string(), LinkType::Temporal, 0.9); - typed_network.add_edge("event".to_string(), "concept".to_string(), LinkType::Semantic, 0.9); - typed_network.add_edge("event".to_string(), "location".to_string(), LinkType::Spatial, 0.9); + typed_network.add_edge( + "event".to_string(), + "cause".to_string(), + LinkType::Causal, + 0.9, + ); + typed_network.add_edge( + "event".to_string(), + "time".to_string(), + LinkType::Temporal, + 0.9, + ); + typed_network.add_edge( + "event".to_string(), + "concept".to_string(), + LinkType::Semantic, + 0.9, + ); + typed_network.add_edge( + "event".to_string(), + "location".to_string(), + LinkType::Spatial, + 0.9, + ); let typed_results = typed_network.activate("event", 1.0); let link_types: HashSet<_> = typed_results.iter().map(|r| r.link_type).collect(); diff --git a/tests/e2e/tests/extreme/research_validation_tests.rs b/tests/e2e/tests/extreme/research_validation_tests.rs index 2fe7716..11006c9 100644 --- a/tests/e2e/tests/extreme/research_validation_tests.rs +++ b/tests/e2e/tests/extreme/research_validation_tests.rs @@ -10,6 +10,10 @@ //! Each test cites the specific research findings being validated. use chrono::{Duration, Utc}; +use std::collections::HashSet; +use vestige_core::neuroscience::hippocampal_index::{ + HippocampalIndex, HippocampalIndexConfig, IndexQuery, +}; use vestige_core::neuroscience::spreading_activation::{ ActivationConfig, ActivationNetwork, LinkType, }; @@ -17,10 +21,6 @@ use vestige_core::neuroscience::synaptic_tagging::{ CaptureWindow, ImportanceEvent, ImportanceEventType, SynapticTaggingConfig, SynapticTaggingSystem, }; -use vestige_core::neuroscience::hippocampal_index::{ - HippocampalIndex, HippocampalIndexConfig, IndexQuery, -}; -use std::collections::HashSet; // ============================================================================ // COLLINS & LOFTUS (1975) SPREADING ACTIVATION VALIDATION (1 test) @@ -39,7 +39,7 @@ use std::collections::HashSet; #[test] fn test_research_collins_loftus_spreading_activation() { let config = ActivationConfig { - decay_factor: 0.75, // Semantic distance decay + decay_factor: 0.75, // Semantic distance decay max_hops: 4, min_threshold: 0.05, allow_cycles: false, @@ -48,29 +48,91 @@ fn test_research_collins_loftus_spreading_activation() { // Recreate classic semantic network from the paper // "Fire truck" example: fire_truck -> red -> roses, fire_truck -> vehicle - network.add_edge("fire_truck".to_string(), "red".to_string(), LinkType::Semantic, 0.9); - network.add_edge("fire_truck".to_string(), "vehicle".to_string(), LinkType::Semantic, 0.85); - network.add_edge("fire_truck".to_string(), "fire".to_string(), LinkType::Semantic, 0.9); - network.add_edge("red".to_string(), "roses".to_string(), LinkType::Semantic, 0.7); - network.add_edge("red".to_string(), "cherries".to_string(), LinkType::Semantic, 0.65); - network.add_edge("red".to_string(), "apples".to_string(), LinkType::Semantic, 0.7); - network.add_edge("vehicle".to_string(), "car".to_string(), LinkType::Semantic, 0.8); - network.add_edge("vehicle".to_string(), "truck".to_string(), LinkType::Semantic, 0.85); - network.add_edge("fire".to_string(), "flames".to_string(), LinkType::Semantic, 0.9); - network.add_edge("fire".to_string(), "heat".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "fire_truck".to_string(), + "red".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "fire_truck".to_string(), + "vehicle".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "fire_truck".to_string(), + "fire".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "red".to_string(), + "roses".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "red".to_string(), + "cherries".to_string(), + LinkType::Semantic, + 0.65, + ); + network.add_edge( + "red".to_string(), + "apples".to_string(), + LinkType::Semantic, + 0.7, + ); + network.add_edge( + "vehicle".to_string(), + "car".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "vehicle".to_string(), + "truck".to_string(), + LinkType::Semantic, + 0.85, + ); + network.add_edge( + "fire".to_string(), + "flames".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "fire".to_string(), + "heat".to_string(), + LinkType::Semantic, + 0.8, + ); // Add convergent paths (multiple routes to same concept) - network.add_edge("apples".to_string(), "fruit".to_string(), LinkType::Semantic, 0.9); - network.add_edge("cherries".to_string(), "fruit".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "apples".to_string(), + "fruit".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "cherries".to_string(), + "fruit".to_string(), + LinkType::Semantic, + 0.9, + ); let results = network.activate("fire_truck", 1.0); // Validation 1: Direct connections (distance 1) have highest activation - let red_activation = results.iter() + let red_activation = results + .iter() .find(|r| r.memory_id == "red") .map(|r| r.activation) .unwrap_or(0.0); - let roses_activation = results.iter() + let roses_activation = results + .iter() .find(|r| r.memory_id == "roses") .map(|r| r.activation) .unwrap_or(0.0); @@ -83,11 +145,13 @@ fn test_research_collins_loftus_spreading_activation() { ); // Validation 2: Activation decreases with semantic distance - let distance_1: Vec = results.iter() + let distance_1: Vec = results + .iter() .filter(|r| r.distance == 1) .map(|r| r.activation) .collect(); - let distance_2: Vec = results.iter() + let distance_2: Vec = results + .iter() .filter(|r| r.distance == 2) .map(|r| r.activation) .collect(); @@ -107,7 +171,10 @@ fn test_research_collins_loftus_spreading_activation() { assert!(reachable.contains("red"), "Should reach 'red'"); assert!(reachable.contains("vehicle"), "Should reach 'vehicle'"); assert!(reachable.contains("fire"), "Should reach 'fire'"); - assert!(reachable.contains("roses"), "Should reach 'roses' through 'red'"); + assert!( + reachable.contains("roses"), + "Should reach 'roses' through 'red'" + ); // Validation 4: Path information is preserved let roses_result = results.iter().find(|r| r.memory_id == "roses").unwrap(); @@ -135,7 +202,7 @@ fn test_research_collins_loftus_spreading_activation() { #[test] fn test_research_frey_morris_synaptic_tagging() { let config = SynapticTaggingConfig { - capture_window: CaptureWindow::new(9.0, 2.0), // Hours: 9 back, 2 forward + capture_window: CaptureWindow::new(9.0, 2.0), // Hours: 9 back, 2 forward prp_threshold: 0.7, tag_lifetime_hours: 12.0, min_tag_strength: 0.3, @@ -148,7 +215,7 @@ fn test_research_frey_morris_synaptic_tagging() { let mut stc = SynapticTaggingSystem::with_config(config); // Finding 1: Weak stimulation creates tags - stc.tag_memory_with_strength("weak_stim_1", 0.4); // Above min (0.3), weak + stc.tag_memory_with_strength("weak_stim_1", 0.4); // Above min (0.3), weak stc.tag_memory_with_strength("weak_stim_2", 0.5); let stats_after_weak = stc.stats(); @@ -164,7 +231,7 @@ fn test_research_frey_morris_synaptic_tagging() { event_type: ImportanceEventType::EmotionalContent, memory_id: Some("strong_trigger".to_string()), timestamp: Utc::now(), - strength: 0.95, // Above threshold (0.7) + strength: 0.95, // Above threshold (0.7) context: Some("Strong emotional event triggers PRP".to_string()), }; @@ -245,21 +312,26 @@ fn test_research_teyler_rudy_hippocampal_indexing() { .map(|i| ((i as f32 / 100.0) * std::f32::consts::PI).sin()) .collect(); - let barcode = index.index_memory( - "episodic_memory_1", - "Detailed episodic memory content with rich context", - "episodic", - now, - Some(full_embedding.clone()), - ).expect("Should create barcode"); + let barcode = index + .index_memory( + "episodic_memory_1", + "Detailed episodic memory content with rich context", + "episodic", + now, + Some(full_embedding.clone()), + ) + .expect("Should create barcode"); // Barcode should be a valid identifier (u64 ID) // First barcode may have id=0, which is valid - assert!(barcode.creation_hash > 0 || barcode.content_fingerprint > 0, - "T&R Finding 1: Barcode should have valid fingerprints"); + assert!( + barcode.creation_hash > 0 || barcode.content_fingerprint > 0, + "T&R Finding 1: Barcode should have valid fingerprints" + ); // Finding 2: Index points to content (content pointers) - let memory_index = index.get_index("episodic_memory_1") + let memory_index = index + .get_index("episodic_memory_1") .expect("Should retrieve") .expect("Should exist"); @@ -348,7 +420,7 @@ fn test_research_ebbinghaus_forgetting_curve() { let forgetting_curve = |t: f64| -> f64 { // Ebbinghaus formula: R = e^(-t/S) where S is stability - let stability = 2.0; // Memory stability parameter + let stability = 2.0; // Memory stability parameter (-t / stability).exp() }; @@ -368,7 +440,10 @@ fn test_research_ebbinghaus_forgetting_curve() { // Collect activations by "age" let mut age_activations: Vec<(u32, f64)> = Vec::new(); for t in 0..10 { - if let Some(result) = results.iter().find(|r| r.memory_id == format!("memory_age_{}", t)) { + if let Some(result) = results + .iter() + .find(|r| r.memory_id == format!("memory_age_{}", t)) + { age_activations.push((t, result.activation)); } } @@ -387,7 +462,8 @@ fn test_research_ebbinghaus_forgetting_curve() { // Check that differences decrease over time if age_activations.len() >= 3 { let diff_early = age_activations[0].1 - age_activations[1].1; - let diff_late = age_activations[age_activations.len() - 2].1 - age_activations[age_activations.len() - 1].1; + let diff_late = age_activations[age_activations.len() - 2].1 + - age_activations[age_activations.len() - 1].1; // Early differences should be larger (rapid initial forgetting) // But we need to account for near-zero values at the end @@ -403,8 +479,18 @@ fn test_research_ebbinghaus_forgetting_curve() { // Finding 3: Test overlearning (reinforcement) let mut overlearned_network = ActivationNetwork::new(); - overlearned_network.add_edge("study".to_string(), "normal_learning".to_string(), LinkType::Semantic, 0.5); - overlearned_network.add_edge("study".to_string(), "overlearned".to_string(), LinkType::Semantic, 0.5); + overlearned_network.add_edge( + "study".to_string(), + "normal_learning".to_string(), + LinkType::Semantic, + 0.5, + ); + overlearned_network.add_edge( + "study".to_string(), + "overlearned".to_string(), + LinkType::Semantic, + 0.5, + ); // Simulate overlearning with multiple reinforcements for _ in 0..5 { @@ -413,11 +499,13 @@ fn test_research_ebbinghaus_forgetting_curve() { let study_results = overlearned_network.activate("study", 1.0); - let normal_act = study_results.iter() + let normal_act = study_results + .iter() .find(|r| r.memory_id == "normal_learning") .map(|r| r.activation) .unwrap_or(0.0); - let overlearned_act = study_results.iter() + let overlearned_act = study_results + .iter() .find(|r| r.memory_id == "overlearned") .map(|r| r.activation) .unwrap_or(0.0); @@ -447,7 +535,7 @@ fn test_research_ebbinghaus_forgetting_curve() { #[test] fn test_research_fsrs6_properties() { // FSRS-6 default weights - const W20: f64 = 0.1542; // Forgetting curve exponent + const W20: f64 = 0.1542; // Forgetting curve exponent // FSRS-6 retrievability formula fn fsrs6_retrievability(stability: f64, elapsed_days: f64, w20: f64) -> f64 { @@ -455,7 +543,9 @@ fn test_research_fsrs6_properties() { return 1.0; } let factor = 0.9_f64.powf(-1.0 / w20) - 1.0; - (1.0 + factor * elapsed_days / stability).powf(-w20).clamp(0.0, 1.0) + (1.0 + factor * elapsed_days / stability) + .powf(-w20) + .clamp(0.0, 1.0) } // Property 1: R = 0.9 when t = S (by design) diff --git a/tests/e2e/tests/journeys/consolidation_workflow.rs b/tests/e2e/tests/journeys/consolidation_workflow.rs index 6577c89..633c5a3 100644 --- a/tests/e2e/tests/journeys/consolidation_workflow.rs +++ b/tests/e2e/tests/journeys/consolidation_workflow.rs @@ -15,8 +15,8 @@ use chrono::{Duration, Utc}; use vestige_core::{ advanced::dreams::{ - ActivityTracker, ConnectionGraph, ConnectionReason, ConsolidationScheduler, - DreamConfig, DreamMemory, MemoryDreamer, + ActivityTracker, ConnectionGraph, ConnectionReason, ConsolidationScheduler, DreamConfig, + DreamMemory, MemoryDreamer, }, consolidation::SleepConsolidation, }; @@ -82,10 +82,7 @@ fn test_consolidation_detects_idle_periods() { // Initially should be idle (no activity) let stats = scheduler.get_activity_stats(); - assert!( - stats.is_idle, - "Fresh scheduler should be idle" - ); + assert!(stats.is_idle, "Fresh scheduler should be idle"); // Record activity - should no longer be idle scheduler.record_activity(); @@ -93,10 +90,7 @@ fn test_consolidation_detects_idle_periods() { scheduler.record_activity(); let active_stats = scheduler.get_activity_stats(); - assert!( - !active_stats.is_idle, - "Should not be idle after activity" - ); + assert!(!active_stats.is_idle, "Should not be idle after activity"); assert_eq!( active_stats.total_events, 3, "Should track 3 activity events" @@ -176,9 +170,24 @@ fn test_connections_form_between_related_memories() { let mut graph = ConnectionGraph::new(); // Add connections simulating discovered relationships - graph.add_connection("rust_async", "tokio_runtime", 0.9, ConnectionReason::Semantic); - graph.add_connection("tokio_runtime", "green_threads", 0.8, ConnectionReason::Semantic); - graph.add_connection("rust_async", "futures_crate", 0.85, ConnectionReason::SharedConcepts); + graph.add_connection( + "rust_async", + "tokio_runtime", + 0.9, + ConnectionReason::Semantic, + ); + graph.add_connection( + "tokio_runtime", + "green_threads", + 0.8, + ConnectionReason::Semantic, + ); + graph.add_connection( + "rust_async", + "futures_crate", + 0.85, + ConnectionReason::SharedConcepts, + ); // Verify graph structure let stats = graph.get_stats(); @@ -329,9 +338,18 @@ fn test_pruning_removes_weak_memories() { // Verify the config accessor works let config = consolidation.config(); - assert!(!config.enable_pruning, "Default should have pruning disabled"); - assert!(config.pruning_threshold > 0.0, "Should have a threshold configured"); - assert!(config.pruning_min_age_days > 0, "Should have a min age configured"); + assert!( + !config.enable_pruning, + "Default should have pruning disabled" + ); + assert!( + config.pruning_threshold > 0.0, + "Should have a threshold configured" + ); + assert!( + config.pruning_min_age_days > 0, + "Should have a min age configured" + ); } // ============================================================================ @@ -419,22 +437,13 @@ fn test_retention_calculation() { // Full retrieval, max storage let r2 = consolidation.calculate_retention(10.0, 1.0); - assert!( - (r2 - 1.0).abs() < 0.01, - "Max everything should be ~1.0" - ); + assert!((r2 - 1.0).abs() < 0.01, "Max everything should be ~1.0"); // Low retrieval, max storage let r3 = consolidation.calculate_retention(10.0, 0.0); - assert!( - (r3 - 0.3).abs() < 0.01, - "Low retrieval should cap at ~0.3" - ); + assert!((r3 - 0.3).abs() < 0.01, "Low retrieval should cap at ~0.3"); // Both low let r4 = consolidation.calculate_retention(0.0, 0.0); - assert!( - r4 < 0.1, - "Both low should mean low retention" - ); + assert!(r4 < 0.1, "Both low should mean low retention"); } diff --git a/tests/e2e/tests/journeys/import_export.rs b/tests/e2e/tests/journeys/import_export.rs index fbea1ba..91847ff 100644 --- a/tests/e2e/tests/journeys/import_export.rs +++ b/tests/e2e/tests/journeys/import_export.rs @@ -13,9 +13,9 @@ //! 5. User merges memories from multiple sources use chrono::{DateTime, Duration, Utc}; -use vestige_core::memory::IngestInput; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use vestige_core::memory::IngestInput; // ============================================================================ // EXPORT/IMPORT FORMAT @@ -183,7 +183,10 @@ fn test_export_serializes_memories_to_json() { assert!(json.contains("\"metadata\""), "Should contain metadata"); // Verify content is present - assert!(json.contains("Rust ownership"), "Should contain memory content"); + assert!( + json.contains("Rust ownership"), + "Should contain memory content" + ); assert!(json.contains("rust"), "Should contain tags"); // Verify FSRS state @@ -219,12 +222,21 @@ fn test_import_deserializes_json_to_memories() { // Verify memories let mem1 = &imported.memories[0]; - assert!(mem1.content.contains("ownership"), "Content should be preserved"); - assert!(mem1.tags.contains(&"rust".to_string()), "Tags should be preserved"); + assert!( + mem1.content.contains("ownership"), + "Content should be preserved" + ); + assert!( + mem1.tags.contains(&"rust".to_string()), + "Tags should be preserved" + ); assert!(mem1.stability > 0.0, "Stability should be preserved"); // Verify metadata - assert_eq!(imported.metadata.get("project"), Some(&"vestige".to_string())); + assert_eq!( + imported.metadata.get("project"), + Some(&"vestige".to_string()) + ); } // ============================================================================ @@ -268,12 +280,24 @@ fn test_roundtrip_preserves_all_data() { assert_eq!(imported.content, original.content, "Content should match"); assert_eq!(imported.node_type, original.node_type, "Type should match"); assert_eq!(imported.tags, original.tags, "Tags should match"); - assert_eq!(imported.stability, original.stability, "Stability should match"); - assert_eq!(imported.difficulty, original.difficulty, "Difficulty should match"); + assert_eq!( + imported.stability, original.stability, + "Stability should match" + ); + assert_eq!( + imported.difficulty, original.difficulty, + "Difficulty should match" + ); assert_eq!(imported.reps, original.reps, "Reps should match"); assert_eq!(imported.lapses, original.lapses, "Lapses should match"); - assert_eq!(imported.sentiment_score, original.sentiment_score, "Sentiment score should match"); - assert_eq!(imported.sentiment_magnitude, original.sentiment_magnitude, "Sentiment magnitude should match"); + assert_eq!( + imported.sentiment_score, original.sentiment_score, + "Sentiment score should match" + ); + assert_eq!( + imported.sentiment_magnitude, original.sentiment_magnitude, + "Sentiment magnitude should match" + ); assert_eq!(imported.source, original.source, "Source should match"); } @@ -290,11 +314,13 @@ fn test_roundtrip_preserves_all_data() { #[test] fn test_selective_export_by_tags() { // Create memories with different tags - let memories = [ExportedMemory::new("Rust ownership", "concept", vec!["rust", "memory"]), + let memories = [ + ExportedMemory::new("Rust ownership", "concept", vec!["rust", "memory"]), ExportedMemory::new("Python generators", "concept", vec!["python", "generators"]), ExportedMemory::new("Rust borrowing", "concept", vec!["rust", "borrowing"]), ExportedMemory::new("JavaScript async", "concept", vec!["javascript", "async"]), - ExportedMemory::new("Rust async", "concept", vec!["rust", "async"])]; + ExportedMemory::new("Rust async", "concept", vec!["rust", "async"]), + ]; // Filter by "rust" tag let rust_memories: Vec<_> = memories @@ -307,12 +333,14 @@ fn test_selective_export_by_tags() { // Filter by multiple tags (rust AND async) let rust_async_memories: Vec<_> = memories .iter() - .filter(|m| { - m.tags.contains(&"rust".to_string()) && m.tags.contains(&"async".to_string()) - }) + .filter(|m| m.tags.contains(&"rust".to_string()) && m.tags.contains(&"async".to_string())) .collect(); - assert_eq!(rust_async_memories.len(), 1, "Should filter to 1 Rust async memory"); + assert_eq!( + rust_async_memories.len(), + 1, + "Should filter to 1 Rust async memory" + ); assert!(rust_async_memories[0].content.contains("Rust async")); // Export filtered @@ -338,8 +366,14 @@ fn test_selective_export_by_tags() { fn test_import_merges_with_existing_data() { // Simulate existing memories let existing: HashMap = [ - ("1".to_string(), ExportedMemory::new("Rust ownership memory safety", "concept", vec!["rust"])), - ("2".to_string(), ExportedMemory::new("Rust borrowing rules explained", "concept", vec!["rust"])), + ( + "1".to_string(), + ExportedMemory::new("Rust ownership memory safety", "concept", vec!["rust"]), + ), + ( + "2".to_string(), + ExportedMemory::new("Rust borrowing rules explained", "concept", vec!["rust"]), + ), ] .into_iter() .collect(); @@ -427,7 +461,10 @@ fn test_empty_bundle_handling() { // Serialize empty bundle let json = bundle.to_json().unwrap(); - assert!(json.contains("\"memories\": []"), "Should have empty memories array"); + assert!( + json.contains("\"memories\": []"), + "Should have empty memories array" + ); // Deserialize and verify let imported = ExportBundle::from_json(&json).unwrap(); diff --git a/tests/e2e/tests/journeys/ingest_recall_review.rs b/tests/e2e/tests/journeys/ingest_recall_review.rs index fb463b1..cecb5b8 100644 --- a/tests/e2e/tests/journeys/ingest_recall_review.rs +++ b/tests/e2e/tests/journeys/ingest_recall_review.rs @@ -12,9 +12,9 @@ //! 5. User benefits from improved recall over time use vestige_core::{ + consolidation::SleepConsolidation, fsrs::{FSRSScheduler, LearningState, Rating}, memory::{IngestInput, RecallInput, SearchMode}, - consolidation::SleepConsolidation, }; // ============================================================================ @@ -139,7 +139,10 @@ fn test_review_strengthens_memory_with_fsrs() { let result = scheduler.review(&initial_state, Rating::Good, 0.0, None); // Stability should be set from initial parameters - assert!(result.state.stability > 0.0, "Stability should be positive after review"); + assert!( + result.state.stability > 0.0, + "Stability should be positive after review" + ); // Reps should increase assert_eq!(result.state.reps, 1, "Reps should increase after review"); @@ -160,7 +163,10 @@ fn test_review_strengthens_memory_with_fsrs() { again_result.interval <= second_result.interval, "Again rating should reduce interval" ); - assert_eq!(again_result.state.lapses, 1, "Lapses should increase on Again"); + assert_eq!( + again_result.state.lapses, 1, + "Lapses should increase on Again" + ); } // ============================================================================ @@ -184,7 +190,11 @@ fn test_memory_lifecycle_follows_expected_pattern() { // Simulate 10 successful reviews for i in 0..10 { - let elapsed = if i == 0 { 0.0 } else { intervals.last().copied().unwrap_or(1) as f64 }; + let elapsed = if i == 0 { + 0.0 + } else { + intervals.last().copied().unwrap_or(1) as f64 + }; let result = scheduler.review(&state, Rating::Good, elapsed, None); intervals.push(result.interval); state = result.state; @@ -192,7 +202,10 @@ fn test_memory_lifecycle_follows_expected_pattern() { // Verify lifecycle progression assert!(state.reps >= 10, "Should have at least 10 reps"); - assert_eq!(state.lapses, 0, "Should have no lapses with all Good ratings"); + assert_eq!( + state.lapses, 0, + "Should have no lapses with all Good ratings" + ); // Verify interval growth (early intervals may be similar, but should eventually grow) let early_avg: f64 = intervals[..3].iter().map(|&i| i as f64).sum::() / 3.0; @@ -260,10 +273,7 @@ fn test_sentiment_affects_memory_consolidation() { // Test promotion boost let boosted = consolidation.promotion_boost(5.0); - assert!( - boosted > 5.0, - "Promotion should increase storage strength" - ); + assert!(boosted > 5.0, "Promotion should increase storage strength"); assert!( boosted <= 10.0, "Promotion should cap at max storage strength" diff --git a/tests/e2e/tests/journeys/intentions_workflow.rs b/tests/e2e/tests/journeys/intentions_workflow.rs index a9b2b8a..f0530ed 100644 --- a/tests/e2e/tests/journeys/intentions_workflow.rs +++ b/tests/e2e/tests/journeys/intentions_workflow.rs @@ -13,8 +13,8 @@ //! 5. User benefits from context-aware assistance use vestige_core::advanced::intent::{ - ActionType, DetectedIntent, IntentDetector, LearningLevel, MaintenanceType, - OptimizationType, UserAction, + ActionType, DetectedIntent, IntentDetector, LearningLevel, MaintenanceType, OptimizationType, + UserAction, }; // ============================================================================ @@ -89,7 +89,10 @@ fn test_debugging_intent_detection() { // Check intent properties match &result.primary_intent { - DetectedIntent::Debugging { suspected_area, symptoms } => { + DetectedIntent::Debugging { + suspected_area, + symptoms, + } => { assert!(!suspected_area.is_empty(), "Should identify suspected area"); // Symptoms may or may not be captured depending on action order } @@ -129,10 +132,7 @@ fn test_learning_intent_detection() { _ => { // Learning actions should typically detect learning intent // But other intents may score higher in some cases - assert!( - result.confidence > 0.0, - "Should detect some intent" - ); + assert!(result.confidence > 0.0, "Should detect some intent"); } } @@ -169,7 +169,9 @@ fn test_refactoring_intent_detection() { assert!(!target.is_empty(), "Should identify refactoring target"); assert!(!goal.is_empty(), "Should identify refactoring goal"); } - DetectedIntent::NewFeature { related_components, .. } => { + DetectedIntent::NewFeature { + related_components, .. + } => { // Multiple edits could also suggest new feature assert!( related_components.len() >= 0, diff --git a/tests/e2e/tests/journeys/spreading_activation.rs b/tests/e2e/tests/journeys/spreading_activation.rs index d8117a5..7ac345c 100644 --- a/tests/e2e/tests/journeys/spreading_activation.rs +++ b/tests/e2e/tests/journeys/spreading_activation.rs @@ -12,10 +12,10 @@ //! 4. Activation spreads to related memories via association links //! 5. User discovers hidden connections they didn't explicitly search for +use std::collections::HashSet; use vestige_core::neuroscience::spreading_activation::{ ActivationConfig, ActivationNetwork, LinkType, }; -use std::collections::HashSet; // ============================================================================ // HELPER FUNCTIONS @@ -26,17 +26,62 @@ fn create_coding_network() -> ActivationNetwork { let mut network = ActivationNetwork::new(); // Rust ecosystem - network.add_edge("rust".to_string(), "ownership".to_string(), LinkType::Semantic, 0.95); - network.add_edge("rust".to_string(), "borrowing".to_string(), LinkType::Semantic, 0.9); - network.add_edge("rust".to_string(), "cargo".to_string(), LinkType::PartOf, 0.85); - network.add_edge("ownership".to_string(), "memory_safety".to_string(), LinkType::Causal, 0.9); - network.add_edge("borrowing".to_string(), "lifetimes".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "rust".to_string(), + "ownership".to_string(), + LinkType::Semantic, + 0.95, + ); + network.add_edge( + "rust".to_string(), + "borrowing".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "rust".to_string(), + "cargo".to_string(), + LinkType::PartOf, + 0.85, + ); + network.add_edge( + "ownership".to_string(), + "memory_safety".to_string(), + LinkType::Causal, + 0.9, + ); + network.add_edge( + "borrowing".to_string(), + "lifetimes".to_string(), + LinkType::Semantic, + 0.85, + ); // Async ecosystem - network.add_edge("rust".to_string(), "async_rust".to_string(), LinkType::Semantic, 0.8); - network.add_edge("async_rust".to_string(), "tokio".to_string(), LinkType::Semantic, 0.9); - network.add_edge("tokio".to_string(), "runtime".to_string(), LinkType::PartOf, 0.85); - network.add_edge("async_rust".to_string(), "futures".to_string(), LinkType::Semantic, 0.85); + network.add_edge( + "rust".to_string(), + "async_rust".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "async_rust".to_string(), + "tokio".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "tokio".to_string(), + "runtime".to_string(), + LinkType::PartOf, + 0.85, + ); + network.add_edge( + "async_rust".to_string(), + "futures".to_string(), + LinkType::Semantic, + 0.85, + ); network } @@ -52,10 +97,30 @@ fn create_chain_network() -> ActivationNetwork { let mut network = ActivationNetwork::with_config(config); // Create a chain: A -> B -> C -> D -> E - network.add_edge("node_a".to_string(), "node_b".to_string(), LinkType::Semantic, 0.9); - network.add_edge("node_b".to_string(), "node_c".to_string(), LinkType::Semantic, 0.9); - network.add_edge("node_c".to_string(), "node_d".to_string(), LinkType::Semantic, 0.9); - network.add_edge("node_d".to_string(), "node_e".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "node_a".to_string(), + "node_b".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "node_b".to_string(), + "node_c".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "node_c".to_string(), + "node_d".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "node_d".to_string(), + "node_e".to_string(), + LinkType::Semantic, + 0.9, + ); network } @@ -81,7 +146,10 @@ fn test_spreading_finds_hidden_chains() { // Should find all nodes in the chain let found_ids: HashSet<_> = results.iter().map(|r| r.memory_id.as_str()).collect(); - assert!(found_ids.contains("node_b"), "Should find direct neighbor node_b"); + assert!( + found_ids.contains("node_b"), + "Should find direct neighbor node_b" + ); assert!(found_ids.contains("node_c"), "Should find 2-hop node_c"); assert!(found_ids.contains("node_d"), "Should find 3-hop node_d"); assert!(found_ids.contains("node_e"), "Should find 4-hop node_e"); @@ -127,9 +195,21 @@ fn test_activation_decays_with_distance() { let results = network.activate("a", 1.0); - let act_b = results.iter().find(|r| r.memory_id == "b").map(|r| r.activation).unwrap_or(0.0); - let act_c = results.iter().find(|r| r.memory_id == "c").map(|r| r.activation).unwrap_or(0.0); - let act_d = results.iter().find(|r| r.memory_id == "d").map(|r| r.activation).unwrap_or(0.0); + let act_b = results + .iter() + .find(|r| r.memory_id == "b") + .map(|r| r.activation) + .unwrap_or(0.0); + let act_c = results + .iter() + .find(|r| r.memory_id == "c") + .map(|r| r.activation) + .unwrap_or(0.0); + let act_d = results + .iter() + .find(|r| r.memory_id == "d") + .map(|r| r.activation) + .unwrap_or(0.0); // Verify monotonic decrease assert!(act_b > act_c, "b ({:.3}) > c ({:.3})", act_b, act_c); @@ -159,7 +239,12 @@ fn test_edge_reinforcement_hebbian() { let mut network = ActivationNetwork::new(); // Add edge with moderate strength - network.add_edge("concept_a".to_string(), "concept_b".to_string(), LinkType::Semantic, 0.5); + network.add_edge( + "concept_a".to_string(), + "concept_b".to_string(), + LinkType::Semantic, + 0.5, + ); // Get initial associations let initial = network.get_associations("concept_a"); @@ -169,7 +254,10 @@ fn test_edge_reinforcement_hebbian() { .map(|a| a.association_strength) .unwrap_or(0.0); - assert!((initial_strength - 0.5).abs() < 0.01, "Initial should be 0.5"); + assert!( + (initial_strength - 0.5).abs() < 0.01, + "Initial should be 0.5" + ); // Reinforce the connection network.reinforce_edge("concept_a", "concept_b", 0.2); @@ -270,10 +358,30 @@ fn test_different_link_types_affect_activation() { let mut network = ActivationNetwork::new(); // Add edges with different link types - network.add_edge("event".to_string(), "semantic_rel".to_string(), LinkType::Semantic, 0.9); - network.add_edge("event".to_string(), "temporal_rel".to_string(), LinkType::Temporal, 0.8); - network.add_edge("event".to_string(), "causal_rel".to_string(), LinkType::Causal, 0.85); - network.add_edge("event".to_string(), "part_of_rel".to_string(), LinkType::PartOf, 0.7); + network.add_edge( + "event".to_string(), + "semantic_rel".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "event".to_string(), + "temporal_rel".to_string(), + LinkType::Temporal, + 0.8, + ); + network.add_edge( + "event".to_string(), + "causal_rel".to_string(), + LinkType::Causal, + 0.85, + ); + network.add_edge( + "event".to_string(), + "part_of_rel".to_string(), + LinkType::PartOf, + 0.7, + ); let results = network.activate("event", 1.0); @@ -285,10 +393,22 @@ fn test_different_link_types_affect_activation() { assert!(found.contains("part_of_rel")); // Verify link types are preserved - let semantic = results.iter().find(|r| r.memory_id == "semantic_rel").unwrap(); - let temporal = results.iter().find(|r| r.memory_id == "temporal_rel").unwrap(); - let causal = results.iter().find(|r| r.memory_id == "causal_rel").unwrap(); - let part_of = results.iter().find(|r| r.memory_id == "part_of_rel").unwrap(); + let semantic = results + .iter() + .find(|r| r.memory_id == "semantic_rel") + .unwrap(); + let temporal = results + .iter() + .find(|r| r.memory_id == "temporal_rel") + .unwrap(); + let causal = results + .iter() + .find(|r| r.memory_id == "causal_rel") + .unwrap(); + let part_of = results + .iter() + .find(|r| r.memory_id == "part_of_rel") + .unwrap(); assert_eq!(semantic.link_type, LinkType::Semantic); assert_eq!(temporal.link_type, LinkType::Temporal); @@ -339,9 +459,9 @@ fn test_max_hops_limit() { #[test] fn test_minimum_threshold() { let config = ActivationConfig { - decay_factor: 0.5, // 50% decay per hop - max_hops: 10, // High limit - min_threshold: 0.2, // But high threshold + decay_factor: 0.5, // 50% decay per hop + max_hops: 10, // High limit + min_threshold: 0.2, // But high threshold allow_cycles: false, }; let mut network = ActivationNetwork::with_config(config); @@ -370,8 +490,18 @@ fn test_minimum_threshold() { fn test_path_tracking() { let mut network = ActivationNetwork::new(); - network.add_edge("start".to_string(), "middle".to_string(), LinkType::Semantic, 0.9); - network.add_edge("middle".to_string(), "end".to_string(), LinkType::Semantic, 0.9); + network.add_edge( + "start".to_string(), + "middle".to_string(), + LinkType::Semantic, + 0.9, + ); + network.add_edge( + "middle".to_string(), + "end".to_string(), + LinkType::Semantic, + 0.9, + ); let results = network.activate("start", 1.0); @@ -390,10 +520,30 @@ fn test_convergent_paths() { let mut network = ActivationNetwork::new(); // Create convergent paths: source -> a -> target and source -> b -> target - network.add_edge("source".to_string(), "path_a".to_string(), LinkType::Semantic, 0.8); - network.add_edge("source".to_string(), "path_b".to_string(), LinkType::Semantic, 0.8); - network.add_edge("path_a".to_string(), "target".to_string(), LinkType::Semantic, 0.8); - network.add_edge("path_b".to_string(), "target".to_string(), LinkType::Semantic, 0.8); + network.add_edge( + "source".to_string(), + "path_a".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "source".to_string(), + "path_b".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "path_a".to_string(), + "target".to_string(), + LinkType::Semantic, + 0.8, + ); + network.add_edge( + "path_b".to_string(), + "target".to_string(), + LinkType::Semantic, + 0.8, + ); let results = network.activate("source", 1.0); diff --git a/tests/e2e/tests/mcp/protocol_tests.rs b/tests/e2e/tests/mcp/protocol_tests.rs index 20bed76..f3a247f 100644 --- a/tests/e2e/tests/mcp/protocol_tests.rs +++ b/tests/e2e/tests/mcp/protocol_tests.rs @@ -25,9 +25,18 @@ fn test_jsonrpc_request_required_fields() { "params": {} }); - assert_eq!(valid_request["jsonrpc"], "2.0", "jsonrpc version must be 2.0"); - assert!(valid_request["method"].is_string(), "method must be a string"); - assert!(valid_request["id"].is_number(), "id should be present for requests"); + assert_eq!( + valid_request["jsonrpc"], "2.0", + "jsonrpc version must be 2.0" + ); + assert!( + valid_request["method"].is_string(), + "method must be a string" + ); + assert!( + valid_request["id"].is_number(), + "id should be present for requests" + ); } /// Test that JSON-RPC notifications have no id field. @@ -40,7 +49,10 @@ fn test_jsonrpc_notification_has_no_id() { "method": "notifications/initialized" }); - assert!(notification.get("id").is_none(), "Notifications must not have an id field"); + assert!( + notification.get("id").is_none(), + "Notifications must not have an id field" + ); assert_eq!(notification["method"], "notifications/initialized"); } @@ -66,8 +78,14 @@ fn test_jsonrpc_success_response_format() { }); assert_eq!(success_response["jsonrpc"], "2.0"); - assert!(success_response["result"].is_object(), "Success response must have result"); - assert!(success_response.get("error").is_none(), "Success response must not have error"); + assert!( + success_response["result"].is_object(), + "Success response must have result" + ); + assert!( + success_response.get("error").is_none(), + "Success response must not have error" + ); } /// Test JSON-RPC response format for errors. @@ -89,10 +107,22 @@ fn test_jsonrpc_error_response_format() { }); assert_eq!(error_response["jsonrpc"], "2.0"); - assert!(error_response["error"].is_object(), "Error response must have error object"); - assert!(error_response["error"]["code"].is_number(), "Error must have code"); - assert!(error_response["error"]["message"].is_string(), "Error must have message"); - assert!(error_response.get("result").is_none(), "Error response must not have result"); + assert!( + error_response["error"].is_object(), + "Error response must have error object" + ); + assert!( + error_response["error"]["code"].is_number(), + "Error must have code" + ); + assert!( + error_response["error"]["message"].is_string(), + "Error must have message" + ); + assert!( + error_response.get("result").is_none(), + "Error response must not have result" + ); } // ============================================================================ @@ -119,8 +149,12 @@ fn test_standard_jsonrpc_error_codes() { for (code, message) in error_codes { // All standard codes are in the reserved range - assert!((-32700..=-32600).contains(&code), - "Standard error code {} ({}) must be in reserved range", code, message); + assert!( + (-32700..=-32600).contains(&code), + "Standard error code {} ({}) must be in reserved range", + code, + message + ); } } @@ -142,8 +176,12 @@ fn test_mcp_specific_error_codes() { for (code, name) in mcp_error_codes { // MCP-specific codes are in the server error range - assert!((-32099..=-32000).contains(&code), - "MCP error code {} ({}) must be in server error range", code, name); + assert!( + (-32099..=-32000).contains(&code), + "MCP error code {} ({}) must be in server error range", + code, + name + ); } } @@ -177,11 +215,20 @@ fn test_mcp_initialize_request_format() { }); let params = &init_request["params"]; - assert!(params["protocolVersion"].is_string(), "protocolVersion required"); + assert!( + params["protocolVersion"].is_string(), + "protocolVersion required" + ); assert!(params["capabilities"].is_object(), "capabilities required"); assert!(params["clientInfo"].is_object(), "clientInfo required"); - assert!(params["clientInfo"]["name"].is_string(), "clientInfo.name required"); - assert!(params["clientInfo"]["version"].is_string(), "clientInfo.version required"); + assert!( + params["clientInfo"]["name"].is_string(), + "clientInfo.name required" + ); + assert!( + params["clientInfo"]["version"].is_string(), + "clientInfo.version required" + ); } /// Test MCP initialize response format. @@ -206,11 +253,26 @@ fn test_mcp_initialize_response_format() { "instructions": "Vestige is your long-term memory system." }); - assert!(init_response["protocolVersion"].is_string(), "protocolVersion required"); - assert!(init_response["serverInfo"].is_object(), "serverInfo required"); - assert!(init_response["serverInfo"]["name"].is_string(), "serverInfo.name required"); - assert!(init_response["serverInfo"]["version"].is_string(), "serverInfo.version required"); - assert!(init_response["capabilities"].is_object(), "capabilities required"); + assert!( + init_response["protocolVersion"].is_string(), + "protocolVersion required" + ); + assert!( + init_response["serverInfo"].is_object(), + "serverInfo required" + ); + assert!( + init_response["serverInfo"]["name"].is_string(), + "serverInfo.name required" + ); + assert!( + init_response["serverInfo"]["version"].is_string(), + "serverInfo.version required" + ); + assert!( + init_response["capabilities"].is_object(), + "capabilities required" + ); } /// Test that requests before initialization are rejected. @@ -229,8 +291,10 @@ fn test_server_rejects_requests_before_initialize() { } }); - assert_eq!(pre_init_error["error"]["code"], -32003, - "Pre-initialization requests should return ServerNotInitialized error"); + assert_eq!( + pre_init_error["error"]["code"], -32003, + "Pre-initialization requests should return ServerNotInitialized error" + ); } // ============================================================================ @@ -277,9 +341,14 @@ fn test_tools_list_response_format() { for tool in tools { assert!(tool["name"].is_string(), "Tool must have name"); - assert!(tool["inputSchema"].is_object(), "Tool must have inputSchema"); - assert_eq!(tool["inputSchema"]["type"], "object", - "inputSchema must be an object type"); + assert!( + tool["inputSchema"].is_object(), + "Tool must have inputSchema" + ); + assert_eq!( + tool["inputSchema"]["type"], "object", + "inputSchema must be an object type" + ); } } @@ -306,7 +375,10 @@ fn test_tools_call_request_format() { let params = &tools_call_request["params"]; assert!(params["name"].is_string(), "Tool name required"); - assert!(params["arguments"].is_object(), "Arguments should be an object"); + assert!( + params["arguments"].is_object(), + "Arguments should be an object" + ); } /// Test tools/call response format. @@ -328,8 +400,14 @@ fn test_tools_call_response_format() { let content = tools_call_response["content"].as_array().unwrap(); assert!(!content.is_empty(), "Content array should not be empty"); - assert!(content[0]["type"].is_string(), "Content item must have type"); - assert!(content[0]["text"].is_string(), "Text content must have text field"); + assert!( + content[0]["type"].is_string(), + "Content item must have type" + ); + assert!( + content[0]["text"].is_string(), + "Text content must have text field" + ); } // ============================================================================ @@ -407,6 +485,8 @@ fn test_resources_read_response_format() { assert!(!contents.is_empty(), "Contents should not be empty"); assert!(contents[0]["uri"].is_string(), "Content must have uri"); // Must have either text or blob - assert!(contents[0]["text"].is_string() || contents[0]["blob"].is_string(), - "Content must have text or blob"); + assert!( + contents[0]["text"].is_string() || contents[0]["blob"].is_string(), + "Content must have text or blob" + ); } diff --git a/tests/e2e/tests/mcp/tool_tests.rs b/tests/e2e/tests/mcp/tool_tests.rs index 4eaaa17..cbc43b5 100644 --- a/tests/e2e/tests/mcp/tool_tests.rs +++ b/tests/e2e/tests/mcp/tool_tests.rs @@ -3,7 +3,7 @@ //! Comprehensive tests for all MCP tools provided by Vestige. //! Tests cover input validation, execution, and response formats. -use serde_json::{json, Value}; +use serde_json::{Value, json}; // ============================================================================ // HELPER FUNCTIONS @@ -11,7 +11,10 @@ use serde_json::{json, Value}; /// Validate a tool call response structure fn validate_tool_response(response: &Value) { - assert!(response["content"].is_array(), "Response must have content array"); + assert!( + response["content"].is_array(), + "Response must have content array" + ); let content = response["content"].as_array().unwrap(); assert!(!content.is_empty(), "Content array must not be empty"); assert!(content[0]["type"].is_string(), "Content must have type"); @@ -74,7 +77,10 @@ fn test_ingest_tool_rejects_empty_content() { "isError": true }); - assert_eq!(expected_error["isError"], true, "Empty content should be an error"); + assert_eq!( + expected_error["isError"], true, + "Empty content should be an error" + ); } /// Test ingest tool with all optional fields. @@ -193,7 +199,10 @@ fn test_semantic_search_valid() { validate_tool_response(&expected_response); let parsed = parse_response_text(&expected_response); - assert_eq!(parsed["method"], "semantic", "Should indicate semantic search"); + assert_eq!( + parsed["method"], "semantic", + "Should indicate semantic search" + ); } /// Test semantic search handles embedding not ready. @@ -209,7 +218,10 @@ fn test_semantic_search_embedding_not_ready() { }); let parsed = parse_response_text(&expected_response); - assert!(parsed["error"].is_string(), "Should explain embedding not ready"); + assert!( + parsed["error"].is_string(), + "Should explain embedding not ready" + ); assert!(parsed["hint"].is_string(), "Should provide hint"); } @@ -337,7 +349,10 @@ fn test_mark_reviewed_with_rating() { let parsed = parse_response_text(&expected_response); assert_eq!(parsed["success"], true, "Review should succeed"); - assert!(parsed["nextReview"].is_string(), "Should return next review date"); + assert!( + parsed["nextReview"].is_string(), + "Should return next review date" + ); } /// Test mark_reviewed with invalid rating. @@ -382,8 +397,14 @@ fn test_get_stats() { validate_tool_response(&expected_response); let parsed = parse_response_text(&expected_response); - assert!(parsed["totalNodes"].is_number(), "Should return total nodes"); - assert!(parsed["averageRetention"].is_number(), "Should return average retention"); + assert!( + parsed["totalNodes"].is_number(), + "Should return total nodes" + ); + assert!( + parsed["averageRetention"].is_number(), + "Should return average retention" + ); } /// Test health_check returns health status. @@ -431,7 +452,10 @@ fn test_set_intention_basic() { let parsed = parse_response_text(&expected_response); assert_eq!(parsed["success"], true, "Should succeed"); - assert!(parsed["intentionId"].is_string(), "Should return intention ID"); + assert!( + parsed["intentionId"].is_string(), + "Should return intention ID" + ); assert_eq!(parsed["priority"], 3, "High priority should be 3"); } @@ -477,8 +501,14 @@ fn test_check_intentions_with_context() { }); let parsed = parse_response_text(&expected_response); - assert!(parsed["triggered"].is_array(), "Should return triggered intentions"); - assert!(parsed["pending"].is_array(), "Should return pending intentions"); + assert!( + parsed["triggered"].is_array(), + "Should return triggered intentions" + ); + assert!( + parsed["pending"].is_array(), + "Should return pending intentions" + ); } /// Test complete_intention marks as fulfilled. @@ -523,7 +553,10 @@ fn test_list_intentions_with_filter() { }); let parsed = parse_response_text(&expected_response); - assert!(parsed["intentions"].is_array(), "Should return intentions array"); + assert!( + parsed["intentions"].is_array(), + "Should return intentions array" + ); assert_eq!(parsed["status"], "active", "Should echo status filter"); } @@ -553,9 +586,18 @@ fn test_tool_schemas_are_valid_json_schema() { "required": ["content"] }); - assert_eq!(ingest_schema["type"], "object", "Schema must be object type"); - assert!(ingest_schema["properties"].is_object(), "Must have properties"); - assert!(ingest_schema["required"].is_array(), "Must specify required fields"); + assert_eq!( + ingest_schema["type"], "object", + "Schema must be object type" + ); + assert!( + ingest_schema["properties"].is_object(), + "Must have properties" + ); + assert!( + ingest_schema["required"].is_array(), + "Must specify required fields" + ); } /// Test all tools have required inputSchema fields. @@ -575,7 +617,10 @@ fn test_all_tools_have_schema() { ]; for (tool_name, required_fields) in tool_definitions { - assert!(!required_fields.is_empty(), - "Tool {} should have at least one required field", tool_name); + assert!( + !required_fields.is_empty(), + "Tool {} should have at least one required field", + tool_name + ); } }